diff options
author | 2020-05-22 14:35:02 +0200 | |
---|---|---|
committer | 2020-05-22 14:35:02 +0200 | |
commit | 90087f07a7cfffe10b4958e13558a0052f85691d (patch) | |
tree | bf0aac44374cfd5cd0da91cc67a6744decd74665 /smtpd | |
parent | moving smtpd to usr.sbin/smtpd to ease cherry-picking of upstream (diff) | |
download | OpenSMTPD-90087f07a7cfffe10b4958e13558a0052f85691d.tar.xz OpenSMTPD-90087f07a7cfffe10b4958e13558a0052f85691d.zip |
Revert "moving smtpd to usr.sbin/smtpd to ease cherry-picking of upstream"
This reverts commit 90620a574d8824e5b2aa18709f2d5b5b6bb3cb38.
Diffstat (limited to 'smtpd')
115 files changed, 54413 insertions, 0 deletions
diff --git a/smtpd/Makefile b/smtpd/Makefile new file mode 100644 index 00000000..a3dbc9d1 --- /dev/null +++ b/smtpd/Makefile @@ -0,0 +1,10 @@ +# $OpenBSD: Makefile,v 1.18 2018/05/24 11:38:24 gilles Exp $ + +.include <bsd.own.mk> + +SUBDIR = smtpd +SUBDIR+= smtpctl +SUBDIR+= smtp +SUBDIR+= mail + +.include <bsd.subdir.mk> diff --git a/smtpd/aliases.5 b/smtpd/aliases.5 new file mode 100644 index 00000000..7c250c81 --- /dev/null +++ b/smtpd/aliases.5 @@ -0,0 +1,102 @@ +.\" $OpenBSD: aliases.5,v 1.16 2020/04/23 21:28:10 jmc Exp $ +.\" +.\" Copyright (c) 2012 Gilles Chehade <gilles@poolp.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: April 23 2020 $ +.Dt ALIASES 5 +.Os +.Sh NAME +.Nm aliases +.Nd aliases file for smtpd +.Sh DESCRIPTION +This manual page describes the format of the +.Nm +file, as used by +.Xr smtpd 8 . +An alias in its simplest form is used to assign an arbitrary name +to an email address, or a group of email addresses. +This provides a convenient way to send mail. +For example an alias could refer to all users of a group: +email to that alias would be sent to all members of the group. +Much more complex aliases can be defined however: +an alias can refer to other aliases, +be used to send mail to a file instead of another person, +or to execute various commands. +.Pp +Within the file, +.Ql # +is a comment delimiter; anything placed after it is discarded. +The file consists of key/value mappings of the form: +.Bd -filled -offset indent +key: value1, value2, value3, ... +.Ed +.Pp +.Em key +is always folded to lowercase before alias lookups to ensure that +there can be no ambiguity. +The key is expanded to the corresponding values, +which consist of one or more of the following: +.Bl -tag -width Ds +.It Em user +A user on the host machine. +The user must have a valid entry in the +.Xr passwd 5 +database file. +.It Ar /path/to/file +Append messages to +.Ar file , +specified by its absolute pathname. +.It | Ns Ar command +Pipe the message to +.Ar command +on its standard input. +The command is run under the privileges of the daemon's unprivileged account. +.It : Ns Ar include : Ns Ar /path/to/file +Include any definitions in +.Ar file +as alias entries. +The format of the file is identical to this one. +.It Ar user-part@domain-part +An email address in RFC 5322 format. +If an address extension is appended to the user-part, +it is first compared for an exact match. +It is then stripped so that an address such as user+ext@example.com +will only use the part that precedes +.Sq + +as a +.Em key . +.It Ar error : Ns Ar code message +A status code and message to return. +The code must be 3 digits, +starting 4XX (TempFail) or 5XX (PermFail). +The message must be present and can be freely chosen. +.El +.Sh FILES +.Bl -tag -width "/etc/mail/aliasesXXX" -compact +.It Pa /etc/mail/aliases +Default +.Nm +file. +.El +.Sh SEE ALSO +.Xr smtpd.conf 5 , +.Xr makemap 8 , +.Xr newaliases 8 , +.Xr smtpd 8 +.Sh HISTORY +The +.Nm +file format appeared in +.Bx 4.0 . diff --git a/smtpd/aliases.c b/smtpd/aliases.c new file mode 100644 index 00000000..0f8a5c1e --- /dev/null +++ b/smtpd/aliases.c @@ -0,0 +1,234 @@ +/* $OpenBSD: aliases.c,v 1.78 2020/04/28 21:46:43 eric Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#ifdef HAVE_UTIL_H +#include <util.h> +#endif +#ifdef HAVE_LIBUTIL_H +#include <libutil.h> +#endif + +#include "smtpd.h" +#include "log.h" + +static int aliases_expand_include(struct expand *, const char *); + +int +aliases_get(struct expand *expand, const char *username) +{ + struct expandnode *xn; + char buf[SMTPD_MAXLOCALPARTSIZE]; + size_t nbaliases; + int ret; + union lookup lk; + struct dispatcher *dsp; + struct table *mapping = NULL; + char *pbuf; + + dsp = dict_xget(env->sc_dispatchers, expand->rule->dispatcher); + mapping = table_find(env, dsp->u.local.table_alias); + + xlowercase(buf, username, sizeof(buf)); + + /* first, check if entry has a user-part tag */ + pbuf = strchr(buf, *env->sc_subaddressing_delim); + if (pbuf) { + ret = table_lookup(mapping, K_ALIAS, buf, &lk); + if (ret < 0) + return (-1); + if (ret) + goto expand; + *pbuf = '\0'; + } + + /* no user-part tag, try looking up user */ + ret = table_lookup(mapping, K_ALIAS, buf, &lk); + if (ret <= 0) + return ret; + +expand: + /* foreach node in table_alias expandtree, we merge */ + nbaliases = 0; + RB_FOREACH(xn, expandtree, &lk.expand->tree) { + if (xn->type == EXPAND_INCLUDE) + nbaliases += aliases_expand_include(expand, + xn->u.buffer); + else { + expand_insert(expand, xn); + nbaliases++; + } + } + + expand_free(lk.expand); + + log_debug("debug: aliases_get: returned %zd aliases", nbaliases); + return nbaliases; +} + +int +aliases_virtual_get(struct expand *expand, const struct mailaddr *maddr) +{ + struct expandnode *xn; + union lookup lk; + char buf[LINE_MAX]; + char user[LINE_MAX]; + char tag[LINE_MAX]; + char domain[LINE_MAX]; + char *pbuf; + int nbaliases; + int ret; + struct dispatcher *dsp; + struct table *mapping = NULL; + + dsp = dict_xget(env->sc_dispatchers, expand->rule->dispatcher); + mapping = table_find(env, dsp->u.local.table_virtual); + + if (!bsnprintf(user, sizeof(user), "%s", maddr->user)) + return 0; + if (!bsnprintf(domain, sizeof(domain), "%s", maddr->domain)) + return 0; + xlowercase(user, user, sizeof(user)); + xlowercase(domain, domain, sizeof(domain)); + + memset(tag, '\0', sizeof tag); + pbuf = strchr(user, *env->sc_subaddressing_delim); + if (pbuf) { + if (!bsnprintf(tag, sizeof(tag), "%s", pbuf + 1)) + return 0; + xlowercase(tag, tag, sizeof(tag)); + *pbuf = '\0'; + } + + /* first, check if entry has a user-part tag */ + if (tag[0]) { + if (!bsnprintf(buf, sizeof(buf), "%s%c%s@%s", + user, *env->sc_subaddressing_delim, tag, domain)) + return 0; + ret = table_lookup(mapping, K_ALIAS, buf, &lk); + if (ret < 0) + return (-1); + if (ret) + goto expand; + } + + /* then, check if entry exists without user-part tag */ + if (!bsnprintf(buf, sizeof(buf), "%s@%s", user, domain)) + return 0; + ret = table_lookup(mapping, K_ALIAS, buf, &lk); + if (ret < 0) + return (-1); + if (ret) + goto expand; + + if (tag[0]) { + /* Failed ? We lookup for username + user-part tag */ + if (!bsnprintf(buf, sizeof(buf), "%s%c%s", + user, *env->sc_subaddressing_delim, tag)) + return 0; + ret = table_lookup(mapping, K_ALIAS, buf, &lk); + if (ret < 0) + return (-1); + if (ret) + goto expand; + } + + /* Failed ? We lookup for username only */ + if (!bsnprintf(buf, sizeof(buf), "%s", user)) + return 0; + ret = table_lookup(mapping, K_ALIAS, buf, &lk); + if (ret < 0) + return (-1); + if (ret) + goto expand; + + /* Do not try catch-all entries if there is no domain */ + if (domain[0] == '\0') + return 0; + + if (!bsnprintf(buf, sizeof(buf), "@%s", domain)) + return 0; + /* Failed ? We lookup for catch all for virtual domain */ + ret = table_lookup(mapping, K_ALIAS, buf, &lk); + if (ret < 0) + return (-1); + if (ret) + goto expand; + + /* Failed ? We lookup for a *global* catch all */ + ret = table_lookup(mapping, K_ALIAS, "@", &lk); + if (ret <= 0) + return (ret); + +expand: + /* foreach node in table_virtual expand, we merge */ + nbaliases = 0; + RB_FOREACH(xn, expandtree, &lk.expand->tree) { + if (xn->type == EXPAND_INCLUDE) + nbaliases += aliases_expand_include(expand, + xn->u.buffer); + else { + expand_insert(expand, xn); + nbaliases++; + } + } + + expand_free(lk.expand); + + log_debug("debug: aliases_virtual_get: '%s' resolved to %d nodes", + buf, nbaliases); + + return nbaliases; +} + +static int +aliases_expand_include(struct expand *expand, const char *filename) +{ + FILE *fp; + char *line; + size_t len, lineno = 0; + char delim[3] = { '\\', '#', '\0' }; + + fp = fopen(filename, "r"); + if (fp == NULL) { + log_warn("warn: failed to open include file \"%s\".", filename); + return 0; + } + + while ((line = fparseln(fp, &len, &lineno, delim, 0)) != NULL) { + expand_line(expand, line, 0); + free(line); + } + + fclose(fp); + return 1; +} diff --git a/smtpd/bounce.c b/smtpd/bounce.c new file mode 100644 index 00000000..4a4a0992 --- /dev/null +++ b/smtpd/bounce.c @@ -0,0 +1,820 @@ +/* $OpenBSD: bounce.c,v 1.82 2020/04/24 11:34:07 eric Exp $ */ + +/* + * Copyright (c) 2009 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +#define BOUNCE_MAXRUN 2 +#define BOUNCE_HIWAT 65535 + +enum { + BOUNCE_EHLO, + BOUNCE_MAIL, + BOUNCE_RCPT, + BOUNCE_DATA, + BOUNCE_DATA_NOTICE, + BOUNCE_DATA_MESSAGE, + BOUNCE_DATA_END, + BOUNCE_QUIT, + BOUNCE_CLOSE, +}; + +struct bounce_envelope { + TAILQ_ENTRY(bounce_envelope) entry; + uint64_t id; + struct mailaddr dest; + char *report; + uint8_t esc_class; + uint8_t esc_code; +}; + +struct bounce_message { + SPLAY_ENTRY(bounce_message) sp_entry; + TAILQ_ENTRY(bounce_message) entry; + uint32_t msgid; + struct delivery_bounce bounce; + char *smtpname; + char *to; + time_t timeout; + TAILQ_HEAD(, bounce_envelope) envelopes; +}; + +struct bounce_session { + char *smtpname; + struct bounce_message *msg; + FILE *msgfp; + int state; + struct io *io; + uint64_t boundary; +}; + +SPLAY_HEAD(bounce_message_tree, bounce_message); +static int bounce_message_cmp(const struct bounce_message *, + const struct bounce_message *); +SPLAY_PROTOTYPE(bounce_message_tree, bounce_message, sp_entry, + bounce_message_cmp); + +static void bounce_drain(void); +static void bounce_send(struct bounce_session *, const char *, ...); +static int bounce_next_message(struct bounce_session *); +static int bounce_next(struct bounce_session *); +static void bounce_delivery(struct bounce_message *, int, const char *); +static void bounce_status(struct bounce_session *, const char *, ...); +static void bounce_io(struct io *, int, void *); +static void bounce_timeout(int, short, void *); +static void bounce_free(struct bounce_session *); +static const char *action_str(const struct delivery_bounce *); + +static struct tree wait_fd; +static struct bounce_message_tree messages; +static TAILQ_HEAD(, bounce_message) pending; + +static int nmessage = 0; +static int running = 0; +static struct event ev_timer; + +static void +bounce_init(void) +{ + static int init = 0; + + if (init == 0) { + TAILQ_INIT(&pending); + SPLAY_INIT(&messages); + tree_init(&wait_fd); + evtimer_set(&ev_timer, bounce_timeout, NULL); + init = 1; + } +} + +void +bounce_add(uint64_t evpid) +{ + char buf[LINE_MAX], *line; + struct envelope evp; + struct bounce_message key, *msg; + struct bounce_envelope *be; + + bounce_init(); + + if (queue_envelope_load(evpid, &evp) == 0) { + m_create(p_scheduler, IMSG_QUEUE_DELIVERY_PERMFAIL, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_close(p_scheduler); + return; + } + + if (evp.type != D_BOUNCE) + errx(1, "bounce: evp:%016" PRIx64 " is not of type D_BOUNCE!", + evp.id); + + key.msgid = evpid_to_msgid(evpid); + key.bounce = evp.agent.bounce; + key.smtpname = evp.smtpname; + + switch (evp.esc_class) { + case ESC_STATUS_OK: + key.bounce.type = B_DELIVERED; + break; + case ESC_STATUS_TEMPFAIL: + key.bounce.type = B_DELAYED; + break; + default: + key.bounce.type = B_FAILED; + } + + key.bounce.dsn_ret = evp.dsn_ret; + key.bounce.ttl = evp.ttl; + msg = SPLAY_FIND(bounce_message_tree, &messages, &key); + if (msg == NULL) { + msg = xcalloc(1, sizeof(*msg)); + msg->msgid = key.msgid; + msg->bounce = key.bounce; + + TAILQ_INIT(&msg->envelopes); + + msg->smtpname = xstrdup(evp.smtpname); + (void)snprintf(buf, sizeof(buf), "%s@%s", evp.sender.user, + evp.sender.domain); + msg->to = xstrdup(buf); + nmessage += 1; + SPLAY_INSERT(bounce_message_tree, &messages, msg); + log_debug("debug: bounce: new message %08" PRIx32, + msg->msgid); + stat_increment("bounce.message", 1); + } else + TAILQ_REMOVE(&pending, msg, entry); + + line = evp.errorline; + if (strlen(line) > 4 && (*line == '1' || *line == '6')) + line += 4; + (void)snprintf(buf, sizeof(buf), "%s@%s: %s", evp.dest.user, + evp.dest.domain, line); + + be = xmalloc(sizeof *be); + be->id = evpid; + be->report = xstrdup(buf); + (void)strlcpy(be->dest.user, evp.dest.user, sizeof(be->dest.user)); + (void)strlcpy(be->dest.domain, evp.dest.domain, + sizeof(be->dest.domain)); + be->esc_class = evp.esc_class; + be->esc_code = evp.esc_code; + TAILQ_INSERT_TAIL(&msg->envelopes, be, entry); + log_debug("debug: bounce: adding report %16"PRIx64": %s", be->id, be->report); + + msg->timeout = time(NULL) + 1; + TAILQ_INSERT_TAIL(&pending, msg, entry); + + stat_increment("bounce.envelope", 1); + bounce_drain(); +} + +void +bounce_fd(int fd) +{ + struct bounce_session *s; + struct bounce_message *msg; + + log_debug("debug: bounce: got enqueue socket %d", fd); + + if (fd == -1 || TAILQ_EMPTY(&pending)) { + log_debug("debug: bounce: cancelling"); + if (fd != -1) + close(fd); + running -= 1; + bounce_drain(); + return; + } + + msg = TAILQ_FIRST(&pending); + + s = xcalloc(1, sizeof(*s)); + s->smtpname = xstrdup(msg->smtpname); + s->state = BOUNCE_EHLO; + s->io = io_new(); + io_set_callback(s->io, bounce_io, s); + io_set_fd(s->io, fd); + io_set_timeout(s->io, 30000); + io_set_read(s->io); + s->boundary = generate_uid(); + + log_debug("debug: bounce: new session %p", s); + stat_increment("bounce.session", 1); +} + +static void +bounce_timeout(int fd, short ev, void *arg) +{ + log_debug("debug: bounce: timeout"); + + bounce_drain(); +} + +static void +bounce_drain() +{ + struct bounce_message *msg; + struct timeval tv; + time_t t; + + log_debug("debug: bounce: drain: nmessage=%d running=%d", + nmessage, running); + + while (1) { + if (running >= BOUNCE_MAXRUN) { + log_debug("debug: bounce: max session reached"); + return; + } + + if (nmessage == 0) { + log_debug("debug: bounce: no more messages"); + return; + } + + if (running >= nmessage) { + log_debug("debug: bounce: enough sessions running"); + return; + } + + if ((msg = TAILQ_FIRST(&pending)) == NULL) { + log_debug("debug: bounce: no more pending messages"); + return; + } + + t = time(NULL); + if (msg->timeout > t) { + log_debug("debug: bounce: next message not ready yet"); + if (!evtimer_pending(&ev_timer, NULL)) { + log_debug("debug: bounce: setting timer"); + tv.tv_sec = msg->timeout - t; + tv.tv_usec = 0; + evtimer_add(&ev_timer, &tv); + } + return; + } + + log_debug("debug: bounce: requesting new enqueue socket..."); + m_compose(p_pony, IMSG_QUEUE_SMTP_SESSION, 0, 0, -1, NULL, 0); + + running += 1; + } +} + +static void +bounce_send(struct bounce_session *s, const char *fmt, ...) +{ + va_list ap; + char *p; + int len; + + va_start(ap, fmt); + if ((len = vasprintf(&p, fmt, ap)) == -1) + fatal("bounce: vasprintf"); + va_end(ap); + + log_trace(TRACE_BOUNCE, "bounce: %p: >>> %s", s, p); + + io_xprintf(s->io, "%s\r\n", p); + + free(p); +} + +static const char * +bounce_duration(long long int d) +{ + static char buf[32]; + + if (d < 60) { + (void)snprintf(buf, sizeof buf, "%lld second%s", d, + (d == 1) ? "" : "s"); + } else if (d < 3600) { + d = d / 60; + (void)snprintf(buf, sizeof buf, "%lld minute%s", d, + (d == 1) ? "" : "s"); + } + else if (d < 3600 * 24) { + d = d / 3600; + (void)snprintf(buf, sizeof buf, "%lld hour%s", d, + (d == 1) ? "" : "s"); + } + else { + d = d / (3600 * 24); + (void)snprintf(buf, sizeof buf, "%lld day%s", d, + (d == 1) ? "" : "s"); + } + return (buf); +} + +#define NOTICE_INTRO \ + " Hi!\r\n\r\n" \ + " This is the MAILER-DAEMON, please DO NOT REPLY to this email.\r\n" + +const char *notice_error = + " An error has occurred while attempting to deliver a message for\r\n" + " the following list of recipients:\r\n\r\n"; + +const char *notice_warning = + " A message is delayed for more than %s for the following\r\n" + " list of recipients:\r\n\r\n"; + +const char *notice_warning2 = + " Please note that this is only a temporary failure report.\r\n" + " The message is kept in the queue for up to %s.\r\n" + " You DO NOT NEED to re-send the message to these recipients.\r\n\r\n"; + +const char *notice_success = + " Your message was successfully delivered to these recipients.\r\n\r\n"; + +const char *notice_relay = + " Your message was relayed to these recipients.\r\n\r\n"; + +static int +bounce_next_message(struct bounce_session *s) +{ + struct bounce_message *msg; + char buf[LINE_MAX]; + int fd; + time_t now; + + again: + + now = time(NULL); + + TAILQ_FOREACH(msg, &pending, entry) { + if (msg->timeout > now) + continue; + if (strcmp(msg->smtpname, s->smtpname)) + continue; + break; + } + if (msg == NULL) + return (0); + + TAILQ_REMOVE(&pending, msg, entry); + SPLAY_REMOVE(bounce_message_tree, &messages, msg); + + if ((fd = queue_message_fd_r(msg->msgid)) == -1) { + bounce_delivery(msg, IMSG_QUEUE_DELIVERY_TEMPFAIL, + "Could not open message fd"); + goto again; + } + + if ((s->msgfp = fdopen(fd, "r")) == NULL) { + (void)snprintf(buf, sizeof(buf), "fdopen: %s", strerror(errno)); + log_warn("warn: bounce: fdopen"); + close(fd); + bounce_delivery(msg, IMSG_QUEUE_DELIVERY_TEMPFAIL, buf); + goto again; + } + + s->msg = msg; + return (1); +} + +static int +bounce_next(struct bounce_session *s) +{ + struct bounce_envelope *evp; + char *line = NULL; + size_t n, sz = 0; + ssize_t len; + + switch (s->state) { + case BOUNCE_EHLO: + bounce_send(s, "EHLO %s", s->smtpname); + s->state = BOUNCE_MAIL; + break; + + case BOUNCE_MAIL: + case BOUNCE_DATA_END: + log_debug("debug: bounce: %p: getting next message...", s); + if (bounce_next_message(s) == 0) { + log_debug("debug: bounce: %p: no more messages", s); + bounce_send(s, "QUIT"); + s->state = BOUNCE_CLOSE; + break; + } + log_debug("debug: bounce: %p: found message %08"PRIx32, + s, s->msg->msgid); + bounce_send(s, "MAIL FROM: <>"); + s->state = BOUNCE_RCPT; + break; + + case BOUNCE_RCPT: + bounce_send(s, "RCPT TO: <%s>", s->msg->to); + s->state = BOUNCE_DATA; + break; + + case BOUNCE_DATA: + bounce_send(s, "DATA"); + s->state = BOUNCE_DATA_NOTICE; + break; + + case BOUNCE_DATA_NOTICE: + /* Construct an appropriate notice. */ + + io_xprintf(s->io, + "Subject: Delivery status notification: %s\r\n" + "From: Mailer Daemon <MAILER-DAEMON@%s>\r\n" + "To: %s\r\n" + "Date: %s\r\n" + "MIME-Version: 1.0\r\n" + "Content-Type: multipart/mixed;" + "boundary=\"%16" PRIu64 "/%s\"\r\n" + "\r\n" + "This is a MIME-encapsulated message.\r\n" + "\r\n", + action_str(&s->msg->bounce), + s->smtpname, + s->msg->to, + time_to_text(time(NULL)), + s->boundary, + s->smtpname); + + io_xprintf(s->io, + "--%16" PRIu64 "/%s\r\n" + "Content-Description: Notification\r\n" + "Content-Type: text/plain; charset=us-ascii\r\n" + "\r\n" + NOTICE_INTRO + "\r\n", + s->boundary, s->smtpname); + + switch (s->msg->bounce.type) { + case B_FAILED: + io_xprint(s->io, notice_error); + break; + case B_DELAYED: + io_xprintf(s->io, notice_warning, + bounce_duration(s->msg->bounce.delay)); + break; + case B_DELIVERED: + io_xprint(s->io, s->msg->bounce.mta_without_dsn ? + notice_relay : notice_success); + break; + default: + log_warn("warn: bounce: unknown bounce_type"); + } + + TAILQ_FOREACH(evp, &s->msg->envelopes, entry) { + io_xprint(s->io, evp->report); + io_xprint(s->io, "\r\n"); + } + io_xprint(s->io, "\r\n"); + + if (s->msg->bounce.type == B_DELAYED) + io_xprintf(s->io, notice_warning2, + bounce_duration(s->msg->bounce.ttl)); + + io_xprintf(s->io, + " Below is a copy of the original message:\r\n" + "\r\n"); + + io_xprintf(s->io, + "--%16" PRIu64 "/%s\r\n" + "Content-Description: Delivery Report\r\n" + "Content-Type: message/delivery-status\r\n" + "\r\n", + s->boundary, s->smtpname); + + io_xprintf(s->io, + "Reporting-MTA: dns; %s\r\n" + "\r\n", + s->smtpname); + + TAILQ_FOREACH(evp, &s->msg->envelopes, entry) { + io_xprintf(s->io, + "Final-Recipient: rfc822; %s@%s\r\n" + "Action: %s\r\n" + "Status: %s\r\n" + "\r\n", + evp->dest.user, + evp->dest.domain, + action_str(&s->msg->bounce), + esc_code(evp->esc_class, evp->esc_code)); + } + + log_trace(TRACE_BOUNCE, "bounce: %p: >>> [... %zu bytes ...]", + s, io_queued(s->io)); + + s->state = BOUNCE_DATA_MESSAGE; + break; + + case BOUNCE_DATA_MESSAGE: + io_xprintf(s->io, + "--%16" PRIu64 "/%s\r\n" + "Content-Description: Message headers\r\n" + "Content-Type: text/rfc822-headers\r\n" + "\r\n", + s->boundary, s->smtpname); + + n = io_queued(s->io); + while (io_queued(s->io) < BOUNCE_HIWAT) { + if ((len = getline(&line, &sz, s->msgfp)) == -1) + break; + if (len == 1 && line[0] == '\n' && /* end of headers */ + s->msg->bounce.type == B_DELIVERED && + s->msg->bounce.dsn_ret == DSN_RETHDRS) { + free(line); + fclose(s->msgfp); + s->msgfp = NULL; + io_xprintf(s->io, + "\r\n--%16" PRIu64 "/%s--\r\n", s->boundary, + s->smtpname); + bounce_send(s, "."); + s->state = BOUNCE_DATA_END; + return (0); + } + line[len - 1] = '\0'; + io_xprintf(s->io, "%s%s\r\n", + (len == 2 && line[0] == '.') ? "." : "", line); + } + free(line); + + if (ferror(s->msgfp)) { + fclose(s->msgfp); + s->msgfp = NULL; + bounce_delivery(s->msg, IMSG_QUEUE_DELIVERY_TEMPFAIL, + "Error reading message"); + s->msg = NULL; + return (-1); + } + + io_xprintf(s->io, + "\r\n--%16" PRIu64 "/%s--\r\n", s->boundary, s->smtpname); + + log_trace(TRACE_BOUNCE, "bounce: %p: >>> [... %zu bytes ...]", + s, io_queued(s->io) - n); + + if (feof(s->msgfp)) { + fclose(s->msgfp); + s->msgfp = NULL; + bounce_send(s, "."); + s->state = BOUNCE_DATA_END; + } + break; + + case BOUNCE_QUIT: + bounce_send(s, "QUIT"); + s->state = BOUNCE_CLOSE; + break; + + default: + fatalx("bounce: bad state"); + } + + return (0); +} + + +static void +bounce_delivery(struct bounce_message *msg, int delivery, const char *status) +{ + struct bounce_envelope *be; + struct envelope evp; + size_t n; + const char *f; + + n = 0; + while ((be = TAILQ_FIRST(&msg->envelopes))) { + if (delivery == IMSG_QUEUE_DELIVERY_TEMPFAIL) { + if (queue_envelope_load(be->id, &evp) == 0) { + fatalx("could not reload envelope!"); + } + evp.retry++; + evp.lasttry = msg->timeout; + envelope_set_errormsg(&evp, "%s", status); + queue_envelope_update(&evp); + m_create(p_scheduler, delivery, 0, 0, -1); + m_add_envelope(p_scheduler, &evp); + m_close(p_scheduler); + } else { + m_create(p_scheduler, delivery, 0, 0, -1); + m_add_evpid(p_scheduler, be->id); + m_close(p_scheduler); + queue_envelope_delete(be->id); + } + TAILQ_REMOVE(&msg->envelopes, be, entry); + free(be->report); + free(be); + n += 1; + } + + + if (delivery == IMSG_QUEUE_DELIVERY_TEMPFAIL) + f = "TempFail"; + else if (delivery == IMSG_QUEUE_DELIVERY_PERMFAIL) + f = "PermFail"; + else + f = NULL; + + if (f) + log_warnx("warn: %s injecting failure report on message %08" + PRIx32 " to <%s> for %zu envelope%s: %s", + f, msg->msgid, msg->to, n, n > 1 ? "s":"", status); + + nmessage -= 1; + stat_decrement("bounce.message", 1); + stat_decrement("bounce.envelope", n); + free(msg->smtpname); + free(msg->to); + free(msg); +} + +static void +bounce_status(struct bounce_session *s, const char *fmt, ...) +{ + va_list ap; + char *status; + int len, delivery; + + /* Ignore if there is no message */ + if (s->msg == NULL) + return; + + va_start(ap, fmt); + if ((len = vasprintf(&status, fmt, ap)) == -1) + fatal("bounce: vasprintf"); + va_end(ap); + + if (*status == '2') + delivery = IMSG_QUEUE_DELIVERY_OK; + else if (*status == '5' || *status == '6') + delivery = IMSG_QUEUE_DELIVERY_PERMFAIL; + else + delivery = IMSG_QUEUE_DELIVERY_TEMPFAIL; + + bounce_delivery(s->msg, delivery, status); + s->msg = NULL; + if (s->msgfp) + fclose(s->msgfp); + + free(status); +} + +static void +bounce_free(struct bounce_session *s) +{ + log_debug("debug: bounce: %p: deleting session", s); + + io_free(s->io); + + free(s->smtpname); + free(s); + + running -= 1; + stat_decrement("bounce.session", 1); + bounce_drain(); +} + +static void +bounce_io(struct io *io, int evt, void *arg) +{ + struct bounce_session *s = arg; + const char *error; + char *line, *msg; + int cont; + size_t len; + + log_trace(TRACE_IO, "bounce: %p: %s %s", s, io_strevent(evt), + io_strio(io)); + + switch (evt) { + case IO_DATAIN: + nextline: + line = io_getline(s->io, &len); + if (line == NULL && io_datalen(s->io) >= LINE_MAX) { + bounce_status(s, "Input too long"); + bounce_free(s); + return; + } + + if (line == NULL) + break; + + /* Strip trailing '\r' */ + if (len && line[len - 1] == '\r') + line[--len] = '\0'; + + log_trace(TRACE_BOUNCE, "bounce: %p: <<< %s", s, line); + + if ((error = parse_smtp_response(line, len, &msg, &cont))) { + bounce_status(s, "Bad response: %s", error); + bounce_free(s); + return; + } + if (cont) + goto nextline; + + if (s->state == BOUNCE_CLOSE) { + bounce_free(s); + return; + } + + if (line[0] != '2' && line[0] != '3') { /* fail */ + bounce_status(s, "%s", line); + s->state = BOUNCE_QUIT; + } else if (s->state == BOUNCE_DATA_END) { /* accepted */ + bounce_status(s, "%s", line); + } + + if (bounce_next(s) == -1) { + bounce_free(s); + return; + } + + io_set_write(io); + break; + + case IO_LOWAT: + if (s->state == BOUNCE_DATA_MESSAGE) + if (bounce_next(s) == -1) { + bounce_free(s); + return; + } + if (io_queued(s->io) == 0) + io_set_read(io); + break; + + default: + bounce_status(s, "442 i/o error %d", evt); + bounce_free(s); + break; + } +} + +static int +bounce_message_cmp(const struct bounce_message *a, + const struct bounce_message *b) +{ + int r; + + if (a->msgid < b->msgid) + return (-1); + if (a->msgid > b->msgid) + return (1); + if ((r = strcmp(a->smtpname, b->smtpname))) + return (r); + + return memcmp(&a->bounce, &b->bounce, sizeof (a->bounce)); +} + +static const char * +action_str(const struct delivery_bounce *b) +{ + switch (b->type) { + case B_FAILED: + return ("failed"); + case B_DELAYED: + return ("delayed"); + case B_DELIVERED: + if (b->mta_without_dsn) + return ("relayed"); + + return ("delivered"); + default: + log_warn("warn: bounce: unknown bounce_type"); + return (""); + } +} + +SPLAY_GENERATE(bounce_message_tree, bounce_message, sp_entry, + bounce_message_cmp); diff --git a/smtpd/ca.c b/smtpd/ca.c new file mode 100644 index 00000000..a27db87a --- /dev/null +++ b/smtpd/ca.c @@ -0,0 +1,777 @@ +/* $OpenBSD: ca.c,v 1.36 2019/09/21 07:46:53 semarie Exp $ */ + +/* + * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org> + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/tree.h> + +#include <grp.h> /* needed for setgroups */ +#include <err.h> +#include <imsg.h> +#include <limits.h> +#include <pwd.h> +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/ssl.h> +#include <openssl/pem.h> +#include <openssl/evp.h> +#include <openssl/ecdsa.h> +#include <openssl/rsa.h> +#include <openssl/engine.h> +#include <openssl/err.h> + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" + +static int ca_verify_cb(int, X509_STORE_CTX *); + +static int rsae_send_imsg(int, const unsigned char *, unsigned char *, + RSA *, int, unsigned int); +static int rsae_pub_enc(int, const unsigned char *, unsigned char *, + RSA *, int); +static int rsae_pub_dec(int,const unsigned char *, unsigned char *, + RSA *, int); +static int rsae_priv_enc(int, const unsigned char *, unsigned char *, + RSA *, int); +static int rsae_priv_dec(int, const unsigned char *, unsigned char *, + RSA *, int); +static int rsae_mod_exp(BIGNUM *, const BIGNUM *, RSA *, BN_CTX *); +static int rsae_bn_mod_exp(BIGNUM *, const BIGNUM *, const BIGNUM *, + const BIGNUM *, BN_CTX *, BN_MONT_CTX *); +static int rsae_init(RSA *); +static int rsae_finish(RSA *); +static int rsae_keygen(RSA *, int, BIGNUM *, BN_GENCB *); + +#if defined(SUPPORT_ECDSA) +static ECDSA_SIG *ecdsae_do_sign(const unsigned char *, int, const BIGNUM *, + const BIGNUM *, EC_KEY *); +static int ecdsae_sign_setup(EC_KEY *, BN_CTX *, BIGNUM **, BIGNUM **); +static int ecdsae_do_verify(const unsigned char *, int, const ECDSA_SIG *, + EC_KEY *); +#endif + +static uint64_t reqid = 0; + +static void +ca_shutdown(void) +{ + log_debug("debug: ca agent exiting"); + _exit(0); +} + +int +ca(void) +{ + struct passwd *pw; + + purge_config(PURGE_LISTENERS|PURGE_TABLES|PURGE_RULES|PURGE_DISPATCHERS); + + if ((pw = getpwnam(SMTPD_USER)) == NULL) + fatalx("unknown user " SMTPD_USER); + + if (chroot(PATH_CHROOT) == -1) + fatal("ca: chroot"); + if (chdir("/") == -1) + fatal("ca: chdir(\"/\")"); + + config_process(PROC_CA); + + 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("ca: cannot drop privileges"); + + imsg_callback = ca_imsg; + event_init(); + + signal(SIGINT, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peer(PROC_CONTROL); + config_peer(PROC_PARENT); + config_peer(PROC_PONY); + + /* Ignore them until we get our config */ + mproc_disable(p_pony); + +#if HAVE_PLEDGE + if (pledge("stdio", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + fatalx("exited event loop"); + + return (0); +} + +void +ca_init(void) +{ + BIO *in = NULL; + EVP_PKEY *pkey = NULL; + struct pki *pki; + const char *k; + void *iter_dict; + + log_debug("debug: init private ssl-tree"); + iter_dict = NULL; + while (dict_iter(env->sc_pki_dict, &iter_dict, &k, (void **)&pki)) { + if (pki->pki_key == NULL) + continue; + + if ((in = BIO_new_mem_buf(pki->pki_key, + pki->pki_key_len)) == NULL) + fatalx("ca_launch: key"); + + if ((pkey = PEM_read_bio_PrivateKey(in, + NULL, NULL, NULL)) == NULL) + fatalx("ca_launch: PEM"); + BIO_free(in); + + pki->pki_pkey = pkey; + + freezero(pki->pki_key, pki->pki_key_len); + pki->pki_key = NULL; + } +} + +static int +ca_verify_cb(int ok, X509_STORE_CTX *ctx) +{ + switch (X509_STORE_CTX_get_error(ctx)) { + case X509_V_OK: + break; + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: + break; + case X509_V_ERR_CERT_NOT_YET_VALID: + case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD: + break; + case X509_V_ERR_CERT_HAS_EXPIRED: + case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD: + break; + case X509_V_ERR_NO_EXPLICIT_POLICY: + break; + } + return ok; +} + +int +ca_X509_verify(void *certificate, void *chain, const char *CAfile, + const char *CRLfile, const char **errstr) +{ + X509_STORE *store = NULL; + X509_STORE_CTX *xsc = NULL; + int ret = 0; + long error = 0; + + if ((store = X509_STORE_new()) == NULL) + goto end; + + if (!X509_STORE_load_locations(store, CAfile, NULL)) { + log_warn("warn: unable to load CA file %s", CAfile); + goto end; + } + X509_STORE_set_default_paths(store); + + if ((xsc = X509_STORE_CTX_new()) == NULL) + goto end; + + if (X509_STORE_CTX_init(xsc, store, certificate, chain) != 1) + goto end; + + X509_STORE_CTX_set_verify_cb(xsc, ca_verify_cb); + + ret = X509_verify_cert(xsc); + +end: + *errstr = NULL; + if (ret != 1) { + if (xsc) { + error = X509_STORE_CTX_get_error(xsc); + *errstr = X509_verify_cert_error_string(error); + } + else if (ERR_peek_last_error()) + *errstr = ERR_error_string(ERR_peek_last_error(), NULL); + } + + X509_STORE_CTX_free(xsc); + X509_STORE_free(store); + + return ret > 0 ? 1 : 0; +} + +void +ca_imsg(struct mproc *p, struct imsg *imsg) +{ + RSA *rsa = NULL; +#if defined(SUPPORT_ECDSA) + EC_KEY *ecdsa = NULL; +#endif + const void *from = NULL; + unsigned char *to = NULL; + struct msg m; + const char *pkiname; + size_t flen, tlen, padding; +#if defined(SUPPORT_ECDSA) + int buf_len; +#endif + struct pki *pki; + int ret = 0; + uint64_t id; + int v; + + if (imsg == NULL) + ca_shutdown(); + + switch (imsg->hdr.type) { + case IMSG_CONF_START: + return; + case IMSG_CONF_END: + ca_init(); + + /* Start fulfilling requests */ + mproc_enable(p_pony); + return; + + case IMSG_CTL_VERBOSE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + log_trace_verbose(v); + return; + + case IMSG_CTL_PROFILE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + profiling = v; + return; + + case IMSG_CA_RSA_PRIVENC: + case IMSG_CA_RSA_PRIVDEC: + m_msg(&m, imsg); + m_get_id(&m, &id); + m_get_string(&m, &pkiname); + m_get_data(&m, &from, &flen); + m_get_size(&m, &tlen); + m_get_size(&m, &padding); + m_end(&m); + + pki = dict_get(env->sc_pki_dict, pkiname); + if (pki == NULL || pki->pki_pkey == NULL || + (rsa = EVP_PKEY_get1_RSA(pki->pki_pkey)) == NULL) + fatalx("ca_imsg: invalid pki"); + + if ((to = calloc(1, tlen)) == NULL) + fatalx("ca_imsg: calloc"); + + switch (imsg->hdr.type) { + case IMSG_CA_RSA_PRIVENC: + ret = RSA_private_encrypt(flen, from, to, rsa, + padding); + break; + case IMSG_CA_RSA_PRIVDEC: + ret = RSA_private_decrypt(flen, from, to, rsa, + padding); + break; + } + + m_create(p, imsg->hdr.type, 0, 0, -1); + m_add_id(p, id); + m_add_int(p, ret); + if (ret > 0) + m_add_data(p, to, (size_t)ret); + m_close(p); + + free(to); + RSA_free(rsa); + return; + +#if defined(SUPPORT_ECDSA) + case IMSG_CA_ECDSA_SIGN: + m_msg(&m, imsg); + m_get_id(&m, &id); + m_get_string(&m, &pkiname); + m_get_data(&m, &from, &flen); + m_end(&m); + + pki = dict_get(env->sc_pki_dict, pkiname); + if (pki == NULL || pki->pki_pkey == NULL || + (ecdsa = EVP_PKEY_get1_EC_KEY(pki->pki_pkey)) == NULL) + fatalx("ca_imsg: invalid pki"); + + buf_len = ECDSA_size(ecdsa); + if ((to = calloc(1, buf_len)) == NULL) + fatalx("ca_imsg: calloc"); + ret = ECDSA_sign(0, from, flen, to, &buf_len, ecdsa); + m_create(p, imsg->hdr.type, 0, 0, -1); + m_add_id(p, id); + m_add_int(p, ret); + if (ret > 0) + m_add_data(p, to, (size_t)buf_len); + m_close(p); + free(to); + EC_KEY_free(ecdsa); + return; +#endif + } + errx(1, "ca_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type)); +} + +/* + * RSA privsep engine (called from unprivileged processes) + */ + +const RSA_METHOD *rsa_default = NULL; + +static RSA_METHOD *rsae_method = NULL; + +static int +rsae_send_imsg(int flen, const unsigned char *from, unsigned char *to, + RSA *rsa, int padding, unsigned int cmd) +{ + int ret = 0; + struct imsgbuf *ibuf; + struct imsg imsg; + int n, done = 0; + const void *toptr; + char *pkiname; + size_t tlen; + struct msg m; + uint64_t id; + + if ((pkiname = RSA_get_ex_data(rsa, 0)) == NULL) + return (0); + + /* + * Send a synchronous imsg because we cannot defer the RSA + * operation in OpenSSL's engine layer. + */ + m_create(p_ca, cmd, 0, 0, -1); + reqid++; + m_add_id(p_ca, reqid); + m_add_string(p_ca, pkiname); + m_add_data(p_ca, (const void *)from, (size_t)flen); + m_add_size(p_ca, (size_t)RSA_size(rsa)); + m_add_size(p_ca, (size_t)padding); + m_flush(p_ca); + + ibuf = &p_ca->imsgbuf; + + while (!done) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatalx("imsg_read"); + if (n == 0) + fatalx("pipe closed"); + + while (!done) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatalx("imsg_get error"); + if (n == 0) + break; + + log_imsg(PROC_PONY, PROC_CA, &imsg); + + switch (imsg.hdr.type) { + case IMSG_CA_RSA_PRIVENC: + case IMSG_CA_RSA_PRIVDEC: + break; + default: + /* Another imsg is queued up in the buffer */ + pony_imsg(p_ca, &imsg); + imsg_free(&imsg); + continue; + } + + m_msg(&m, &imsg); + m_get_id(&m, &id); + if (id != reqid) + fatalx("invalid response id"); + m_get_int(&m, &ret); + if (ret > 0) + m_get_data(&m, &toptr, &tlen); + m_end(&m); + + if (ret > 0) + memcpy(to, toptr, tlen); + done = 1; + + imsg_free(&imsg); + } + } + mproc_event_add(p_ca); + + return (ret); +} + +static int +rsae_pub_enc(int flen,const unsigned char *from, unsigned char *to, RSA *rsa, + int padding) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + return (RSA_meth_get_pub_enc(rsa_default)(flen, from, to, rsa, padding)); +} + +static int +rsae_pub_dec(int flen,const unsigned char *from, unsigned char *to, RSA *rsa, + int padding) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + return (RSA_meth_get_pub_dec(rsa_default)(flen, from, to, rsa, padding)); +} + +static int +rsae_priv_enc(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, + int padding) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + if (RSA_get_ex_data(rsa, 0) != NULL) + return (rsae_send_imsg(flen, from, to, rsa, padding, + IMSG_CA_RSA_PRIVENC)); + return (RSA_meth_get_priv_enc(rsa_default)(flen, from, to, rsa, padding)); +} + +static int +rsae_priv_dec(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, + int padding) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + if (RSA_get_ex_data(rsa, 0) != NULL) + return (rsae_send_imsg(flen, from, to, rsa, padding, + IMSG_CA_RSA_PRIVDEC)); + + return (RSA_meth_get_priv_dec(rsa_default)(flen, from, to, rsa, padding)); +} + +static int +rsae_mod_exp(BIGNUM *r0, const BIGNUM *I, RSA *rsa, BN_CTX *ctx) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + return (RSA_meth_get_mod_exp(rsa_default)(r0, I, rsa, ctx)); +} + +static int +rsae_bn_mod_exp(BIGNUM *r, const BIGNUM *a, const BIGNUM *p, + const BIGNUM *m, BN_CTX *ctx, BN_MONT_CTX *m_ctx) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + return (RSA_meth_get_bn_mod_exp(rsa_default)(r, a, p, m, ctx, m_ctx)); +} + +static int +rsae_init(RSA *rsa) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + if (RSA_meth_get_init(rsa_default) == NULL) + return (1); + return (RSA_meth_get_init(rsa_default)(rsa)); +} + +static int +rsae_finish(RSA *rsa) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + if (RSA_meth_get_finish(rsa_default) == NULL) + return (1); + return (RSA_meth_get_finish(rsa_default)(rsa)); +} + +static int +rsae_keygen(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + return (RSA_meth_get_keygen(rsa_default)(rsa, bits, e, cb)); +} + + +#if defined(SUPPORT_ECDSA) +/* + * ECDSA privsep engine (called from unprivileged processes) + */ + +const ECDSA_METHOD *ecdsa_default = NULL; + +static ECDSA_METHOD *ecdsae_method = NULL; + +ECDSA_METHOD * +ECDSA_METHOD_new_temporary(const char *name, int); + +ECDSA_METHOD * +ECDSA_METHOD_new_temporary(const char *name, int flags) +{ + ECDSA_METHOD *ecdsa; + + if ((ecdsa = calloc(1, sizeof (*ecdsa))) == NULL) + return NULL; + + if ((ecdsa->name = strdup(name)) == NULL) { + free(ecdsa); + return NULL; + } + + ecdsa->flags = flags; + return ecdsa; +} + +static ECDSA_SIG * +ecdsae_send_enc_imsg(const unsigned char *dgst, int dgst_len, + const BIGNUM *inv, const BIGNUM *rp, EC_KEY *eckey) +{ + int ret = 0; + struct imsgbuf *ibuf; + struct imsg imsg; + int n, done = 0; + const void *toptr; + char *pkiname; + size_t tlen; + struct msg m; + uint64_t id; + ECDSA_SIG *sig = NULL; + + if ((pkiname = ECDSA_get_ex_data(eckey, 0)) == NULL) + return (0); + + /* + * Send a synchronous imsg because we cannot defer the ECDSA + * operation in OpenSSL's engine layer. + */ + m_create(p_ca, IMSG_CA_ECDSA_SIGN, 0, 0, -1); + reqid++; + m_add_id(p_ca, reqid); + m_add_string(p_ca, pkiname); + m_add_data(p_ca, (const void *)dgst, (size_t)dgst_len); + m_flush(p_ca); + + ibuf = &p_ca->imsgbuf; + + while (!done) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + fatalx("imsg_read"); + if (n == 0) + fatalx("pipe closed"); + while (!done) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatalx("imsg_get error"); + if (n == 0) + break; + + log_imsg(PROC_PONY, PROC_CA, &imsg); + + switch (imsg.hdr.type) { + case IMSG_CA_ECDSA_SIGN: + break; + default: + /* Another imsg is queued up in the buffer */ + pony_imsg(p_ca, &imsg); + imsg_free(&imsg); + continue; + } + + m_msg(&m, &imsg); + m_get_id(&m, &id); + if (id != reqid) + fatalx("invalid response id"); + m_get_int(&m, &ret); + if (ret > 0) + m_get_data(&m, &toptr, &tlen); + m_end(&m); + done = 1; + + if (ret > 0) + d2i_ECDSA_SIG(&sig, (const unsigned char **)&toptr, tlen); + imsg_free(&imsg); + } + } + mproc_event_add(p_ca); + + return (sig); +} + +ECDSA_SIG * +ecdsae_do_sign(const unsigned char *dgst, int dgst_len, + const BIGNUM *inv, const BIGNUM *rp, EC_KEY *eckey) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + if (ECDSA_get_ex_data(eckey, 0) != NULL) + return (ecdsae_send_enc_imsg(dgst, dgst_len, inv, rp, eckey)); + return (ecdsa_default->ecdsa_do_sign(dgst, dgst_len, inv, rp, eckey)); +} + +int +ecdsae_sign_setup(EC_KEY *eckey, BN_CTX *ctx, BIGNUM **kinv, + BIGNUM **r) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + return (ecdsa_default->ecdsa_sign_setup(eckey, ctx, kinv, r)); +} + +int +ecdsae_do_verify(const unsigned char *dgst, int dgst_len, + const ECDSA_SIG *sig, EC_KEY *eckey) +{ + log_debug("debug: %s: %s", proc_name(smtpd_process), __func__); + return (ecdsa_default->ecdsa_do_verify(dgst, dgst_len, sig, eckey)); +} +#endif + +static void +rsa_engine_init(void) +{ + ENGINE *e; + const char *errstr, *name; + + if ((rsae_method = RSA_meth_new("RSA privsep engine", 0)) == NULL) { + errstr = "RSA_meth_new"; + goto fail; + } + + RSA_meth_set_pub_enc(rsae_method, rsae_pub_enc); + RSA_meth_set_pub_dec(rsae_method, rsae_pub_dec); + RSA_meth_set_priv_enc(rsae_method, rsae_priv_enc); + RSA_meth_set_priv_dec(rsae_method, rsae_priv_dec); + RSA_meth_set_mod_exp(rsae_method, rsae_mod_exp); + RSA_meth_set_bn_mod_exp(rsae_method, rsae_bn_mod_exp); + RSA_meth_set_init(rsae_method, rsae_init); + RSA_meth_set_finish(rsae_method, rsae_finish); + RSA_meth_set_keygen(rsae_method, rsae_keygen); + + if ((e = ENGINE_get_default_RSA()) == NULL) { + if ((e = ENGINE_new()) == NULL) { + errstr = "ENGINE_new"; + goto fail; + } + if (!ENGINE_set_name(e, RSA_meth_get0_name(rsae_method))) { + errstr = "ENGINE_set_name"; + goto fail; + } + if ((rsa_default = RSA_get_default_method()) == NULL) { + errstr = "RSA_get_default_method"; + goto fail; + } + } else if ((rsa_default = ENGINE_get_RSA(e)) == NULL) { + errstr = "ENGINE_get_RSA"; + goto fail; + } + + if ((name = ENGINE_get_name(e)) == NULL) + name = "unknown RSA engine"; + + log_debug("debug: %s: using %s", __func__, name); + + if (RSA_meth_get_mod_exp(rsa_default) == NULL) + RSA_meth_set_mod_exp(rsae_method, NULL); + if (RSA_meth_get_bn_mod_exp(rsa_default) == NULL) + RSA_meth_set_bn_mod_exp(rsae_method, NULL); + if (RSA_meth_get_keygen(rsa_default) == NULL) + RSA_meth_set_keygen(rsae_method, NULL); + RSA_meth_set_flags(rsae_method, + RSA_meth_get_flags(rsa_default) | RSA_METHOD_FLAG_NO_CHECK); + RSA_meth_set0_app_data(rsae_method, + RSA_meth_get0_app_data(rsa_default)); + + if (!ENGINE_set_RSA(e, rsae_method)) { + errstr = "ENGINE_set_RSA"; + goto fail; + } + if (!ENGINE_set_default_RSA(e)) { + errstr = "ENGINE_set_default_RSA"; + goto fail; + } + + return; + + fail: + ssl_error(errstr); + fatalx("%s", errstr); +} + +#if defined(SUPPORT_ECDSA) +static void +ecdsa_engine_init(void) +{ + ENGINE *e; + const char *errstr, *name; + + if ((ecdsae_method = ECDSA_METHOD_new_temporary("ECDSA privsep engine", 0)) == NULL) { + errstr = "ECDSA_METHOD_new_temporary"; + goto fail; + } + + ecdsae_method->ecdsa_do_sign = ecdsae_do_sign; + ecdsae_method->ecdsa_sign_setup = ecdsae_sign_setup; + ecdsae_method->ecdsa_do_verify = ecdsae_do_verify; + + if ((e = ENGINE_get_default_ECDSA()) == NULL) { + if ((e = ENGINE_new()) == NULL) { + errstr = "ENGINE_new"; + goto fail; + } + if (!ENGINE_set_name(e, ecdsae_method->name)) { + errstr = "ENGINE_set_name"; + goto fail; + } + if ((ecdsa_default = ECDSA_get_default_method()) == NULL) { + errstr = "ECDSA_get_default_method"; + goto fail; + } + } else if ((ecdsa_default = ENGINE_get_ECDSA(e)) == NULL) { + errstr = "ENGINE_get_ECDSA"; + goto fail; + } + + if ((name = ENGINE_get_name(e)) == NULL) + name = "unknown ECDSA engine"; + + log_debug("debug: %s: using %s", __func__, name); + + if (!ENGINE_set_ECDSA(e, ecdsae_method)) { + errstr = "ENGINE_set_ECDSA"; + goto fail; + } + if (!ENGINE_set_default_ECDSA(e)) { + errstr = "ENGINE_set_default_ECDSA"; + goto fail; + } + + return; + + fail: + ssl_error(errstr); + fatalx("%s", errstr); +} +#endif + +void +ca_engine_init(void) +{ + rsa_engine_init(); +#if defined(SUPPORT_ECDSA) + ecdsa_engine_init(); +#endif +} diff --git a/smtpd/cert.c b/smtpd/cert.c new file mode 100644 index 00000000..79b1df91 --- /dev/null +++ b/smtpd/cert.c @@ -0,0 +1,416 @@ +/* $OpenBSD: cert.c,v 1.2 2018/12/11 07:25:57 eric Exp $ */ + +/* + * Copyright (c) 2018 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/tree.h> +#include <sys/queue.h> +#include <netinet/in.h> + +#include <imsg.h> +#include <limits.h> +#include <openssl/err.h> +#include <openssl/ssl.h> +#include <stdio.h> +#include <string.h> + +#include "log.h" +#include "smtpd.h" +#include "ssl.h" + +#define p_cert p_lka + +struct request { + SPLAY_ENTRY(request) entry; + uint32_t id; + void (*cb_get_certificate)(void *, int, const char *, + const void *, size_t); + void (*cb_verify)(void *, int); + void *arg; +}; + +#define MAX_CERTS 16 +#define MAX_CERT_LEN (MAX_IMSGSIZE - (IMSG_HEADER_SIZE + sizeof(size_t))) + +struct session { + SPLAY_ENTRY(session) entry; + uint32_t id; + struct mproc *proc; + char *cert[MAX_CERTS]; + size_t cert_len[MAX_CERTS]; + int cert_count; +}; + +SPLAY_HEAD(cert_reqtree, request); +SPLAY_HEAD(cert_sestree, session); + +static int request_cmp(struct request *, struct request *); +static int session_cmp(struct session *, struct session *); +SPLAY_PROTOTYPE(cert_reqtree, request, entry, request_cmp); +SPLAY_PROTOTYPE(cert_sestree, session, entry, session_cmp); + +static void cert_do_verify(struct session *, const char *, int); +static int cert_X509_verify(struct session *, const char *, const char *); + +static struct cert_reqtree reqs = SPLAY_INITIALIZER(&reqs); +static struct cert_sestree sess = SPLAY_INITIALIZER(&sess); + +int +cert_init(const char *name, int fallback, void (*cb)(void *, int, + const char *, const void *, size_t), void *arg) +{ + struct request *req; + + req = calloc(1, sizeof(*req)); + if (req == NULL) { + cb(arg, CA_FAIL, NULL, NULL, 0); + return 0; + } + while (req->id == 0 || SPLAY_FIND(cert_reqtree, &reqs, req)) + req->id = arc4random(); + req->cb_get_certificate = cb; + req->arg = arg; + SPLAY_INSERT(cert_reqtree, &reqs, req); + + m_create(p_cert, IMSG_CERT_INIT, req->id, 0, -1); + m_add_string(p_cert, name); + m_add_int(p_cert, fallback); + m_close(p_cert); + + return 1; +} + +int +cert_verify(const void *ssl, const char *name, int fallback, + void (*cb)(void *, int), void *arg) +{ + struct request *req; + X509 *x; + STACK_OF(X509) *xchain; + unsigned char *cert_der[MAX_CERTS]; + int cert_len[MAX_CERTS]; + int i, cert_count, ret; + + x = SSL_get_peer_certificate(ssl); + if (x == NULL) { + cb(arg, CERT_NOCERT); + return 0; + } + + ret = 0; + memset(cert_der, 0, sizeof(cert_der)); + + req = calloc(1, sizeof(*req)); + if (req == NULL) + goto end; + while (req->id == 0 || SPLAY_FIND(cert_reqtree, &reqs, req)) + req->id = arc4random(); + req->cb_verify = cb; + req->arg = arg; + SPLAY_INSERT(cert_reqtree, &reqs, req); + + cert_count = 1; + if ((xchain = SSL_get_peer_cert_chain(ssl))) { + cert_count += sk_X509_num(xchain); + if (cert_count > MAX_CERTS) { + log_warnx("warn: certificate chain too long"); + goto end; + } + } + + for (i = 0; i < cert_count; ++i) { + if (i != 0) { + if ((x = sk_X509_value(xchain, i - 1)) == NULL) { + log_warnx("warn: failed to retrieve certificate"); + goto end; + } + } + + cert_len[i] = i2d_X509(x, &cert_der[i]); + if (i == 0) + X509_free(x); + + if (cert_len[i] < 0) { + log_warnx("warn: failed to encode certificate"); + goto end; + } + + log_debug("debug: certificate %i: len=%d", i, cert_len[i]); + if (cert_len[i] > (int)MAX_CERT_LEN) { + log_warnx("warn: certificate too long"); + goto end; + } + } + + /* Send the cert chain, one cert at a time */ + for (i = 0; i < cert_count; ++i) { + m_create(p_cert, IMSG_CERT_CERTIFICATE, req->id, 0, -1); + m_add_data(p_cert, cert_der[i], cert_len[i]); + m_close(p_cert); + } + + /* Tell lookup process that it can start verifying, we're done */ + m_create(p_cert, IMSG_CERT_VERIFY, req->id, 0, -1); + m_add_string(p_cert, name); + m_add_int(p_cert, fallback); + m_close(p_cert); + + ret = 1; + + end: + for (i = 0; i < MAX_CERTS; ++i) + free(cert_der[i]); + + if (ret == 0) { + if (req) + SPLAY_REMOVE(cert_reqtree, &reqs, req); + free(req); + cb(arg, CERT_ERROR); + } + + return ret; +} + + +void +cert_dispatch_request(struct mproc *proc, struct imsg *imsg) +{ + struct pki *pki; + struct session key, *s; + const char *name; + const void *data; + size_t datalen; + struct msg m; + uint32_t reqid; + char buf[LINE_MAX]; + int fallback; + + reqid = imsg->hdr.peerid; + m_msg(&m, imsg); + + switch (imsg->hdr.type) { + + case IMSG_CERT_INIT: + m_get_string(&m, &name); + m_get_int(&m, &fallback); + m_end(&m); + + xlowercase(buf, name, sizeof(buf)); + log_debug("debug: looking up pki \"%s\"", buf); + pki = dict_get(env->sc_pki_dict, buf); + if (pki == NULL && fallback) + pki = dict_get(env->sc_pki_dict, "*"); + + m_create(proc, IMSG_CERT_INIT, reqid, 0, -1); + if (pki) { + m_add_int(proc, CA_OK); + m_add_string(proc, pki->pki_name); + m_add_data(proc, pki->pki_cert, pki->pki_cert_len); + } else { + m_add_int(proc, CA_FAIL); + m_add_string(proc, NULL); + m_add_data(proc, NULL, 0); + } + m_close(proc); + return; + + case IMSG_CERT_CERTIFICATE: + m_get_data(&m, &data, &datalen); + m_end(&m); + + key.id = reqid; + key.proc = proc; + s = SPLAY_FIND(cert_sestree, &sess, &key); + if (s == NULL) { + s = calloc(1, sizeof(*s)); + s->proc = proc; + s->id = reqid; + SPLAY_INSERT(cert_sestree, &sess, s); + } + + if (s->cert_count == MAX_CERTS) + fatalx("%s: certificate chain too long", __func__); + + s->cert[s->cert_count] = xmemdup(data, datalen); + s->cert_len[s->cert_count] = datalen; + s->cert_count++; + return; + + case IMSG_CERT_VERIFY: + m_get_string(&m, &name); + m_get_int(&m, &fallback); + m_end(&m); + + key.id = reqid; + key.proc = proc; + s = SPLAY_FIND(cert_sestree, &sess, &key); + if (s == NULL) + fatalx("%s: no certificate", __func__); + + SPLAY_REMOVE(cert_sestree, &sess, s); + cert_do_verify(s, name, fallback); + return; + + default: + fatalx("%s: %s", __func__, imsg_to_str(imsg->hdr.type)); + } +} + +void +cert_dispatch_result(struct mproc *proc, struct imsg *imsg) +{ + struct request key, *req; + struct msg m; + const void *cert; + const char *name; + size_t cert_len; + int res; + + key.id = imsg->hdr.peerid; + req = SPLAY_FIND(cert_reqtree, &reqs, &key); + if (req == NULL) + fatalx("%s: unknown request %08x", __func__, imsg->hdr.peerid); + + m_msg(&m, imsg); + + switch (imsg->hdr.type) { + + case IMSG_CERT_INIT: + m_get_int(&m, &res); + m_get_string(&m, &name); + m_get_data(&m, &cert, &cert_len); + m_end(&m); + SPLAY_REMOVE(cert_reqtree, &reqs, req); + req->cb_get_certificate(req->arg, res, name, cert, cert_len); + free(req); + break; + + case IMSG_CERT_VERIFY: + m_get_int(&m, &res); + m_end(&m); + SPLAY_REMOVE(cert_reqtree, &reqs, req); + req->cb_verify(req->arg, res); + free(req); + break; + } +} + +static void +cert_do_verify(struct session *s, const char *name, int fallback) +{ + struct ca *ca; + const char *cafile; + int i, res; + + ca = dict_get(env->sc_ca_dict, name); + if (ca == NULL) + if (fallback) + ca = dict_get(env->sc_ca_dict, "*"); + cafile = ca ? ca->ca_cert_file : CA_FILE; + + if (ca == NULL && !fallback) + res = CERT_NOCA; + else if (!cert_X509_verify(s, cafile, NULL)) + res = CERT_INVALID; + else + res = CERT_OK; + + for (i = 0; i < s->cert_count; ++i) + free(s->cert[i]); + + m_create(s->proc, IMSG_CERT_VERIFY, s->id, 0, -1); + m_add_int(s->proc, res); + m_close(s->proc); + + free(s); +} + +static int +cert_X509_verify(struct session *s, const char *CAfile, + const char *CRLfile) +{ + X509 *x509; + X509 *x509_tmp; + STACK_OF(X509) *x509_chain; + const unsigned char *d2i; + int i, ret = 0; + const char *errstr; + + x509 = NULL; + x509_tmp = NULL; + x509_chain = NULL; + + d2i = s->cert[0]; + if (d2i_X509(&x509, &d2i, s->cert_len[0]) == NULL) { + x509 = NULL; + goto end; + } + + if (s->cert_count > 1) { + x509_chain = sk_X509_new_null(); + for (i = 1; i < s->cert_count; ++i) { + d2i = s->cert[i]; + if (d2i_X509(&x509_tmp, &d2i, s->cert_len[i]) == NULL) + goto end; + sk_X509_insert(x509_chain, x509_tmp, i); + x509_tmp = NULL; + } + } + if (!ca_X509_verify(x509, x509_chain, CAfile, NULL, &errstr)) + log_debug("debug: X509 verify: %s", errstr); + else + ret = 1; + +end: + X509_free(x509); + X509_free(x509_tmp); + if (x509_chain) + sk_X509_pop_free(x509_chain, X509_free); + + return ret; +} + +static int +request_cmp(struct request *a, struct request *b) +{ + if (a->id < b->id) + return -1; + if (a->id > b->id) + return 1; + return 0; +} + +SPLAY_GENERATE(cert_reqtree, request, entry, request_cmp); + +static int +session_cmp(struct session *a, struct session *b) +{ + if (a->id < b->id) + return -1; + if (a->id > b->id) + return 1; + if (a->proc < b->proc) + return -1; + if (a->proc > b->proc) + return 1; + return 0; +} + +SPLAY_GENERATE(cert_sestree, session, entry, session_cmp); diff --git a/smtpd/compress_backend.c b/smtpd/compress_backend.c new file mode 100644 index 00000000..1b974662 --- /dev/null +++ b/smtpd/compress_backend.c @@ -0,0 +1,72 @@ +/* $OpenBSD: compress_backend.c,v 1.9 2015/01/20 17:37:54 deraadt Exp $ */ + +/* + * Copyright (c) 2012 Charles Longeau <chl@openbsd.org> + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" + +#define BUFFER_SIZE 16364 + +extern struct compress_backend compress_gzip; + +struct compress_backend * +compress_backend_lookup(const char *name) +{ + if (!strcmp(name, "gzip")) + return &compress_gzip; + + return NULL; +} + +size_t +compress_chunk(void *ib, size_t ibsz, void *ob, size_t obsz) +{ + return (env->sc_comp->compress_chunk(ib, ibsz, ob, obsz)); +} + +size_t +uncompress_chunk(void *ib, size_t ibsz, void *ob, size_t obsz) +{ + return (env->sc_comp->uncompress_chunk(ib, ibsz, ob, obsz)); +} + +int +compress_file(FILE *ifile, FILE *ofile) +{ + return (env->sc_comp->compress_file(ifile, ofile)); +} + +int +uncompress_file(FILE *ifile, FILE *ofile) +{ + return (env->sc_comp->uncompress_file(ifile, ofile)); +} diff --git a/smtpd/compress_gzip.c b/smtpd/compress_gzip.c new file mode 100644 index 00000000..dd60aeec --- /dev/null +++ b/smtpd/compress_gzip.c @@ -0,0 +1,186 @@ +/* $OpenBSD: compress_gzip.c,v 1.10 2015/12/28 22:08:30 jung Exp $ */ + +/* + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2012 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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <err.h> +#include <fcntl.h> +#include <imsg.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +#include <zlib.h> + +#include "smtpd.h" +#include "log.h" + + +#define GZIP_BUFFER_SIZE 16384 + + +static size_t compress_gzip_chunk(void *, size_t, void *, size_t); +static size_t uncompress_gzip_chunk(void *, size_t, void *, size_t); +static int compress_gzip_file(FILE *, FILE *); +static int uncompress_gzip_file(FILE *, FILE *); + + +struct compress_backend compress_gzip = { + compress_gzip_chunk, + uncompress_gzip_chunk, + + compress_gzip_file, + uncompress_gzip_file, +}; + +static size_t +compress_gzip_chunk(void *ib, size_t ibsz, void *ob, size_t obsz) +{ + z_stream *strm; + size_t ret = 0; + + if ((strm = calloc(1, sizeof *strm)) == NULL) + return 0; + + strm->zalloc = Z_NULL; + strm->zfree = Z_NULL; + strm->opaque = Z_NULL; + if (deflateInit2(strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + (15+16), 8, Z_DEFAULT_STRATEGY) != Z_OK) + goto end; + + strm->avail_in = ibsz; + strm->next_in = (unsigned char *)ib; + strm->avail_out = obsz; + strm->next_out = (unsigned char *)ob; + if (deflate(strm, Z_FINISH) != Z_STREAM_END) + goto end; + + ret = strm->total_out; + +end: + deflateEnd(strm); + free(strm); + return ret; +} + + +static size_t +uncompress_gzip_chunk(void *ib, size_t ibsz, void *ob, size_t obsz) +{ + z_stream *strm; + size_t ret = 0; + + if ((strm = calloc(1, sizeof *strm)) == NULL) + return 0; + + strm->zalloc = Z_NULL; + strm->zfree = Z_NULL; + strm->opaque = Z_NULL; + strm->avail_in = 0; + strm->next_in = Z_NULL; + + if (inflateInit2(strm, (15+16)) != Z_OK) + goto end; + + strm->avail_in = ibsz; + strm->next_in = (unsigned char *)ib; + strm->avail_out = obsz; + strm->next_out = (unsigned char *)ob; + + if (inflate(strm, Z_FINISH) != Z_STREAM_END) + goto end; + + ret = strm->total_out; + +end: + deflateEnd(strm); + free(strm); + return ret; +} + + +static int +compress_gzip_file(FILE *in, FILE *out) +{ + gzFile gzf; + char ibuf[GZIP_BUFFER_SIZE]; + int r, w; + int ret = 0; + + if (in == NULL || out == NULL) + return (0); + + gzf = gzdopen(fileno(out), "wb"); + if (gzf == NULL) + return (0); + + while ((r = fread(ibuf, 1, GZIP_BUFFER_SIZE, in)) != 0) { + if ((w = gzwrite(gzf, ibuf, r)) != r) + goto end; + } + if (!feof(in)) + goto end; + + ret = 1; + +end: + gzclose(gzf); + return (ret); +} + + +static int +uncompress_gzip_file(FILE *in, FILE *out) +{ + gzFile gzf; + char obuf[GZIP_BUFFER_SIZE]; + int r, w; + int ret = 0; + + if (in == NULL || out == NULL) + return (0); + + gzf = gzdopen(fileno(in), "r"); + if (gzf == NULL) + return (0); + + while ((r = gzread(gzf, obuf, sizeof(obuf))) > 0) { + if ((w = fwrite(obuf, r, 1, out)) != 1) + goto end; + } + if (!gzeof(gzf)) + goto end; + + ret = 1; + +end: + gzclose(gzf); + return (ret); +} diff --git a/smtpd/config.c b/smtpd/config.c new file mode 100644 index 00000000..8fe983d6 --- /dev/null +++ b/smtpd/config.c @@ -0,0 +1,350 @@ +/* $OpenBSD: config.c,v 1.51 2019/12/18 10:00:39 gilles Exp $ */ + +/* + * 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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/resource.h> + +#include <event.h> +#include <ifaddrs.h> +#include <imsg.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <limits.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/ssl.h> + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" + +void set_local(struct smtpd *, const char *); +void set_localaddrs(struct smtpd *, struct table *); + +struct smtpd * +config_default(void) +{ + struct smtpd *conf = NULL; + struct mta_limits *limits = NULL; + struct table *t = NULL; + char hostname[HOST_NAME_MAX+1]; + + if (getmailname(hostname, sizeof hostname) == -1) + return NULL; + + if ((conf = calloc(1, sizeof(*conf))) == NULL) + return conf; + + (void)strlcpy(conf->sc_hostname, hostname, sizeof(conf->sc_hostname)); + + conf->sc_maxsize = DEFAULT_MAX_BODY_SIZE; + conf->sc_subaddressing_delim = SUBADDRESSING_DELIMITER; + conf->sc_ttl = SMTPD_QUEUE_EXPIRY; + conf->sc_srs_ttl = SMTPD_QUEUE_EXPIRY / 86400; + + conf->sc_mta_max_deferred = 100; + conf->sc_scheduler_max_inflight = 5000; + conf->sc_scheduler_max_schedule = 10; + conf->sc_scheduler_max_evp_batch_size = 256; + conf->sc_scheduler_max_msg_batch_size = 1024; + + conf->sc_session_max_rcpt = 1000; + conf->sc_session_max_mails = 100; + + conf->sc_mda_max_session = 50; + conf->sc_mda_max_user_session = 7; + conf->sc_mda_task_hiwat = 50; + conf->sc_mda_task_lowat = 30; + conf->sc_mda_task_release = 10; + + /* Report mails delayed for more than 4 hours */ + conf->sc_bounce_warn[0] = 3600 * 4; + + conf->sc_tables_dict = calloc(1, sizeof(*conf->sc_tables_dict)); + conf->sc_rules = calloc(1, sizeof(*conf->sc_rules)); + conf->sc_dispatchers = calloc(1, sizeof(*conf->sc_dispatchers)); + conf->sc_listeners = calloc(1, sizeof(*conf->sc_listeners)); + conf->sc_ca_dict = calloc(1, sizeof(*conf->sc_ca_dict)); + conf->sc_pki_dict = calloc(1, sizeof(*conf->sc_pki_dict)); + conf->sc_ssl_dict = calloc(1, sizeof(*conf->sc_ssl_dict)); + conf->sc_limits_dict = calloc(1, sizeof(*conf->sc_limits_dict)); + conf->sc_mda_wrappers = calloc(1, sizeof(*conf->sc_mda_wrappers)); + conf->sc_filter_processes_dict = calloc(1, sizeof(*conf->sc_filter_processes_dict)); + conf->sc_dispatcher_bounce = calloc(1, sizeof(*conf->sc_dispatcher_bounce)); + conf->sc_filters_dict = calloc(1, sizeof(*conf->sc_filters_dict)); + limits = calloc(1, sizeof(*limits)); + + if (conf->sc_tables_dict == NULL || + conf->sc_rules == NULL || + conf->sc_dispatchers == NULL || + conf->sc_listeners == NULL || + conf->sc_ca_dict == NULL || + conf->sc_pki_dict == NULL || + conf->sc_ssl_dict == NULL || + conf->sc_limits_dict == NULL || + conf->sc_mda_wrappers == NULL || + conf->sc_filter_processes_dict == NULL || + conf->sc_dispatcher_bounce == NULL || + conf->sc_filters_dict == NULL || + limits == NULL) + goto error; + + dict_init(conf->sc_dispatchers); + dict_init(conf->sc_mda_wrappers); + dict_init(conf->sc_ca_dict); + dict_init(conf->sc_pki_dict); + dict_init(conf->sc_ssl_dict); + dict_init(conf->sc_tables_dict); + dict_init(conf->sc_limits_dict); + dict_init(conf->sc_filter_processes_dict); + + limit_mta_set_defaults(limits); + + dict_xset(conf->sc_limits_dict, "default", limits); + + TAILQ_INIT(conf->sc_listeners); + TAILQ_INIT(conf->sc_rules); + + + /* bounce dispatcher */ + conf->sc_dispatcher_bounce->type = DISPATCHER_BOUNCE; + + /* + * declare special "localhost", "anyhost" and "localnames" tables + */ + set_local(conf, conf->sc_hostname); + + t = table_create(conf, "static", "<anydestination>", NULL); + table_add(t, "*", NULL); + + hostname[strcspn(hostname, ".")] = '\0'; + if (strcmp(conf->sc_hostname, hostname) != 0) + table_add(t, hostname, NULL); + + table_create(conf, "getpwnam", "<getpwnam>", NULL); + + return conf; + +error: + free(conf->sc_tables_dict); + free(conf->sc_rules); + free(conf->sc_dispatchers); + free(conf->sc_listeners); + free(conf->sc_ca_dict); + free(conf->sc_pki_dict); + free(conf->sc_ssl_dict); + free(conf->sc_limits_dict); + free(conf->sc_mda_wrappers); + free(conf->sc_filter_processes_dict); + free(conf->sc_dispatcher_bounce); + free(conf->sc_filters_dict); + free(limits); + free(conf); + return NULL; +} + +void +set_local(struct smtpd *conf, const char *hostname) +{ + struct table *t; + + t = table_create(conf, "static", "<localnames>", NULL); + table_add(t, "localhost", NULL); + table_add(t, hostname, NULL); + + set_localaddrs(conf, t); +} + +void +set_localaddrs(struct smtpd *conf, struct table *localnames) +{ + struct ifaddrs *ifap, *p; + struct sockaddr_storage ss; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct table *t; + char buf[NI_MAXHOST + 5]; + + t = table_create(conf, "static", "<anyhost>", NULL); + table_add(t, "local", NULL); + table_add(t, "0.0.0.0/0", NULL); + table_add(t, "::/0", NULL); + + if (getifaddrs(&ifap) == -1) + fatal("getifaddrs"); + + t = table_create(conf, "static", "<localhost>", NULL); + table_add(t, "local", NULL); + + for (p = ifap; p != NULL; p = p->ifa_next) { + if (p->ifa_addr == NULL) + continue; + switch (p->ifa_addr->sa_family) { + case AF_INET: + sain = (struct sockaddr_in *)&ss; + *sain = *(struct sockaddr_in *)p->ifa_addr; +#ifdef HAVE_STRUCT_SOCKADDR_IN_SIN_LEN + sain->sin_len = sizeof(struct sockaddr_in); +#endif + table_add(t, ss_to_text(&ss), NULL); + table_add(localnames, ss_to_text(&ss), NULL); + (void)snprintf(buf, sizeof buf, "[%s]", ss_to_text(&ss)); + table_add(localnames, buf, NULL); + break; + + case AF_INET6: + sin6 = (struct sockaddr_in6 *)&ss; + *sin6 = *(struct sockaddr_in6 *)p->ifa_addr; +#ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_LEN + sin6->sin6_len = sizeof(struct sockaddr_in6); +#endif + table_add(t, ss_to_text(&ss), NULL); + table_add(localnames, ss_to_text(&ss), NULL); + (void)snprintf(buf, sizeof buf, "[%s]", ss_to_text(&ss)); + table_add(localnames, buf, NULL); + (void)snprintf(buf, sizeof buf, "[ipv6:%s]", ss_to_text(&ss)); + table_add(localnames, buf, NULL); + break; + } + } + + freeifaddrs(ifap); +} + +void +purge_config(uint8_t what) +{ + struct dispatcher *d; + struct listener *l; + struct table *t; + struct rule *r; + struct pki *p; + const char *k; + void *iter_dict; + + if (what & PURGE_LISTENERS) { + while ((l = TAILQ_FIRST(env->sc_listeners)) != NULL) { + TAILQ_REMOVE(env->sc_listeners, l, entry); + free(l); + } + free(env->sc_listeners); + env->sc_listeners = NULL; + } + if (what & PURGE_TABLES) { + while (dict_root(env->sc_tables_dict, NULL, (void **)&t)) + table_destroy(env, t); + free(env->sc_tables_dict); + env->sc_tables_dict = NULL; + } + if (what & PURGE_RULES) { + while ((r = TAILQ_FIRST(env->sc_rules)) != NULL) { + TAILQ_REMOVE(env->sc_rules, r, r_entry); + free(r); + } + free(env->sc_rules); + env->sc_rules = NULL; + } + if (what & PURGE_DISPATCHERS) { + while (dict_poproot(env->sc_dispatchers, (void **)&d)) { + free(d); + } + free(env->sc_dispatchers); + env->sc_dispatchers = NULL; + } + if (what & PURGE_PKI) { + while (dict_poproot(env->sc_pki_dict, (void **)&p)) { + freezero(p->pki_cert, p->pki_cert_len); + freezero(p->pki_key, p->pki_key_len); + EVP_PKEY_free(p->pki_pkey); + free(p); + } + free(env->sc_pki_dict); + env->sc_pki_dict = NULL; + } else if (what & PURGE_PKI_KEYS) { + iter_dict = NULL; + while (dict_iter(env->sc_pki_dict, &iter_dict, &k, + (void **)&p)) { + freezero(p->pki_cert, p->pki_cert_len); + p->pki_cert = NULL; + freezero(p->pki_key, p->pki_key_len); + p->pki_key = NULL; + EVP_PKEY_free(p->pki_pkey); + p->pki_pkey = NULL; + } + } +} + +#ifndef CONFIG_MINIMUM + +void +config_process(enum smtp_proc_type proc) +{ + struct rlimit rl; + + smtpd_process = proc; + setproctitle("%s", proc_title(proc)); + + if (getrlimit(RLIMIT_NOFILE, &rl) == -1) + fatal("fdlimit: getrlimit"); + rl.rlim_cur = rl.rlim_max; + if (setrlimit(RLIMIT_NOFILE, &rl) == -1) + if (errno != EINVAL) + fatal("fdlimit: setrlimit"); +} + +void +config_peer(enum smtp_proc_type proc) +{ + struct mproc *p; + + if (proc == smtpd_process) + fatal("config_peers: cannot peer with oneself"); + + if (proc == PROC_CONTROL) + p = p_control; + else if (proc == PROC_LKA) + p = p_lka; + else if (proc == PROC_PARENT) + p = p_parent; + else if (proc == PROC_QUEUE) + p = p_queue; + else if (proc == PROC_SCHEDULER) + p = p_scheduler; + else if (proc == PROC_PONY) + p = p_pony; + else if (proc == PROC_CA) + p = p_ca; + else + fatalx("bad peer"); + + mproc_enable(p); +} + +#else + +void config_process(enum smtp_proc_type proc) {} +void config_peer(enum smtp_proc_type proc) {} + +#endif diff --git a/smtpd/control.c b/smtpd/control.c new file mode 100644 index 00000000..0e35bbd1 --- /dev/null +++ b/smtpd/control.c @@ -0,0 +1,817 @@ +/* $OpenBSD: control.c,v 1.123 2018/05/31 21:06:12 gilles Exp $ */ + +/* + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.org> + * 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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <sys/un.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <grp.h> /* needed for setgroups */ +#include <imsg.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +#define CONTROL_BACKLOG 5 + +struct ctl_conn { + uint32_t id; + uint8_t flags; +#define CTL_CONN_NOTIFY 0x01 + struct mproc mproc; + uid_t euid; + gid_t egid; +}; + +struct { + struct event ev; + int fd; +} control_state; + +static void control_imsg(struct mproc *, struct imsg *); +static void control_shutdown(void); +static void control_listen(void); +static void control_accept(int, short, void *); +static void control_close(struct ctl_conn *); +static void control_dispatch_ext(struct mproc *, struct imsg *); +static void control_digest_update(const char *, size_t, int); +static void control_broadcast_verbose(int, int); + +static struct stat_backend *stat_backend = NULL; +extern const char *backend_stat; + +static uint64_t connid = 0; +static struct tree ctl_conns; +static struct tree ctl_count; +static struct stat_digest digest; + +#define CONTROL_FD_RESERVE 5 +#define CONTROL_MAXCONN_PER_CLIENT 32 + +static void +control_imsg(struct mproc *p, struct imsg *imsg) +{ + struct ctl_conn *c; + struct stat_value val; + struct msg m; + const char *key; + const void *data; + size_t sz; + + if (imsg == NULL) { + if (p->proc != PROC_CLIENT) + control_shutdown(); + return; + } + + switch (imsg->hdr.type) { + case IMSG_CTL_OK: + case IMSG_CTL_FAIL: + case IMSG_CTL_LIST_MESSAGES: + case IMSG_CTL_LIST_ENVELOPES: + case IMSG_CTL_DISCOVER_EVPID: + case IMSG_CTL_DISCOVER_MSGID: + case IMSG_CTL_MTA_SHOW_HOSTS: + case IMSG_CTL_MTA_SHOW_RELAYS: + case IMSG_CTL_MTA_SHOW_ROUTES: + case IMSG_CTL_MTA_SHOW_HOSTSTATS: + case IMSG_CTL_MTA_SHOW_BLOCK: + c = tree_get(&ctl_conns, imsg->hdr.peerid); + if (c == NULL) + return; + imsg->hdr.peerid = 0; + m_forward(&c->mproc, imsg); + return; + + case IMSG_CTL_SMTP_SESSION: + c = tree_get(&ctl_conns, imsg->hdr.peerid); + if (c == NULL) + return; + m_compose(&c->mproc, IMSG_CTL_OK, 0, 0, imsg->fd, NULL, 0); + return; + + case IMSG_STAT_INCREMENT: + m_msg(&m, imsg); + m_get_string(&m, &key); + m_get_data(&m, &data, &sz); + m_end(&m); + if (sz != sizeof(val)) + fatalx("control: IMSG_STAT_INCREMENT size mismatch"); + memmove(&val, data, sz); + if (stat_backend) + stat_backend->increment(key, val.u.counter); + control_digest_update(key, val.u.counter, 1); + return; + + case IMSG_STAT_DECREMENT: + m_msg(&m, imsg); + m_get_string(&m, &key); + m_get_data(&m, &data, &sz); + m_end(&m); + if (sz != sizeof(val)) + fatalx("control: IMSG_STAT_DECREMENT size mismatch"); + memmove(&val, data, sz); + if (stat_backend) + stat_backend->decrement(key, val.u.counter); + control_digest_update(key, val.u.counter, 0); + return; + + case IMSG_STAT_SET: + m_msg(&m, imsg); + m_get_string(&m, &key); + m_get_data(&m, &data, &sz); + m_end(&m); + if (sz != sizeof(val)) + fatalx("control: IMSG_STAT_SET size mismatch"); + memmove(&val, data, sz); + if (stat_backend) + stat_backend->set(key, &val); + return; + } + + errx(1, "control_imsg: unexpected %s imsg", + imsg_to_str(imsg->hdr.type)); +} + +int +control_create_socket(void) +{ + struct sockaddr_un s_un; + int fd; + mode_t old_umask; + + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + fatal("control: socket"); + + memset(&s_un, 0, sizeof(s_un)); + s_un.sun_family = AF_UNIX; + if (strlcpy(s_un.sun_path, SMTPD_SOCKET, + sizeof(s_un.sun_path)) >= sizeof(s_un.sun_path)) + fatal("control: socket name too long"); + + if (connect(fd, (struct sockaddr *)&s_un, sizeof(s_un)) == 0) + fatalx("control socket already listening"); + + 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 *)&s_un, sizeof(s_un)) == -1) { + (void)umask(old_umask); + fatal("control: bind"); + } + (void)umask(old_umask); + + if (chmod(SMTPD_SOCKET, + S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) == -1) { + (void)unlink(SMTPD_SOCKET); + fatal("control: chmod"); + } + + io_set_nonblocking(fd); + control_state.fd = fd; + + return fd; +} + +int +control(void) +{ + struct passwd *pw; + + purge_config(PURGE_EVERYTHING); + + if ((pw = getpwnam(SMTPD_USER)) == NULL) + fatalx("unknown user " SMTPD_USER); + + stat_backend = env->sc_stat; + stat_backend->init(); + + if (chroot(PATH_CHROOT) == -1) + fatal("control: chroot"); + if (chdir("/") == -1) + fatal("control: chdir(\"/\")"); + + config_process(PROC_CONTROL); + + 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"); + + imsg_callback = control_imsg; + event_init(); + + signal(SIGINT, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + tree_init(&ctl_conns); + tree_init(&ctl_count); + + memset(&digest, 0, sizeof digest); + digest.startup = time(NULL); + + config_peer(PROC_SCHEDULER); + config_peer(PROC_QUEUE); + config_peer(PROC_PARENT); + config_peer(PROC_LKA); + config_peer(PROC_PONY); + config_peer(PROC_CA); + + control_listen(); + +#if HAVE_PLEDGE + if (pledge("stdio unix recvfd sendfd", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + fatalx("exited event loop"); + + return (0); +} + +static void +control_shutdown(void) +{ + log_debug("debug: control agent exiting"); + _exit(0); +} + +static void +control_listen(void) +{ + if (listen(control_state.fd, CONTROL_BACKLOG) == -1) + fatal("control_listen"); + + event_set(&control_state.ev, control_state.fd, EV_READ|EV_PERSIST, + control_accept, NULL); + event_add(&control_state.ev, NULL); +} + +/* ARGSUSED */ +static void +control_accept(int listenfd, short event, void *arg) +{ + int connfd; + socklen_t len; + struct sockaddr_un s_un; + struct ctl_conn *c; + size_t *count; + uid_t euid; + gid_t egid; + +#if defined(HAVE_GETDTABLESIZE) && defined(HAVE_GETDTABLECOUNT) + if (getdtablesize() - getdtablecount() < CONTROL_FD_RESERVE) + goto pause; +#else + if (available_fds(CONTROL_FD_RESERVE)) + goto pause; +#endif + + len = sizeof(s_un); + if ((connfd = accept(listenfd, (struct sockaddr *)&s_un, &len)) == -1) { + if (errno == ENFILE || errno == EMFILE) + goto pause; + if (errno == EINTR || errno == EWOULDBLOCK || + errno == ECONNABORTED) + return; + fatal("control_accept: accept"); + } + + io_set_nonblocking(connfd); + + if (getpeereid(connfd, &euid, &egid) == -1) + fatal("getpeereid"); + + count = tree_get(&ctl_count, euid); + if (count == NULL) { + count = xcalloc(1, sizeof *count); + tree_xset(&ctl_count, euid, count); + } + + if (*count == CONTROL_MAXCONN_PER_CLIENT) { + close(connfd); + log_warnx("warn: too many connections to control socket " + "from user with uid %lu", (unsigned long int)euid); + return; + } + (*count)++; + + do { + ++connid; + } while (tree_get(&ctl_conns, connid)); + + c = xcalloc(1, sizeof(*c)); + c->euid = euid; + c->egid = egid; + c->id = connid; + c->mproc.proc = PROC_CLIENT; + c->mproc.handler = control_dispatch_ext; + c->mproc.data = c; + if ((c->mproc.name = strdup(proc_title(c->mproc.proc))) == NULL) + fatal("strdup"); + mproc_init(&c->mproc, connfd); + mproc_enable(&c->mproc); + tree_xset(&ctl_conns, c->id, c); + + stat_backend->increment("control.session", 1); + return; + +pause: + log_warnx("warn: ctl client limit hit, disabling new connections"); + event_del(&control_state.ev); +} + +static void +control_close(struct ctl_conn *c) +{ + size_t *count; + + count = tree_xget(&ctl_count, c->euid); + (*count)--; + if (*count == 0) { + tree_xpop(&ctl_count, c->euid); + free(count); + } + tree_xpop(&ctl_conns, c->id); + mproc_clear(&c->mproc); + free(c); + + stat_backend->decrement("control.session", 1); + +#if defined(HAVE_GETDTABLESIZE) && defined(HAVE_GETDTABLECOUNT) + if (getdtablesize() - getdtablecount() < CONTROL_FD_RESERVE) + return; +#else + if (available_fds(CONTROL_FD_RESERVE)) + return; +#endif + + if (!event_pending(&control_state.ev, EV_READ, NULL)) { + log_warnx("warn: re-enabling ctl connections"); + event_add(&control_state.ev, NULL); + } +} + +static void +control_digest_update(const char *key, size_t value, int incr) +{ + size_t *p; + + p = NULL; + + if (!strcmp(key, "smtp.session")) { + if (incr) + p = &digest.clt_connect; + else + digest.clt_disconnect += value; + } + else if (!strcmp(key, "scheduler.envelope")) { + if (incr) + p = &digest.evp_enqueued; + else + digest.evp_dequeued += value; + } + else if (!strcmp(key, "scheduler.envelope.expired")) + p = &digest.evp_expired; + else if (!strcmp(key, "scheduler.envelope.removed")) + p = &digest.evp_removed; + else if (!strcmp(key, "scheduler.delivery.ok")) + p = &digest.dlv_ok; + else if (!strcmp(key, "scheduler.delivery.permfail")) + p = &digest.dlv_permfail; + else if (!strcmp(key, "scheduler.delivery.tempfail")) + p = &digest.dlv_tempfail; + else if (!strcmp(key, "scheduler.delivery.loop")) + p = &digest.dlv_loop; + + else if (!strcmp(key, "queue.bounce")) + p = &digest.evp_bounce; + + if (p) { + if (incr) + *p = *p + value; + else + *p = *p - value; + } +} + +/* ARGSUSED */ +static void +control_dispatch_ext(struct mproc *p, struct imsg *imsg) +{ + struct sockaddr_storage ss; + struct ctl_conn *c; + int v; + struct stat_kv *kvp; + char *key; + struct stat_value val; + size_t len; + uint64_t evpid; + uint32_t msgid; + + c = p->data; + + if (imsg == NULL) { + control_close(c); + return; + } + + if (imsg->hdr.peerid != IMSG_VERSION) { + m_compose(p, IMSG_CTL_FAIL, IMSG_VERSION, 0, -1, NULL, 0); + return; + } + + switch (imsg->hdr.type) { + case IMSG_CTL_SMTP_SESSION: + if (env->sc_flags & SMTPD_SMTP_PAUSED) { + m_compose(p, IMSG_CTL_FAIL, 0, 0, -1, NULL, 0); + return; + } + m_compose(p_pony, IMSG_CTL_SMTP_SESSION, c->id, 0, -1, + &c->euid, sizeof(c->euid)); + return; + + case IMSG_CTL_GET_DIGEST: + if (c->euid) + goto badcred; + digest.timestamp = time(NULL); + m_compose(p, IMSG_CTL_GET_DIGEST, 0, 0, -1, &digest, sizeof digest); + return; + + case IMSG_CTL_GET_STATS: + if (c->euid) + goto badcred; + kvp = imsg->data; + if (!stat_backend->iter(&kvp->iter, &key, &val)) + kvp->iter = NULL; + else { + (void)strlcpy(kvp->key, key, sizeof kvp->key); + kvp->val = val; + } + m_compose(p, IMSG_CTL_GET_STATS, 0, 0, -1, kvp, sizeof *kvp); + return; + + case IMSG_CTL_VERBOSE: + if (c->euid) + goto badcred; + + if (imsg->hdr.len - IMSG_HEADER_SIZE != sizeof(v)) + goto badcred; + + memcpy(&v, imsg->data, sizeof(v)); + log_trace_verbose(v); + + control_broadcast_verbose(IMSG_CTL_VERBOSE, v); + + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_TRACE_ENABLE: + if (c->euid) + goto badcred; + + if (imsg->hdr.len - IMSG_HEADER_SIZE != sizeof(v)) + goto badcred; + + memcpy(&v, imsg->data, sizeof(v)); + tracing |= v; + log_trace_verbose(tracing); + + control_broadcast_verbose(IMSG_CTL_VERBOSE, tracing); + + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_TRACE_DISABLE: + if (c->euid) + goto badcred; + + if (imsg->hdr.len - IMSG_HEADER_SIZE != sizeof(v)) + goto badcred; + + memcpy(&v, imsg->data, sizeof(v)); + tracing &= ~v; + log_trace_verbose(tracing); + + control_broadcast_verbose(IMSG_CTL_VERBOSE, tracing); + + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_PROFILE_ENABLE: + if (c->euid) + goto badcred; + + if (imsg->hdr.len - IMSG_HEADER_SIZE != sizeof(v)) + goto badcred; + + memcpy(&v, imsg->data, sizeof(v)); + profiling |= v; + + control_broadcast_verbose(IMSG_CTL_PROFILE, profiling); + + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_PROFILE_DISABLE: + if (c->euid) + goto badcred; + + if (imsg->hdr.len - IMSG_HEADER_SIZE != sizeof(v)) + goto badcred; + + memcpy(&v, imsg->data, sizeof(v)); + profiling &= ~v; + + control_broadcast_verbose(IMSG_CTL_PROFILE, profiling); + + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_PAUSE_EVP: + if (c->euid) + goto badcred; + + imsg->hdr.peerid = c->id; + m_forward(p_scheduler, imsg); + return; + + case IMSG_CTL_PAUSE_MDA: + if (c->euid) + goto badcred; + + if (env->sc_flags & SMTPD_MDA_PAUSED) { + m_compose(p, IMSG_CTL_FAIL, 0, 0, -1, NULL, 0); + return; + } + log_info("info: mda paused"); + env->sc_flags |= SMTPD_MDA_PAUSED; + m_compose(p_queue, IMSG_CTL_PAUSE_MDA, 0, 0, -1, NULL, 0); + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_PAUSE_MTA: + if (c->euid) + goto badcred; + + if (env->sc_flags & SMTPD_MTA_PAUSED) { + m_compose(p, IMSG_CTL_FAIL, 0, 0, -1, NULL, 0); + return; + } + log_info("info: mta paused"); + env->sc_flags |= SMTPD_MTA_PAUSED; + m_compose(p_queue, IMSG_CTL_PAUSE_MTA, 0, 0, -1, NULL, 0); + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_PAUSE_SMTP: + if (c->euid) + goto badcred; + + if (env->sc_flags & SMTPD_SMTP_PAUSED) { + m_compose(p, IMSG_CTL_FAIL, 0, 0, -1, NULL, 0); + return; + } + log_info("info: smtp paused"); + env->sc_flags |= SMTPD_SMTP_PAUSED; + m_compose(p_pony, IMSG_CTL_PAUSE_SMTP, 0, 0, -1, NULL, 0); + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_RESUME_EVP: + if (c->euid) + goto badcred; + + imsg->hdr.peerid = c->id; + m_forward(p_scheduler, imsg); + return; + + case IMSG_CTL_RESUME_MDA: + if (c->euid) + goto badcred; + + if (!(env->sc_flags & SMTPD_MDA_PAUSED)) { + m_compose(p, IMSG_CTL_FAIL, 0, 0, -1, NULL, 0); + return; + } + log_info("info: mda resumed"); + env->sc_flags &= ~SMTPD_MDA_PAUSED; + m_compose(p_queue, IMSG_CTL_RESUME_MDA, 0, 0, -1, NULL, 0); + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_RESUME_MTA: + if (c->euid) + goto badcred; + + if (!(env->sc_flags & SMTPD_MTA_PAUSED)) { + m_compose(p, IMSG_CTL_FAIL, 0, 0, -1, NULL, 0); + return; + } + log_info("info: mta resumed"); + env->sc_flags &= ~SMTPD_MTA_PAUSED; + m_compose(p_queue, IMSG_CTL_RESUME_MTA, 0, 0, -1, NULL, 0); + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_RESUME_SMTP: + if (c->euid) + goto badcred; + + if (!(env->sc_flags & SMTPD_SMTP_PAUSED)) { + m_compose(p, IMSG_CTL_FAIL, 0, 0, -1, NULL, 0); + return; + } + log_info("info: smtp resumed"); + env->sc_flags &= ~SMTPD_SMTP_PAUSED; + m_forward(p_pony, imsg); + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_RESUME_ROUTE: + if (c->euid) + goto badcred; + + m_forward(p_pony, imsg); + m_compose(p, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + return; + + case IMSG_CTL_LIST_MESSAGES: + if (c->euid) + goto badcred; + m_compose(p_scheduler, IMSG_CTL_LIST_MESSAGES, c->id, 0, -1, + imsg->data, imsg->hdr.len - sizeof(imsg->hdr)); + return; + + case IMSG_CTL_LIST_ENVELOPES: + if (c->euid) + goto badcred; + m_compose(p_scheduler, IMSG_CTL_LIST_ENVELOPES, c->id, 0, -1, + imsg->data, imsg->hdr.len - sizeof(imsg->hdr)); + return; + + case IMSG_CTL_MTA_SHOW_HOSTS: + case IMSG_CTL_MTA_SHOW_RELAYS: + case IMSG_CTL_MTA_SHOW_ROUTES: + case IMSG_CTL_MTA_SHOW_HOSTSTATS: + case IMSG_CTL_MTA_SHOW_BLOCK: + if (c->euid) + goto badcred; + + imsg->hdr.peerid = c->id; + m_forward(p_pony, imsg); + return; + + case IMSG_CTL_SHOW_STATUS: + if (c->euid) + goto badcred; + + m_compose(p, IMSG_CTL_SHOW_STATUS, 0, 0, -1, &env->sc_flags, + sizeof(env->sc_flags)); + return; + + case IMSG_CTL_MTA_BLOCK: + case IMSG_CTL_MTA_UNBLOCK: + if (c->euid) + goto badcred; + + if (imsg->hdr.len - IMSG_HEADER_SIZE <= sizeof(ss)) + goto invalid; + memmove(&ss, imsg->data, sizeof(ss)); + m_create(p_pony, imsg->hdr.type, c->id, 0, -1); + m_add_sockaddr(p_pony, (struct sockaddr *)&ss); + m_add_string(p_pony, (char *)imsg->data + sizeof(ss)); + m_close(p_pony); + return; + + case IMSG_CTL_SCHEDULE: + if (c->euid) + goto badcred; + + imsg->hdr.peerid = c->id; + m_forward(p_scheduler, imsg); + return; + + case IMSG_CTL_REMOVE: + if (c->euid) + goto badcred; + + imsg->hdr.peerid = c->id; + m_forward(p_scheduler, imsg); + return; + + case IMSG_CTL_UPDATE_TABLE: + if (c->euid) + goto badcred; + + /* table name too long */ + len = strlen(imsg->data); + if (len >= LINE_MAX) + goto invalid; + + imsg->hdr.peerid = c->id; + m_forward(p_lka, imsg); + return; + + case IMSG_CTL_DISCOVER_EVPID: + if (c->euid) + goto badcred; + + if (imsg->hdr.len - IMSG_HEADER_SIZE != sizeof evpid) + goto invalid; + + memmove(&evpid, imsg->data, sizeof evpid); + m_create(p_queue, imsg->hdr.type, c->id, 0, -1); + m_add_evpid(p_queue, evpid); + m_close(p_queue); + return; + + case IMSG_CTL_DISCOVER_MSGID: + if (c->euid) + goto badcred; + + if (imsg->hdr.len - IMSG_HEADER_SIZE != sizeof msgid) + goto invalid; + + memmove(&msgid, imsg->data, sizeof msgid); + m_create(p_queue, imsg->hdr.type, c->id, 0, -1); + m_add_msgid(p_queue, msgid); + m_close(p_queue); + return; + + default: + log_debug("debug: control_dispatch_ext: " + "error handling %s imsg", + imsg_to_str(imsg->hdr.type)); + return; + } +badcred: +invalid: + m_compose(p, IMSG_CTL_FAIL, 0, 0, -1, NULL, 0); +} + +static void +control_broadcast_verbose(int msg, int v) +{ + m_create(p_lka, msg, 0, 0, -1); + m_add_int(p_lka, v); + m_close(p_lka); + + m_create(p_pony, msg, 0, 0, -1); + m_add_int(p_pony, v); + m_close(p_pony); + + m_create(p_queue, msg, 0, 0, -1); + m_add_int(p_queue, v); + m_close(p_queue); + + m_create(p_ca, msg, 0, 0, -1); + m_add_int(p_ca, v); + m_close(p_ca); + + m_create(p_scheduler, msg, 0, 0, -1); + m_add_int(p_scheduler, v); + m_close(p_scheduler); + + m_create(p_parent, msg, 0, 0, -1); + m_add_int(p_parent, v); + m_close(p_parent); +} diff --git a/smtpd/crypto.c b/smtpd/crypto.c new file mode 100644 index 00000000..20a422cd --- /dev/null +++ b/smtpd/crypto.c @@ -0,0 +1,400 @@ +/* $OpenBSD: crypto.c,v 1.8 2019/06/28 13:32:50 deraadt Exp $ */ + +/* + * Copyright (c) 2013 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 "includes.h" + +#include <sys/types.h> +#include <sys/stat.h> + +#include <stdlib.h> +#include <string.h> + +#include <openssl/evp.h> + + +#define CRYPTO_BUFFER_SIZE 16384 + +#define GCM_TAG_SIZE 16 +#define IV_SIZE 12 +#define KEY_SIZE 32 + +/* bump if we ever switch from aes-256-gcm to anything else */ +#define API_VERSION 1 + + +int crypto_setup(const char *, size_t); +int crypto_encrypt_file(FILE *, FILE *); +int crypto_decrypt_file(FILE *, FILE *); +size_t crypto_encrypt_buffer(const char *, size_t, char *, size_t); +size_t crypto_decrypt_buffer(const char *, size_t, char *, size_t); + +static struct crypto_ctx { + unsigned char key[KEY_SIZE]; +} cp; + +int +crypto_setup(const char *key, size_t len) +{ + if (len != KEY_SIZE) + return 0; + + memset(&cp, 0, sizeof cp); + + /* openssl rand -hex 16 */ + memcpy(cp.key, key, sizeof cp.key); + + return 1; +} + +int +crypto_encrypt_file(FILE * in, FILE * out) +{ + EVP_CIPHER_CTX *ctx; + uint8_t ibuf[CRYPTO_BUFFER_SIZE]; + uint8_t obuf[CRYPTO_BUFFER_SIZE]; + uint8_t iv[IV_SIZE]; + uint8_t tag[GCM_TAG_SIZE]; + uint8_t version = API_VERSION; + size_t r, w; + int len; + int ret = 0; + struct stat sb; + + /* XXX - Do NOT encrypt files bigger than 64GB */ + if (fstat(fileno(in), &sb) == -1) + return 0; + if (sb.st_size >= 0x1000000000LL) + return 0; + + /* prepend version byte*/ + if ((w = fwrite(&version, 1, sizeof version, out)) != sizeof version) + return 0; + + /* generate and prepend IV */ + memset(iv, 0, sizeof iv); + arc4random_buf(iv, sizeof iv); + if ((w = fwrite(iv, 1, sizeof iv, out)) != sizeof iv) + return 0; + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + return 0; + + EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, cp.key, iv); + + /* encrypt until end of file */ + while ((r = fread(ibuf, 1, CRYPTO_BUFFER_SIZE, in)) != 0) { + if (!EVP_EncryptUpdate(ctx, obuf, &len, ibuf, r)) + goto end; + if (len && (w = fwrite(obuf, len, 1, out)) != 1) + goto end; + } + if (!feof(in)) + goto end; + + /* finalize and write last chunk if any */ + if (!EVP_EncryptFinal_ex(ctx, obuf, &len)) + goto end; + if (len && (w = fwrite(obuf, len, 1, out)) != 1) + goto end; + + /* get and append tag */ + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, sizeof tag, tag); + if ((w = fwrite(tag, sizeof tag, 1, out)) != 1) + goto end; + + fflush(out); + ret = 1; + +end: + EVP_CIPHER_CTX_free(ctx); + return ret; +} + +int +crypto_decrypt_file(FILE * in, FILE * out) +{ + EVP_CIPHER_CTX *ctx; + uint8_t ibuf[CRYPTO_BUFFER_SIZE]; + uint8_t obuf[CRYPTO_BUFFER_SIZE]; + uint8_t iv[IV_SIZE]; + uint8_t tag[GCM_TAG_SIZE]; + uint8_t version; + size_t r, w; + off_t sz; + int len; + int ret = 0; + struct stat sb; + + /* input file too small to be an encrypted file */ + if (fstat(fileno(in), &sb) == -1) + return 0; + if (sb.st_size <= (off_t) (sizeof version + sizeof tag + sizeof iv)) + return 0; + sz = sb.st_size; + + /* extract tag */ + if (fseek(in, -sizeof(tag), SEEK_END) == -1) + return 0; + if ((r = fread(tag, 1, sizeof tag, in)) != sizeof tag) + return 0; + + if (fseek(in, 0, SEEK_SET) == -1) + return 0; + + /* extract version */ + if ((r = fread(&version, 1, sizeof version, in)) != sizeof version) + return 0; + if (version != API_VERSION) + return 0; + + /* extract IV */ + memset(iv, 0, sizeof iv); + if ((r = fread(iv, 1, sizeof iv, in)) != sizeof iv) + return 0; + + /* real ciphertext length */ + sz -= sizeof version; + sz -= sizeof iv; + sz -= sizeof tag; + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + return 0; + + EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, cp.key, iv); + + /* set expected tag */ + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, sizeof tag, tag); + + /* decrypt until end of ciphertext */ + while (sz) { + if (sz > CRYPTO_BUFFER_SIZE) + r = fread(ibuf, 1, CRYPTO_BUFFER_SIZE, in); + else + r = fread(ibuf, 1, sz, in); + if (!r) + break; + if (!EVP_DecryptUpdate(ctx, obuf, &len, ibuf, r)) + goto end; + if (len && (w = fwrite(obuf, len, 1, out)) != 1) + goto end; + sz -= r; + } + if (ferror(in)) + goto end; + + /* finalize, write last chunk if any and perform authentication check */ + if (!EVP_DecryptFinal_ex(ctx, obuf, &len)) + goto end; + if (len && (w = fwrite(obuf, len, 1, out)) != 1) + goto end; + + fflush(out); + ret = 1; + +end: + EVP_CIPHER_CTX_free(ctx); + return ret; +} + +size_t +crypto_encrypt_buffer(const char *in, size_t inlen, char *out, size_t outlen) +{ + EVP_CIPHER_CTX *ctx; + uint8_t iv[IV_SIZE]; + uint8_t tag[GCM_TAG_SIZE]; + uint8_t version = API_VERSION; + off_t sz; + int olen; + int len = 0; + int ret = 0; + + /* output buffer does not have enough room */ + if (outlen < inlen + sizeof version + sizeof tag + sizeof iv) + return 0; + + /* input should not exceed 64GB */ + sz = inlen; + if (sz >= 0x1000000000LL) + return 0; + + /* prepend version */ + *out = version; + len++; + + /* generate IV */ + memset(iv, 0, sizeof iv); + arc4random_buf(iv, sizeof iv); + memcpy(out + len, iv, sizeof iv); + len += sizeof iv; + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + return 0; + + EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, cp.key, iv); + + /* encrypt buffer */ + if (!EVP_EncryptUpdate(ctx, out + len, &olen, in, inlen)) + goto end; + len += olen; + + /* finalize and write last chunk if any */ + if (!EVP_EncryptFinal_ex(ctx, out + len, &olen)) + goto end; + len += olen; + + /* get and append tag */ + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, sizeof tag, tag); + memcpy(out + len, tag, sizeof tag); + ret = len + sizeof tag; + +end: + EVP_CIPHER_CTX_free(ctx); + return ret; +} + +size_t +crypto_decrypt_buffer(const char *in, size_t inlen, char *out, size_t outlen) +{ + EVP_CIPHER_CTX *ctx; + uint8_t iv[IV_SIZE]; + uint8_t tag[GCM_TAG_SIZE]; + int olen; + int len = 0; + int ret = 0; + + /* out does not have enough room */ + if (outlen < inlen - sizeof tag + sizeof iv) + return 0; + + /* extract tag */ + memcpy(tag, in + inlen - sizeof tag, sizeof tag); + inlen -= sizeof tag; + + /* check version */ + if (*in != API_VERSION) + return 0; + in++; + inlen--; + + /* extract IV */ + memset(iv, 0, sizeof iv); + memcpy(iv, in, sizeof iv); + inlen -= sizeof iv; + in += sizeof iv; + + ctx = EVP_CIPHER_CTX_new(); + if (ctx == NULL) + return 0; + + EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, cp.key, iv); + + /* set expected tag */ + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, sizeof tag, tag); + + /* decrypt buffer */ + if (!EVP_DecryptUpdate(ctx, out, &olen, in, inlen)) + goto end; + len += olen; + + /* finalize, write last chunk if any and perform authentication check */ + if (!EVP_DecryptFinal_ex(ctx, out + len, &olen)) + goto end; + ret = len + olen; + +end: + EVP_CIPHER_CTX_free(ctx); + return ret; +} + +#if 0 +int +main(int argc, char *argv[]) +{ + if (argc != 3) { + printf("usage: crypto <key> <buffer>\n"); + return 1; + } + + if (!crypto_setup(argv[1], strlen(argv[1]))) { + printf("crypto_setup failed\n"); + return 1; + } + + { + char encbuffer[4096]; + size_t enclen; + char decbuffer[4096]; + size_t declen; + + printf("encrypt/decrypt buffer: "); + enclen = crypto_encrypt_buffer(argv[2], strlen(argv[2]), + encbuffer, sizeof encbuffer); + + /* uncomment below to provoke integrity check failure */ + /* + * encbuffer[13] = 0x42; + * encbuffer[14] = 0x42; + * encbuffer[15] = 0x42; + * encbuffer[16] = 0x42; + */ + + declen = crypto_decrypt_buffer(encbuffer, enclen, + decbuffer, sizeof decbuffer); + if (declen != 0 && !strncmp(argv[2], decbuffer, declen)) + printf("ok\n"); + else + printf("nope\n"); + } + + { + FILE *fpin; + FILE *fpout; + printf("encrypt/decrypt file: "); + + fpin = fopen("/etc/passwd", "r"); + fpout = fopen("/tmp/passwd.enc", "w"); + if (!crypto_encrypt_file(fpin, fpout)) { + printf("encryption failed\n"); + return 1; + } + fclose(fpin); + fclose(fpout); + + /* uncomment below to provoke integrity check failure */ + /* + * fpin = fopen("/tmp/passwd.enc", "a"); + * fprintf(fpin, "borken"); + * fclose(fpin); + */ + fpin = fopen("/tmp/passwd.enc", "r"); + fpout = fopen("/tmp/passwd.dec", "w"); + if (!crypto_decrypt_file(fpin, fpout)) + printf("nope\n"); + else + printf("ok\n"); + fclose(fpin); + fclose(fpout); + } + + + return 0; +} +#endif diff --git a/smtpd/dict.c b/smtpd/dict.c new file mode 100644 index 00000000..e660f0a5 --- /dev/null +++ b/smtpd/dict.c @@ -0,0 +1,269 @@ +/* $OpenBSD: dict.c,v 1.6 2018/12/23 16:06:24 gilles Exp $ */ + +/* + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/tree.h> + +#include <err.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +#include "dict.h" + +struct dictentry { + SPLAY_ENTRY(dictentry) entry; + const char *key; + void *data; +}; + +static int dictentry_cmp(struct dictentry *, struct dictentry *); + +SPLAY_PROTOTYPE(_dict, dictentry, entry, dictentry_cmp); + +int +dict_check(struct dict *d, const char *k) +{ + struct dictentry key; + + key.key = k; + return (SPLAY_FIND(_dict, &d->dict, &key) != NULL); +} + +static inline struct dictentry * +dict_alloc(const char *k, void *data) +{ + struct dictentry *e; + size_t s = strlen(k) + 1; + void *t; + + if ((e = malloc(sizeof(*e) + s)) == NULL) + return NULL; + + e->key = t = (char*)(e) + sizeof(*e); + e->data = data; + memmove(t, k, s); + + return (e); +} + +void * +dict_set(struct dict *d, const char *k, void *data) +{ + struct dictentry *entry, key; + char *old; + + key.key = k; + if ((entry = SPLAY_FIND(_dict, &d->dict, &key)) == NULL) { + if ((entry = dict_alloc(k, data)) == NULL) + err(1, "dict_set: malloc"); + SPLAY_INSERT(_dict, &d->dict, entry); + old = NULL; + d->count += 1; + } else { + old = entry->data; + entry->data = data; + } + + return (old); +} + +void +dict_xset(struct dict *d, const char * k, void *data) +{ + struct dictentry *entry; + + if ((entry = dict_alloc(k, data)) == NULL) + err(1, "dict_xset: malloc"); + if (SPLAY_INSERT(_dict, &d->dict, entry)) + errx(1, "dict_xset(%p, %s)", d, k); + d->count += 1; +} + +void * +dict_get(struct dict *d, const char *k) +{ + struct dictentry key, *entry; + + key.key = k; + if ((entry = SPLAY_FIND(_dict, &d->dict, &key)) == NULL) + return (NULL); + + return (entry->data); +} + +void * +dict_xget(struct dict *d, const char *k) +{ + struct dictentry key, *entry; + + key.key = k; + if ((entry = SPLAY_FIND(_dict, &d->dict, &key)) == NULL) + errx(1, "dict_xget(%p, %s)", d, k); + + return (entry->data); +} + +void * +dict_pop(struct dict *d, const char *k) +{ + struct dictentry key, *entry; + void *data; + + key.key = k; + if ((entry = SPLAY_FIND(_dict, &d->dict, &key)) == NULL) + return (NULL); + + data = entry->data; + SPLAY_REMOVE(_dict, &d->dict, entry); + free(entry); + d->count -= 1; + + return (data); +} + +void * +dict_xpop(struct dict *d, const char *k) +{ + struct dictentry key, *entry; + void *data; + + key.key = k; + if ((entry = SPLAY_FIND(_dict, &d->dict, &key)) == NULL) + errx(1, "dict_xpop(%p, %s)", d, k); + + data = entry->data; + SPLAY_REMOVE(_dict, &d->dict, entry); + free(entry); + d->count -= 1; + + return (data); +} + +int +dict_poproot(struct dict *d, void **data) +{ + struct dictentry *entry; + + entry = SPLAY_ROOT(&d->dict); + if (entry == NULL) + return (0); + if (data) + *data = entry->data; + SPLAY_REMOVE(_dict, &d->dict, entry); + free(entry); + d->count -= 1; + + return (1); +} + +int +dict_root(struct dict *d, const char **k, void **data) +{ + struct dictentry *entry; + + entry = SPLAY_ROOT(&d->dict); + if (entry == NULL) + return (0); + if (k) + *k = entry->key; + if (data) + *data = entry->data; + return (1); +} + +int +dict_iter(struct dict *d, void **hdl, const char **k, void **data) +{ + struct dictentry *curr = *hdl; + + if (curr == NULL) + curr = SPLAY_MIN(_dict, &d->dict); + else + curr = SPLAY_NEXT(_dict, &d->dict, curr); + + if (curr) { + *hdl = curr; + if (k) + *k = curr->key; + if (data) + *data = curr->data; + return (1); + } + + return (0); +} + +int +dict_iterfrom(struct dict *d, void **hdl, const char *kfrom, const char **k, + void **data) +{ + struct dictentry *curr = *hdl, key; + + if (curr == NULL) { + if (kfrom == NULL) + curr = SPLAY_MIN(_dict, &d->dict); + else { + key.key = kfrom; + curr = SPLAY_FIND(_dict, &d->dict, &key); + if (curr == NULL) { + SPLAY_INSERT(_dict, &d->dict, &key); + curr = SPLAY_NEXT(_dict, &d->dict, &key); + SPLAY_REMOVE(_dict, &d->dict, &key); + } + } + } else + curr = SPLAY_NEXT(_dict, &d->dict, curr); + + if (curr) { + *hdl = curr; + if (k) + *k = curr->key; + if (data) + *data = curr->data; + return (1); + } + + return (0); +} + +void +dict_merge(struct dict *dst, struct dict *src) +{ + struct dictentry *entry; + + while (!SPLAY_EMPTY(&src->dict)) { + entry = SPLAY_ROOT(&src->dict); + SPLAY_REMOVE(_dict, &src->dict, entry); + if (SPLAY_INSERT(_dict, &dst->dict, entry)) + errx(1, "dict_merge: duplicate"); + } + dst->count += src->count; + src->count = 0; +} + +static int +dictentry_cmp(struct dictentry *a, struct dictentry *b) +{ + return strcmp(a->key, b->key); +} + +SPLAY_GENERATE(_dict, dictentry, entry, dictentry_cmp); diff --git a/smtpd/dict.h b/smtpd/dict.h new file mode 100644 index 00000000..c5d47e1a --- /dev/null +++ b/smtpd/dict.h @@ -0,0 +1,48 @@ +/* $OpenBSD: dict.h,v 1.1 2018/12/23 16:06:24 gilles Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2011 Gilles Chehade <gilles@poolp.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. + */ + +#ifndef _DICT_H_ +#define _DICT_H_ + +SPLAY_HEAD(_dict, dictentry); + +struct dict { + struct _dict dict; + size_t count; +}; + + +/* dict.c */ +#define dict_init(d) do { SPLAY_INIT(&((d)->dict)); (d)->count = 0; } while(0) +#define dict_empty(d) SPLAY_EMPTY(&((d)->dict)) +#define dict_count(d) ((d)->count) +int dict_check(struct dict *, const char *); +void *dict_set(struct dict *, const char *, void *); +void dict_xset(struct dict *, const char *, void *); +void *dict_get(struct dict *, const char *); +void *dict_xget(struct dict *, const char *); +void *dict_pop(struct dict *, const char *); +void *dict_xpop(struct dict *, const char *); +int dict_poproot(struct dict *, void **); +int dict_root(struct dict *, const char **, void **); +int dict_iter(struct dict *, void **, const char **, void **); +int dict_iterfrom(struct dict *, void **, const char *, const char **, void **); +void dict_merge(struct dict *, struct dict *); + +#endif diff --git a/smtpd/dns.c b/smtpd/dns.c new file mode 100644 index 00000000..a3107e89 --- /dev/null +++ b/smtpd/dns.c @@ -0,0 +1,379 @@ +/* $OpenBSD: dns.c,v 1.89 2019/09/18 11:26:30 eric Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2011-2014 Eric Faurot <eric@faurot.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/tree.h> +#include <sys/queue.h> +#include <sys/uio.h> + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <arpa/nameser.h> +#ifdef HAVE_ARPA_NAMESER_COMPAT_H +#include <arpa/nameser_compat.h> +#endif +#include <netdb.h> + +#include <asr.h> +#include <event.h> +#include <netdb.h> +#include <resolv.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" +#include "unpack_dns.h" + +/* On OpenBSD, this function is not needed because we don't free addrinfo */ +#if defined(NOOP_ASR_FREEADDRINFO) +#define asr_freeaddrinfo(x) do { } while(0); +#endif + +struct dns_lookup { + struct dns_session *session; + char *host; + int preference; +}; + +struct dns_session { + struct mproc *p; + uint64_t reqid; + int type; + char name[HOST_NAME_MAX+1]; + size_t mxfound; + int error; + int refcount; +}; + +static void dns_lookup_host(struct dns_session *, const char *, int); +static void dns_dispatch_host(struct asr_result *, void *); +static void dns_dispatch_mx(struct asr_result *, void *); +static void dns_dispatch_mx_preference(struct asr_result *, void *); + +static int +domainname_is_addr(const char *s, struct sockaddr *sa, socklen_t *sl) +{ + struct addrinfo hints, *res; + socklen_t sl2; + size_t l; + char buf[SMTPD_MAXDOMAINPARTSIZE]; + int i6, error; + + if (*s != '[') + return (0); + + i6 = (strncasecmp("[IPv6:", s, 6) == 0); + s += i6 ? 6 : 1; + + l = strlcpy(buf, s, sizeof(buf)); + if (l >= sizeof(buf) || l == 0 || buf[l - 1] != ']') + return (0); + + buf[l - 1] = '\0'; + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_NUMERICHOST; + hints.ai_socktype = SOCK_STREAM; + if (i6) + hints.ai_family = AF_INET6; + + res = NULL; + if ((error = getaddrinfo(buf, NULL, &hints, &res))) { + log_warnx("getaddrinfo: %s", gai_strerror(error)); + } + + if (!res) + return (0); + + if (sa && sl) { + sl2 = *sl; + if (sl2 > res->ai_addrlen) + sl2 = res->ai_addrlen; + memmove(sa, res->ai_addr, sl2); + *sl = res->ai_addrlen; + } + + freeaddrinfo(res); + return (1); +} + +void +dns_imsg(struct mproc *p, struct imsg *imsg) +{ + struct sockaddr_storage ss; + struct dns_session *s; + struct sockaddr *sa; + struct asr_query *as; + struct msg m; + const char *domain, *mx, *host; + socklen_t sl; + + s = xcalloc(1, sizeof *s); + s->type = imsg->hdr.type; + s->p = p; + + m_msg(&m, imsg); + m_get_id(&m, &s->reqid); + + switch (s->type) { + + case IMSG_MTA_DNS_HOST: + m_get_string(&m, &host); + m_end(&m); + dns_lookup_host(s, host, -1); + return; + + case IMSG_MTA_DNS_MX: + m_get_string(&m, &domain); + m_end(&m); + (void)strlcpy(s->name, domain, sizeof(s->name)); + + sa = (struct sockaddr *)&ss; + sl = sizeof(ss); + + if (domainname_is_addr(domain, sa, &sl)) { + m_create(s->p, IMSG_MTA_DNS_HOST, 0, 0, -1); + m_add_id(s->p, s->reqid); + m_add_string(s->p, sockaddr_to_text(sa)); + m_add_sockaddr(s->p, sa); + m_add_int(s->p, -1); + m_close(s->p); + + m_create(s->p, IMSG_MTA_DNS_HOST_END, 0, 0, -1); + m_add_id(s->p, s->reqid); + m_add_int(s->p, DNS_OK); + m_close(s->p); + free(s); + return; + } + + as = res_query_async(s->name, C_IN, T_MX, NULL); + if (as == NULL) { + log_warn("warn: res_query_async: %s", s->name); + m_create(s->p, IMSG_MTA_DNS_HOST_END, 0, 0, -1); + m_add_id(s->p, s->reqid); + m_add_int(s->p, DNS_EINVAL); + m_close(s->p); + free(s); + return; + } + + event_asr_run(as, dns_dispatch_mx, s); + return; + + case IMSG_MTA_DNS_MX_PREFERENCE: + m_get_string(&m, &domain); + m_get_string(&m, &mx); + m_end(&m); + (void)strlcpy(s->name, mx, sizeof(s->name)); + + as = res_query_async(domain, C_IN, T_MX, NULL); + if (as == NULL) { + m_create(s->p, IMSG_MTA_DNS_MX_PREFERENCE, 0, 0, -1); + m_add_id(s->p, s->reqid); + m_add_int(s->p, DNS_ENOTFOUND); + m_close(s->p); + free(s); + return; + } + + event_asr_run(as, dns_dispatch_mx_preference, s); + return; + + default: + log_warnx("warn: bad dns request %d", s->type); + fatal(NULL); + } +} + +static void +dns_dispatch_host(struct asr_result *ar, void *arg) +{ + struct dns_session *s; + struct dns_lookup *lookup = arg; + struct addrinfo *ai; + + s = lookup->session; + + for (ai = ar->ar_addrinfo; ai; ai = ai->ai_next) { + s->mxfound++; + m_create(s->p, IMSG_MTA_DNS_HOST, 0, 0, -1); + m_add_id(s->p, s->reqid); + m_add_string(s->p, lookup->host); + m_add_sockaddr(s->p, ai->ai_addr); + m_add_int(s->p, lookup->preference); + m_close(s->p); + } + free(lookup->host); + free(lookup); + if (ar->ar_addrinfo) + asr_freeaddrinfo(ar->ar_addrinfo); + + if (ar->ar_gai_errno) + s->error = ar->ar_gai_errno; + + if (--s->refcount) + return; + + m_create(s->p, IMSG_MTA_DNS_HOST_END, 0, 0, -1); + m_add_id(s->p, s->reqid); + m_add_int(s->p, s->mxfound ? DNS_OK : DNS_ENOTFOUND); + m_close(s->p); + free(s); +} + +static void +dns_dispatch_mx(struct asr_result *ar, void *arg) +{ + struct dns_session *s = arg; + struct unpack pack; + struct dns_header h; + struct dns_query q; + struct dns_rr rr; + char buf[512]; + size_t found; + + if (ar->ar_h_errno && ar->ar_h_errno != NO_DATA && + ar->ar_h_errno != NOTIMP) { + + m_create(s->p, IMSG_MTA_DNS_HOST_END, 0, 0, -1); + m_add_id(s->p, s->reqid); + if (ar->ar_rcode == NXDOMAIN) + m_add_int(s->p, DNS_ENONAME); + else if (ar->ar_h_errno == NO_RECOVERY) + m_add_int(s->p, DNS_EINVAL); + else + m_add_int(s->p, DNS_RETRY); + m_close(s->p); + free(s); + free(ar->ar_data); + return; + } + + unpack_init(&pack, ar->ar_data, ar->ar_datalen); + unpack_header(&pack, &h); + unpack_query(&pack, &q); + + found = 0; + for (; h.ancount; h.ancount--) { + unpack_rr(&pack, &rr); + if (rr.rr_type != T_MX) + continue; + print_dname(rr.rr.mx.exchange, buf, sizeof(buf)); + buf[strlen(buf) - 1] = '\0'; + dns_lookup_host(s, buf, rr.rr.mx.preference); + found++; + } + free(ar->ar_data); + + /* fallback to host if no MX is found. */ + if (found == 0) + dns_lookup_host(s, s->name, 0); +} + +static void +dns_dispatch_mx_preference(struct asr_result *ar, void *arg) +{ + struct dns_session *s = arg; + struct unpack pack; + struct dns_header h; + struct dns_query q; + struct dns_rr rr; + char buf[512]; + int error; + + if (ar->ar_h_errno) { + if (ar->ar_rcode == NXDOMAIN) + error = DNS_ENONAME; + else if (ar->ar_h_errno == NO_RECOVERY + || ar->ar_h_errno == NO_DATA) + error = DNS_EINVAL; + else + error = DNS_RETRY; + } + else { + error = DNS_ENOTFOUND; + unpack_init(&pack, ar->ar_data, ar->ar_datalen); + unpack_header(&pack, &h); + unpack_query(&pack, &q); + for (; h.ancount; h.ancount--) { + unpack_rr(&pack, &rr); + if (rr.rr_type != T_MX) + continue; + print_dname(rr.rr.mx.exchange, buf, sizeof(buf)); + buf[strlen(buf) - 1] = '\0'; + if (!strcasecmp(s->name, buf)) { + error = DNS_OK; + break; + } + } + } + + free(ar->ar_data); + + m_create(s->p, IMSG_MTA_DNS_MX_PREFERENCE, 0, 0, -1); + m_add_id(s->p, s->reqid); + m_add_int(s->p, error); + if (error == DNS_OK) + m_add_int(s->p, rr.rr.mx.preference); + m_close(s->p); + free(s); +} + +static void +dns_lookup_host(struct dns_session *s, const char *host, int preference) +{ + struct dns_lookup *lookup; + struct addrinfo hints; + char hostcopy[HOST_NAME_MAX+1]; + char *p; + void *as; + + lookup = xcalloc(1, sizeof *lookup); + lookup->preference = preference; + lookup->host = xstrdup(host); + lookup->session = s; + s->refcount++; + + if (*host == '[') { + if (strncasecmp("[IPv6:", host, 6) == 0) + host += 6; + else + host += 1; + (void)strlcpy(hostcopy, host, sizeof hostcopy); + p = strchr(hostcopy, ']'); + if (p) + *p = 0; + host = hostcopy; + } + + memset(&hints, 0, sizeof(hints)); + hints.ai_flags = AI_ADDRCONFIG; + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + as = getaddrinfo_async(host, NULL, &hints, NULL); + event_asr_run(as, dns_dispatch_host, lookup); +} diff --git a/smtpd/enqueue.c b/smtpd/enqueue.c new file mode 100644 index 00000000..0ef694b5 --- /dev/null +++ b/smtpd/enqueue.c @@ -0,0 +1,932 @@ +/* $OpenBSD: enqueue.c,v 1.118 2020/03/18 20:17:14 eric Exp $ */ + +/* + * Copyright (c) 2005 Henning Brauer <henning@bulabula.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/tree.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <grp.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" + +extern struct imsgbuf *ibuf; + +void usage(void); +static void build_from(char *, struct passwd *); +static int parse_message(FILE *, int, int, FILE *); +static void parse_addr(char *, size_t, int); +static void parse_addr_terminal(int); +static char *qualify_addr(char *); +static void rcpt_add(char *); +static int open_connection(void); +static int get_responses(FILE *, int); +static int send_line(FILE *, int, char *, ...); +static int enqueue_offline(int, char *[], FILE *, FILE *); +static int savedeadletter(struct passwd *, FILE *); + +extern int srv_connected(void); + +enum headerfields { + HDR_NONE, + HDR_FROM, + HDR_TO, + HDR_CC, + HDR_BCC, + HDR_SUBJECT, + HDR_DATE, + HDR_MSGID, + HDR_MIME_VERSION, + HDR_CONTENT_TYPE, + HDR_CONTENT_DISPOSITION, + HDR_CONTENT_TRANSFER_ENCODING, + HDR_USER_AGENT +}; + +struct { + char *word; + enum headerfields type; +} keywords[] = { + { "From:", HDR_FROM }, + { "To:", HDR_TO }, + { "Cc:", HDR_CC }, + { "Bcc:", HDR_BCC }, + { "Subject:", HDR_SUBJECT }, + { "Date:", HDR_DATE }, + { "Message-Id:", HDR_MSGID }, + { "MIME-Version:", HDR_MIME_VERSION }, + { "Content-Type:", HDR_CONTENT_TYPE }, + { "Content-Disposition:", HDR_CONTENT_DISPOSITION }, + { "Content-Transfer-Encoding:", HDR_CONTENT_TRANSFER_ENCODING }, + { "User-Agent:", HDR_USER_AGENT }, +}; + +#define LINESPLIT 990 +#define SMTP_LINELEN 1000 +#define TIMEOUTMSG "Timeout\n" + +#define WSP(c) (c == ' ' || c == '\t') + +int verbose = 0; +static char host[HOST_NAME_MAX+1]; +char *user = NULL; +time_t timestamp; + +struct { + int fd; + char *from; + char *fromname; + char **rcpts; + char *dsn_notify; + char *dsn_ret; + char *dsn_envid; + int rcpt_cnt; + int need_linesplit; + int saw_date; + int saw_msgid; + int saw_from; + int saw_mime_version; + int saw_content_type; + int saw_content_disposition; + int saw_content_transfer_encoding; + int saw_user_agent; + int noheader; +} msg; + +struct { + uint quote; + uint comment; + uint esc; + uint brackets; + size_t wpos; + char buf[SMTP_LINELEN]; +} pstate; + +#define QP_TEST_WRAP(fp, buf, linelen, size) do { \ + if (((linelen) += (size)) + 1 > 76) { \ + fprintf((fp), "=\r\n"); \ + if (buf[0] == '.') \ + fprintf((fp), "."); \ + (linelen) = (size); \ + } \ +} while (0) + +/* RFC 2045 section 6.7 */ +static void +qp_encoded_write(FILE *fp, char *buf) +{ + size_t linelen = 0; + + for (;buf[0] != '\0' && buf[0] != '\n'; buf++) { + /* + * Point 3: Any TAB (HT) or SPACE characters on an encoded line + * MUST thus be followed on that line by a printable character. + * + * Ergo, only encode if the next character is EOL. + */ + if (buf[0] == ' ' || buf[0] == '\t') { + if (buf[1] == '\n') { + QP_TEST_WRAP(fp, buf, linelen, 3); + fprintf(fp, "=%2X", *buf & 0xff); + } else { + QP_TEST_WRAP(fp, buf, linelen, 1); + fprintf(fp, "%c", *buf & 0xff); + } + /* + * Point 1, with exclusion of point 2, skip EBCDIC NOTE. + * Do this after whitespace check, else they would match here. + */ + } else if (!((buf[0] >= 33 && buf[0] <= 60) || + (buf[0] >= 62 && buf[0] <= 126))) { + QP_TEST_WRAP(fp, buf, linelen, 3); + fprintf(fp, "=%2X", *buf & 0xff); + /* Point 2: 33 through 60 inclusive, and 62 through 126 */ + } else { + QP_TEST_WRAP(fp, buf, linelen, 1); + fprintf(fp, "%c", *buf); + } + } + fprintf(fp, "\r\n"); +} + +int +enqueue(int argc, char *argv[], FILE *ofp) +{ + int i, ch, tflag = 0; + char *fake_from = NULL, *buf = NULL; + struct passwd *pw; + FILE *fp = NULL, *fout; + size_t sz = 0, envid_sz = 0; + ssize_t len; + char *line; + int inheaders = 1; + int save_argc; + char **save_argv; + int no_getlogin = 0; + + memset(&msg, 0, sizeof(msg)); + time(×tamp); + + save_argc = argc; + save_argv = argv; + + while ((ch = getopt(argc, argv, + "A:B:b:E::e:F:f:iJ::L:mN:o:p:qr:R:StvV:x")) != -1) { + switch (ch) { + case 'f': + fake_from = optarg; + break; + case 'F': + msg.fromname = optarg; + break; + case 'N': + msg.dsn_notify = optarg; + break; + case 'r': + fake_from = optarg; + break; + case 'R': + msg.dsn_ret = optarg; + break; + case 'S': + no_getlogin = 1; + break; + case 't': + tflag = 1; + break; + case 'v': + verbose = 1; + break; + case 'V': + msg.dsn_envid = optarg; + break; + /* all remaining: ignored, sendmail compat */ + case 'A': + case 'B': + case 'b': + case 'E': + case 'e': + case 'i': + case 'L': + case 'm': + case 'o': + case 'p': + case 'x': + break; + case 'q': + /* XXX: implement "process all now" */ + return (EX_SOFTWARE); + default: + usage(); + } + } + + argc -= optind; + argv += optind; + + if (getmailname(host, sizeof(host)) == -1) + errx(EX_NOHOST, "getmailname"); + if (no_getlogin) { + if ((pw = getpwuid(getuid())) == NULL) + user = "anonymous"; + if (pw != NULL) + user = xstrdup(pw->pw_name); + } + else { + uid_t ruid = getuid(); + + if ((user = getlogin()) != NULL && *user != '\0') { + if ((pw = getpwnam(user)) == NULL || + (ruid != 0 && ruid != pw->pw_uid)) + pw = getpwuid(ruid); + } else if ((pw = getpwuid(ruid)) == NULL) { + user = "anonymous"; + } + user = xstrdup(pw ? pw->pw_name : user); + } + + build_from(fake_from, pw); + + while (argc > 0) { + rcpt_add(argv[0]); + argv++; + argc--; + } + + if ((fp = tmpfile()) == NULL) + err(EX_UNAVAILABLE, "tmpfile"); + + msg.noheader = parse_message(stdin, fake_from == NULL, tflag, fp); + + if (msg.rcpt_cnt == 0) + errx(EX_SOFTWARE, "no recipients"); + + /* init session */ + rewind(fp); + + /* check if working in offline mode */ + /* If the server is not running, enqueue the message offline */ + + if (!srv_connected()) { +#if HAVE_PLEDGE + if (pledge("stdio", NULL) == -1) + err(1, "pledge"); +#endif + return (enqueue_offline(save_argc, save_argv, fp, ofp)); + } + + if ((msg.fd = open_connection()) == -1) + errx(EX_UNAVAILABLE, "server too busy"); + +#if HAVE_PLEDGE + if (pledge("stdio wpath cpath", NULL) == -1) + err(1, "pledge"); +#endif + + fout = fdopen(msg.fd, "a+"); + if (fout == NULL) + err(EX_UNAVAILABLE, "fdopen"); + + /* + * We need to call get_responses after every command because we don't + * support PIPELINING on the server-side yet. + */ + + /* banner */ + if (!get_responses(fout, 1)) + goto fail; + + if (!send_line(fout, verbose, "EHLO localhost\r\n")) + goto fail; + if (!get_responses(fout, 1)) + goto fail; + + if (msg.dsn_envid != NULL) + envid_sz = strlen(msg.dsn_envid); + + if (!send_line(fout, verbose, "MAIL FROM:<%s> %s%s %s%s\r\n", + msg.from, + msg.dsn_ret ? "RET=" : "", + msg.dsn_ret ? msg.dsn_ret : "", + envid_sz ? "ENVID=" : "", + envid_sz ? msg.dsn_envid : "")) + goto fail; + if (!get_responses(fout, 1)) + goto fail; + + for (i = 0; i < msg.rcpt_cnt; i++) { + if (!send_line(fout, verbose, "RCPT TO:<%s> %s%s\r\n", + msg.rcpts[i], + msg.dsn_notify ? "NOTIFY=" : "", + msg.dsn_notify ? msg.dsn_notify : "")) + goto fail; + if (!get_responses(fout, 1)) + goto fail; + } + + if (!send_line(fout, verbose, "DATA\r\n")) + goto fail; + if (!get_responses(fout, 1)) + goto fail; + + /* add From */ + if (!msg.saw_from && !send_line(fout, 0, "From: %s%s<%s>\r\n", + msg.fromname ? msg.fromname : "", msg.fromname ? " " : "", + msg.from)) + goto fail; + + /* add Date */ + if (!msg.saw_date && !send_line(fout, 0, "Date: %s\r\n", + time_to_text(timestamp))) + goto fail; + + if (msg.need_linesplit) { + /* we will always need to mime encode for long lines */ + if (!msg.saw_mime_version && !send_line(fout, 0, + "MIME-Version: 1.0\r\n")) + goto fail; + if (!msg.saw_content_type && !send_line(fout, 0, + "Content-Type: text/plain; charset=unknown-8bit\r\n")) + goto fail; + if (!msg.saw_content_disposition && !send_line(fout, 0, + "Content-Disposition: inline\r\n")) + goto fail; + if (!msg.saw_content_transfer_encoding && !send_line(fout, 0, + "Content-Transfer-Encoding: quoted-printable\r\n")) + goto fail; + } + + /* add separating newline */ + if (msg.noheader) { + if (!send_line(fout, 0, "\r\n")) + goto fail; + inheaders = 0; + } + + for (;;) { + if ((len = getline(&buf, &sz, fp)) == -1) { + if (feof(fp)) + break; + else + err(EX_UNAVAILABLE, "getline"); + } + + /* newlines have been normalized on first parsing */ + if (buf[len-1] != '\n') + errx(EX_SOFTWARE, "expect EOL"); + len--; + + if (buf[0] == '.') { + if (fputc('.', fout) == EOF) + goto fail; + } + + line = buf; + + if (inheaders) { + if (strncasecmp("from ", line, 5) == 0) + continue; + if (strncasecmp("return-path: ", line, 13) == 0) + continue; + } + + if (msg.saw_content_transfer_encoding || msg.noheader || + inheaders || !msg.need_linesplit) { + if (!send_line(fout, 0, "%.*s\r\n", (int)len, line)) + goto fail; + if (inheaders && buf[0] == '\n') + inheaders = 0; + continue; + } + + /* we don't have a content transfer encoding, use our default */ + qp_encoded_write(fout, line); + } + free(buf); + if (!send_line(fout, verbose, ".\r\n")) + goto fail; + if (!get_responses(fout, 1)) + goto fail; + + if (!send_line(fout, verbose, "QUIT\r\n")) + goto fail; + if (!get_responses(fout, 1)) + goto fail; + + fclose(fp); + fclose(fout); + + exit(EX_OK); + +fail: + if (pw) + savedeadletter(pw, fp); + exit(EX_SOFTWARE); +} + +static int +get_responses(FILE *fin, int n) +{ + char *buf = NULL; + size_t sz = 0; + ssize_t len; + int e, ret = 0; + + fflush(fin); + if ((e = ferror(fin))) { + warnx("ferror: %d", e); + goto err; + } + + while (n) { + if ((len = getline(&buf, &sz, fin)) == -1) { + if (ferror(fin)) { + warn("getline"); + goto err; + } else if (feof(fin)) + break; + else + err(EX_UNAVAILABLE, "getline"); + } + + /* account for \r\n linebreaks */ + if (len >= 2 && buf[len - 2] == '\r' && buf[len - 1] == '\n') + buf[--len - 1] = '\n'; + + if (len < 4) { + warnx("bad response"); + goto err; + } + + if (verbose) + printf("<<< %.*s", (int)len, buf); + + if (buf[3] == '-') + continue; + if (buf[0] != '2' && buf[0] != '3') { + warnx("command failed: %.*s", (int)len, buf); + goto err; + } + n--; + } + + ret = 1; +err: + free(buf); + return ret; +} + +static int +send_line(FILE *fp, int v, char *fmt, ...) +{ + int ret = 0; + va_list ap; + + va_start(ap, fmt); + if (vfprintf(fp, fmt, ap) >= 0) + ret = 1; + va_end(ap); + + if (ret && v) { + printf(">>> "); + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); + } + + return (ret); +} + +static void +build_from(char *fake_from, struct passwd *pw) +{ + char *p; + + if (fake_from == NULL) + msg.from = qualify_addr(user); + else { + if (fake_from[0] == '<') { + if (fake_from[strlen(fake_from) - 1] != '>') + errx(1, "leading < but no trailing >"); + fake_from[strlen(fake_from) - 1] = 0; + p = xstrdup(fake_from + 1); + + msg.from = qualify_addr(p); + free(p); + } else + msg.from = qualify_addr(fake_from); + } + + if (msg.fromname == NULL && fake_from == NULL && pw != NULL) { + int len, apos; + + len = strcspn(pw->pw_gecos, ","); + if ((p = memchr(pw->pw_gecos, '&', len))) { + apos = p - pw->pw_gecos; + if (asprintf(&msg.fromname, "%.*s%s%.*s", + apos, pw->pw_gecos, + pw->pw_name, + len - apos - 1, p + 1) == -1) + err(1, "asprintf"); + msg.fromname[apos] = toupper((unsigned char)msg.fromname[apos]); + } else { + if (asprintf(&msg.fromname, "%.*s", len, + pw->pw_gecos) == -1) + err(1, "asprintf"); + } + } +} + +static int +parse_message(FILE *fin, int get_from, int tflag, FILE *fout) +{ + char *buf = NULL; + size_t sz = 0; + ssize_t len; + uint i, cur = HDR_NONE; + uint header_seen = 0, header_done = 0; + + memset(&pstate, 0, sizeof(pstate)); + for (;;) { + if ((len = getline(&buf, &sz, fin)) == -1) { + if (feof(fin)) + break; + else + err(EX_UNAVAILABLE, "getline"); + } + + /* account for \r\n linebreaks */ + if (len >= 2 && buf[len - 2] == '\r' && buf[len - 1] == '\n') + buf[--len - 1] = '\n'; + + if (len == 1 && buf[0] == '\n') /* end of header */ + header_done = 1; + + if (!WSP(buf[0])) { /* whitespace -> continuation */ + if (cur == HDR_FROM) + parse_addr_terminal(1); + if (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC) + parse_addr_terminal(0); + cur = HDR_NONE; + } + + /* not really exact, if we are still in headers */ + if (len + (buf[len - 1] == '\n' ? 0 : 1) >= LINESPLIT) + msg.need_linesplit = 1; + + for (i = 0; !header_done && cur == HDR_NONE && + i < nitems(keywords); i++) + if ((size_t)len > strlen(keywords[i].word) && + !strncasecmp(buf, keywords[i].word, + strlen(keywords[i].word))) + cur = keywords[i].type; + + if (cur != HDR_NONE) + header_seen = 1; + + if (cur != HDR_BCC) { + if (!send_line(fout, 0, "%.*s", (int)len, buf)) + err(1, "write error"); + if (buf[len - 1] != '\n') { + if (fputc('\n', fout) == EOF) + err(1, "write error"); + } + } + + /* + * using From: as envelope sender is not sendmail compatible, + * but I really want it that way - maybe needs a knob + */ + if (cur == HDR_FROM) { + msg.saw_from++; + if (get_from) + parse_addr(buf, len, 1); + } + + if (tflag && (cur == HDR_TO || cur == HDR_CC || cur == HDR_BCC)) + parse_addr(buf, len, 0); + + if (cur == HDR_DATE) + msg.saw_date++; + if (cur == HDR_MSGID) + msg.saw_msgid++; + if (cur == HDR_MIME_VERSION) + msg.saw_mime_version = 1; + if (cur == HDR_CONTENT_TYPE) + msg.saw_content_type = 1; + if (cur == HDR_CONTENT_DISPOSITION) + msg.saw_content_disposition = 1; + if (cur == HDR_CONTENT_TRANSFER_ENCODING) + msg.saw_content_transfer_encoding = 1; + if (cur == HDR_USER_AGENT) + msg.saw_user_agent = 1; + } + + free(buf); + return (!header_seen); +} + +static void +parse_addr(char *s, size_t len, int is_from) +{ + size_t pos = 0; + int terminal = 0; + + /* unless this is a continuation... */ + if (!WSP(s[pos]) && s[pos] != ',' && s[pos] != ';') { + /* ... skip over everything before the ':' */ + for (; pos < len && s[pos] != ':'; pos++) + ; /* nothing */ + /* ... and check & reset parser state */ + parse_addr_terminal(is_from); + } + + /* skip over ':' ',' ';' and whitespace */ + for (; pos < len && !pstate.quote && (WSP(s[pos]) || s[pos] == ':' || + s[pos] == ',' || s[pos] == ';'); pos++) + ; /* nothing */ + + for (; pos < len; pos++) { + if (!pstate.esc && !pstate.quote && s[pos] == '(') + pstate.comment++; + if (!pstate.comment && !pstate.esc && s[pos] == '"') + pstate.quote = !pstate.quote; + + if (!pstate.comment && !pstate.quote && !pstate.esc) { + if (s[pos] == ':') { /* group */ + for (pos++; pos < len && WSP(s[pos]); pos++) + ; /* nothing */ + pstate.wpos = 0; + } + if (s[pos] == '\n' || s[pos] == '\r') + break; + if (s[pos] == ',' || s[pos] == ';') { + terminal = 1; + break; + } + if (s[pos] == '<') { + pstate.brackets = 1; + pstate.wpos = 0; + } + if (pstate.brackets && s[pos] == '>') + terminal = 1; + } + + if (!pstate.comment && !terminal && (!(!(pstate.quote || + pstate.esc) && (s[pos] == '<' || WSP(s[pos]))))) { + if (pstate.wpos >= sizeof(pstate.buf)) + errx(1, "address exceeds buffer size"); + pstate.buf[pstate.wpos++] = s[pos]; + } + + if (!pstate.quote && pstate.comment && s[pos] == ')') + pstate.comment--; + + if (!pstate.esc && !pstate.comment && s[pos] == '\\') + pstate.esc = 1; + else + pstate.esc = 0; + } + + if (terminal) + parse_addr_terminal(is_from); + + for (; pos < len && (s[pos] == '\r' || s[pos] == '\n'); pos++) + ; /* nothing */ + + if (pos < len) + parse_addr(s + pos, len - pos, is_from); +} + +static void +parse_addr_terminal(int is_from) +{ + if (pstate.comment || pstate.quote || pstate.esc) + errx(1, "syntax error in address"); + if (pstate.wpos) { + if (pstate.wpos >= sizeof(pstate.buf)) + errx(1, "address exceeds buffer size"); + pstate.buf[pstate.wpos] = '\0'; + if (is_from) + msg.from = qualify_addr(pstate.buf); + else + rcpt_add(pstate.buf); + pstate.wpos = 0; + } +} + +static char * +qualify_addr(char *in) +{ + char *out; + + if (strlen(in) > 0 && strchr(in, '@') == NULL) { + if (asprintf(&out, "%s@%s", in, host) == -1) + err(1, "qualify asprintf"); + } else + out = xstrdup(in); + + return (out); +} + +static void +rcpt_add(char *addr) +{ + void *nrcpts; + char *p; + int n; + + n = 1; + p = addr; + while ((p = strchr(p, ',')) != NULL) { + n++; + p++; + } + + if ((nrcpts = reallocarray(msg.rcpts, + msg.rcpt_cnt + n, sizeof(char *))) == NULL) + err(1, "rcpt_add realloc"); + msg.rcpts = nrcpts; + + while (n--) { + if ((p = strchr(addr, ',')) != NULL) + *p++ = '\0'; + msg.rcpts[msg.rcpt_cnt++] = qualify_addr(addr); + if (p == NULL) + break; + addr = p; + } +} + +static int +open_connection(void) +{ + struct imsg imsg; + int fd; + int n; + + imsg_compose(ibuf, IMSG_CTL_SMTP_SESSION, IMSG_VERSION, 0, -1, NULL, 0); + + while (ibuf->w.queued) + if (msgbuf_write(&ibuf->w) <= 0 && errno != EAGAIN) + err(1, "write error"); + + while (1) { + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + errx(1, "imsg_read error"); + if (n == 0) + errx(1, "pipe closed"); + + if ((n = imsg_get(ibuf, &imsg)) == -1) + errx(1, "imsg_get error"); + if (n == 0) + continue; + + switch (imsg.hdr.type) { + case IMSG_CTL_OK: + break; + case IMSG_CTL_FAIL: + errx(1, "server disallowed submission request"); + default: + errx(1, "unexpected imsg reply type"); + } + + fd = imsg.fd; + imsg_free(&imsg); + + break; + } + + return fd; +} + +static int +enqueue_offline(int argc, char *argv[], FILE *ifile, FILE *ofile) +{ + int i, ch; + + for (i = 1; i < argc; i++) { + if (strchr(argv[i], '|') != NULL) { + warnx("%s contains illegal character", argv[i]); + ftruncate(fileno(ofile), 0); + exit(EX_SOFTWARE); + } + if (fprintf(ofile, "%s%s", i == 1 ? "" : "|", argv[i]) < 0) + goto write_error; + } + + if (fputc('\n', ofile) == EOF) + goto write_error; + + while ((ch = fgetc(ifile)) != EOF) { + if (fputc(ch, ofile) == EOF) + goto write_error; + } + + if (ferror(ifile)) { + warn("read error"); + ftruncate(fileno(ofile), 0); + exit(EX_UNAVAILABLE); + } + + if (fclose(ofile) == EOF) + goto write_error; + + return (EX_TEMPFAIL); +write_error: + warn("write error"); + ftruncate(fileno(ofile), 0); + exit(EX_UNAVAILABLE); +} + +static int +savedeadletter(struct passwd *pw, FILE *in) +{ + char buffer[PATH_MAX]; + FILE *fp; + char *buf = NULL; + size_t sz = 0; + ssize_t len; + + (void)snprintf(buffer, sizeof buffer, "%s/dead.letter", pw->pw_dir); + + if (fseek(in, 0, SEEK_SET) != 0) + return 0; + + if ((fp = fopen(buffer, "w")) == NULL) + return 0; + + /* add From */ + if (!msg.saw_from) + fprintf(fp, "From: %s%s<%s>\n", + msg.fromname ? msg.fromname : "", + msg.fromname ? " " : "", + msg.from); + + /* add Date */ + if (!msg.saw_date) + fprintf(fp, "Date: %s\n", time_to_text(timestamp)); + + if (msg.need_linesplit) { + /* we will always need to mime encode for long lines */ + if (!msg.saw_mime_version) + fprintf(fp, "MIME-Version: 1.0\n"); + if (!msg.saw_content_type) + fprintf(fp, "Content-Type: text/plain; " + "charset=unknown-8bit\n"); + if (!msg.saw_content_disposition) + fprintf(fp, "Content-Disposition: inline\n"); + if (!msg.saw_content_transfer_encoding) + fprintf(fp, "Content-Transfer-Encoding: " + "quoted-printable\n"); + } + + /* add separating newline */ + if (msg.noheader) + fprintf(fp, "\n"); + + while ((len = getline(&buf, &sz, in)) != -1) { + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + fprintf(fp, "%s\n", buf); + } + + free(buf); + fprintf(fp, "\n"); + fclose(fp); + return 1; +} diff --git a/smtpd/envelope.c b/smtpd/envelope.c new file mode 100644 index 00000000..35d98b79 --- /dev/null +++ b/smtpd/envelope.c @@ -0,0 +1,786 @@ +/* $OpenBSD: envelope.c,v 1.47 2019/11/25 14:18:32 gilles Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2011-2013 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.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 <fcntl.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +static int envelope_ascii_load(struct envelope *, struct dict *); +static void envelope_ascii_dump(const struct envelope *, char **, size_t *, + const char *); + +void +envelope_set_errormsg(struct envelope *e, char *fmt, ...) +{ + int ret; + va_list ap; + + va_start(ap, fmt); + ret = vsnprintf(e->errorline, sizeof(e->errorline), fmt, ap); + va_end(ap); + + /* this should not happen */ + if (ret < 0) + err(1, "vsnprintf"); + + if ((size_t)ret >= sizeof(e->errorline)) + (void)strlcpy(e->errorline + (sizeof(e->errorline) - 4), + "...", 4); +} + +void +envelope_set_esc_class(struct envelope *e, enum enhanced_status_class class) +{ + e->esc_class = class; +} + +void +envelope_set_esc_code(struct envelope *e, enum enhanced_status_code code) +{ + e->esc_code = code; +} + +static int +envelope_buffer_to_dict(struct dict *d, const char *ibuf, size_t buflen) +{ + static char lbuf[sizeof(struct envelope)]; + size_t len; + char *buf, *field, *nextline; + + memset(lbuf, 0, sizeof lbuf); + if (strlcpy(lbuf, ibuf, sizeof lbuf) >= sizeof lbuf) + goto err; + buf = lbuf; + + while (buflen > 0) { + len = strcspn(buf, "\n"); + buf[len] = '\0'; + nextline = buf + len + 1; + buflen -= (nextline - buf); + + field = buf; + while (*buf && (isalnum((unsigned char)*buf) || *buf == '-')) + buf++; + if (!*buf) + goto err; + + /* skip whitespaces before separator */ + while (*buf && isspace((unsigned char)*buf)) + *buf++ = 0; + + /* we *want* ':' */ + if (*buf != ':') + goto err; + *buf++ = 0; + + /* skip whitespaces after separator */ + while (*buf && isspace((unsigned char)*buf)) + *buf++ = 0; + dict_set(d, field, buf); + buf = nextline; + } + + return (1); + +err: + return (0); +} + +int +envelope_load_buffer(struct envelope *ep, const char *ibuf, size_t buflen) +{ + struct dict d; + const char *val, *errstr; + long long version; + int ret = 0; + + dict_init(&d); + if (!envelope_buffer_to_dict(&d, ibuf, buflen)) { + log_debug("debug: cannot parse envelope to dict"); + goto end; + } + + val = dict_get(&d, "version"); + if (val == NULL) { + log_debug("debug: envelope version not found"); + goto end; + } + version = strtonum(val, 1, 64, &errstr); + if (errstr) { + log_debug("debug: cannot parse envelope version: %s", val); + goto end; + } + + if (version != SMTPD_ENVELOPE_VERSION) { + log_debug("debug: bad envelope version %lld", version); + goto end; + } + + memset(ep, 0, sizeof *ep); + ret = envelope_ascii_load(ep, &d); + if (ret) + ep->version = SMTPD_ENVELOPE_VERSION; +end: + while (dict_poproot(&d, NULL)) + ; + return (ret); +} + +int +envelope_dump_buffer(const struct envelope *ep, char *dest, size_t len) +{ + char *p = dest; + + envelope_ascii_dump(ep, &dest, &len, "version"); + envelope_ascii_dump(ep, &dest, &len, "dispatcher"); + envelope_ascii_dump(ep, &dest, &len, "tag"); + envelope_ascii_dump(ep, &dest, &len, "type"); + envelope_ascii_dump(ep, &dest, &len, "smtpname"); + envelope_ascii_dump(ep, &dest, &len, "helo"); + envelope_ascii_dump(ep, &dest, &len, "hostname"); + envelope_ascii_dump(ep, &dest, &len, "username"); + envelope_ascii_dump(ep, &dest, &len, "errorline"); + envelope_ascii_dump(ep, &dest, &len, "sockaddr"); + envelope_ascii_dump(ep, &dest, &len, "sender"); + envelope_ascii_dump(ep, &dest, &len, "rcpt"); + envelope_ascii_dump(ep, &dest, &len, "dest"); + envelope_ascii_dump(ep, &dest, &len, "ctime"); + envelope_ascii_dump(ep, &dest, &len, "last-try"); + envelope_ascii_dump(ep, &dest, &len, "last-bounce"); + envelope_ascii_dump(ep, &dest, &len, "ttl"); + envelope_ascii_dump(ep, &dest, &len, "retry"); + envelope_ascii_dump(ep, &dest, &len, "flags"); + envelope_ascii_dump(ep, &dest, &len, "dsn-notify"); + envelope_ascii_dump(ep, &dest, &len, "dsn-ret"); + envelope_ascii_dump(ep, &dest, &len, "dsn-envid"); + envelope_ascii_dump(ep, &dest, &len, "dsn-orcpt"); + envelope_ascii_dump(ep, &dest, &len, "esc-class"); + envelope_ascii_dump(ep, &dest, &len, "esc-code"); + + switch (ep->type) { + case D_MDA: + envelope_ascii_dump(ep, &dest, &len, "mda-exec"); + envelope_ascii_dump(ep, &dest, &len, "mda-subaddress"); + envelope_ascii_dump(ep, &dest, &len, "mda-user"); + break; + case D_MTA: + break; + case D_BOUNCE: + envelope_ascii_dump(ep, &dest, &len, "bounce-ttl"); + envelope_ascii_dump(ep, &dest, &len, "bounce-delay"); + envelope_ascii_dump(ep, &dest, &len, "bounce-type"); + break; + default: + return (0); + } + + if (dest == NULL) + return (0); + + return (dest - p); +} + +static int +ascii_load_uint8(uint8_t *dest, char *buf) +{ + const char *errstr; + + *dest = strtonum(buf, 0, 0xff, &errstr); + if (errstr) + return 0; + return 1; +} + +static int +ascii_load_uint16(uint16_t *dest, char *buf) +{ + const char *errstr; + + *dest = strtonum(buf, 0, 0xffff, &errstr); + if (errstr) + return 0; + return 1; +} + +static int +ascii_load_uint32(uint32_t *dest, char *buf) +{ + const char *errstr; + + *dest = strtonum(buf, 0, 0xffffffff, &errstr); + if (errstr) + return 0; + return 1; +} + +static int +ascii_load_time(time_t *dest, char *buf) +{ + const char *errstr; + + *dest = strtonum(buf, 0, LLONG_MAX, &errstr); + if (errstr) + return 0; + return 1; +} + +static int +ascii_load_type(enum delivery_type *dest, char *buf) +{ + if (strcasecmp(buf, "mda") == 0) + *dest = D_MDA; + else if (strcasecmp(buf, "mta") == 0) + *dest = D_MTA; + else if (strcasecmp(buf, "bounce") == 0) + *dest = D_BOUNCE; + else + return 0; + return 1; +} + +static int +ascii_load_string(char *dest, char *buf, size_t len) +{ + if (strlcpy(dest, buf, len) >= len) + return 0; + return 1; +} + +static int +ascii_load_sockaddr(struct sockaddr_storage *ss, char *buf) +{ + struct sockaddr_in6 ssin6; + struct sockaddr_in ssin; + + memset(&ssin, 0, sizeof ssin); + memset(&ssin6, 0, sizeof ssin6); + + if (!strcmp("local", buf)) { + ss->ss_family = AF_LOCAL; + } + else if (strncasecmp("IPv6:", buf, 5) == 0) { + /* XXX - remove this after 6.6 release */ + if (inet_pton(AF_INET6, buf + 5, &ssin6.sin6_addr) != 1) + return 0; + ssin6.sin6_family = AF_INET6; + memcpy(ss, &ssin6, sizeof(ssin6)); +#ifdef HAVE_STRUCT_SOCKADDR_STORAGE_SS_LEN + ss->ss_len = sizeof(struct sockaddr_in6); +#endif + } + else if (buf[0] == '[' && buf[strlen(buf)-1] == ']') { + buf[strlen(buf)-1] = '\0'; + if (inet_pton(AF_INET6, buf+1, &ssin6.sin6_addr) != 1) + return 0; + ssin6.sin6_family = AF_INET6; + memcpy(ss, &ssin6, sizeof(ssin6)); +#ifdef HAVE_STRUCT_SOCKADDR_STORAGE_SS_LEN + ss->ss_len = sizeof(struct sockaddr_in6); +#endif + } + else { + if (inet_pton(AF_INET, buf, &ssin.sin_addr) != 1) + return 0; + ssin.sin_family = AF_INET; + memcpy(ss, &ssin, sizeof(ssin)); +#ifdef HAVE_STRUCT_SOCKADDR_STORAGE_SS_LEN + ss->ss_len = sizeof(struct sockaddr_in); +#endif + } + return 1; +} + +static int +ascii_load_mailaddr(struct mailaddr *dest, char *buf) +{ + if (!text_to_mailaddr(dest, buf)) + return 0; + return 1; +} + +static int +ascii_load_flags(enum envelope_flags *dest, char *buf) +{ + char *flag; + + while ((flag = strsep(&buf, " ,|")) != NULL) { + if (strcasecmp(flag, "authenticated") == 0) + *dest |= EF_AUTHENTICATED; + else if (strcasecmp(flag, "enqueued") == 0) + ; + else if (strcasecmp(flag, "bounce") == 0) + *dest |= EF_BOUNCE; + else if (strcasecmp(flag, "internal") == 0) + *dest |= EF_INTERNAL; + else + return 0; + } + return 1; +} + +static int +ascii_load_bounce_type(enum bounce_type *dest, char *buf) +{ + if (strcasecmp(buf, "error") == 0 || strcasecmp(buf, "failed") == 0) + *dest = B_FAILED; + else if (strcasecmp(buf, "warn") == 0 || + strcasecmp(buf, "delayed") == 0) + *dest = B_DELAYED; + else if (strcasecmp(buf, "dsn") == 0 || + strcasecmp(buf, "delivered") == 0) + *dest = B_DELIVERED; + else + return 0; + return 1; +} + +static int +ascii_load_dsn_ret(enum dsn_ret *ret, char *buf) +{ + if (strcasecmp(buf, "HDRS") == 0) + *ret = DSN_RETHDRS; + else if (strcasecmp(buf, "FULL") == 0) + *ret = DSN_RETFULL; + else + return 0; + return 1; +} + +static int +ascii_load_field(const char *field, struct envelope *ep, char *buf) +{ + if (strcasecmp("dispatcher", field) == 0) + return ascii_load_string(ep->dispatcher, buf, + sizeof ep->dispatcher); + + if (strcasecmp("bounce-delay", field) == 0) + return ascii_load_time(&ep->agent.bounce.delay, buf); + + if (strcasecmp("bounce-ttl", field) == 0) + return ascii_load_time(&ep->agent.bounce.ttl, buf); + + if (strcasecmp("bounce-type", field) == 0) + return ascii_load_bounce_type(&ep->agent.bounce.type, buf); + + if (strcasecmp("ctime", field) == 0) + return ascii_load_time(&ep->creation, buf); + + if (strcasecmp("dest", field) == 0) + return ascii_load_mailaddr(&ep->dest, buf); + + if (strcasecmp("username", field) == 0) + return ascii_load_string(ep->username, buf, sizeof(ep->username)); + + if (strcasecmp("errorline", field) == 0) + return ascii_load_string(ep->errorline, buf, + sizeof ep->errorline); + + if (strcasecmp("ttl", field) == 0) + return ascii_load_time(&ep->ttl, buf); + + if (strcasecmp("flags", field) == 0) + return ascii_load_flags(&ep->flags, buf); + + if (strcasecmp("helo", field) == 0) + return ascii_load_string(ep->helo, buf, sizeof ep->helo); + + if (strcasecmp("hostname", field) == 0) + return ascii_load_string(ep->hostname, buf, + sizeof ep->hostname); + + if (strcasecmp("last-bounce", field) == 0) + return ascii_load_time(&ep->lastbounce, buf); + + if (strcasecmp("last-try", field) == 0) + return ascii_load_time(&ep->lasttry, buf); + + if (strcasecmp("retry", field) == 0) + return ascii_load_uint16(&ep->retry, buf); + + if (strcasecmp("rcpt", field) == 0) + return ascii_load_mailaddr(&ep->rcpt, buf); + + if (strcasecmp("mda-exec", field) == 0) + return ascii_load_string(ep->mda_exec, buf, sizeof(ep->mda_exec)); + + if (strcasecmp("mda-subaddress", field) == 0) + return ascii_load_string(ep->mda_subaddress, buf, sizeof(ep->mda_subaddress)); + + if (strcasecmp("mda-user", field) == 0) + return ascii_load_string(ep->mda_user, buf, sizeof(ep->mda_user)); + + if (strcasecmp("sender", field) == 0) + return ascii_load_mailaddr(&ep->sender, buf); + + if (strcasecmp("smtpname", field) == 0) + return ascii_load_string(ep->smtpname, buf, + sizeof(ep->smtpname)); + + if (strcasecmp("sockaddr", field) == 0) + return ascii_load_sockaddr(&ep->ss, buf); + + if (strcasecmp("tag", field) == 0) + return ascii_load_string(ep->tag, buf, sizeof ep->tag); + + if (strcasecmp("type", field) == 0) + return ascii_load_type(&ep->type, buf); + + if (strcasecmp("version", field) == 0) + return ascii_load_uint32(&ep->version, buf); + + if (strcasecmp("dsn-notify", field) == 0) + return ascii_load_uint8(&ep->dsn_notify, buf); + + if (strcasecmp("dsn-orcpt", field) == 0) + return ascii_load_mailaddr(&ep->dsn_orcpt, buf); + + if (strcasecmp("dsn-ret", field) == 0) + return ascii_load_dsn_ret(&ep->dsn_ret, buf); + + if (strcasecmp("dsn-envid", field) == 0) + return ascii_load_string(ep->dsn_envid, buf, + sizeof(ep->dsn_envid)); + + if (strcasecmp("esc-class", field) == 0) + return ascii_load_uint8(&ep->esc_class, buf); + + if (strcasecmp("esc-code", field) == 0) + return ascii_load_uint8(&ep->esc_code, buf); + + return (0); +} + +static int +envelope_ascii_load(struct envelope *ep, struct dict *d) +{ + const char *field; + char *value; + void *hdl; + + hdl = NULL; + while (dict_iter(d, &hdl, &field, (void **)&value)) + if (!ascii_load_field(field, ep, value)) + goto err; + + return (1); + +err: + log_warnx("envelope: invalid field \"%s\"", field); + return (0); +} + + +static int +ascii_dump_uint8(uint8_t src, char *dest, size_t len) +{ + return bsnprintf(dest, len, "%d", src); +} + +static int +ascii_dump_uint16(uint16_t src, char *dest, size_t len) +{ + return bsnprintf(dest, len, "%d", src); +} + +static int +ascii_dump_uint32(uint32_t src, char *dest, size_t len) +{ + return bsnprintf(dest, len, "%d", src); +} + +static int +ascii_dump_time(time_t src, char *dest, size_t len) +{ + return bsnprintf(dest, len, "%lld", (long long) src); +} + +static int +ascii_dump_string(const char *src, char *dest, size_t len) +{ + return bsnprintf(dest, len, "%s", src); +} + +static int +ascii_dump_type(enum delivery_type type, char *dest, size_t len) +{ + char *p = NULL; + + switch (type) { + case D_MDA: + p = "mda"; + break; + case D_MTA: + p = "mta"; + break; + case D_BOUNCE: + p = "bounce"; + break; + default: + return 0; + } + + return bsnprintf(dest, len, "%s", p); +} + +static int +ascii_dump_mailaddr(const struct mailaddr *addr, char *dest, size_t len) +{ + return bsnprintf(dest, len, "%s@%s", + addr->user, addr->domain); +} + +static int +ascii_dump_flags(enum envelope_flags flags, char *buf, size_t len) +{ + size_t cpylen = 0; + + buf[0] = '\0'; + if (flags) { + if (flags & EF_AUTHENTICATED) + cpylen = strlcat(buf, "authenticated", len); + if (flags & EF_BOUNCE) { + if (buf[0] != '\0') + (void)strlcat(buf, " ", len); + cpylen = strlcat(buf, "bounce", len); + } + if (flags & EF_INTERNAL) { + if (buf[0] != '\0') + (void)strlcat(buf, " ", len); + cpylen = strlcat(buf, "internal", len); + } + } + + return cpylen < len ? 1 : 0; +} + +static int +ascii_dump_bounce_type(enum bounce_type type, char *dest, size_t len) +{ + char *p = NULL; + + switch (type) { + case B_FAILED: + p = "failed"; + break; + case B_DELAYED: + p = "delayed"; + break; + case B_DELIVERED: + p = "delivered"; + break; + default: + return 0; + } + return bsnprintf(dest, len, "%s", p); +} + + +static int +ascii_dump_dsn_ret(enum dsn_ret flag, char *dest, size_t len) +{ + size_t cpylen = 0; + + dest[0] = '\0'; + if (flag == DSN_RETFULL) + cpylen = strlcat(dest, "FULL", len); + else if (flag == DSN_RETHDRS) + cpylen = strlcat(dest, "HDRS", len); + + return cpylen < len ? 1 : 0; +} + +static int +ascii_dump_field(const char *field, const struct envelope *ep, + char *buf, size_t len) +{ + if (strcasecmp(field, "dispatcher") == 0) + return ascii_dump_string(ep->dispatcher, buf, len); + + if (strcasecmp(field, "bounce-delay") == 0) { + if (ep->agent.bounce.type != B_DELAYED) + return (1); + return ascii_dump_time(ep->agent.bounce.delay, buf, len); + } + + if (strcasecmp(field, "bounce-ttl") == 0) { + if (ep->agent.bounce.type != B_DELAYED) + return (1); + return ascii_dump_time(ep->agent.bounce.ttl, buf, len); + } + + if (strcasecmp(field, "bounce-type") == 0) + return ascii_dump_bounce_type(ep->agent.bounce.type, buf, len); + + if (strcasecmp(field, "ctime") == 0) + return ascii_dump_time(ep->creation, buf, len); + + if (strcasecmp(field, "dest") == 0) + return ascii_dump_mailaddr(&ep->dest, buf, len); + + if (strcasecmp(field, "username") == 0) { + if (ep->username[0]) + return ascii_dump_string(ep->username, buf, len); + return 1; + } + + if (strcasecmp(field, "errorline") == 0) + return ascii_dump_string(ep->errorline, buf, len); + + if (strcasecmp(field, "ttl") == 0) + return ascii_dump_time(ep->ttl, buf, len); + + if (strcasecmp(field, "flags") == 0) + return ascii_dump_flags(ep->flags, buf, len); + + if (strcasecmp(field, "helo") == 0) + return ascii_dump_string(ep->helo, buf, len); + + if (strcasecmp(field, "hostname") == 0) + return ascii_dump_string(ep->hostname, buf, len); + + if (strcasecmp(field, "last-bounce") == 0) + return ascii_dump_time(ep->lastbounce, buf, len); + + if (strcasecmp(field, "last-try") == 0) + return ascii_dump_time(ep->lasttry, buf, len); + + if (strcasecmp(field, "retry") == 0) + return ascii_dump_uint16(ep->retry, buf, len); + + if (strcasecmp(field, "rcpt") == 0) + return ascii_dump_mailaddr(&ep->rcpt, buf, len); + + if (strcasecmp(field, "mda-exec") == 0) { + if (ep->mda_exec[0]) + return ascii_dump_string(ep->mda_exec, buf, len); + return 1; + } + + if (strcasecmp(field, "mda-subaddress") == 0) { + if (ep->mda_subaddress[0]) + return ascii_dump_string(ep->mda_subaddress, buf, len); + return 1; + } + + if (strcasecmp(field, "mda-user") == 0) { + if (ep->mda_user[0]) + return ascii_dump_string(ep->mda_user, buf, len); + return 1; + } + + if (strcasecmp(field, "sender") == 0) + return ascii_dump_mailaddr(&ep->sender, buf, len); + + if (strcasecmp(field, "smtpname") == 0) + return ascii_dump_string(ep->smtpname, buf, len); + + if (strcasecmp(field, "sockaddr") == 0) + return ascii_dump_string(ss_to_text(&ep->ss), buf, len); + + if (strcasecmp(field, "tag") == 0) + return ascii_dump_string(ep->tag, buf, len); + + if (strcasecmp(field, "type") == 0) + return ascii_dump_type(ep->type, buf, len); + + if (strcasecmp(field, "version") == 0) + return ascii_dump_uint32(SMTPD_ENVELOPE_VERSION, buf, len); + + if (strcasecmp(field, "dsn-notify") == 0) + return ascii_dump_uint8(ep->dsn_notify, buf, len); + + if (strcasecmp(field, "dsn-ret") == 0) + return ascii_dump_dsn_ret(ep->dsn_ret, buf, len); + + if (strcasecmp(field, "dsn-orcpt") == 0) { + if (ep->dsn_orcpt.user[0] && ep->dsn_orcpt.domain[0]) + return ascii_dump_mailaddr(&ep->dsn_orcpt, buf, len); + return 1; + } + + if (strcasecmp(field, "dsn-envid") == 0) + return ascii_dump_string(ep->dsn_envid, buf, len); + + if (strcasecmp(field, "esc-class") == 0) { + if (ep->esc_class) + return ascii_dump_uint8(ep->esc_class, buf, len); + return 1; + } + + if (strcasecmp(field, "esc-code") == 0) { + /* this is not a pasto, we dump esc_code if esc_class is !0 */ + if (ep->esc_class) + return ascii_dump_uint8(ep->esc_code, buf, len); + return 1; + } + + return (0); +} + +static void +envelope_ascii_dump(const struct envelope *ep, char **dest, size_t *len, + const char *field) +{ + char buf[8192]; + int l; + + if (*dest == NULL) + return; + + memset(buf, 0, sizeof buf); + if (!ascii_dump_field(field, ep, buf, sizeof buf)) + goto err; + if (buf[0] == '\0') + return; + + l = snprintf(*dest, *len, "%s: %s\n", field, buf); + if (l < 0 || (size_t) l >= *len) + goto err; + *dest += l; + *len -= l; + + return; +err: + *dest = NULL; +} diff --git a/smtpd/esc.c b/smtpd/esc.c new file mode 100644 index 00000000..64a44c79 --- /dev/null +++ b/smtpd/esc.c @@ -0,0 +1,116 @@ +/* $OpenBSD: esc.c,v 1.5 2016/09/03 22:16:39 gilles Exp $ */ + +/* + * Copyright (c) 2014 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <stdio.h> +#include <limits.h> + +#include "smtpd-defines.h" +#include "smtpd-api.h" + +static struct escode { + enum enhanced_status_code code; + const char *description; +} esc[] = { + /* 0.0 */ + { ESC_OTHER_STATUS, "Other/Undefined" }, + + /* 1.x */ + { ESC_OTHER_ADDRESS_STATUS, "Other/Undefined address status" }, + { ESC_BAD_DESTINATION_MAILBOX_ADDRESS, "Bad destination mailbox address" }, + { ESC_BAD_DESTINATION_SYSTEM_ADDRESS, "Bad destination system address" }, + { ESC_BAD_DESTINATION_MAILBOX_ADDRESS_SYNTAX, "Bad destination mailbox address syntax" }, + { ESC_DESTINATION_MAILBOX_ADDRESS_AMBIGUOUS, "Destination mailbox address ambiguous" }, + { ESC_DESTINATION_ADDRESS_VALID, "Destination address valid" }, + { ESC_DESTINATION_MAILBOX_HAS_MOVED, "Destination mailbox has moved, No forwarding address" }, + { ESC_BAD_SENDER_MAILBOX_ADDRESS_SYNTAX, "Bad sender's mailbox address syntax" }, + { ESC_BAD_SENDER_SYSTEM_ADDRESS, "Bad sender's system address syntax" }, + + /* 2.x */ + { ESC_OTHER_MAILBOX_STATUS, "Other/Undefined mailbox status" }, + { ESC_MAILBOX_DISABLED, "Mailbox disabled, not accepting messages" }, + { ESC_MAILBOX_FULL, "Mailbox full" }, + { ESC_MESSAGE_LENGTH_TOO_LARGE, "Message length exceeds administrative limit" }, + { ESC_MAILING_LIST_EXPANSION_PROBLEM, "Mailing list expansion problem" }, + + /* 3.x */ + { ESC_OTHER_MAIL_SYSTEM_STATUS, "Other/Undefined mail system status" }, + { ESC_MAIL_SYSTEM_FULL, "Mail system full" }, + { ESC_SYSTEM_NOT_ACCEPTING_MESSAGES, "System not accepting network messages" }, + { ESC_SYSTEM_NOT_CAPABLE_OF_SELECTED_FEATURES, "System not capable of selected features" }, + { ESC_MESSAGE_TOO_BIG_FOR_SYSTEM, "Message too big for system" }, + { ESC_SYSTEM_INCORRECTLY_CONFIGURED, "System incorrectly configured" }, + + /* 4.x */ + { ESC_OTHER_NETWORK_ROUTING_STATUS, "Other/Undefined network or routing status" }, + { ESC_NO_ANSWER_FROM_HOST, "No answer from host" }, + { ESC_BAD_CONNECTION, "Bad connection" }, + { ESC_DIRECTORY_SERVER_FAILURE, "Directory server failure" }, + { ESC_UNABLE_TO_ROUTE, "Unable to route" }, + { ESC_MAIL_SYSTEM_CONGESTION, "Mail system congestion" }, + { ESC_ROUTING_LOOP_DETECTED, "Routing loop detected" }, + { ESC_DELIVERY_TIME_EXPIRED, "Delivery time expired" }, + + /* 5.x */ + { ESC_INVALID_RECIPIENT, "Invalid recipient" }, + { ESC_INVALID_COMMAND, "Invalid command" }, + { ESC_SYNTAX_ERROR, "Syntax error" }, + { ESC_TOO_MANY_RECIPIENTS, "Too many recipients" }, + { ESC_INVALID_COMMAND_ARGUMENTS, "Invalid command arguments" }, + { ESC_WRONG_PROTOCOL_VERSION, "Wrong protocol version" }, + + /* 6.x */ + { ESC_OTHER_MEDIA_ERROR, "Other/Undefined media error" }, + { ESC_MEDIA_NOT_SUPPORTED, "Media not supported" }, + { ESC_CONVERSION_REQUIRED_AND_PROHIBITED, "Conversion required and prohibited" }, + { ESC_CONVERSION_REQUIRED_BUT_NOT_SUPPORTED, "Conversion required but not supported" }, + { ESC_CONVERSION_WITH_LOSS_PERFORMED, "Conversion with loss performed" }, + { ESC_CONVERSION_FAILED, "Conversion failed" }, + + /* 7.x */ + { ESC_OTHER_SECURITY_STATUS, "Other/Undefined security status" }, + { ESC_DELIVERY_NOT_AUTHORIZED_MESSAGE_REFUSED, "Delivery not authorized, message refused" }, + { ESC_MAILING_LIST_EXPANSION_PROHIBITED, "Mailing list expansion prohibited" }, + { ESC_SECURITY_CONVERSION_REQUIRED_NOT_POSSIBLE,"Security conversion required but not possible" }, + { ESC_SECURITY_FEATURES_NOT_SUPPORTED, "Security features not supported" }, + { ESC_CRYPTOGRAPHIC_FAILURE, "Cryptographic failure" }, + { ESC_CRYPTOGRAPHIC_ALGORITHM_NOT_SUPPORTED, "Cryptographic algorithm not supported" }, + { ESC_MESSAGE_TOO_BIG_FOR_SYSTEM, "Message integrity failure" }, +}; + +const char * +esc_code(enum enhanced_status_class class, enum enhanced_status_code code) +{ + static char buffer[6]; + + (void)snprintf(buffer, sizeof buffer, "%d.%d.%d", class, code / 10, code % 10); + return buffer; + +} + +const char * +esc_description(enum enhanced_status_code code) +{ + uint32_t i; + + for (i = 0; i < nitems(esc); ++i) + if (code == esc[i].code) + return esc[i].description; + return "Other/Undefined"; +} diff --git a/smtpd/expand.c b/smtpd/expand.c new file mode 100644 index 00000000..a4306fc0 --- /dev/null +++ b/smtpd/expand.c @@ -0,0 +1,332 @@ +/* $OpenBSD: expand.c,v 1.31 2018/05/31 21:06:12 gilles Exp $ */ + +/* + * Copyright (c) 2009 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <limits.h> +#include <stdlib.h> +#include <string.h> +#ifdef HAVE_UTIL_H +#include <util.h> +#endif +#ifdef HAVE_LIBUTIL_H +#include <libutil.h> +#endif + +#include "smtpd.h" +#include "log.h" + +static const char *expandnode_info(struct expandnode *); + +struct expandnode * +expand_lookup(struct expand *expand, struct expandnode *key) +{ + return RB_FIND(expandtree, &expand->tree, key); +} + +int +expand_to_text(struct expand *expand, char *buf, size_t sz) +{ + struct expandnode *xn; + + buf[0] = '\0'; + + RB_FOREACH(xn, expandtree, &expand->tree) { + if (buf[0]) + (void)strlcat(buf, ", ", sz); + if (strlcat(buf, expandnode_to_text(xn), sz) >= sz) + return 0; + } + + return 1; +} + +void +expand_insert(struct expand *expand, struct expandnode *node) +{ + struct expandnode *xn; + + node->rule = expand->rule; + node->parent = expand->parent; + + log_trace(TRACE_EXPAND, "expand: %p: expand_insert() called for %s", + expand, expandnode_info(node)); + if (node->type == EXPAND_USERNAME && + expand->parent && + expand->parent->type == EXPAND_USERNAME && + !strcmp(expand->parent->u.user, node->u.user)) { + log_trace(TRACE_EXPAND, "expand: %p: setting sameuser = 1", + expand); + node->sameuser = 1; + } + + if (expand_lookup(expand, node)) { + log_trace(TRACE_EXPAND, "expand: %p: node found, discarding", + expand); + return; + } + + xn = xmemdup(node, sizeof *xn); + xn->rule = expand->rule; + xn->parent = expand->parent; + if (xn->parent) + xn->depth = xn->parent->depth + 1; + else + xn->depth = 0; + RB_INSERT(expandtree, &expand->tree, xn); + if (expand->queue) + TAILQ_INSERT_TAIL(expand->queue, xn, tq_entry); + expand->nb_nodes++; + log_trace(TRACE_EXPAND, "expand: %p: inserted node %p", expand, xn); +} + +void +expand_clear(struct expand *expand) +{ + struct expandnode *xn; + + log_trace(TRACE_EXPAND, "expand: %p: clearing expand tree", expand); + if (expand->queue) + while ((xn = TAILQ_FIRST(expand->queue))) + TAILQ_REMOVE(expand->queue, xn, tq_entry); + + while ((xn = RB_ROOT(&expand->tree)) != NULL) { + RB_REMOVE(expandtree, &expand->tree, xn); + free(xn); + } +} + +void +expand_free(struct expand *expand) +{ + expand_clear(expand); + + log_trace(TRACE_EXPAND, "expand: %p: freeing expand tree", expand); + free(expand); +} + +int +expand_cmp(struct expandnode *e1, struct expandnode *e2) +{ + struct expandnode *p1, *p2; + int r; + + if (e1->type < e2->type) + return -1; + if (e1->type > e2->type) + return 1; + if (e1->sameuser < e2->sameuser) + return -1; + if (e1->sameuser > e2->sameuser) + return 1; + if (e1->realuser < e2->realuser) + return -1; + if (e1->realuser > e2->realuser) + return 1; + + r = memcmp(&e1->u, &e2->u, sizeof(e1->u)); + if (r) + return (r); + + if (e1->parent == e2->parent) + return (0); + + if (e1->parent == NULL) + return (-1); + if (e2->parent == NULL) + return (1); + + /* + * The same node can be expanded in for different dest context. + * Wen need to distinguish between those. + */ + for(p1 = e1->parent; p1->type != EXPAND_ADDRESS; p1 = p1->parent) + ; + for(p2 = e2->parent; p2->type != EXPAND_ADDRESS; p2 = p2->parent) + ; + if (p1 < p2) + return (-1); + if (p1 > p2) + return (1); + + if (e1->type != EXPAND_FILENAME && e1->type != EXPAND_FILTER) + return (0); + + /* + * For external delivery, we need to distinguish between users. + * If we can't find a username, we assume it is _smtpd. + */ + for(p1 = e1->parent; p1 && p1->type != EXPAND_USERNAME; p1 = p1->parent) + ; + for(p2 = e2->parent; p2 && p2->type != EXPAND_USERNAME; p2 = p2->parent) + ; + if (p1 < p2) + return (-1); + if (p1 > p2) + return (1); + + return (0); +} + +static int +expand_line_split(char **line, char **ret) +{ + static char buffer[LINE_MAX]; + int esc, dq, sq; + size_t i; + char *s; + + memset(buffer, 0, sizeof buffer); + esc = dq = sq = 0; + i = 0; + for (s = *line; (*s) && (i < sizeof(buffer)); ++s) { + if (esc) { + buffer[i++] = *s; + esc = 0; + continue; + } + if (*s == '\\') { + esc = 1; + continue; + } + if (*s == ',' && !dq && !sq) { + *ret = buffer; + *line = s+1; + return (1); + } + + buffer[i++] = *s; + esc = 0; + + if (*s == '"' && !sq) + dq ^= 1; + if (*s == '\'' && !dq) + sq ^= 1; + } + + if (esc || dq || sq || i == sizeof(buffer)) + return (-1); + + *ret = buffer; + *line = s; + return (i ? 1 : 0); +} + +int +expand_line(struct expand *expand, const char *s, int do_includes) +{ + struct expandnode xn; + char buffer[LINE_MAX]; + char *p, *subrcpt; + int ret; + + memset(buffer, 0, sizeof buffer); + if (strlcpy(buffer, s, sizeof buffer) >= sizeof buffer) + return 0; + + p = buffer; + while ((ret = expand_line_split(&p, &subrcpt)) > 0) { + subrcpt = strip(subrcpt); + if (subrcpt[0] == '\0') + continue; + if (!text_to_expandnode(&xn, subrcpt)) + return 0; + if (!do_includes) + if (xn.type == EXPAND_INCLUDE) + continue; + expand_insert(expand, &xn); + } + + if (ret >= 0) + return 1; + + /* expand_line_split() returned < 0 */ + return 0; +} + +static const char * +expandnode_info(struct expandnode *e) +{ + static char buffer[1024]; + const char *type = NULL; + const char *value = NULL; + char tmp[64]; + + switch (e->type) { + case EXPAND_FILTER: + type = "filter"; + break; + case EXPAND_FILENAME: + type = "filename"; + break; + case EXPAND_INCLUDE: + type = "include"; + break; + case EXPAND_USERNAME: + type = "username"; + break; + case EXPAND_ADDRESS: + type = "address"; + break; + case EXPAND_ERROR: + type = "error"; + break; + case EXPAND_INVALID: + default: + return NULL; + } + + if ((value = expandnode_to_text(e)) == NULL) + return NULL; + + (void)strlcpy(buffer, type, sizeof buffer); + (void)strlcat(buffer, ":", sizeof buffer); + if (strlcat(buffer, value, sizeof buffer) >= sizeof buffer) + return NULL; + + (void)snprintf(tmp, sizeof(tmp), "[parent=%p", e->parent); + if (strlcat(buffer, tmp, sizeof buffer) >= sizeof buffer) + return NULL; + + (void)snprintf(tmp, sizeof(tmp), ", rule=%p", e->rule); + if (strlcat(buffer, tmp, sizeof buffer) >= sizeof buffer) + return NULL; + + if (e->rule) { + (void)snprintf(tmp, sizeof(tmp), ", dispatcher=%p", e->rule->dispatcher); + if (strlcat(buffer, tmp, sizeof buffer) >= sizeof buffer) + return NULL; + } + + if (strlcat(buffer, "]", sizeof buffer) >= sizeof buffer) + return NULL; + + return buffer; +} + +RB_GENERATE(expandtree, expandnode, entry, expand_cmp); diff --git a/smtpd/filter.c b/smtpd/filter.c new file mode 100644 index 00000000..614486b7 --- /dev/null +++ b/smtpd/filter.c @@ -0,0 +1,868 @@ +/* $OpenBSD: filter.c,v 1.25 2017/01/09 09:53:23 reyk Exp $ */ + +/* + * Copyright (c) 2011 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/wait.h> + +#include <netinet/in.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <limits.h> +#include <resolv.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +enum { + QUERY_READY, + QUERY_RUNNING, + QUERY_DONE +}; + + +struct filter_proc { + TAILQ_ENTRY(filter_proc) entry; + struct mproc mproc; + int hooks; + int flags; + int ready; +}; + +struct filter { + TAILQ_ENTRY(filter) entry; + struct filter_proc *proc; +}; +TAILQ_HEAD(filter_lst, filter); + +TAILQ_HEAD(filter_query_lst, filter_query); +struct filter_session { + uint64_t id; + int terminate; + struct filter_lst *filters; + struct filter *fcurr; + + int error; + struct io *iev; + size_t idatalen; + FILE *ofile; + + struct filter_query *eom; +}; + +struct filter_query { + uint64_t qid; + int type; + struct filter_session *session; + + int state; + struct filter *current; + + /* current data */ + union { + struct { + struct sockaddr_storage local; + struct sockaddr_storage remote; + char hostname[HOST_NAME_MAX+1]; + } connect; + char line[LINE_MAX]; + struct mailaddr maddr; + size_t datalen; + } u; + + /* current response */ + struct { + int status; + int code; + char *response; + } smtp; +}; + +static void filter_imsg(struct mproc *, struct imsg *); +static void filter_post_event(uint64_t, int, struct filter *, struct filter *); +static struct filter_query *filter_query(struct filter_session *, int); +static void filter_drain_query(struct filter_query *); +static void filter_run_query(struct filter *, struct filter_query *); +static void filter_end_query(struct filter_query *); +static void filter_set_sink(struct filter_session *, int); +static int filter_tx(struct filter_session *, int); +static void filter_tx_io(struct io *, int, void *); + +static TAILQ_HEAD(, filter_proc) procs; +struct dict chains; + +static const char * filter_session_to_text(struct filter_session *); +static const char * filter_query_to_text(struct filter_query *); +static const char * filter_to_text(struct filter *); +static const char * filter_proc_to_text(struct filter_proc *); +static const char * query_to_str(int); +static const char * event_to_str(int); +static const char * status_to_str(int); +static const char * filterimsg_to_str(int); + +struct tree sessions; +struct tree queries; + +static void +filter_add_arg(struct filter_conf *filter, char *arg) +{ + if (filter->argc == MAX_FILTER_ARGS) { + log_warnx("warn: filter \"%s\" is full", filter->name); + fatalx("exiting"); + } + filter->argv[filter->argc++] = arg; +} + +static void +filter_extend_chain(struct filter_lst *chain, const char *name) +{ + struct filter *n; + struct filter_lst *fchain; + struct filter_conf *fconf; + int i; + + fconf = dict_xget(&env->sc_filters, name); + if (fconf->chain) { + log_debug("filter: extending with \"%s\"", name); + for (i = 0; i < fconf->argc; i++) + filter_extend_chain(chain, fconf->argv[i]); + } + else { + log_debug("filter: adding filter \"%s\"", name); + n = xcalloc(1, sizeof(*n), "filter_extend_chain"); + fchain = dict_get(&chains, name); + n->proc = TAILQ_FIRST(fchain)->proc; + TAILQ_INSERT_TAIL(chain, n, entry); + } +} + +void +filter_postfork(void) +{ + static int prepare = 0; + struct filter_conf *filter; + void *iter; + struct filter_proc *proc; + struct filter_lst *fchain; + struct filter *f; + struct mproc *p; + int done, i; + + if (prepare) + return; + prepare = 1; + + TAILQ_INIT(&procs); + dict_init(&chains); + + log_debug("filter: building simple chains..."); + + /* create all filter proc and associated chains */ + iter = NULL; + while (dict_iter(&env->sc_filters, &iter, NULL, (void **)&filter)) { + if (filter->chain) + continue; + + log_debug("filter: building simple chain \"%s\"", filter->name); + proc = xcalloc(1, sizeof(*proc), "filter_postfork"); + p = &proc->mproc; + p->handler = filter_imsg; + p->proc = PROC_FILTER; + p->name = xstrdup(filter->name, "filter_postfork"); + p->data = proc; + if (tracing & TRACE_DEBUG) + filter_add_arg(filter, "-v"); + if (foreground_log) + filter_add_arg(filter, "-d"); + if (mproc_fork(p, filter->path, filter->argv) < 0) + fatalx("filter_postfork"); + + log_debug("filter: registering proc \"%s\"", filter->name); + f = xcalloc(1, sizeof(*f), "filter_postfork"); + f->proc = proc; + + TAILQ_INSERT_TAIL(&procs, proc, entry); + fchain = xcalloc(1, sizeof(*fchain), "filter_postfork"); + TAILQ_INIT(fchain); + TAILQ_INSERT_TAIL(fchain, f, entry); + dict_xset(&chains, filter->name, fchain); + filter->done = 1; + } + + log_debug("filter: building complex chains..."); + + /* resolve all chains */ + done = 0; + while (!done) { + done = 1; + iter = NULL; + while (dict_iter(&env->sc_filters, &iter, NULL, + (void **)&filter)) { + if (filter->done) + continue; + done = 0; + filter->done = 1; + for (i = 0; i < filter->argc; i++) { + if (!dict_get(&chains, filter->argv[i])) { + filter->done = 0; + break; + } + } + if (filter->done == 0) + continue; + fchain = xcalloc(1, sizeof(*fchain), "filter_postfork"); + TAILQ_INIT(fchain); + log_debug("filter: building chain \"%s\"...", + filter->name); + for (i = 0; i < filter->argc; i++) + filter_extend_chain(fchain, filter->argv[i]); + log_debug("filter: done building chain \"%s\"", + filter->name); + dict_xset(&chains, filter->name, fchain); + } + } + log_debug("filter: done building complex chains"); + + fchain = xcalloc(1, sizeof(*fchain), "filter_postfork"); + TAILQ_INIT(fchain); + dict_xset(&chains, "<no-filter>", fchain); +} + +void +filter_configure(void) +{ + static int init = 0; + struct filter_proc *p; + + if (init) + return; + init = 1; + + tree_init(&sessions); + tree_init(&queries); + + TAILQ_FOREACH(p, &procs, entry) { + m_create(&p->mproc, IMSG_FILTER_REGISTER, 0, 0, -1); + m_add_u32(&p->mproc, FILTER_API_VERSION); + m_add_string(&p->mproc, p->mproc.name); + m_close(&p->mproc); + mproc_enable(&p->mproc); + } + + if (TAILQ_FIRST(&procs) == NULL) + smtp_configure(); +} + +void +filter_event(uint64_t id, int event) +{ + struct filter_session *s; + + if (event == EVENT_DISCONNECT) + /* On disconnect, the session is virtualy dead */ + s = tree_xpop(&sessions, id); + else + s = tree_xget(&sessions, id); + + filter_post_event(id, event, TAILQ_FIRST(s->filters), NULL); + + if (event == EVENT_DISCONNECT) { + if (s->iev) + io_free(s->iev); + if (s->ofile) + fclose(s->ofile); + free(s); + } +} + +void +filter_connect(uint64_t id, const struct sockaddr *local, + const struct sockaddr *remote, const char *host, const char *filter) +{ + struct filter_session *s; + struct filter_query *q; + + s = xcalloc(1, sizeof(*s), "filter_event"); + s->id = id; + if (filter == NULL) + filter = "<no-filter>"; + s->filters = dict_xget(&chains, filter); + tree_xset(&sessions, s->id, s); + + filter_event(id, EVENT_CONNECT); + q = filter_query(s, QUERY_CONNECT); + + memmove(&q->u.connect.local, local, SA_LEN(local)); + memmove(&q->u.connect.remote, remote, SA_LEN(remote)); + strlcpy(q->u.connect.hostname, host, sizeof(q->u.connect.hostname)); + + q->smtp.status = FILTER_OK; + q->smtp.code = 0; + q->smtp.response = NULL; + + filter_drain_query(q); +} + +void +filter_mailaddr(uint64_t id, int type, const struct mailaddr *maddr) +{ + struct filter_session *s; + struct filter_query *q; + + s = tree_xget(&sessions, id); + q = filter_query(s, type); + + strlcpy(q->u.maddr.user, maddr->user, sizeof(q->u.maddr.user)); + strlcpy(q->u.maddr.domain, maddr->domain, sizeof(q->u.maddr.domain)); + + filter_drain_query(q); +} + +void +filter_line(uint64_t id, int type, const char *line) +{ + struct filter_session *s; + struct filter_query *q; + + s = tree_xget(&sessions, id); + q = filter_query(s, type); + + if (line) + strlcpy(q->u.line, line, sizeof(q->u.line)); + + filter_drain_query(q); +} + +void +filter_eom(uint64_t id, int type, size_t datalen) +{ + struct filter_session *s; + struct filter_query *q; + + s = tree_xget(&sessions, id); + q = filter_query(s, type); + q->u.datalen = datalen; + + filter_drain_query(q); +} + +static void +filter_set_sink(struct filter_session *s, int sink) +{ + struct mproc *p; + + while (s->fcurr) { + if (s->fcurr->proc->hooks & HOOK_DATALINE) { + log_trace(TRACE_FILTERS, "filter: sending fd %d to %s", + sink, filter_to_text(s->fcurr)); + p = &s->fcurr->proc->mproc; + m_create(p, IMSG_FILTER_PIPE, 0, 0, sink); + m_add_id(p, s->id); + m_close(p); + return; + } + s->fcurr = TAILQ_PREV(s->fcurr, filter_lst, entry); + } + + log_trace(TRACE_FILTERS, "filter: chain input is %d", sink); + smtp_filter_fd(s->id, sink); +} + +void +filter_build_fd_chain(uint64_t id, int sink) +{ + struct filter_session *s; + int fd; + + s = tree_xget(&sessions, id); + s->fcurr = TAILQ_LAST(s->filters, filter_lst); + + fd = filter_tx(s, sink); + filter_set_sink(s, fd); +} + +void +filter_post_event(uint64_t id, int event, struct filter *f, struct filter *end) +{ + for(; f && f != end; f = TAILQ_NEXT(f, entry)) { + log_trace(TRACE_FILTERS, "filter: post-event event=%s filter=%s", + event_to_str(event), f->proc->mproc.name); + + m_create(&f->proc->mproc, IMSG_FILTER_EVENT, 0, 0, -1); + m_add_id(&f->proc->mproc, id); + m_add_int(&f->proc->mproc, event); + m_close(&f->proc->mproc); + } +} + +static struct filter_query * +filter_query(struct filter_session *s, int type) +{ + struct filter_query *q; + + q = xcalloc(1, sizeof(*q), "filter_query"); + q->qid = generate_uid(); + q->session = s; + q->type = type; + + q->state = QUERY_READY; + q->current = TAILQ_FIRST(s->filters); + + log_trace(TRACE_FILTERS, "filter: new query %s", query_to_str(type)); + + return (q); +} + +static void +filter_drain_query(struct filter_query *q) +{ + log_trace(TRACE_FILTERS, "filter: filter_drain_query %s", + filter_query_to_text(q)); + + /* + * The query must be passed through all filters that registered + * a hook, until one rejects it. + */ + while (q->state != QUERY_DONE) { + /* Walk over all filters */ + while (q->current) { + filter_run_query(q->current, q); + if (q->state == QUERY_RUNNING) { + log_trace(TRACE_FILTERS, + "filter: waiting for running query %s", + filter_query_to_text(q)); + return; + } + } + q->state = QUERY_DONE; + } + + /* Defer the response if the file is not closed yet. */ + if (q->type == QUERY_EOM && q->session->ofile && q->smtp.status == FILTER_OK) { + log_debug("filter: deferring eom query..."); + q->session->eom = q; + return; + } + + filter_end_query(q); +} + +static void +filter_run_query(struct filter *f, struct filter_query *q) +{ + log_trace(TRACE_FILTERS, + "filter: running filter %s for query %s", + filter_to_text(f), filter_query_to_text(q)); + + m_create(&f->proc->mproc, IMSG_FILTER_QUERY, 0, 0, -1); + m_add_id(&f->proc->mproc, q->session->id); + m_add_id(&f->proc->mproc, q->qid); + m_add_int(&f->proc->mproc, q->type); + + switch (q->type) { + case QUERY_CONNECT: + m_add_sockaddr(&f->proc->mproc, + (struct sockaddr *)&q->u.connect.local); + m_add_sockaddr(&f->proc->mproc, + (struct sockaddr *)&q->u.connect.remote); + m_add_string(&f->proc->mproc, q->u.connect.hostname); + break; + case QUERY_HELO: + m_add_string(&f->proc->mproc, q->u.line); + break; + case QUERY_MAIL: + case QUERY_RCPT: + m_add_mailaddr(&f->proc->mproc, &q->u.maddr); + break; + case QUERY_EOM: + m_add_u32(&f->proc->mproc, q->u.datalen); + break; + default: + break; + } + m_close(&f->proc->mproc); + + tree_xset(&queries, q->qid, q); + q->state = QUERY_RUNNING; +} + +static void +filter_end_query(struct filter_query *q) +{ + struct filter_session *s = q->session; + const char *response = q->smtp.response; + + log_trace(TRACE_FILTERS, "filter: filter_end_query %s", + filter_query_to_text(q)); + + if (q->type == QUERY_EOM && q->smtp.status == FILTER_OK) { + if (s->error || q->u.datalen != s->idatalen) { + response = "Internal error"; + q->smtp.code = 451; + q->smtp.status = FILTER_FAIL; + if (!s->error) + log_warnx("filter: datalen mismatch on session %" PRIx64 + ": %zu/%zu", s->id, s->idatalen, q->u.datalen); + } + } + + log_trace(TRACE_FILTERS, + "filter: query %016"PRIx64" done: " + "status=%s code=%d response=\"%s\"", + q->qid, + status_to_str(q->smtp.status), + q->smtp.code, + response); + + smtp_filter_response(s->id, q->type, q->smtp.status, q->smtp.code, + response); + free(q->smtp.response); + free(q); +} + +static void +filter_imsg(struct mproc *p, struct imsg *imsg) +{ + struct filter_proc *proc = p->data; + struct filter_session *s; + struct filter_query *q; + struct msg m; + const char *line; + uint64_t qid; + uint32_t datalen; + int type, status, code; + + if (imsg == NULL) { + log_warnx("warn: filter \"%s\" closed unexpectedly", p->name); + fatalx("exiting"); + } + + log_trace(TRACE_FILTERS, "filter: imsg %s from procfilter %s", + filterimsg_to_str(imsg->hdr.type), + filter_proc_to_text(proc)); + + switch (imsg->hdr.type) { + + case IMSG_FILTER_REGISTER: + if (proc->ready) { + log_warnx("warn: filter \"%s\" already registered", + proc->mproc.name); + exit(1); + } + + m_msg(&m, imsg); + m_get_int(&m, &proc->hooks); + m_get_int(&m, &proc->flags); + m_end(&m); + proc->ready = 1; + + log_debug("debug: filter \"%s\": hooks 0x%08x flags 0x%04x", + proc->mproc.name, proc->hooks, proc->flags); + + TAILQ_FOREACH(proc, &procs, entry) + if (!proc->ready) + return; + + smtp_configure(); + break; + + case IMSG_FILTER_RESPONSE: + m_msg(&m, imsg); + m_get_id(&m, &qid); + m_get_int(&m, &type); + if (type == QUERY_EOM) + m_get_u32(&m, &datalen); + m_get_int(&m, &status); + m_get_int(&m, &code); + if (m_is_eom(&m)) + line = NULL; + else + m_get_string(&m, &line); + m_end(&m); + + q = tree_xpop(&queries, qid); + if (q->type != type) { + log_warnx("warn: filter: type mismatch %d != %d", + q->type, type); + fatalx("exiting"); + } + q->smtp.status = status; + if (code) + q->smtp.code = code; + if (line) { + free(q->smtp.response); + q->smtp.response = xstrdup(line, "filter_imsg"); + } + q->state = (status == FILTER_OK) ? QUERY_READY : QUERY_DONE; + if (type == QUERY_EOM) + q->u.datalen = datalen; + + q->current = TAILQ_NEXT(q->current, entry); + filter_drain_query(q); + break; + + case IMSG_FILTER_PIPE: + m_msg(&m, imsg); + m_get_id(&m, &qid); + m_end(&m); + + s = tree_xget(&sessions, qid); + s->fcurr = TAILQ_PREV(s->fcurr, filter_lst, entry); + filter_set_sink(s, imsg->fd); + break; + + default: + log_warnx("warn: bad imsg from filter %s", p->name); + exit(1); + } +} + +static int +filter_tx(struct filter_session *s, int sink) +{ + int sp[2]; + + s->idatalen = 0; + s->eom = NULL; + s->error = 0; + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) { + log_warn("warn: filter: socketpair"); + return (-1); + } + + if ((s->ofile = fdopen(sink, "w")) == NULL) { + log_warn("warn: filter: fdopen"); + close(sp[0]); + close(sp[1]); + return (-1); + } + + io_set_nonblocking(sp[0]); + io_set_nonblocking(sp[1]); + + s->iev = io_new(); + io_set_callback(s->iev, filter_tx_io, s); + io_set_fd(s->iev, sp[0]); + io_set_read(s->iev); + + return (sp[1]); +} + +static void +filter_tx_io(struct io *io, int evt, void *arg) +{ + struct filter_session *s = arg; + size_t len, n; + char *data; + + log_trace(TRACE_FILTERS, "filter: filter_tx_io(%p, %s)", + s, io_strevent(evt)); + + switch (evt) { + case IO_DATAIN: + data = io_data(s->iev); + len = io_datalen(s->iev); + + log_trace(TRACE_FILTERS, + "filter: filter_tx_io: datain (%zu) for req %016"PRIx64"", + len, s->id); + + n = fwrite(data, 1, len, s->ofile); + if (n != len) { + log_warnx("warn: filter_tx_io: fwrite %zu/%zu", n, len); + s->error = 1; + break; + } + s->idatalen += n; + io_drop(s->iev, n); + return; + + case IO_DISCONNECTED: + log_trace(TRACE_FILTERS, + "debug: filter: tx done (%zu) for req %016"PRIx64, + s->idatalen, s->id); + break; + + default: + log_warn("warn: filter_tx_io: bad evt (%d) for req %016"PRIx64, + evt, s->id); + s->error = 1; + break; + } + + io_free(s->iev); + s->iev = NULL; + fclose(s->ofile); + s->ofile = NULL; + + /* deferred eom request */ + if (s->eom) { + log_debug("filter: running eom query..."); + filter_end_query(s->eom); + } else { + log_debug("filter: eom not received yet"); + } +} + +static const char * +filter_query_to_text(struct filter_query *q) +{ + static char buf[1024]; + char tmp[1024]; + + tmp[0] = '\0'; + + switch (q->type) { + case QUERY_CONNECT: + strlcat(tmp, "=", sizeof tmp); + strlcat(tmp, ss_to_text(&q->u.connect.local), + sizeof tmp); + strlcat(tmp, " <-> ", sizeof tmp); + strlcat(tmp, ss_to_text(&q->u.connect.remote), + sizeof tmp); + strlcat(tmp, "(", sizeof tmp); + strlcat(tmp, q->u.connect.hostname, sizeof tmp); + strlcat(tmp, ")", sizeof tmp); + break; + case QUERY_MAIL: + case QUERY_RCPT: + snprintf(tmp, sizeof tmp, "=%s@%s", + q->u.maddr.user, q->u.maddr.domain); + break; + case QUERY_HELO: + snprintf(tmp, sizeof tmp, "=%s", q->u.line); + break; + default: + break; + } + snprintf(buf, sizeof buf, "%016"PRIx64"[%s%s,%s]", + q->qid, query_to_str(q->type), tmp, + filter_session_to_text(q->session)); + + return (buf); +} + +static const char * +filter_session_to_text(struct filter_session *s) +{ + static char buf[1024]; + + if (s == NULL) + return "filter_session@NULL"; + + snprintf(buf, sizeof(buf), + "filter_session@%p[datalen=%zu,eom=%p,ofile=%p]", + s, s->idatalen, s->eom, s->ofile); + + return buf; +} + +static const char * +filter_to_text(struct filter *f) +{ + static char buf[1024]; + + snprintf(buf, sizeof buf, "filter:%s", filter_proc_to_text(f->proc)); + + return (buf); +} + +static const char * +filter_proc_to_text(struct filter_proc *proc) +{ + static char buf[1024]; + + snprintf(buf, sizeof buf, "%s[hooks=0x%08x,flags=0x%04x]", + proc->mproc.name, proc->hooks, proc->flags); + + return (buf); +} + +#define CASE(x) case x : return #x + +static const char * +filterimsg_to_str(int imsg) +{ + switch (imsg) { + CASE(IMSG_FILTER_REGISTER); + CASE(IMSG_FILTER_EVENT); + CASE(IMSG_FILTER_QUERY); + CASE(IMSG_FILTER_PIPE); + CASE(IMSG_FILTER_RESPONSE); + default: + return "IMSG_FILTER_???"; + } +} + +static const char * +query_to_str(int query) +{ + switch (query) { + CASE(QUERY_CONNECT); + CASE(QUERY_HELO); + CASE(QUERY_MAIL); + CASE(QUERY_RCPT); + CASE(QUERY_DATA); + CASE(QUERY_EOM); + CASE(QUERY_DATALINE); + default: + return "QUERY_???"; + } +} + +static const char * +event_to_str(int event) +{ + switch (event) { + CASE(EVENT_CONNECT); + CASE(EVENT_RESET); + CASE(EVENT_DISCONNECT); + CASE(EVENT_TX_BEGIN); + CASE(EVENT_TX_COMMIT); + CASE(EVENT_TX_ROLLBACK); + default: + return "EVENT_???"; + } +} + +static const char * +status_to_str(int status) +{ + switch (status) { + CASE(FILTER_OK); + CASE(FILTER_FAIL); + CASE(FILTER_CLOSE); + default: + return "FILTER_???"; + } +} diff --git a/smtpd/forward.5 b/smtpd/forward.5 new file mode 100644 index 00000000..5a68f229 --- /dev/null +++ b/smtpd/forward.5 @@ -0,0 +1,83 @@ +.\" $OpenBSD: forward.5,v 1.9 2015/03/13 22:41:54 eric Exp $ +.\" +.\" Copyright (c) 2012 Gilles Chehade <gilles@poolp.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: March 13 2015 $ +.Dt FORWARD 5 +.Os +.Sh NAME +.Nm forward +.Nd email forwarding information file +.Sh DESCRIPTION +Users may put a +.Nm .forward +file in their home directory. +If this file exists, +.Xr smtpd 8 +forwards email to the destinations specified therein. +.Pp +A +.Nm .forward +file contains a list of expansion values, as described in +.Xr aliases 5 . +Each expansion value should be on a line by itself. +However, the +.Nm .forward +mechanism differs from the aliases mechanism in that it disallows +file inclusion +.Pq :include: +and it performs expansion under the user ID of the +.Nm .forward +file owner. +.Pp +Permissions on the +.Nm .forward +file are very strict and expansion is rejected if the file is +group or world-writable; +if the home directory is group writeable; +or if the file is not owned by the user. +.Pp +Users should avoid editing directly the +.Nm .forward +file to prevent delivery failures from occurring if a message +arrives while the file is not fully written. +The best option is to use a temporary file and use the +.Xr mv 1 +command to atomically overwrite the former +.Nm .forward . +Alternatively, setting the +.Xr sticky 8 +bit on the home directory will cause the +.Nm .forward +lookup to return a temporary failure, causing mails to be deferred. +.Sh FILES +.Bl -tag -width "~/.forwardXXX" -compact +.It Pa ~/.forward +Email forwarding information. +.El +.Sh EXAMPLES +The following file forwards mail to +.Dq user@example.com , +and pipes the same mail to +.Dq examplemda . +.Bd -literal -offset indent +# empty lines are ignored + +user@example.com # anything after # is ignored +"|/path/to/examplemda" +.Ed +.Sh SEE ALSO +.Xr aliases 5 , +.Xr smtpd 8 diff --git a/smtpd/forward.c b/smtpd/forward.c new file mode 100644 index 00000000..7494c6ce --- /dev/null +++ b/smtpd/forward.c @@ -0,0 +1,104 @@ +/* $OpenBSD: forward.c,v 1.39 2015/12/28 22:08:30 jung Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#ifdef HAVE_UTIL_H +#include <util.h> +#endif +#ifdef HAVE_LIBUTIL_H +#include <libutil.h> +#endif +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +#define MAX_FORWARD_SIZE (4 * 1024) +#define MAX_EXPAND_NODES (100) + +int +forwards_get(int fd, struct expand *expand) +{ + FILE *fp = NULL; + char *line = NULL; + size_t len; + size_t lineno; + size_t save; + int ret; + struct stat sb; + + ret = -1; + if (fstat(fd, &sb) == -1) + goto end; + + /* if it's empty just pretend that no expansion took place */ + if (sb.st_size == 0) { + log_info("info: forward file is empty"); + ret = 0; + goto end; + } + + /* over MAX_FORWARD_SIZE, temporarily fail */ + if (sb.st_size >= MAX_FORWARD_SIZE) { + log_info("info: forward file exceeds max size"); + goto end; + } + + if ((fp = fdopen(fd, "r")) == NULL) { + log_warn("warn: fdopen failure in forwards_get()"); + goto end; + } + + lineno = 0; + save = expand->nb_nodes; + while ((line = fparseln(fp, &len, &lineno, NULL, 0)) != NULL) { + if (!expand_line(expand, line, 0)) { + log_info("info: parse error in forward file"); + goto end; + } + if (expand->nb_nodes > MAX_EXPAND_NODES) { + log_info("info: forward file expanded too many nodes"); + goto end; + } + free(line); + } + + ret = expand->nb_nodes > save ? 1 : 0; + +end: + free(line); + if (fp) + fclose(fp); + else + close(fd); + return ret; +} diff --git a/smtpd/iobuf.c b/smtpd/iobuf.c new file mode 100644 index 00000000..dec10660 --- /dev/null +++ b/smtpd/iobuf.c @@ -0,0 +1,462 @@ +/* $OpenBSD: iobuf.c,v 1.13 2020/04/24 11:34:07 eric Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <errno.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#ifdef IO_TLS +#include <openssl/err.h> +#include <openssl/ssl.h> +#endif + +#include "iobuf.h" + +#define IOBUF_MAX 65536 +#define IOBUFQ_MIN 4096 + +struct ioqbuf *ioqbuf_alloc(struct iobuf *, size_t); +void iobuf_drain(struct iobuf *, size_t); + +int +iobuf_init(struct iobuf *io, size_t size, size_t max) +{ + memset(io, 0, sizeof *io); + + if (max == 0) + max = IOBUF_MAX; + + if (size == 0) + size = max; + + if (size > max) + return (-1); + + if ((io->buf = calloc(size, 1)) == NULL) + return (-1); + + io->size = size; + io->max = max; + + return (0); +} + +void +iobuf_clear(struct iobuf *io) +{ + struct ioqbuf *q; + + free(io->buf); + + while ((q = io->outq)) { + io->outq = q->next; + free(q); + } + + memset(io, 0, sizeof (*io)); +} + +void +iobuf_drain(struct iobuf *io, size_t n) +{ + struct ioqbuf *q; + size_t left = n; + + while ((q = io->outq) && left) { + if ((q->wpos - q->rpos) > left) { + q->rpos += left; + left = 0; + } else { + left -= q->wpos - q->rpos; + io->outq = q->next; + free(q); + } + } + + io->queued -= (n - left); + if (io->outq == NULL) + io->outqlast = NULL; +} + +int +iobuf_extend(struct iobuf *io, size_t n) +{ + char *t; + + if (n > io->max) + return (-1); + + if (io->max - io->size < n) + return (-1); + + t = recallocarray(io->buf, io->size, io->size + n, 1); + if (t == NULL) + return (-1); + + io->size += n; + io->buf = t; + + return (0); +} + +size_t +iobuf_left(struct iobuf *io) +{ + return io->size - io->wpos; +} + +size_t +iobuf_space(struct iobuf *io) +{ + return io->size - (io->wpos - io->rpos); +} + +size_t +iobuf_len(struct iobuf *io) +{ + return io->wpos - io->rpos; +} + +char * +iobuf_data(struct iobuf *io) +{ + return io->buf + io->rpos; +} + +void +iobuf_drop(struct iobuf *io, size_t n) +{ + if (n >= iobuf_len(io)) { + io->rpos = io->wpos = 0; + return; + } + + io->rpos += n; +} + +char * +iobuf_getline(struct iobuf *iobuf, size_t *rlen) +{ + char *buf; + size_t len, i; + + buf = iobuf_data(iobuf); + len = iobuf_len(iobuf); + + for (i = 0; i + 1 <= len; i++) + if (buf[i] == '\n') { + /* Note: the returned address points into the iobuf + * buffer. We NUL-end it for convenience, and discard + * the data from the iobuf, so that the caller doesn't + * have to do it. The data remains "valid" as long + * as the iobuf does not overwrite it, that is until + * the next call to iobuf_normalize() or iobuf_extend(). + */ + iobuf_drop(iobuf, i + 1); + buf[i] = '\0'; + if (rlen) + *rlen = i; + return (buf); + } + + return (NULL); +} + +void +iobuf_normalize(struct iobuf *io) +{ + if (io->rpos == 0) + return; + + if (io->rpos == io->wpos) { + io->rpos = io->wpos = 0; + return; + } + + memmove(io->buf, io->buf + io->rpos, io->wpos - io->rpos); + io->wpos -= io->rpos; + io->rpos = 0; +} + +ssize_t +iobuf_read(struct iobuf *io, int fd) +{ + ssize_t n; + + n = read(fd, io->buf + io->wpos, iobuf_left(io)); + if (n == -1) { + /* XXX is this really what we want? */ + if (errno == EAGAIN || errno == EINTR) + return (IOBUF_WANT_READ); + return (IOBUF_ERROR); + } + if (n == 0) + return (IOBUF_CLOSED); + + io->wpos += n; + + return (n); +} + +struct ioqbuf * +ioqbuf_alloc(struct iobuf *io, size_t len) +{ + struct ioqbuf *q; + + if (len < IOBUFQ_MIN) + len = IOBUFQ_MIN; + + if ((q = malloc(sizeof(*q) + len)) == NULL) + return (NULL); + + q->rpos = 0; + q->wpos = 0; + q->size = len; + q->next = NULL; + q->buf = (char *)(q) + sizeof(*q); + + if (io->outqlast == NULL) + io->outq = q; + else + io->outqlast->next = q; + io->outqlast = q; + + return (q); +} + +size_t +iobuf_queued(struct iobuf *io) +{ + return io->queued; +} + +void * +iobuf_reserve(struct iobuf *io, size_t len) +{ + struct ioqbuf *q; + void *r; + + if (len == 0) + return (NULL); + + if (((q = io->outqlast) == NULL) || q->size - q->wpos <= len) { + if ((q = ioqbuf_alloc(io, len)) == NULL) + return (NULL); + } + + r = q->buf + q->wpos; + q->wpos += len; + io->queued += len; + + return (r); +} + +int +iobuf_queue(struct iobuf *io, const void *data, size_t len) +{ + void *buf; + + if (len == 0) + return (0); + + if ((buf = iobuf_reserve(io, len)) == NULL) + return (-1); + + memmove(buf, data, len); + + return (len); +} + +int +iobuf_queuev(struct iobuf *io, const struct iovec *iov, int iovcnt) +{ + int i; + size_t len = 0; + char *buf; + + for (i = 0; i < iovcnt; i++) + len += iov[i].iov_len; + + if ((buf = iobuf_reserve(io, len)) == NULL) + return (-1); + + for (i = 0; i < iovcnt; i++) { + if (iov[i].iov_len == 0) + continue; + memmove(buf, iov[i].iov_base, iov[i].iov_len); + buf += iov[i].iov_len; + } + + return (0); + +} + +int +iobuf_fqueue(struct iobuf *io, const char *fmt, ...) +{ + va_list ap; + int len; + + va_start(ap, fmt); + len = iobuf_vfqueue(io, fmt, ap); + va_end(ap); + + return (len); +} + +int +iobuf_vfqueue(struct iobuf *io, const char *fmt, va_list ap) +{ + char *buf; + int len; + + len = vasprintf(&buf, fmt, ap); + + if (len == -1) + return (-1); + + len = iobuf_queue(io, buf, len); + free(buf); + + return (len); +} + +ssize_t +iobuf_write(struct iobuf *io, int fd) +{ + struct iovec iov[IOV_MAX]; + struct ioqbuf *q; + int i; + ssize_t n; + + i = 0; + for (q = io->outq; q ; q = q->next) { + if (i >= IOV_MAX) + break; + iov[i].iov_base = q->buf + q->rpos; + iov[i].iov_len = q->wpos - q->rpos; + i++; + } + + n = writev(fd, iov, i); + if (n == -1) { + if (errno == EAGAIN || errno == EINTR) + return (IOBUF_WANT_WRITE); + if (errno == EPIPE) + return (IOBUF_CLOSED); + return (IOBUF_ERROR); + } + + iobuf_drain(io, n); + + return (n); +} + +int +iobuf_flush(struct iobuf *io, int fd) +{ + ssize_t s; + + while (io->queued) + if ((s = iobuf_write(io, fd)) < 0) + return (s); + + return (0); +} + +#ifdef IO_TLS + +int +iobuf_flush_tls(struct iobuf *io, void *tls) +{ + ssize_t s; + + while (io->queued) + if ((s = iobuf_write_tls(io, tls)) < 0) + return (s); + + return (0); +} + +ssize_t +iobuf_write_tls(struct iobuf *io, void *tls) +{ + struct ioqbuf *q; + int r; + ssize_t n; + + q = io->outq; + n = SSL_write(tls, q->buf + q->rpos, q->wpos - q->rpos); + if (n <= 0) { + switch ((r = SSL_get_error(tls, n))) { + case SSL_ERROR_WANT_READ: + return (IOBUF_WANT_READ); + case SSL_ERROR_WANT_WRITE: + return (IOBUF_WANT_WRITE); + case SSL_ERROR_ZERO_RETURN: /* connection closed */ + return (IOBUF_CLOSED); + case SSL_ERROR_SYSCALL: + if (ERR_peek_last_error()) + return (IOBUF_TLSERROR); + return (IOBUF_ERROR); + default: + return (IOBUF_TLSERROR); + } + } + iobuf_drain(io, n); + + return (n); +} + +ssize_t +iobuf_read_tls(struct iobuf *io, void *tls) +{ + ssize_t n; + int r; + + n = SSL_read(tls, io->buf + io->wpos, iobuf_left(io)); + if (n < 0) { + switch ((r = SSL_get_error(tls, n))) { + case SSL_ERROR_WANT_READ: + return (IOBUF_WANT_READ); + case SSL_ERROR_WANT_WRITE: + return (IOBUF_WANT_WRITE); + case SSL_ERROR_SYSCALL: + if (ERR_peek_last_error()) + return (IOBUF_TLSERROR); + return (IOBUF_ERROR); + default: + return (IOBUF_TLSERROR); + } + } else if (n == 0) + return (IOBUF_CLOSED); + + io->wpos += n; + + return (n); +} + +#endif /* IO_TLS */ diff --git a/smtpd/iobuf.h b/smtpd/iobuf.h new file mode 100644 index 00000000..c454d0a1 --- /dev/null +++ b/smtpd/iobuf.h @@ -0,0 +1,67 @@ +/* $OpenBSD: iobuf.h,v 1.5 2019/06/12 17:42:53 eric Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot <eric@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. + */ + +struct ioqbuf { + struct ioqbuf *next; + char *buf; + size_t size; + size_t wpos; + size_t rpos; +}; + +struct iobuf { + char *buf; + size_t max; + size_t size; + size_t wpos; + size_t rpos; + + size_t queued; + struct ioqbuf *outq; + struct ioqbuf *outqlast; +}; + +#define IOBUF_WANT_READ -1 +#define IOBUF_WANT_WRITE -2 +#define IOBUF_CLOSED -3 +#define IOBUF_ERROR -4 +#define IOBUF_TLSERROR -5 + +int iobuf_init(struct iobuf *, size_t, size_t); +void iobuf_clear(struct iobuf *); + +int iobuf_extend(struct iobuf *, size_t); +void iobuf_normalize(struct iobuf *); +void iobuf_drop(struct iobuf *, size_t); +size_t iobuf_space(struct iobuf *); +size_t iobuf_len(struct iobuf *); +size_t iobuf_left(struct iobuf *); +char *iobuf_data(struct iobuf *); +char *iobuf_getline(struct iobuf *, size_t *); +ssize_t iobuf_read(struct iobuf *, int); +ssize_t iobuf_read_tls(struct iobuf *, void *); + +size_t iobuf_queued(struct iobuf *); +void* iobuf_reserve(struct iobuf *, size_t); +int iobuf_queue(struct iobuf *, const void*, size_t); +int iobuf_queuev(struct iobuf *, const struct iovec *, int); +int iobuf_fqueue(struct iobuf *, const char *, ...); +int iobuf_vfqueue(struct iobuf *, const char *, va_list); +int iobuf_flush(struct iobuf *, int); +int iobuf_flush_tls(struct iobuf *, void *); +ssize_t iobuf_write(struct iobuf *, int); +ssize_t iobuf_write_tls(struct iobuf *, void *); diff --git a/smtpd/ioev.c b/smtpd/ioev.c new file mode 100644 index 00000000..e0a8a096 --- /dev/null +++ b/smtpd/ioev.c @@ -0,0 +1,1064 @@ +/* $OpenBSD: ioev.c,v 1.42 2019/06/12 17:42:53 eric Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <inttypes.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> + +#include "ioev.h" +#include "iobuf.h" + +#ifdef IO_TLS +#include <openssl/err.h> +#include <openssl/ssl.h> +#endif + +enum { + IO_STATE_NONE, + IO_STATE_CONNECT, + IO_STATE_CONNECT_TLS, + IO_STATE_ACCEPT_TLS, + IO_STATE_UP, + + IO_STATE_MAX, +}; + +#define IO_PAUSE_IN IO_IN +#define IO_PAUSE_OUT IO_OUT +#define IO_READ 0x04 +#define IO_WRITE 0x08 +#define IO_RW (IO_READ | IO_WRITE) +#define IO_RESET 0x10 /* internal */ +#define IO_HELD 0x20 /* internal */ + +struct io { + int sock; + void *arg; + void (*cb)(struct io*, int, void *); + struct iobuf iobuf; + size_t lowat; + int timeout; + int flags; + int state; + struct event ev; + void *tls; + const char *error; /* only valid immediately on callback */ +}; + +const char* io_strflags(int); +const char* io_evstr(short); + +void _io_init(void); +void io_hold(struct io *); +void io_release(struct io *); +void io_callback(struct io*, int); +void io_dispatch(int, short, void *); +void io_dispatch_connect(int, short, void *); +size_t io_pending(struct io *); +size_t io_queued(struct io*); +void io_reset(struct io *, short, void (*)(int, short, void*)); +void io_frame_enter(const char *, struct io *, int); +void io_frame_leave(struct io *); + +#ifdef IO_TLS +void ssl_error(const char *); /* XXX external */ + +static const char* io_tls_error(void); +void io_dispatch_accept_tls(int, short, void *); +void io_dispatch_connect_tls(int, short, void *); +void io_dispatch_read_tls(int, short, void *); +void io_dispatch_write_tls(int, short, void *); +void io_reload_tls(struct io *io); +#endif + +static struct io *current = NULL; +static uint64_t frame = 0; +static int _io_debug = 0; + +#define io_debug(args...) do { if (_io_debug) printf(args); } while(0) + + +const char* +io_strio(struct io *io) +{ + static char buf[128]; + char ssl[128]; + + ssl[0] = '\0'; +#ifdef IO_TLS + if (io->tls) { + (void)snprintf(ssl, sizeof ssl, " tls=%s:%s:%d", + SSL_get_version(io->tls), + SSL_get_cipher_name(io->tls), + SSL_get_cipher_bits(io->tls, NULL)); + } +#endif + + (void)snprintf(buf, sizeof buf, + "<io:%p fd=%d to=%d fl=%s%s ib=%zu ob=%zu>", + io, io->sock, io->timeout, io_strflags(io->flags), ssl, + io_pending(io), io_queued(io)); + + return (buf); +} + +#define CASE(x) case x : return #x + +const char* +io_strevent(int evt) +{ + static char buf[32]; + + switch (evt) { + CASE(IO_CONNECTED); + CASE(IO_TLSREADY); + CASE(IO_DATAIN); + CASE(IO_LOWAT); + CASE(IO_DISCONNECTED); + CASE(IO_TIMEOUT); + CASE(IO_ERROR); + default: + (void)snprintf(buf, sizeof(buf), "IO_? %d", evt); + return buf; + } +} + +void +io_set_nonblocking(int fd) +{ + int flags; + + if ((flags = fcntl(fd, F_GETFL)) == -1) + err(1, "io_set_blocking:fcntl(F_GETFL)"); + + flags |= O_NONBLOCK; + + if (fcntl(fd, F_SETFL, flags) == -1) + err(1, "io_set_blocking:fcntl(F_SETFL)"); +} + +void +io_set_nolinger(int fd) +{ + struct linger l; + + memset(&l, 0, sizeof(l)); + if (setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) == -1) + err(1, "io_set_linger:setsockopt()"); +} + +/* + * Event framing must not rely on an io pointer to refer to the "same" io + * throughout the frame, because this is not always the case: + * + * 1) enter(addr0) -> free(addr0) -> leave(addr0) = SEGV + * 2) enter(addr0) -> free(addr0) -> malloc == addr0 -> leave(addr0) = BAD! + * + * In both case, the problem is that the io is freed in the callback, so + * the pointer becomes invalid. If that happens, the user is required to + * call io_clear, so we can adapt the frame state there. + */ +void +io_frame_enter(const char *where, struct io *io, int ev) +{ + io_debug("\n=== %" PRIu64 " ===\n" + "io_frame_enter(%s, %s, %s)\n", + frame, where, io_evstr(ev), io_strio(io)); + + if (current) + errx(1, "io_frame_enter: interleaved frames"); + + current = io; + + io_hold(io); +} + +void +io_frame_leave(struct io *io) +{ + io_debug("io_frame_leave(%" PRIu64 ")\n", frame); + + if (current && current != io) + errx(1, "io_frame_leave: io mismatch"); + + /* io has been cleared */ + if (current == NULL) + goto done; + + /* TODO: There is a possible optimization there: + * In a typical half-duplex request/response scenario, + * the io is waiting to read a request, and when done, it queues + * the response in the output buffer and goes to write mode. + * There, the write event is set and will be triggered in the next + * event frame. In most case, the write call could be done + * immediately as part of the last read frame, thus avoiding to go + * through the event loop machinery. So, as an optimisation, we + * could detect that case here and force an event dispatching. + */ + + /* Reload the io if it has not been reset already. */ + io_release(io); + current = NULL; + done: + io_debug("=== /%" PRIu64 "\n", frame); + + frame += 1; +} + +void +_io_init() +{ + static int init = 0; + + if (init) + return; + + init = 1; + _io_debug = getenv("IO_DEBUG") != NULL; +} + +struct io * +io_new(void) +{ + struct io *io; + + _io_init(); + + if ((io = calloc(1, sizeof(*io))) == NULL) + return NULL; + + io->sock = -1; + io->timeout = -1; + + if (iobuf_init(&io->iobuf, 0, 0) == -1) { + free(io); + return NULL; + } + + return io; +} + +void +io_free(struct io *io) +{ + io_debug("io_clear(%p)\n", io); + + /* the current io is virtually dead */ + if (io == current) + current = NULL; + +#ifdef IO_TLS + SSL_free(io->tls); + io->tls = NULL; +#endif + + if (event_initialized(&io->ev)) + event_del(&io->ev); + if (io->sock != -1) { + close(io->sock); + io->sock = -1; + } + + iobuf_clear(&io->iobuf); + free(io); +} + +void +io_hold(struct io *io) +{ + io_debug("io_enter(%p)\n", io); + + if (io->flags & IO_HELD) + errx(1, "io_hold: io is already held"); + + io->flags &= ~IO_RESET; + io->flags |= IO_HELD; +} + +void +io_release(struct io *io) +{ + if (!(io->flags & IO_HELD)) + errx(1, "io_release: io is not held"); + + io->flags &= ~IO_HELD; + if (!(io->flags & IO_RESET)) + io_reload(io); +} + +void +io_set_fd(struct io *io, int fd) +{ + io->sock = fd; + if (fd != -1) + io_reload(io); +} + +void +io_set_callback(struct io *io, void(*cb)(struct io *, int, void *), void *arg) +{ + io->cb = cb; + io->arg = arg; +} + +void +io_set_timeout(struct io *io, int msec) +{ + io_debug("io_set_timeout(%p, %d)\n", io, msec); + + io->timeout = msec; +} + +void +io_set_lowat(struct io *io, size_t lowat) +{ + io_debug("io_set_lowat(%p, %zu)\n", io, lowat); + + io->lowat = lowat; +} + +void +io_pause(struct io *io, int dir) +{ + io_debug("io_pause(%p, %x)\n", io, dir); + + io->flags |= dir & (IO_PAUSE_IN | IO_PAUSE_OUT); + io_reload(io); +} + +void +io_resume(struct io *io, int dir) +{ + io_debug("io_resume(%p, %x)\n", io, dir); + + io->flags &= ~(dir & (IO_PAUSE_IN | IO_PAUSE_OUT)); + io_reload(io); +} + +void +io_set_read(struct io *io) +{ + int mode; + + io_debug("io_set_read(%p)\n", io); + + mode = io->flags & IO_RW; + if (!(mode == 0 || mode == IO_WRITE)) + errx(1, "io_set_read(): full-duplex or reading"); + + io->flags &= ~IO_RW; + io->flags |= IO_READ; + io_reload(io); +} + +void +io_set_write(struct io *io) +{ + int mode; + + io_debug("io_set_write(%p)\n", io); + + mode = io->flags & IO_RW; + if (!(mode == 0 || mode == IO_READ)) + errx(1, "io_set_write(): full-duplex or writing"); + + io->flags &= ~IO_RW; + io->flags |= IO_WRITE; + io_reload(io); +} + +const char * +io_error(struct io *io) +{ + return io->error; +} + +void * +io_tls(struct io *io) +{ + return io->tls; +} + +int +io_fileno(struct io *io) +{ + return io->sock; +} + +int +io_paused(struct io *io, int what) +{ + return (io->flags & (IO_PAUSE_IN | IO_PAUSE_OUT)) == what; +} + +/* + * Buffered output functions + */ + +int +io_write(struct io *io, const void *buf, size_t len) +{ + int r; + + r = iobuf_queue(&io->iobuf, buf, len); + + io_reload(io); + + return r; +} + +int +io_writev(struct io *io, const struct iovec *iov, int iovcount) +{ + int r; + + r = iobuf_queuev(&io->iobuf, iov, iovcount); + + io_reload(io); + + return r; +} + +int +io_print(struct io *io, const char *s) +{ + return io_write(io, s, strlen(s)); +} + +int +io_printf(struct io *io, const char *fmt, ...) +{ + va_list ap; + int r; + + va_start(ap, fmt); + r = io_vprintf(io, fmt, ap); + va_end(ap); + + return r; +} + +int +io_vprintf(struct io *io, const char *fmt, va_list ap) +{ + + char *buf; + int len; + + len = vasprintf(&buf, fmt, ap); + if (len == -1) + return -1; + len = io_write(io, buf, len); + free(buf); + + return len; +} + +size_t +io_queued(struct io *io) +{ + return iobuf_queued(&io->iobuf); +} + +/* + * Buffered input functions + */ + +void * +io_data(struct io *io) +{ + return iobuf_data(&io->iobuf); +} + +size_t +io_datalen(struct io *io) +{ + return iobuf_len(&io->iobuf); +} + +char * +io_getline(struct io *io, size_t *sz) +{ + return iobuf_getline(&io->iobuf, sz); +} + +void +io_drop(struct io *io, size_t sz) +{ + return iobuf_drop(&io->iobuf, sz); +} + + +#define IO_READING(io) (((io)->flags & IO_RW) != IO_WRITE) +#define IO_WRITING(io) (((io)->flags & IO_RW) != IO_READ) + +/* + * Setup the necessary events as required by the current io state, + * honouring duplex mode and i/o pauses. + */ +void +io_reload(struct io *io) +{ + short events; + + /* io will be reloaded at release time */ + if (io->flags & IO_HELD) + return; + + iobuf_normalize(&io->iobuf); + +#ifdef IO_TLS + if (io->tls) { + io_reload_tls(io); + return; + } +#endif + + io_debug("io_reload(%p)\n", io); + + events = 0; + if (IO_READING(io) && !(io->flags & IO_PAUSE_IN)) + events = EV_READ; + if (IO_WRITING(io) && !(io->flags & IO_PAUSE_OUT) && io_queued(io)) + events |= EV_WRITE; + + io_reset(io, events, io_dispatch); +} + +/* Set the requested event. */ +void +io_reset(struct io *io, short events, void (*dispatch)(int, short, void*)) +{ + struct timeval tv, *ptv; + + io_debug("io_reset(%p, %s, %p) -> %s\n", + io, io_evstr(events), dispatch, io_strio(io)); + + /* + * Indicate that the event has already been reset so that reload + * is not called on frame_leave. + */ + io->flags |= IO_RESET; + + if (event_initialized(&io->ev)) + event_del(&io->ev); + + /* + * The io is paused by the user, so we don't want the timeout to be + * effective. + */ + if (events == 0) + return; + + event_set(&io->ev, io->sock, events, dispatch, io); + if (io->timeout >= 0) { + tv.tv_sec = io->timeout / 1000; + tv.tv_usec = (io->timeout % 1000) * 1000; + ptv = &tv; + } else + ptv = NULL; + + event_add(&io->ev, ptv); +} + +size_t +io_pending(struct io *io) +{ + return iobuf_len(&io->iobuf); +} + +const char* +io_strflags(int flags) +{ + static char buf[64]; + + buf[0] = '\0'; + + switch (flags & IO_RW) { + case 0: + (void)strlcat(buf, "rw", sizeof buf); + break; + case IO_READ: + (void)strlcat(buf, "R", sizeof buf); + break; + case IO_WRITE: + (void)strlcat(buf, "W", sizeof buf); + break; + case IO_RW: + (void)strlcat(buf, "RW", sizeof buf); + break; + } + + if (flags & IO_PAUSE_IN) + (void)strlcat(buf, ",F_PI", sizeof buf); + if (flags & IO_PAUSE_OUT) + (void)strlcat(buf, ",F_PO", sizeof buf); + + return buf; +} + +const char* +io_evstr(short ev) +{ + static char buf[64]; + char buf2[16]; + int n; + + n = 0; + buf[0] = '\0'; + + if (ev == 0) { + (void)strlcat(buf, "<NONE>", sizeof(buf)); + return buf; + } + + if (ev & EV_TIMEOUT) { + (void)strlcat(buf, "EV_TIMEOUT", sizeof(buf)); + ev &= ~EV_TIMEOUT; + n++; + } + + if (ev & EV_READ) { + if (n) + (void)strlcat(buf, "|", sizeof(buf)); + (void)strlcat(buf, "EV_READ", sizeof(buf)); + ev &= ~EV_READ; + n++; + } + + if (ev & EV_WRITE) { + if (n) + (void)strlcat(buf, "|", sizeof(buf)); + (void)strlcat(buf, "EV_WRITE", sizeof(buf)); + ev &= ~EV_WRITE; + n++; + } + + if (ev & EV_SIGNAL) { + if (n) + (void)strlcat(buf, "|", sizeof(buf)); + (void)strlcat(buf, "EV_SIGNAL", sizeof(buf)); + ev &= ~EV_SIGNAL; + n++; + } + + if (ev) { + if (n) + (void)strlcat(buf, "|", sizeof(buf)); + (void)strlcat(buf, "EV_?=0x", sizeof(buf)); + (void)snprintf(buf2, sizeof(buf2), "%hx", ev); + (void)strlcat(buf, buf2, sizeof(buf)); + } + + return buf; +} + +void +io_dispatch(int fd, short ev, void *humppa) +{ + struct io *io = humppa; + size_t w; + ssize_t n; + int saved_errno; + + io_frame_enter("io_dispatch", io, ev); + + if (ev == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + if (ev & EV_WRITE && (w = io_queued(io))) { + if ((n = iobuf_write(&io->iobuf, io->sock)) < 0) { + if (n == IOBUF_WANT_WRITE) /* kqueue bug? */ + goto read; + if (n == IOBUF_CLOSED) + io_callback(io, IO_DISCONNECTED); + else { + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + io_callback(io, IO_ERROR); + } + goto leave; + } + if (w > io->lowat && w - n <= io->lowat) + io_callback(io, IO_LOWAT); + } + read: + + if (ev & EV_READ) { + iobuf_normalize(&io->iobuf); + if ((n = iobuf_read(&io->iobuf, io->sock)) < 0) { + if (n == IOBUF_CLOSED) + io_callback(io, IO_DISCONNECTED); + else { + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + io_callback(io, IO_ERROR); + } + goto leave; + } + if (n) + io_callback(io, IO_DATAIN); + } + +leave: + io_frame_leave(io); +} + +void +io_callback(struct io *io, int evt) +{ + io->cb(io, evt, io->arg); +} + +int +io_connect(struct io *io, const struct sockaddr *sa, const struct sockaddr *bsa) +{ + int sock, errno_save; + + if ((sock = socket(sa->sa_family, SOCK_STREAM, 0)) == -1) + goto fail; + + io_set_nonblocking(sock); + io_set_nolinger(sock); + + if (bsa && bind(sock, bsa, SA_LEN(bsa)) == -1) + goto fail; + + if (connect(sock, sa, SA_LEN(sa)) == -1) + if (errno != EINPROGRESS) + goto fail; + + io->sock = sock; + io_reset(io, EV_WRITE, io_dispatch_connect); + + return (sock); + + fail: + if (sock != -1) { + errno_save = errno; + close(sock); + errno = errno_save; + io->error = strerror(errno); + } + return (-1); +} + +void +io_dispatch_connect(int fd, short ev, void *humppa) +{ + struct io *io = humppa; + int r, e; + socklen_t sl; + + io_frame_enter("io_dispatch_connect", io, ev); + + if (ev == EV_TIMEOUT) { + close(fd); + io->sock = -1; + io_callback(io, IO_TIMEOUT); + } else { + sl = sizeof(e); + r = getsockopt(fd, SOL_SOCKET, SO_ERROR, &e, &sl); + if (r == -1) { + warn("io_dispatch_connect: getsockopt"); + e = errno; + } + if (e) { + close(fd); + io->sock = -1; + io->error = strerror(e); + io_callback(io, e == ETIMEDOUT ? IO_TIMEOUT : IO_ERROR); + } + else { + io->state = IO_STATE_UP; + io_callback(io, IO_CONNECTED); + } + } + + io_frame_leave(io); +} + +#ifdef IO_TLS + +static const char* +io_tls_error(void) +{ + static char buf[128]; + unsigned long e; + + e = ERR_peek_last_error(); + if (e) { + ERR_error_string(e, buf); + return (buf); + } + + return ("No TLS error"); +} + +int +io_start_tls(struct io *io, void *tls) +{ + int mode; + + mode = io->flags & IO_RW; + if (mode == 0 || mode == IO_RW) + errx(1, "io_start_tls(): full-duplex or unset"); + + if (io->tls) + errx(1, "io_start_tls(): TLS already started"); + io->tls = tls; + + if (SSL_set_fd(io->tls, io->sock) == 0) { + ssl_error("io_start_tls:SSL_set_fd"); + return (-1); + } + + if (mode == IO_WRITE) { + io->state = IO_STATE_CONNECT_TLS; + SSL_set_connect_state(io->tls); + io_reset(io, EV_WRITE, io_dispatch_connect_tls); + } else { + io->state = IO_STATE_ACCEPT_TLS; + SSL_set_accept_state(io->tls); + io_reset(io, EV_READ, io_dispatch_accept_tls); + } + + return (0); +} + +void +io_dispatch_accept_tls(int fd, short event, void *humppa) +{ + struct io *io = humppa; + int e, ret; + + io_frame_enter("io_dispatch_accept_tls", io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + if ((ret = SSL_accept(io->tls)) > 0) { + io->state = IO_STATE_UP; + io_callback(io, IO_TLSREADY); + goto leave; + } + + switch ((e = SSL_get_error(io->tls, ret))) { + case SSL_ERROR_WANT_READ: + io_reset(io, EV_READ, io_dispatch_accept_tls); + break; + case SSL_ERROR_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_accept_tls); + break; + default: + io->error = io_tls_error(); + ssl_error("io_dispatch_accept_tls:SSL_accept"); + io_callback(io, IO_ERROR); + break; + } + + leave: + io_frame_leave(io); +} + +void +io_dispatch_connect_tls(int fd, short event, void *humppa) +{ + struct io *io = humppa; + int e, ret; + + io_frame_enter("io_dispatch_connect_tls", io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + if ((ret = SSL_connect(io->tls)) > 0) { + io->state = IO_STATE_UP; + io_callback(io, IO_TLSREADY); + goto leave; + } + + switch ((e = SSL_get_error(io->tls, ret))) { + case SSL_ERROR_WANT_READ: + io_reset(io, EV_READ, io_dispatch_connect_tls); + break; + case SSL_ERROR_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_connect_tls); + break; + default: + io->error = io_tls_error(); + ssl_error("io_dispatch_connect_ssl:SSL_connect"); + io_callback(io, IO_TLSERROR); + break; + } + + leave: + io_frame_leave(io); +} + +void +io_dispatch_read_tls(int fd, short event, void *humppa) +{ + struct io *io = humppa; + int n, saved_errno; + + io_frame_enter("io_dispatch_read_tls", io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + +again: + iobuf_normalize(&io->iobuf); + switch ((n = iobuf_read_tls(&io->iobuf, (SSL*)io->tls))) { + case IOBUF_WANT_READ: + io_reset(io, EV_READ, io_dispatch_read_tls); + break; + case IOBUF_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_read_tls); + break; + case IOBUF_CLOSED: + io_callback(io, IO_DISCONNECTED); + break; + case IOBUF_ERROR: + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + io_callback(io, IO_ERROR); + break; + case IOBUF_TLSERROR: + io->error = io_tls_error(); + ssl_error("io_dispatch_read_tls:SSL_read"); + io_callback(io, IO_ERROR); + break; + default: + io_debug("io_dispatch_read_tls(...) -> r=%d\n", n); + io_callback(io, IO_DATAIN); + if (current == io && IO_READING(io) && SSL_pending(io->tls)) + goto again; + } + + leave: + io_frame_leave(io); +} + +void +io_dispatch_write_tls(int fd, short event, void *humppa) +{ + struct io *io = humppa; + int n, saved_errno; + size_t w2, w; + + io_frame_enter("io_dispatch_write_tls", io, event); + + if (event == EV_TIMEOUT) { + io_callback(io, IO_TIMEOUT); + goto leave; + } + + w = io_queued(io); + switch ((n = iobuf_write_tls(&io->iobuf, (SSL*)io->tls))) { + case IOBUF_WANT_READ: + io_reset(io, EV_READ, io_dispatch_write_tls); + break; + case IOBUF_WANT_WRITE: + io_reset(io, EV_WRITE, io_dispatch_write_tls); + break; + case IOBUF_CLOSED: + io_callback(io, IO_DISCONNECTED); + break; + case IOBUF_ERROR: + saved_errno = errno; + io->error = strerror(errno); + errno = saved_errno; + io_callback(io, IO_ERROR); + break; + case IOBUF_TLSERROR: + io->error = io_tls_error(); + ssl_error("io_dispatch_write_tls:SSL_write"); + io_callback(io, IO_ERROR); + break; + default: + io_debug("io_dispatch_write_tls(...) -> w=%d\n", n); + w2 = io_queued(io); + if (w > io->lowat && w2 <= io->lowat) + io_callback(io, IO_LOWAT); + break; + } + + leave: + io_frame_leave(io); +} + +void +io_reload_tls(struct io *io) +{ + short ev = 0; + void (*dispatch)(int, short, void*) = NULL; + + switch (io->state) { + case IO_STATE_CONNECT_TLS: + ev = EV_WRITE; + dispatch = io_dispatch_connect_tls; + break; + case IO_STATE_ACCEPT_TLS: + ev = EV_READ; + dispatch = io_dispatch_accept_tls; + break; + case IO_STATE_UP: + ev = 0; + if (IO_READING(io) && !(io->flags & IO_PAUSE_IN)) { + ev = EV_READ; + dispatch = io_dispatch_read_tls; + } + else if (IO_WRITING(io) && !(io->flags & IO_PAUSE_OUT) && + io_queued(io)) { + ev = EV_WRITE; + dispatch = io_dispatch_write_tls; + } + if (!ev) + return; /* paused */ + break; + default: + errx(1, "io_reload_tls(): bad state"); + } + + io_reset(io, ev, dispatch); +} + +#endif /* IO_TLS */ diff --git a/smtpd/ioev.h b/smtpd/ioev.h new file mode 100644 index 00000000..f155a7fc --- /dev/null +++ b/smtpd/ioev.h @@ -0,0 +1,70 @@ +/* $OpenBSD: ioev.h,v 1.18 2019/09/11 04:19:19 martijn Exp $ */ +/* + * Copyright (c) 2012 Eric Faurot <eric@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. + */ + +enum { + IO_CONNECTED = 0, /* connection successful */ + IO_TLSREADY, /* TLS started successfully */ + IO_TLSERROR, /* XXX - needs more work */ + IO_DATAIN, /* new data in input buffer */ + IO_LOWAT, /* output queue running low */ + IO_DISCONNECTED, /* error? */ + IO_TIMEOUT, /* error? */ + IO_ERROR, /* details? */ +}; + +#define IO_IN 0x01 +#define IO_OUT 0x02 + +struct io; + +void io_set_nonblocking(int); +void io_set_nolinger(int); + +struct io *io_new(void); +void io_free(struct io *); +void io_set_read(struct io *); +void io_set_write(struct io *); +void io_set_fd(struct io *, int); +void io_set_callback(struct io *io, void(*)(struct io *, int, void *), void *); +void io_set_timeout(struct io *, int); +void io_set_lowat(struct io *, size_t); +void io_pause(struct io *, int); +void io_resume(struct io *, int); +void io_reload(struct io *); +int io_connect(struct io *, const struct sockaddr *, const struct sockaddr *); +int io_start_tls(struct io *, void *); +const char* io_strio(struct io *); +const char* io_strevent(int); +const char* io_error(struct io *); +void* io_tls(struct io *); +int io_fileno(struct io *); +int io_paused(struct io *, int); + +/* Buffered output functions */ +int io_write(struct io *, const void *, size_t); +int io_writev(struct io *, const struct iovec *, int); +int io_print(struct io *, const char *); +int io_printf(struct io *, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +int io_vprintf(struct io *, const char *, va_list); +size_t io_queued(struct io *); + +/* Buffered input functions */ +void* io_data(struct io *); +size_t io_datalen(struct io *); +char* io_getline(struct io *, size_t *); +void io_drop(struct io *, size_t); diff --git a/smtpd/libressl.c b/smtpd/libressl.c new file mode 100644 index 00000000..57d74389 --- /dev/null +++ b/smtpd/libressl.c @@ -0,0 +1,213 @@ +/* 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 "includes.h" + +#include <sys/types.h> + +#include <limits.h> +#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> + +#include "log.h" +#include "ssl.h" + +#define SSL_ECDH_CURVE "prime256v1" + +/* + * Read a bio that contains our certificate in "PEM" format, + * possibly followed by a sequence of CA certificates that should be + * sent to the peer in the Certificate message. + */ +static int +ssl_ctx_use_certificate_chain_bio(SSL_CTX *ctx, BIO *in) +{ + int ret = 0; + X509 *x = NULL; + + ERR_clear_error(); /* clear error stack for SSL_CTX_use_certificate() */ + + x = PEM_read_bio_X509_AUX(in, NULL, ctx->default_passwd_callback, + ctx->default_passwd_callback_userdata); + if (x == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_CHAIN_FILE, ERR_R_PEM_LIB); + goto end; + } + + ret = SSL_CTX_use_certificate(ctx, x); + + if (ERR_peek_error() != 0) + ret = 0; + /* Key/certificate mismatch doesn't imply ret==0 ... */ + if (ret) { + /* + * If we could set up our certificate, now proceed to + * the CA certificates. + */ + X509 *ca; + int r; + unsigned long err; + + 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) { + r = SSL_CTX_add_extra_chain_cert(ctx, ca); + if (!r) { + X509_free(ca); + ret = 0; + goto end; + } + /* + * Note that we must not free r if it was successfully + * added to the chain (while we must free the main + * certificate, since its reference count is increased + * by SSL_CTX_use_certificate). + */ + } + + /* When the while loop ends, it's usually just EOF. */ + 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 + ret = 0; /* some real error */ + } + +end: + if (x != NULL) + X509_free(x); + return (ret); +} + +int +SSL_CTX_use_certificate_chain_mem(SSL_CTX *ctx, void *buf, int len) +{ + BIO *in; + int ret = 0; + + in = BIO_new_mem_buf(buf, len); + if (in == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_CHAIN_FILE, ERR_R_BUF_LIB); + goto end; + } + + ret = ssl_ctx_use_certificate_chain_bio(ctx, in); + +end: + BIO_free(in); + return (ret); +} + +#ifndef HAVE_SSL_CTX_SET_ECDH_AUTO +void +SSL_CTX_set_ecdh_auto(SSL_CTX *ctx, int enable) +{ + int nid; + EC_KEY *ecdh; + + if (!enable) + return; + + if ((nid = OBJ_sn2nid(SSL_ECDH_CURVE)) == 0) { + ssl_error("ssl_set_ecdh_auto"); + fatal("ssl_set_ecdh_auto: unknown curve name " + SSL_ECDH_CURVE); + } + + if ((ecdh = EC_KEY_new_by_curve_name(nid)) == NULL) { + ssl_error("ssl_set_ecdh_auto"); + fatal("ssl_set_ecdh_auto: unable to create curve " + SSL_ECDH_CURVE); + } + + SSL_CTX_set_tmp_ecdh(ctx, ecdh); + SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE); + EC_KEY_free(ecdh); +} +#endif + +#ifndef HAVE_SSL_CTX_SET_DH_AUTO +void +SSL_CTX_set_dh_auto(SSL_CTX *ctx, int enable) +{ + if (!enable) + return; + + /* stub until OpenSSL catches up with this ... */ + log_warnx("OpenSSL does not support SSL_CTX_set_dh_auto (yet ?)"); + return; +} +#endif diff --git a/smtpd/limit.c b/smtpd/limit.c new file mode 100644 index 00000000..25e7a026 --- /dev/null +++ b/smtpd/limit.c @@ -0,0 +1,124 @@ +/* $OpenBSD: limit.c,v 1.5 2016/06/15 19:59:03 gilles Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <limits.h> +#include <string.h> + +#include "smtpd.h" +#include "log.h" + +void +limit_mta_set_defaults(struct mta_limits *limits) +{ + limits->maxconn_per_host = 10; + limits->maxconn_per_route = 5; + limits->maxconn_per_source = 100; + limits->maxconn_per_connector = 20; + limits->maxconn_per_relay = 100; + limits->maxconn_per_domain = 100; + + limits->conndelay_host = 0; + limits->conndelay_route = 5; + limits->conndelay_source = 0; + limits->conndelay_connector = 0; + limits->conndelay_relay = 2; + limits->conndelay_domain = 0; + + limits->discdelay_route = 3; + + limits->max_mail_per_session = 100; + limits->sessdelay_transaction = 0; + limits->sessdelay_keepalive = 10; + + limits->max_failures_per_session = 25; + + limits->family = AF_UNSPEC; + + limits->task_hiwat = 50; + limits->task_lowat = 30; + limits->task_release = 10; +} + +int +limit_mta_set(struct mta_limits *limits, const char *key, int64_t value) +{ + if (!strcmp(key, "max-conn-per-host")) + limits->maxconn_per_host = value; + else if (!strcmp(key, "max-conn-per-route")) + limits->maxconn_per_route = value; + else if (!strcmp(key, "max-conn-per-source")) + limits->maxconn_per_source = value; + else if (!strcmp(key, "max-conn-per-connector")) + limits->maxconn_per_connector = value; + else if (!strcmp(key, "max-conn-per-relay")) + limits->maxconn_per_relay = value; + else if (!strcmp(key, "max-conn-per-domain")) + limits->maxconn_per_domain = value; + + else if (!strcmp(key, "conn-delay-host")) + limits->conndelay_host = value; + else if (!strcmp(key, "conn-delay-route")) + limits->conndelay_route = value; + else if (!strcmp(key, "conn-delay-source")) + limits->conndelay_source = value; + else if (!strcmp(key, "conn-delay-connector")) + limits->conndelay_connector = value; + else if (!strcmp(key, "conn-delay-relay")) + limits->conndelay_relay = value; + else if (!strcmp(key, "conn-delay-domain")) + limits->conndelay_domain = value; + + else if (!strcmp(key, "reconn-delay-route")) + limits->discdelay_route = value; + + else if (!strcmp(key, "session-mail-max")) + limits->max_mail_per_session = value; + else if (!strcmp(key, "session-transaction-delay")) + limits->sessdelay_transaction = value; + else if (!strcmp(key, "session-keepalive")) + limits->sessdelay_keepalive = value; + + else if (!strcmp(key, "max-failures-per-session")) + limits->max_failures_per_session = value; + + else if (!strcmp(key, "task-hiwat")) + limits->task_hiwat = value; + else if (!strcmp(key, "task-lowat")) + limits->task_lowat = value; + else if (!strcmp(key, "task-release")) + limits->task_release = value; + + else + return (0); + + return (1); +} diff --git a/smtpd/lka.c b/smtpd/lka.c new file mode 100644 index 00000000..6ac21245 --- /dev/null +++ b/smtpd/lka.c @@ -0,0 +1,914 @@ +/* $OpenBSD: lka.c,v 1.243 2019/12/21 10:23:37 gilles Exp $ */ + +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2012 Eric Faurot <eric@faurot.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <sys/uio.h> + +#include <netinet/in.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <netdb.h> +#include <grp.h> /* needed for setgroups */ +#include <imsg.h> +#include <openssl/err.h> +#include <openssl/ssl.h> +#include <pwd.h> +#include <resolv.h> +#include <limits.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" + +static void lka_imsg(struct mproc *, struct imsg *); +static void lka_shutdown(void); +static void lka_sig_handler(int, short, void *); +static int lka_authenticate(const char *, const char *, const char *); +static int lka_credentials(const char *, const char *, char *, size_t); +static int lka_userinfo(const char *, const char *, struct userinfo *); +static int lka_addrname(const char *, const struct sockaddr *, + struct addrname *); +static int lka_mailaddrmap(const char *, const char *, const struct mailaddr *); + +static void proc_timeout(int fd, short event, void *p); + +struct event ev_proc_ready; + +static void +lka_imsg(struct mproc *p, struct imsg *imsg) +{ + struct table *table; + int ret; + struct sockaddr_storage ss; + struct userinfo userinfo; + struct addrname addrname; + struct envelope evp; + struct mailaddr maddr; + struct msg m; + union lookup lk; + char buf[LINE_MAX]; + const char *tablename, *username, *password, *label, *procname; + uint64_t reqid; + int v; + struct timeval tv; + const char *direction; + const char *rdns; + const char *command; + const char *response; + const char *ciphers; + const char *address; + const char *domain; + const char *helomethod; + const char *heloname; + const char *filter_name; + const char *result; + struct sockaddr_storage ss_src, ss_dest; + int filter_response; + int filter_phase; + const char *filter_param; + uint32_t msgid; + uint32_t subsystems; + uint64_t evpid; + size_t msgsz; + int ok; + int fcrdns; + + if (imsg == NULL) + lka_shutdown(); + + switch (imsg->hdr.type) { + + case IMSG_GETADDRINFO: + case IMSG_GETNAMEINFO: + case IMSG_RES_QUERY: + resolver_dispatch_request(p, imsg); + return; + + case IMSG_CERT_INIT: + case IMSG_CERT_CERTIFICATE: + case IMSG_CERT_VERIFY: + cert_dispatch_request(p, imsg); + return; + + case IMSG_MTA_DNS_HOST: + case IMSG_MTA_DNS_MX: + case IMSG_MTA_DNS_MX_PREFERENCE: + dns_imsg(p, imsg); + return; + + case IMSG_SMTP_CHECK_SENDER: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &tablename); + m_get_string(&m, &username); + m_get_mailaddr(&m, &maddr); + m_end(&m); + + ret = lka_mailaddrmap(tablename, username, &maddr); + + m_create(p, IMSG_SMTP_CHECK_SENDER, 0, 0, -1); + m_add_id(p, reqid); + m_add_int(p, ret); + m_close(p); + return; + + case IMSG_SMTP_EXPAND_RCPT: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_envelope(&m, &evp); + m_end(&m); + lka_session(reqid, &evp); + return; + + case IMSG_SMTP_LOOKUP_HELO: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &tablename); + m_get_sockaddr(&m, (struct sockaddr *)&ss); + m_end(&m); + + ret = lka_addrname(tablename, (struct sockaddr*)&ss, + &addrname); + + m_create(p, IMSG_SMTP_LOOKUP_HELO, 0, 0, -1); + m_add_id(p, reqid); + m_add_int(p, ret); + if (ret == LKA_OK) + m_add_string(p, addrname.name); + m_close(p); + return; + + case IMSG_SMTP_AUTHENTICATE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &tablename); + m_get_string(&m, &username); + m_get_string(&m, &password); + m_end(&m); + + if (!tablename[0]) { + m_create(p_parent, IMSG_LKA_AUTHENTICATE, + 0, 0, -1); + m_add_id(p_parent, reqid); + m_add_string(p_parent, username); + m_add_string(p_parent, password); + m_close(p_parent); + return; + } + + ret = lka_authenticate(tablename, username, password); + + m_create(p, IMSG_SMTP_AUTHENTICATE, 0, 0, -1); + m_add_id(p, reqid); + m_add_int(p, ret); + m_close(p); + return; + + case IMSG_MDA_LOOKUP_USERINFO: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &tablename); + m_get_string(&m, &username); + m_end(&m); + + ret = lka_userinfo(tablename, username, &userinfo); + + m_create(p, IMSG_MDA_LOOKUP_USERINFO, 0, 0, -1); + m_add_id(p, reqid); + m_add_int(p, ret); + if (ret == LKA_OK) + m_add_data(p, &userinfo, sizeof(userinfo)); + m_close(p); + return; + + case IMSG_MTA_LOOKUP_CREDENTIALS: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &tablename); + m_get_string(&m, &label); + m_end(&m); + + lka_credentials(tablename, label, buf, sizeof(buf)); + + m_create(p, IMSG_MTA_LOOKUP_CREDENTIALS, 0, 0, -1); + m_add_id(p, reqid); + m_add_string(p, buf); + m_close(p); + return; + + case IMSG_MTA_LOOKUP_SOURCE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &tablename); + m_end(&m); + + table = table_find(env, tablename); + + m_create(p, IMSG_MTA_LOOKUP_SOURCE, 0, 0, -1); + m_add_id(p, reqid); + + if (table == NULL) { + log_warn("warn: source address table %s missing", + tablename); + m_add_int(p, LKA_TEMPFAIL); + } + else { + ret = table_fetch(table, K_SOURCE, &lk); + if (ret == -1) + m_add_int(p, LKA_TEMPFAIL); + else if (ret == 0) + m_add_int(p, LKA_PERMFAIL); + else { + m_add_int(p, LKA_OK); + m_add_sockaddr(p, + (struct sockaddr *)&lk.source.addr); + } + } + m_close(p); + return; + + case IMSG_MTA_LOOKUP_HELO: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &tablename); + m_get_sockaddr(&m, (struct sockaddr *)&ss); + m_end(&m); + + ret = lka_addrname(tablename, (struct sockaddr*)&ss, + &addrname); + + m_create(p, IMSG_MTA_LOOKUP_HELO, 0, 0, -1); + m_add_id(p, reqid); + m_add_int(p, ret); + if (ret == LKA_OK) + m_add_string(p, addrname.name); + m_close(p); + return; + + case IMSG_MTA_LOOKUP_SMARTHOST: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &domain); + m_get_string(&m, &tablename); + m_end(&m); + + table = table_find(env, tablename); + + m_create(p, IMSG_MTA_LOOKUP_SMARTHOST, 0, 0, -1); + m_add_id(p, reqid); + + if (table == NULL) { + log_warn("warn: smarthost table %s missing", tablename); + m_add_int(p, LKA_TEMPFAIL); + } + else { + if (domain == NULL) + ret = table_fetch(table, K_RELAYHOST, &lk); + else + ret = table_lookup(table, K_RELAYHOST, domain, &lk); + + if (ret == -1) + m_add_int(p, LKA_TEMPFAIL); + else if (ret == 0) + m_add_int(p, LKA_PERMFAIL); + else { + m_add_int(p, LKA_OK); + m_add_string(p, lk.relayhost); + } + } + m_close(p); + return; + + case IMSG_CONF_START: + return; + + case IMSG_CONF_END: + if (tracing & TRACE_TABLES) + table_dump_all(env); + + /* fork & exec tables that need it */ + table_open_all(env); + +#if HAVE_PLEDGE + /* revoke proc & exec */ + if (pledge("stdio rpath inet dns getpw recvfd sendfd", + NULL) == -1) + err(1, "pledge"); +#endif + + /* setup proc registering task */ + evtimer_set(&ev_proc_ready, proc_timeout, &ev_proc_ready); + tv.tv_sec = 0; + tv.tv_usec = 10; + evtimer_add(&ev_proc_ready, &tv); + return; + + case IMSG_LKA_OPEN_FORWARD: + lka_session_forward_reply(imsg->data, imsg->fd); + return; + + case IMSG_LKA_AUTHENTICATE: + imsg->hdr.type = IMSG_SMTP_AUTHENTICATE; + m_forward(p_pony, imsg); + return; + + case IMSG_CTL_VERBOSE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + log_trace_verbose(v); + return; + + case IMSG_CTL_PROFILE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + profiling = v; + return; + + case IMSG_CTL_UPDATE_TABLE: + ret = 0; + table = table_find(env, imsg->data); + if (table == NULL) { + log_warnx("warn: Lookup table not found: " + "\"%s\"", (char *)imsg->data); + } else + ret = table_update(table); + + m_compose(p_control, + (ret == 1) ? IMSG_CTL_OK : IMSG_CTL_FAIL, + imsg->hdr.peerid, 0, -1, NULL, 0); + return; + + case IMSG_LKA_PROCESSOR_FORK: + m_msg(&m, imsg); + m_get_string(&m, &procname); + m_get_u32(&m, &subsystems); + m_end(&m); + + m_create(p, IMSG_LKA_PROCESSOR_ERRFD, 0, 0, -1); + m_add_string(p, procname); + m_close(p); + + lka_proc_forked(procname, subsystems, imsg->fd); + return; + + case IMSG_LKA_PROCESSOR_ERRFD: + m_msg(&m, imsg); + m_get_string(&m, &procname); + m_end(&m); + + lka_proc_errfd(procname, imsg->fd); + shutdown(imsg->fd, SHUT_WR); + return; + + case IMSG_REPORT_SMTP_LINK_CONNECT: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_string(&m, &rdns); + m_get_int(&m, &fcrdns); + m_get_sockaddr(&m, (struct sockaddr *)&ss_src); + m_get_sockaddr(&m, (struct sockaddr *)&ss_dest); + m_end(&m); + + lka_report_smtp_link_connect(direction, &tv, reqid, rdns, fcrdns, &ss_src, &ss_dest); + return; + + case IMSG_REPORT_SMTP_LINK_GREETING: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_string(&m, &domain); + m_end(&m); + + lka_report_smtp_link_greeting(direction, reqid, &tv, domain); + return; + + case IMSG_REPORT_SMTP_LINK_DISCONNECT: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_end(&m); + + lka_report_smtp_link_disconnect(direction, &tv, reqid); + return; + + case IMSG_REPORT_SMTP_LINK_IDENTIFY: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_string(&m, &helomethod); + m_get_string(&m, &heloname); + m_end(&m); + + lka_report_smtp_link_identify(direction, &tv, reqid, helomethod, heloname); + return; + + case IMSG_REPORT_SMTP_LINK_TLS: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_string(&m, &ciphers); + m_end(&m); + + lka_report_smtp_link_tls(direction, &tv, reqid, ciphers); + return; + + case IMSG_REPORT_SMTP_LINK_AUTH: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_string(&m, &username); + m_get_string(&m, &result); + m_end(&m); + + lka_report_smtp_link_auth(direction, &tv, reqid, username, result); + return; + + case IMSG_REPORT_SMTP_TX_RESET: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_u32(&m, &msgid); + m_end(&m); + + lka_report_smtp_tx_reset(direction, &tv, reqid, msgid); + return; + + case IMSG_REPORT_SMTP_TX_BEGIN: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_u32(&m, &msgid); + m_end(&m); + + lka_report_smtp_tx_begin(direction, &tv, reqid, msgid); + return; + + case IMSG_REPORT_SMTP_TX_MAIL: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_u32(&m, &msgid); + m_get_string(&m, &address); + m_get_int(&m, &ok); + m_end(&m); + + lka_report_smtp_tx_mail(direction, &tv, reqid, msgid, address, ok); + return; + + case IMSG_REPORT_SMTP_TX_RCPT: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_u32(&m, &msgid); + m_get_string(&m, &address); + m_get_int(&m, &ok); + m_end(&m); + + lka_report_smtp_tx_rcpt(direction, &tv, reqid, msgid, address, ok); + return; + + case IMSG_REPORT_SMTP_TX_ENVELOPE: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_u32(&m, &msgid); + m_get_id(&m, &evpid); + m_end(&m); + + lka_report_smtp_tx_envelope(direction, &tv, reqid, msgid, evpid); + return; + + case IMSG_REPORT_SMTP_TX_DATA: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_u32(&m, &msgid); + m_get_int(&m, &ok); + m_end(&m); + + lka_report_smtp_tx_data(direction, &tv, reqid, msgid, ok); + return; + + case IMSG_REPORT_SMTP_TX_COMMIT: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_u32(&m, &msgid); + m_get_size(&m, &msgsz); + m_end(&m); + + lka_report_smtp_tx_commit(direction, &tv, reqid, msgid, msgsz); + return; + + case IMSG_REPORT_SMTP_TX_ROLLBACK: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_u32(&m, &msgid); + m_end(&m); + + lka_report_smtp_tx_rollback(direction, &tv, reqid, msgid); + return; + + case IMSG_REPORT_SMTP_PROTOCOL_CLIENT: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_string(&m, &command); + m_end(&m); + + lka_report_smtp_protocol_client(direction, &tv, reqid, command); + return; + + case IMSG_REPORT_SMTP_PROTOCOL_SERVER: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_string(&m, &response); + m_end(&m); + + lka_report_smtp_protocol_server(direction, &tv, reqid, response); + return; + + case IMSG_REPORT_SMTP_FILTER_RESPONSE: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_get_int(&m, &filter_phase); + m_get_int(&m, &filter_response); + m_get_string(&m, &filter_param); + m_end(&m); + + lka_report_smtp_filter_response(direction, &tv, reqid, + filter_phase, filter_response, filter_param); + return; + + case IMSG_REPORT_SMTP_TIMEOUT: + m_msg(&m, imsg); + m_get_string(&m, &direction); + m_get_timeval(&m, &tv); + m_get_id(&m, &reqid); + m_end(&m); + + lka_report_smtp_timeout(direction, &tv, reqid); + return; + + case IMSG_FILTER_SMTP_PROTOCOL: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &filter_phase); + m_get_string(&m, &filter_param); + m_end(&m); + + lka_filter_protocol(reqid, filter_phase, filter_param); + return; + + case IMSG_FILTER_SMTP_BEGIN: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &filter_name); + m_end(&m); + + lka_filter_begin(reqid, filter_name); + return; + + case IMSG_FILTER_SMTP_END: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_end(&m); + + lka_filter_end(reqid); + return; + + case IMSG_FILTER_SMTP_DATA_BEGIN: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_end(&m); + + lka_filter_data_begin(reqid); + return; + + case IMSG_FILTER_SMTP_DATA_END: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_end(&m); + + lka_filter_data_end(reqid); + return; + + } + + errx(1, "lka_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type)); +} + +static void +lka_sig_handler(int sig, short event, void *p) +{ + int status; + pid_t pid; + + switch (sig) { + case SIGCHLD: + do { + pid = waitpid(-1, &status, WNOHANG); + } while (pid > 0 || (pid == -1 && errno == EINTR)); + break; + default: + fatalx("lka_sig_handler: unexpected signal"); + } +} + +void +lka_shutdown(void) +{ + log_debug("debug: lookup agent exiting"); + _exit(0); +} + +int +lka(void) +{ + struct passwd *pw; + struct event ev_sigchld; + + purge_config(PURGE_LISTENERS); + + if ((pw = getpwnam(SMTPD_USER)) == NULL) + fatalx("unknown user " SMTPD_USER); + + config_process(PROC_LKA); + + if (initgroups(pw->pw_name, 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"); + + imsg_callback = lka_imsg; + event_init(); + + signal_set(&ev_sigchld, SIGCHLD, lka_sig_handler, NULL); + signal_add(&ev_sigchld, NULL); + signal(SIGINT, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peer(PROC_PARENT); + config_peer(PROC_QUEUE); + config_peer(PROC_CONTROL); + config_peer(PROC_PONY); + + /* Ignore them until we get our config */ + mproc_disable(p_pony); + + lka_report_init(); + lka_filter_init(); + +#if HAVE_PLEDGE + /* proc & exec will be revoked before serving requests */ + if (pledge("stdio rpath inet dns getpw recvfd sendfd proc exec", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + fatalx("exited event loop"); + + return (0); +} + +static void +proc_timeout(int fd, short event, void *p) +{ + struct event *ev = p; + struct timeval tv; + + if (!lka_proc_ready()) + goto reset; + + lka_filter_ready(); + mproc_enable(p_pony); + return; + +reset: + tv.tv_sec = 0; + tv.tv_usec = 10; + evtimer_add(ev, &tv); +} + + +static int +lka_authenticate(const char *tablename, const char *user, const char *password) +{ + struct table *table; + union lookup lk; + + log_debug("debug: lka: authenticating for %s:%s", tablename, user); + table = table_find(env, tablename); + if (table == NULL) { + log_warnx("warn: could not find table %s needed for authentication", + tablename); + return (LKA_TEMPFAIL); + } + + switch (table_lookup(table, K_CREDENTIALS, user, &lk)) { + case -1: + log_warnx("warn: user credentials lookup fail for %s:%s", + tablename, user); + return (LKA_TEMPFAIL); + case 0: + return (LKA_PERMFAIL); + default: + if (crypt_checkpass(password, lk.creds.password) == 0) + return (LKA_OK); + return (LKA_PERMFAIL); + } +} + +static int +lka_credentials(const char *tablename, const char *label, char *dst, size_t sz) +{ + struct table *table; + union lookup lk; + char *buf; + int buflen, r; + + table = table_find(env, tablename); + if (table == NULL) { + log_warnx("warn: credentials table %s missing", tablename); + return (LKA_TEMPFAIL); + } + + dst[0] = '\0'; + + switch (table_lookup(table, K_CREDENTIALS, label, &lk)) { + case -1: + log_warnx("warn: credentials lookup fail for %s:%s", + tablename, label); + return (LKA_TEMPFAIL); + case 0: + log_warnx("warn: credentials not found for %s:%s", + tablename, label); + return (LKA_PERMFAIL); + default: + if ((buflen = asprintf(&buf, "%c%s%c%s", '\0', + lk.creds.username, '\0', lk.creds.password)) == -1) { + log_warn("warn"); + return (LKA_TEMPFAIL); + } + + r = base64_encode((unsigned char *)buf, buflen, dst, sz); + free(buf); + + if (r == -1) { + log_warnx("warn: credentials parse error for %s:%s", + tablename, label); + return (LKA_TEMPFAIL); + } + return (LKA_OK); + } +} + +static int +lka_userinfo(const char *tablename, const char *username, struct userinfo *res) +{ + struct table *table; + union lookup lk; + + log_debug("debug: lka: userinfo %s:%s", tablename, username); + table = table_find(env, tablename); + if (table == NULL) { + log_warnx("warn: cannot find user table %s", tablename); + return (LKA_TEMPFAIL); + } + + switch (table_lookup(table, K_USERINFO, username, &lk)) { + case -1: + log_warnx("warn: failure during userinfo lookup %s:%s", + tablename, username); + return (LKA_TEMPFAIL); + case 0: + return (LKA_PERMFAIL); + default: + *res = lk.userinfo; + return (LKA_OK); + } +} + +static int +lka_addrname(const char *tablename, const struct sockaddr *sa, + struct addrname *res) +{ + struct table *table; + union lookup lk; + const char *source; + + source = sa_to_text(sa); + + log_debug("debug: lka: helo %s:%s", tablename, source); + table = table_find(env, tablename); + if (table == NULL) { + log_warnx("warn: cannot find helo table %s", tablename); + return (LKA_TEMPFAIL); + } + + switch (table_lookup(table, K_ADDRNAME, source, &lk)) { + case -1: + log_warnx("warn: failure during helo lookup %s:%s", + tablename, source); + return (LKA_TEMPFAIL); + case 0: + return (LKA_PERMFAIL); + default: + *res = lk.addrname; + return (LKA_OK); + } +} + +static int +lka_mailaddrmap(const char *tablename, const char *username, const struct mailaddr *maddr) +{ + struct table *table; + struct maddrnode *mn; + union lookup lk; + int found; + + log_debug("debug: lka: mailaddrmap %s:%s", tablename, username); + table = table_find(env, tablename); + if (table == NULL) { + log_warnx("warn: cannot find mailaddrmap table %s", tablename); + return (LKA_TEMPFAIL); + } + + switch (table_lookup(table, K_MAILADDRMAP, username, &lk)) { + case -1: + log_warnx("warn: failure during mailaddrmap lookup %s:%s", + tablename, username); + return (LKA_TEMPFAIL); + case 0: + return (LKA_PERMFAIL); + default: + found = 0; + TAILQ_FOREACH(mn, &lk.maddrmap->queue, entries) { + if (!mailaddr_match(maddr, &mn->mailaddr)) + continue; + found = 1; + break; + } + maddrmap_free(lk.maddrmap); + if (found) + return (LKA_OK); + return (LKA_PERMFAIL); + } + return (LKA_OK); +} diff --git a/smtpd/lka_filter.c b/smtpd/lka_filter.c new file mode 100644 index 00000000..2dc66057 --- /dev/null +++ b/smtpd/lka_filter.c @@ -0,0 +1,1746 @@ +/* $OpenBSD: lka_filter.c,v 1.62 2020/04/24 11:34:07 eric Exp $ */ + +/* + * Copyright (c) 2018 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <netinet/in.h> + +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "smtpd.h" +#include "log.h" + +#define PROTOCOL_VERSION "0.6" + +struct filter; +struct filter_session; +static void filter_protocol_internal(struct filter_session *, uint64_t *, uint64_t, enum filter_phase, const char *); +static void filter_protocol(uint64_t, enum filter_phase, const char *); +static void filter_protocol_next(uint64_t, uint64_t, enum filter_phase); +static void filter_protocol_query(struct filter *, uint64_t, uint64_t, const char *, const char *); + +static void filter_data_internal(struct filter_session *, uint64_t, uint64_t, const char *); +static void filter_data(uint64_t, const char *); +static void filter_data_next(uint64_t, uint64_t, const char *); +static void filter_data_query(struct filter *, uint64_t, uint64_t, const char *); + +static int filter_builtins_notimpl(struct filter_session *, struct filter *, uint64_t, const char *); +static int filter_builtins_connect(struct filter_session *, struct filter *, uint64_t, const char *); +static int filter_builtins_helo(struct filter_session *, struct filter *, uint64_t, const char *); +static int filter_builtins_mail_from(struct filter_session *, struct filter *, uint64_t, const char *); +static int filter_builtins_rcpt_to(struct filter_session *, struct filter *, uint64_t, const char *); +static int filter_builtins_data(struct filter_session *, struct filter *, uint64_t, const char *); +static int filter_builtins_commit(struct filter_session *, struct filter *, uint64_t, const char *); + +static void filter_result_proceed(uint64_t); +static void filter_result_junk(uint64_t); +static void filter_result_rewrite(uint64_t, const char *); +static void filter_result_reject(uint64_t, const char *); +static void filter_result_disconnect(uint64_t, const char *); + +static void filter_session_io(struct io *, int, void *); +void lka_filter_process_response(const char *, const char *); + + +struct filter_session { + uint64_t id; + struct io *io; + + char *lastparam; + + char *filter_name; + struct sockaddr_storage ss_src; + struct sockaddr_storage ss_dest; + char *rdns; + int fcrdns; + + char *helo; + char *username; + char *mail_from; + + enum filter_phase phase; +}; + +static struct filter_exec { + enum filter_phase phase; + const char *phase_name; + int (*func)(struct filter_session *, struct filter *, uint64_t, const char *); +} filter_execs[FILTER_PHASES_COUNT] = { + { FILTER_CONNECT, "connect", filter_builtins_connect }, + { FILTER_HELO, "helo", filter_builtins_helo }, + { FILTER_EHLO, "ehlo", filter_builtins_helo }, + { FILTER_STARTTLS, "starttls", filter_builtins_notimpl }, + { FILTER_AUTH, "auth", filter_builtins_notimpl }, + { FILTER_MAIL_FROM, "mail-from", filter_builtins_mail_from }, + { FILTER_RCPT_TO, "rcpt-to", filter_builtins_rcpt_to }, + { FILTER_DATA, "data", filter_builtins_data }, + { FILTER_DATA_LINE, "data-line", filter_builtins_notimpl }, + { FILTER_RSET, "rset", filter_builtins_notimpl }, + { FILTER_QUIT, "quit", filter_builtins_notimpl }, + { FILTER_NOOP, "noop", filter_builtins_notimpl }, + { FILTER_HELP, "help", filter_builtins_notimpl }, + { FILTER_WIZ, "wiz", filter_builtins_notimpl }, + { FILTER_COMMIT, "commit", filter_builtins_commit }, +}; + +struct filter { + uint64_t id; + uint32_t phases; + const char *name; + const char *proc; + struct filter **chain; + size_t chain_size; + struct filter_config *config; +}; +static struct dict filters; + +struct filter_entry { + TAILQ_ENTRY(filter_entry) entries; + uint64_t id; + const char *name; +}; + +struct filter_chain { + TAILQ_HEAD(, filter_entry) chain[nitems(filter_execs)]; +}; + +static struct dict filter_smtp_in; + +static struct tree sessions; +static int filters_inited; + +static struct dict filter_chains; + +struct reporter_proc { + TAILQ_ENTRY(reporter_proc) entries; + const char *name; +}; +TAILQ_HEAD(reporters, reporter_proc); + +static struct dict report_smtp_in; +static struct dict report_smtp_out; + +static struct smtp_events { + const char *event; +} smtp_events[] = { + { "link-connect" }, + { "link-disconnect" }, + { "link-greeting" }, + { "link-identify" }, + { "link-tls" }, + { "link-auth" }, + + { "tx-reset" }, + { "tx-begin" }, + { "tx-mail" }, + { "tx-rcpt" }, + { "tx-envelope" }, + { "tx-data" }, + { "tx-commit" }, + { "tx-rollback" }, + + { "protocol-client" }, + { "protocol-server" }, + + { "filter-report" }, + { "filter-response" }, + + { "timeout" }, +}; + +static int processors_inited = 0; +static struct dict processors; + +struct processor_instance { + char *name; + struct io *io; + struct io *errfd; + int ready; + uint32_t subsystems; +}; + +static void processor_io(struct io *, int, void *); +static void processor_errfd(struct io *, int, void *); +void lka_filter_process_response(const char *, const char *); + +int +lka_proc_ready(void) +{ + void *iter; + struct processor_instance *pi; + + iter = NULL; + while (dict_iter(&processors, &iter, NULL, (void **)&pi)) + if (!pi->ready) + return 0; + return 1; +} + +static void +lka_proc_config(struct processor_instance *pi) +{ + io_printf(pi->io, "config|smtpd-version|%s\n", SMTPD_VERSION); + io_printf(pi->io, "config|smtp-session-timeout|%d\n", SMTPD_SESSION_TIMEOUT); + if (pi->subsystems & FILTER_SUBSYSTEM_SMTP_IN) + io_printf(pi->io, "config|subsystem|smtp-in\n"); + if (pi->subsystems & FILTER_SUBSYSTEM_SMTP_OUT) + io_printf(pi->io, "config|subsystem|smtp-out\n"); + io_printf(pi->io, "config|ready\n"); +} + +void +lka_proc_forked(const char *name, uint32_t subsystems, int fd) +{ + struct processor_instance *processor; + + if (!processors_inited) { + dict_init(&processors); + processors_inited = 1; + } + + processor = xcalloc(1, sizeof *processor); + processor->name = xstrdup(name); + processor->io = io_new(); + processor->subsystems = subsystems; + + io_set_nonblocking(fd); + + io_set_fd(processor->io, fd); + io_set_callback(processor->io, processor_io, processor->name); + dict_xset(&processors, name, processor); +} + +void +lka_proc_errfd(const char *name, int fd) +{ + struct processor_instance *processor; + + processor = dict_xget(&processors, name); + + io_set_nonblocking(fd); + + processor->errfd = io_new(); + io_set_fd(processor->errfd, fd); + io_set_callback(processor->errfd, processor_errfd, processor->name); + + lka_proc_config(processor); +} + +struct io * +lka_proc_get_io(const char *name) +{ + struct processor_instance *processor; + + processor = dict_xget(&processors, name); + + return processor->io; +} + +static void +processor_register(const char *name, const char *line) +{ + struct processor_instance *processor; + + processor = dict_xget(&processors, name); + + if (strcmp(line, "register|ready") == 0) { + processor->ready = 1; + return; + } + + if (strncmp(line, "register|report|", 16) == 0) { + lka_report_register_hook(name, line+16); + return; + } + + if (strncmp(line, "register|filter|", 16) == 0) { + lka_filter_register_hook(name, line+16); + return; + } + + fatalx("Invalid register line received: %s", line); +} + +static void +processor_io(struct io *io, int evt, void *arg) +{ + struct processor_instance *processor; + const char *name = arg; + char *line = NULL; + ssize_t len; + + switch (evt) { + case IO_DATAIN: + while ((line = io_getline(io, &len)) != NULL) { + if (strncmp("register|", line, 9) == 0) { + processor_register(name, line); + continue; + } + + processor = dict_xget(&processors, name); + if (!processor->ready) + fatalx("Non-register message before register|" + "ready: %s", line); + else if (strncmp(line, "filter-result|", 14) == 0 || + strncmp(line, "filter-dataline|", 16) == 0) + lka_filter_process_response(name, line); + else if (strncmp(line, "report|", 7) == 0) + lka_report_proc(name, line); + else + fatalx("Invalid filter message type: %s", line); + } + } +} + +static void +processor_errfd(struct io *io, int evt, void *arg) +{ + const char *name = arg; + char *line = NULL; + ssize_t len; + + switch (evt) { + case IO_DATAIN: + while ((line = io_getline(io, &len)) != NULL) + log_warnx("%s: %s", name, line); + } +} + +void +lka_filter_init(void) +{ + void *iter; + const char *name; + struct filter *filter; + struct filter_config *filter_config; + size_t i; + char buffer[LINE_MAX]; /* for traces */ + + dict_init(&filters); + dict_init(&filter_chains); + + /* first pass, allocate and init individual filters */ + iter = NULL; + while (dict_iter(env->sc_filters_dict, &iter, &name, (void **)&filter_config)) { + switch (filter_config->filter_type) { + case FILTER_TYPE_BUILTIN: + filter = xcalloc(1, sizeof(*filter)); + filter->name = name; + filter->phases |= (1<<filter_config->phase); + filter->config = filter_config; + dict_set(&filters, name, filter); + log_trace(TRACE_FILTERS, "filters init type=builtin, name=%s, hooks=%08x", + name, filter->phases); + break; + + case FILTER_TYPE_PROC: + filter = xcalloc(1, sizeof(*filter)); + filter->name = name; + filter->proc = filter_config->proc; + filter->config = filter_config; + dict_set(&filters, name, filter); + log_trace(TRACE_FILTERS, "filters init type=proc, name=%s, proc=%s", + name, filter_config->proc); + break; + + case FILTER_TYPE_CHAIN: + break; + } + } + + /* second pass, allocate and init filter chains but don't build yet */ + iter = NULL; + while (dict_iter(env->sc_filters_dict, &iter, &name, (void **)&filter_config)) { + switch (filter_config->filter_type) { + case FILTER_TYPE_CHAIN: + filter = xcalloc(1, sizeof(*filter)); + filter->name = name; + filter->chain = xcalloc(filter_config->chain_size, sizeof(void **)); + filter->chain_size = filter_config->chain_size; + filter->config = filter_config; + + buffer[0] = '\0'; + for (i = 0; i < filter->chain_size; ++i) { + filter->chain[i] = dict_xget(&filters, filter_config->chain[i]); + if (i) + (void)strlcat(buffer, ", ", sizeof buffer); + (void)strlcat(buffer, filter->chain[i]->name, sizeof buffer); + } + log_trace(TRACE_FILTERS, "filters init type=chain, name=%s { %s }", name, buffer); + + dict_set(&filters, name, filter); + break; + + case FILTER_TYPE_BUILTIN: + case FILTER_TYPE_PROC: + break; + } + } +} + +void +lka_filter_register_hook(const char *name, const char *hook) +{ + struct dict *subsystem; + struct filter *filter; + const char *filter_name; + void *iter; + size_t i; + + if (strncasecmp(hook, "smtp-in|", 8) == 0) { + subsystem = &filter_smtp_in; + hook += 8; + } + else + fatalx("Invalid message direction: %s", hook); + + for (i = 0; i < nitems(filter_execs); i++) + if (strcmp(hook, filter_execs[i].phase_name) == 0) + break; + if (i == nitems(filter_execs)) + fatalx("Unrecognized report name: %s", hook); + + iter = NULL; + while (dict_iter(&filters, &iter, &filter_name, (void **)&filter)) + if (filter->proc && strcmp(name, filter->proc) == 0) + filter->phases |= (1<<filter_execs[i].phase); +} + +void +lka_filter_ready(void) +{ + struct filter *filter; + struct filter *subfilter; + const char *filter_name; + struct filter_entry *filter_entry; + struct filter_chain *filter_chain; + void *iter; + size_t i; + size_t j; + + /* all filters are ready, actually build the filter chains */ + iter = NULL; + while (dict_iter(&filters, &iter, &filter_name, (void **)&filter)) { + filter_chain = xcalloc(1, sizeof *filter_chain); + for (i = 0; i < nitems(filter_execs); i++) + TAILQ_INIT(&filter_chain->chain[i]); + dict_set(&filter_chains, filter_name, filter_chain); + + if (filter->chain) { + for (i = 0; i < filter->chain_size; i++) { + subfilter = filter->chain[i]; + for (j = 0; j < nitems(filter_execs); ++j) { + if (subfilter->phases & (1<<j)) { + filter_entry = xcalloc(1, sizeof *filter_entry); + filter_entry->id = generate_uid(); + filter_entry->name = subfilter->name; + TAILQ_INSERT_TAIL(&filter_chain->chain[j], + filter_entry, entries); + } + } + } + continue; + } + + for (i = 0; i < nitems(filter_execs); ++i) { + if (filter->phases & (1<<i)) { + filter_entry = xcalloc(1, sizeof *filter_entry); + filter_entry->id = generate_uid(); + filter_entry->name = filter_name; + TAILQ_INSERT_TAIL(&filter_chain->chain[i], + filter_entry, entries); + } + } + } +} + +int +lka_filter_proc_in_session(uint64_t reqid, const char *proc) +{ + struct filter_session *fs; + struct filter *filter; + size_t i; + + if ((fs = tree_get(&sessions, reqid)) == NULL) + return 0; + + filter = dict_get(&filters, fs->filter_name); + if (filter == NULL || (filter->proc == NULL && filter->chain == NULL)) + return 0; + + if (filter->proc) + return strcmp(filter->proc, proc) == 0 ? 1 : 0; + + for (i = 0; i < filter->chain_size; i++) + if (filter->chain[i]->proc && + strcmp(filter->chain[i]->proc, proc) == 0) + return 1; + + return 0; +} + +void +lka_filter_begin(uint64_t reqid, const char *filter_name) +{ + struct filter_session *fs; + + if (!filters_inited) { + tree_init(&sessions); + filters_inited = 1; + } + + fs = xcalloc(1, sizeof (struct filter_session)); + fs->id = reqid; + fs->filter_name = xstrdup(filter_name); + tree_xset(&sessions, fs->id, fs); + + log_trace(TRACE_FILTERS, "%016"PRIx64" filters session-begin", reqid); +} + +void +lka_filter_end(uint64_t reqid) +{ + struct filter_session *fs; + + fs = tree_xpop(&sessions, reqid); + free(fs->rdns); + free(fs->helo); + free(fs->mail_from); + free(fs->username); + free(fs->lastparam); + free(fs); + log_trace(TRACE_FILTERS, "%016"PRIx64" filters session-end", reqid); +} + +void +lka_filter_data_begin(uint64_t reqid) +{ + struct filter_session *fs; + int sp[2]; + int fd = -1; + + fs = tree_xget(&sessions, reqid); + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) + goto end; + io_set_nonblocking(sp[0]); + io_set_nonblocking(sp[1]); + fd = sp[0]; + fs->io = io_new(); + io_set_fd(fs->io, sp[1]); + io_set_callback(fs->io, filter_session_io, fs); + +end: + m_create(p_pony, IMSG_FILTER_SMTP_DATA_BEGIN, 0, 0, fd); + m_add_id(p_pony, reqid); + m_add_int(p_pony, fd != -1 ? 1 : 0); + m_close(p_pony); + log_trace(TRACE_FILTERS, "%016"PRIx64" filters data-begin fd=%d", reqid, fd); +} + +void +lka_filter_data_end(uint64_t reqid) +{ + struct filter_session *fs; + + fs = tree_xget(&sessions, reqid); + if (fs->io) { + io_free(fs->io); + fs->io = NULL; + } + log_trace(TRACE_FILTERS, "%016"PRIx64" filters data-end", reqid); +} + +static void +filter_session_io(struct io *io, int evt, void *arg) +{ + struct filter_session *fs = arg; + char *line = NULL; + ssize_t len; + + log_trace(TRACE_IO, "filter session: %p: %s %s", fs, io_strevent(evt), + io_strio(io)); + + switch (evt) { + case IO_DATAIN: + nextline: + line = io_getline(fs->io, &len); + /* No complete line received */ + if (line == NULL) + return; + + filter_data(fs->id, line); + + goto nextline; + + case IO_DISCONNECTED: + io_free(fs->io); + fs->io = NULL; + break; + } +} + +void +lka_filter_process_response(const char *name, const char *line) +{ + uint64_t reqid; + uint64_t token; + char buffer[LINE_MAX]; + char *ep = NULL; + char *kind = NULL; + char *qid = NULL; + /*char *phase = NULL;*/ + char *response = NULL; + char *parameter = NULL; + struct filter_session *fs; + + (void)strlcpy(buffer, line, sizeof buffer); + if ((ep = strchr(buffer, '|')) == NULL) + fatalx("Missing token: %s", line); + ep[0] = '\0'; + + kind = buffer; + + qid = ep+1; + if ((ep = strchr(qid, '|')) == NULL) + fatalx("Missing reqid: %s", line); + ep[0] = '\0'; + + reqid = strtoull(qid, &ep, 16); + if (qid[0] == '\0' || *ep != '\0') + fatalx("Invalid reqid: %s", line); + if (errno == ERANGE && reqid == ULLONG_MAX) + fatal("Invalid reqid: %s", line); + + qid = ep+1; + if ((ep = strchr(qid, '|')) == NULL) + fatal("Missing directive: %s", line); + ep[0] = '\0'; + + token = strtoull(qid, &ep, 16); + if (qid[0] == '\0' || *ep != '\0') + fatalx("Invalid token: %s", line); + if (errno == ERANGE && token == ULLONG_MAX) + fatal("Invalid token: %s", line); + + response = ep+1; + + /* session can legitimately disappear on a resume */ + if ((fs = tree_get(&sessions, reqid)) == NULL) + return; + + if (strcmp(kind, "filter-dataline") == 0) { + if (fs->phase != FILTER_DATA_LINE) + fatalx("filter-dataline out of dataline phase"); + filter_data_next(token, reqid, response); + return; + } + if (fs->phase == FILTER_DATA_LINE) + fatalx("filter-result in dataline phase"); + + if ((ep = strchr(response, '|'))) { + parameter = ep + 1; + ep[0] = '\0'; + } + + if (strcmp(response, "proceed") == 0) { + if (parameter != NULL) + fatalx("Unexpected parameter after proceed: %s", line); + filter_protocol_next(token, reqid, 0); + return; + } else if (strcmp(response, "junk") == 0) { + if (parameter != NULL) + fatalx("Unexpected parameter after junk: %s", line); + if (fs->phase == FILTER_COMMIT) + fatalx("filter-reponse junk after DATA"); + filter_result_junk(reqid); + return; + } else { + if (parameter == NULL) + fatalx("Missing parameter: %s", line); + + if (strcmp(response, "rewrite") == 0) + filter_result_rewrite(reqid, parameter); + else if (strcmp(response, "reject") == 0) + filter_result_reject(reqid, parameter); + else if (strcmp(response, "disconnect") == 0) + filter_result_disconnect(reqid, parameter); + else + fatalx("Invalid directive: %s", line); + } +} + +void +lka_filter_protocol(uint64_t reqid, enum filter_phase phase, const char *param) +{ + filter_protocol(reqid, phase, param); +} + +static void +filter_protocol_internal(struct filter_session *fs, uint64_t *token, uint64_t reqid, enum filter_phase phase, const char *param) +{ + struct filter_chain *filter_chain; + struct filter_entry *filter_entry; + struct filter *filter; + struct timeval tv; + const char *phase_name = filter_execs[phase].phase_name; + int resume = 1; + + if (!*token) { + fs->phase = phase; + resume = 0; + } + + /* XXX - this sanity check requires a protocol change, stub for now */ + phase = fs->phase; + if (fs->phase != phase) + fatalx("misbehaving filter"); + + /* based on token, identify the filter_entry we should apply */ + filter_chain = dict_get(&filter_chains, fs->filter_name); + filter_entry = TAILQ_FIRST(&filter_chain->chain[fs->phase]); + if (*token) { + TAILQ_FOREACH(filter_entry, &filter_chain->chain[fs->phase], entries) + if (filter_entry->id == *token) + break; + if (filter_entry == NULL) + fatalx("misbehaving filter"); + filter_entry = TAILQ_NEXT(filter_entry, entries); + } + + /* no filter_entry, we either had none or reached end of chain */ + if (filter_entry == NULL) { + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, resume=%s, " + "action=proceed", + fs->id, phase_name, resume ? "y" : "n"); + filter_result_proceed(reqid); + return; + } + + /* process param with current filter_entry */ + *token = filter_entry->id; + filter = dict_get(&filters, filter_entry->name); + if (filter->proc) { + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, " + "resume=%s, action=deferred, filter=%s", + fs->id, phase_name, resume ? "y" : "n", + filter->name); + filter_protocol_query(filter, filter_entry->id, reqid, + filter_execs[fs->phase].phase_name, param); + return; /* deferred response */ + } + + if (filter_execs[fs->phase].func(fs, filter, reqid, param)) { + if (filter->config->rewrite) { + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, " + "resume=%s, action=rewrite, filter=%s, query=%s, response=%s", + fs->id, phase_name, resume ? "y" : "n", + filter->name, + param, + filter->config->rewrite); + filter_result_rewrite(reqid, filter->config->rewrite); + return; + } + else if (filter->config->disconnect) { + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, " + "resume=%s, action=disconnect, filter=%s, query=%s, response=%s", + fs->id, phase_name, resume ? "y" : "n", + filter->name, + param, + filter->config->disconnect); + filter_result_disconnect(reqid, filter->config->disconnect); + return; + } + else if (filter->config->junk) { + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, " + "resume=%s, action=junk, filter=%s, query=%s", + fs->id, phase_name, resume ? "y" : "n", + filter->name, + param); + filter_result_junk(reqid); + return; + } else if (filter->config->report) { + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, " + "resume=%s, action=report, filter=%s, query=%s response=%s", + fs->id, phase_name, resume ? "y" : "n", + filter->name, + param, filter->config->report); + + gettimeofday(&tv, NULL); + lka_report_filter_report(fs->id, filter->name, 1, + "smtp-in", &tv, filter->config->report); + } else if (filter->config->bypass) { + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, " + "resume=%s, action=bypass, filter=%s, query=%s", + fs->id, phase_name, resume ? "y" : "n", + filter->name, + param); + filter_result_proceed(reqid); + return; + } else { + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, " + "resume=%s, action=reject, filter=%s, query=%s, response=%s", + fs->id, phase_name, resume ? "y" : "n", + filter->name, + param, + filter->config->reject); + filter_result_reject(reqid, filter->config->reject); + return; + } + } + + log_trace(TRACE_FILTERS, "%016"PRIx64" filters protocol phase=%s, " + "resume=%s, action=proceed, filter=%s, query=%s", + fs->id, phase_name, resume ? "y" : "n", + filter->name, + param); + + /* filter_entry resulted in proceed, try next filter */ + filter_protocol_internal(fs, token, reqid, phase, param); + return; +} + +static void +filter_data_internal(struct filter_session *fs, uint64_t token, uint64_t reqid, const char *line) +{ + struct filter_chain *filter_chain; + struct filter_entry *filter_entry; + struct filter *filter; + + if (!token) + fs->phase = FILTER_DATA_LINE; + if (fs->phase != FILTER_DATA_LINE) + fatalx("misbehaving filter"); + + /* based on token, identify the filter_entry we should apply */ + filter_chain = dict_get(&filter_chains, fs->filter_name); + filter_entry = TAILQ_FIRST(&filter_chain->chain[fs->phase]); + if (token) { + TAILQ_FOREACH(filter_entry, &filter_chain->chain[fs->phase], entries) + if (filter_entry->id == token) + break; + if (filter_entry == NULL) + fatalx("misbehaving filter"); + filter_entry = TAILQ_NEXT(filter_entry, entries); + } + + /* no filter_entry, we either had none or reached end of chain */ + if (filter_entry == NULL) { + io_printf(fs->io, "%s\n", line); + return; + } + + /* pass data to the filter */ + filter = dict_get(&filters, filter_entry->name); + filter_data_query(filter, filter_entry->id, reqid, line); +} + +static void +filter_protocol(uint64_t reqid, enum filter_phase phase, const char *param) +{ + struct filter_session *fs; + uint64_t token = 0; + char *nparam = NULL; + + fs = tree_xget(&sessions, reqid); + + switch (phase) { + case FILTER_HELO: + case FILTER_EHLO: + free(fs->helo); + fs->helo = xstrdup(param); + break; + case FILTER_MAIL_FROM: + free(fs->mail_from); + fs->mail_from = xstrdup(param + 1); + *strchr(fs->mail_from, '>') = '\0'; + param = fs->mail_from; + + break; + case FILTER_RCPT_TO: + nparam = xstrdup(param + 1); + *strchr(nparam, '>') = '\0'; + param = nparam; + break; + case FILTER_STARTTLS: + /* TBD */ + break; + default: + break; + } + + free(fs->lastparam); + fs->lastparam = xstrdup(param); + + filter_protocol_internal(fs, &token, reqid, phase, param); + if (nparam) + free(nparam); +} + +static void +filter_protocol_next(uint64_t token, uint64_t reqid, enum filter_phase phase) +{ + struct filter_session *fs; + + /* session can legitimately disappear on a resume */ + if ((fs = tree_get(&sessions, reqid)) == NULL) + return; + + filter_protocol_internal(fs, &token, reqid, phase, fs->lastparam); +} + +static void +filter_data(uint64_t reqid, const char *line) +{ + struct filter_session *fs; + + fs = tree_xget(&sessions, reqid); + + filter_data_internal(fs, 0, reqid, line); +} + +static void +filter_data_next(uint64_t token, uint64_t reqid, const char *line) +{ + struct filter_session *fs; + + /* session can legitimately disappear on a resume */ + if ((fs = tree_get(&sessions, reqid)) == NULL) + return; + + filter_data_internal(fs, token, reqid, line); +} + +static void +filter_protocol_query(struct filter *filter, uint64_t token, uint64_t reqid, const char *phase, const char *param) +{ + int n; + struct filter_session *fs; + struct timeval tv; + + gettimeofday(&tv, NULL); + + fs = tree_xget(&sessions, reqid); + if (strcmp(phase, "connect") == 0) + n = io_printf(lka_proc_get_io(filter->proc), + "filter|%s|%lld.%06ld|smtp-in|%s|%016"PRIx64"|%016"PRIx64"|%s|%s\n", + PROTOCOL_VERSION, + (long long int)tv.tv_sec, tv.tv_usec, + phase, reqid, token, fs->rdns, param); + else + n = io_printf(lka_proc_get_io(filter->proc), + "filter|%s|%lld.%06ld|smtp-in|%s|%016"PRIx64"|%016"PRIx64"|%s\n", + PROTOCOL_VERSION, + (long long int)tv.tv_sec, tv.tv_usec, + phase, reqid, token, param); + if (n == -1) + fatalx("failed to write to processor"); +} + +static void +filter_data_query(struct filter *filter, uint64_t token, uint64_t reqid, const char *line) +{ + int n; + struct timeval tv; + + gettimeofday(&tv, NULL); + + n = io_printf(lka_proc_get_io(filter->proc), + "filter|%s|%lld.%06ld|smtp-in|data-line|" + "%016"PRIx64"|%016"PRIx64"|%s\n", + PROTOCOL_VERSION, + (long long int)tv.tv_sec, tv.tv_usec, + reqid, token, line); + if (n == -1) + fatalx("failed to write to processor"); +} + +static void +filter_result_proceed(uint64_t reqid) +{ + m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1); + m_add_id(p_pony, reqid); + m_add_int(p_pony, FILTER_PROCEED); + m_close(p_pony); +} + +static void +filter_result_junk(uint64_t reqid) +{ + m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1); + m_add_id(p_pony, reqid); + m_add_int(p_pony, FILTER_JUNK); + m_close(p_pony); +} + +static void +filter_result_rewrite(uint64_t reqid, const char *param) +{ + m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1); + m_add_id(p_pony, reqid); + m_add_int(p_pony, FILTER_REWRITE); + m_add_string(p_pony, param); + m_close(p_pony); +} + +static void +filter_result_reject(uint64_t reqid, const char *message) +{ + m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1); + m_add_id(p_pony, reqid); + m_add_int(p_pony, FILTER_REJECT); + m_add_string(p_pony, message); + m_close(p_pony); +} + +static void +filter_result_disconnect(uint64_t reqid, const char *message) +{ + m_create(p_pony, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1); + m_add_id(p_pony, reqid); + m_add_int(p_pony, FILTER_DISCONNECT); + m_add_string(p_pony, message); + m_close(p_pony); +} + + +/* below is code for builtin filters */ + +static int +filter_check_rdns_table(struct filter *filter, enum table_service kind, const char *key) +{ + int ret = 0; + + if (filter->config->rdns_table == NULL) + return 0; + + if (table_match(filter->config->rdns_table, kind, key) > 0) + ret = 1; + + return filter->config->not_rdns_table < 0 ? !ret : ret; +} + +static int +filter_check_rdns_regex(struct filter *filter, const char *key) +{ + int ret = 0; + + if (filter->config->rdns_regex == NULL) + return 0; + + if (table_match(filter->config->rdns_regex, K_REGEX, key) > 0) + ret = 1; + return filter->config->not_rdns_regex < 0 ? !ret : ret; +} + +static int +filter_check_src_table(struct filter *filter, enum table_service kind, const char *key) +{ + int ret = 0; + + if (filter->config->src_table == NULL) + return 0; + + if (table_match(filter->config->src_table, kind, key) > 0) + ret = 1; + return filter->config->not_src_table < 0 ? !ret : ret; +} + +static int +filter_check_src_regex(struct filter *filter, const char *key) +{ + int ret = 0; + + if (filter->config->src_regex == NULL) + return 0; + + if (table_match(filter->config->src_regex, K_REGEX, key) > 0) + ret = 1; + return filter->config->not_src_regex < 0 ? !ret : ret; +} + +static int +filter_check_helo_table(struct filter *filter, enum table_service kind, const char *key) +{ + int ret = 0; + + if (filter->config->helo_table == NULL) + return 0; + + if (table_match(filter->config->helo_table, kind, key) > 0) + ret = 1; + return filter->config->not_helo_table < 0 ? !ret : ret; +} + +static int +filter_check_helo_regex(struct filter *filter, const char *key) +{ + int ret = 0; + + if (filter->config->helo_regex == NULL) + return 0; + + if (table_match(filter->config->helo_regex, K_REGEX, key) > 0) + ret = 1; + return filter->config->not_helo_regex < 0 ? !ret : ret; +} + +static int +filter_check_auth(struct filter *filter, const char *username) +{ + int ret = 0; + + if (!filter->config->auth) + return 0; + + ret = username ? 1 : 0; + + return filter->config->not_auth < 0 ? !ret : ret; +} + +static int +filter_check_auth_table(struct filter *filter, enum table_service kind, const char *key) +{ + int ret = 0; + + if (filter->config->auth_table == NULL) + return 0; + + if (key && table_match(filter->config->auth_table, kind, key) > 0) + ret = 1; + + return filter->config->not_auth_table < 0 ? !ret : ret; +} + +static int +filter_check_auth_regex(struct filter *filter, const char *key) +{ + int ret = 0; + + if (filter->config->auth_regex == NULL) + return 0; + + if (key && table_match(filter->config->auth_regex, K_REGEX, key) > 0) + ret = 1; + return filter->config->not_auth_regex < 0 ? !ret : ret; +} + + +static int +filter_check_mail_from_table(struct filter *filter, enum table_service kind, const char *key) +{ + int ret = 0; + + if (filter->config->mail_from_table == NULL) + return 0; + + if (table_match(filter->config->mail_from_table, kind, key) > 0) + ret = 1; + return filter->config->not_mail_from_table < 0 ? !ret : ret; +} + +static int +filter_check_mail_from_regex(struct filter *filter, const char *key) +{ + int ret = 0; + + if (filter->config->mail_from_regex == NULL) + return 0; + + if (table_match(filter->config->mail_from_regex, K_REGEX, key) > 0) + ret = 1; + return filter->config->not_mail_from_regex < 0 ? !ret : ret; +} + +static int +filter_check_rcpt_to_table(struct filter *filter, enum table_service kind, const char *key) +{ + int ret = 0; + + if (filter->config->rcpt_to_table == NULL) + return 0; + + if (table_match(filter->config->rcpt_to_table, kind, key) > 0) + ret = 1; + return filter->config->not_rcpt_to_table < 0 ? !ret : ret; +} + +static int +filter_check_rcpt_to_regex(struct filter *filter, const char *key) +{ + int ret = 0; + + if (filter->config->rcpt_to_regex == NULL) + return 0; + + if (table_match(filter->config->rcpt_to_regex, K_REGEX, key) > 0) + ret = 1; + return filter->config->not_rcpt_to_regex < 0 ? !ret : ret; +} + +static int +filter_check_fcrdns(struct filter *filter, int fcrdns) +{ + int ret = 0; + + if (!filter->config->fcrdns) + return 0; + + ret = fcrdns == 1; + return filter->config->not_fcrdns < 0 ? !ret : ret; +} + +static int +filter_check_rdns(struct filter *filter, const char *hostname) +{ + int ret = 0; + struct netaddr netaddr; + + if (!filter->config->rdns) + return 0; + + /* this is a hack until smtp session properly deals with lack of rdns */ + ret = strcmp("<unknown>", hostname); + if (ret == 0) + return filter->config->not_rdns < 0 ? !ret : ret; + + /* if text_to_netaddress succeeds, + * we don't have an rDNS so the filter should match + */ + ret = !text_to_netaddr(&netaddr, hostname); + return filter->config->not_rdns < 0 ? !ret : ret; +} + +static int +filter_builtins_notimpl(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param) +{ + return 0; +} + +static int +filter_builtins_global(struct filter_session *fs, struct filter *filter, uint64_t reqid) +{ + return filter_check_fcrdns(filter, fs->fcrdns) || + filter_check_rdns(filter, fs->rdns) || + filter_check_rdns_table(filter, K_DOMAIN, fs->rdns) || + filter_check_rdns_regex(filter, fs->rdns) || + filter_check_src_table(filter, K_NETADDR, ss_to_text(&fs->ss_src)) || + filter_check_src_regex(filter, ss_to_text(&fs->ss_src)) || + filter_check_helo_table(filter, K_DOMAIN, fs->helo) || + filter_check_helo_regex(filter, fs->helo) || + filter_check_auth(filter, fs->username) || + filter_check_auth_table(filter, K_STRING, fs->username) || + filter_check_auth_table(filter, K_CREDENTIALS, fs->username) || + filter_check_auth_regex(filter, fs->username) || + filter_check_mail_from_table(filter, K_MAILADDR, fs->mail_from) || + filter_check_mail_from_regex(filter, fs->mail_from); +} + +static int +filter_builtins_connect(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param) +{ + return filter_builtins_global(fs, filter, reqid); +} + +static int +filter_builtins_helo(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param) +{ + return filter_builtins_global(fs, filter, reqid); +} + +static int +filter_builtins_mail_from(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param) +{ + return filter_builtins_global(fs, filter, reqid); +} + +static int +filter_builtins_rcpt_to(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param) +{ + return filter_builtins_global(fs, filter, reqid) || + filter_check_rcpt_to_table(filter, K_MAILADDR, param) || + filter_check_rcpt_to_regex(filter, param); +} + +static int +filter_builtins_data(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param) +{ + return filter_builtins_global(fs, filter, reqid); +} + +static int +filter_builtins_commit(struct filter_session *fs, struct filter *filter, uint64_t reqid, const char *param) +{ + return filter_builtins_global(fs, filter, reqid); +} + +static void +report_smtp_broadcast(uint64_t, const char *, struct timeval *, const char *, + const char *, ...) __attribute__((__format__ (printf, 5, 6))); + +void +lka_report_init(void) +{ + struct reporters *tailq; + size_t i; + + dict_init(&report_smtp_in); + dict_init(&report_smtp_out); + + for (i = 0; i < nitems(smtp_events); ++i) { + tailq = xcalloc(1, sizeof (struct reporters)); + TAILQ_INIT(tailq); + dict_xset(&report_smtp_in, smtp_events[i].event, tailq); + + tailq = xcalloc(1, sizeof (struct reporters)); + TAILQ_INIT(tailq); + dict_xset(&report_smtp_out, smtp_events[i].event, tailq); + } +} + +void +lka_report_register_hook(const char *name, const char *hook) +{ + struct dict *subsystem; + struct reporter_proc *rp; + struct reporters *tailq; + void *iter; + size_t i; + + if (strncmp(hook, "smtp-in|", 8) == 0) { + subsystem = &report_smtp_in; + hook += 8; + } + else if (strncmp(hook, "smtp-out|", 9) == 0) { + subsystem = &report_smtp_out; + hook += 9; + } + else + fatalx("Invalid message direction: %s", hook); + + if (strcmp(hook, "*") == 0) { + iter = NULL; + while (dict_iter(subsystem, &iter, NULL, (void **)&tailq)) { + rp = xcalloc(1, sizeof *rp); + rp->name = xstrdup(name); + TAILQ_INSERT_TAIL(tailq, rp, entries); + } + return; + } + + for (i = 0; i < nitems(smtp_events); i++) + if (strcmp(hook, smtp_events[i].event) == 0) + break; + if (i == nitems(smtp_events)) + fatalx("Unrecognized report name: %s", hook); + + tailq = dict_get(subsystem, hook); + rp = xcalloc(1, sizeof *rp); + rp->name = xstrdup(name); + TAILQ_INSERT_TAIL(tailq, rp, entries); +} + +static void +report_smtp_broadcast(uint64_t reqid, const char *direction, struct timeval *tv, const char *event, + const char *format, ...) +{ + va_list ap; + struct dict *d; + struct reporters *tailq; + struct reporter_proc *rp; + + if (strcmp("smtp-in", direction) == 0) + d = &report_smtp_in; + + else if (strcmp("smtp-out", direction) == 0) + d = &report_smtp_out; + + else + fatalx("unexpected direction: %s", direction); + + tailq = dict_xget(d, event); + TAILQ_FOREACH(rp, tailq, entries) { + if (!lka_filter_proc_in_session(reqid, rp->name)) + continue; + + va_start(ap, format); + if (io_printf(lka_proc_get_io(rp->name), + "report|%s|%lld.%06ld|%s|%s|%016"PRIx64"%s", + PROTOCOL_VERSION, (long long int)tv->tv_sec, tv->tv_usec, direction, + event, reqid, format[0] != '\n' ? "|" : "") == -1 || + io_vprintf(lka_proc_get_io(rp->name), format, ap) == -1) + fatalx("failed to write to processor"); + va_end(ap); + } +} + +void +lka_report_smtp_link_connect(const char *direction, struct timeval *tv, uint64_t reqid, const char *rdns, + int fcrdns, + const struct sockaddr_storage *ss_src, + const struct sockaddr_storage *ss_dest) +{ + struct filter_session *fs; + char src[NI_MAXHOST + 5]; + char dest[NI_MAXHOST + 5]; + uint16_t src_port = 0; + uint16_t dest_port = 0; + const char *fcrdns_str; + + if (ss_src->ss_family == AF_INET) + src_port = ntohs(((const struct sockaddr_in *)ss_src)->sin_port); + else if (ss_src->ss_family == AF_INET6) + src_port = ntohs(((const struct sockaddr_in6 *)ss_src)->sin6_port); + + if (ss_dest->ss_family == AF_INET) + dest_port = ntohs(((const struct sockaddr_in *)ss_dest)->sin_port); + else if (ss_dest->ss_family == AF_INET6) + dest_port = ntohs(((const struct sockaddr_in6 *)ss_dest)->sin6_port); + + if (strcmp(ss_to_text(ss_src), "local") == 0) { + (void)snprintf(src, sizeof src, "unix:%s", SMTPD_SOCKET); + (void)snprintf(dest, sizeof dest, "unix:%s", SMTPD_SOCKET); + } else { + (void)snprintf(src, sizeof src, "%s:%d", ss_to_text(ss_src), src_port); + (void)snprintf(dest, sizeof dest, "%s:%d", ss_to_text(ss_dest), dest_port); + } + + switch (fcrdns) { + case 1: + fcrdns_str = "pass"; + break; + case 0: + fcrdns_str = "fail"; + break; + default: + fcrdns_str = "error"; + break; + } + + fs = tree_xget(&sessions, reqid); + fs->rdns = xstrdup(rdns); + fs->fcrdns = fcrdns; + fs->ss_src = *ss_src; + fs->ss_dest = *ss_dest; + + report_smtp_broadcast(reqid, direction, tv, "link-connect", + "%s|%s|%s|%s\n", rdns, fcrdns_str, src, dest); +} + +void +lka_report_smtp_link_disconnect(const char *direction, struct timeval *tv, uint64_t reqid) +{ + report_smtp_broadcast(reqid, direction, tv, "link-disconnect", "\n"); +} + +void +lka_report_smtp_link_greeting(const char *direction, uint64_t reqid, + struct timeval *tv, const char *domain) +{ + report_smtp_broadcast(reqid, direction, tv, "link-greeting", "%s\n", + domain); +} + +void +lka_report_smtp_link_auth(const char *direction, struct timeval *tv, uint64_t reqid, + const char *username, const char *result) +{ + struct filter_session *fs; + + if (strcmp(result, "pass") == 0) { + fs = tree_xget(&sessions, reqid); + fs->username = xstrdup(username); + } + report_smtp_broadcast(reqid, direction, tv, "link-auth", "%s|%s\n", + username, result); +} + +void +lka_report_smtp_link_identify(const char *direction, struct timeval *tv, + uint64_t reqid, const char *method, const char *heloname) +{ + report_smtp_broadcast(reqid, direction, tv, "link-identify", "%s|%s\n", + method, heloname); +} + +void +lka_report_smtp_link_tls(const char *direction, struct timeval *tv, uint64_t reqid, const char *ciphers) +{ + report_smtp_broadcast(reqid, direction, tv, "link-tls", "%s\n", + ciphers); +} + +void +lka_report_smtp_tx_reset(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid) +{ + report_smtp_broadcast(reqid, direction, tv, "tx-reset", "%08x\n", + msgid); +} + +void +lka_report_smtp_tx_begin(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid) +{ + report_smtp_broadcast(reqid, direction, tv, "tx-begin", "%08x\n", + msgid); +} + +void +lka_report_smtp_tx_mail(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, const char *address, int ok) +{ + const char *result; + + switch (ok) { + case 1: + result = "ok"; + break; + case 0: + result = "permfail"; + break; + default: + result = "tempfail"; + break; + } + report_smtp_broadcast(reqid, direction, tv, "tx-mail", "%08x|%s|%s\n", + msgid, result, address); +} + +void +lka_report_smtp_tx_rcpt(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, const char *address, int ok) +{ + const char *result; + + switch (ok) { + case 1: + result = "ok"; + break; + case 0: + result = "permfail"; + break; + default: + result = "tempfail"; + break; + } + report_smtp_broadcast(reqid, direction, tv, "tx-rcpt", "%08x|%s|%s\n", + msgid, result, address); +} + +void +lka_report_smtp_tx_envelope(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, uint64_t evpid) +{ + report_smtp_broadcast(reqid, direction, tv, "tx-envelope", + "%08x|%016"PRIx64"\n", msgid, evpid); +} + +void +lka_report_smtp_tx_data(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, int ok) +{ + const char *result; + + switch (ok) { + case 1: + result = "ok"; + break; + case 0: + result = "permfail"; + break; + default: + result = "tempfail"; + break; + } + report_smtp_broadcast(reqid, direction, tv, "tx-data", "%08x|%s\n", + msgid, result); +} + +void +lka_report_smtp_tx_commit(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid, size_t msgsz) +{ + report_smtp_broadcast(reqid, direction, tv, "tx-commit", "%08x|%zd\n", + msgid, msgsz); +} + +void +lka_report_smtp_tx_rollback(const char *direction, struct timeval *tv, uint64_t reqid, uint32_t msgid) +{ + report_smtp_broadcast(reqid, direction, tv, "tx-rollback", "%08x\n", + msgid); +} + +void +lka_report_smtp_protocol_client(const char *direction, struct timeval *tv, uint64_t reqid, const char *command) +{ + report_smtp_broadcast(reqid, direction, tv, "protocol-client", "%s\n", + command); +} + +void +lka_report_smtp_protocol_server(const char *direction, struct timeval *tv, uint64_t reqid, const char *response) +{ + report_smtp_broadcast(reqid, direction, tv, "protocol-server", "%s\n", + response); +} + +void +lka_report_smtp_filter_response(const char *direction, struct timeval *tv, uint64_t reqid, + int phase, int response, const char *param) +{ + const char *phase_name; + const char *response_name; + + switch (phase) { + case FILTER_CONNECT: + phase_name = "connected"; + break; + case FILTER_HELO: + phase_name = "helo"; + break; + case FILTER_EHLO: + phase_name = "ehlo"; + break; + case FILTER_STARTTLS: + phase_name = "tls"; + break; + case FILTER_AUTH: + phase_name = "auth"; + break; + case FILTER_MAIL_FROM: + phase_name = "mail-from"; + break; + case FILTER_RCPT_TO: + phase_name = "rcpt-to"; + break; + case FILTER_DATA: + phase_name = "data"; + break; + case FILTER_DATA_LINE: + phase_name = "data-line"; + break; + case FILTER_RSET: + phase_name = "rset"; + break; + case FILTER_QUIT: + phase_name = "quit"; + break; + case FILTER_NOOP: + phase_name = "noop"; + break; + case FILTER_HELP: + phase_name = "help"; + break; + case FILTER_WIZ: + phase_name = "wiz"; + break; + case FILTER_COMMIT: + phase_name = "commit"; + break; + default: + phase_name = ""; + } + + switch (response) { + case FILTER_PROCEED: + response_name = "proceed"; + break; + case FILTER_JUNK: + response_name = "junk"; + break; + case FILTER_REWRITE: + response_name = "rewrite"; + break; + case FILTER_REJECT: + response_name = "reject"; + break; + case FILTER_DISCONNECT: + response_name = "disconnect"; + break; + default: + response_name = ""; + } + + report_smtp_broadcast(reqid, direction, tv, "filter-response", + "%s|%s%s%s\n", phase_name, response_name, param ? "|" : "", + param ? param : ""); +} + +void +lka_report_smtp_timeout(const char *direction, struct timeval *tv, uint64_t reqid) +{ + report_smtp_broadcast(reqid, direction, tv, "timeout", "\n"); +} + +void +lka_report_filter_report(uint64_t reqid, const char *name, int builtin, + const char *direction, struct timeval *tv, const char *message) +{ + report_smtp_broadcast(reqid, direction, tv, "filter-report", + "%s|%s|%s\n", builtin ? "builtin" : "proc", + name, message); +} + +void +lka_report_proc(const char *name, const char *line) +{ + char buffer[LINE_MAX]; + struct timeval tv; + char *ep, *sp, *direction; + uint64_t reqid; + + if (strlcpy(buffer, line + 7, sizeof(buffer)) >= sizeof(buffer)) + fatalx("Invalid report: line too long: %s", line); + + errno = 0; + tv.tv_sec = strtoll(buffer, &ep, 10); + if (ep[0] != '.' || errno != 0) + fatalx("Invalid report: invalid time: %s", line); + sp = ep + 1; + tv.tv_usec = strtol(sp, &ep, 10); + if (ep[0] != '|' || errno != 0) + fatalx("Invalid report: invalid time: %s", line); + if (ep - sp != 6) + fatalx("Invalid report: invalid time: %s", line); + + direction = ep + 1; + if (strncmp(direction, "smtp-in|", 8) == 0) { + direction[7] = '\0'; + direction += 7; +#if 0 + } else if (strncmp(direction, "smtp-out|", 9) == 0) { + direction[8] = '\0'; + direction += 8; +#endif + } else + fatalx("Invalid report: invalid direction: %s", line); + + reqid = strtoull(sp, &ep, 16); + if (ep[0] != '|' || errno != 0) + fatalx("Invalid report: invalid reqid: %s", line); + sp = ep + 1; + + lka_report_filter_report(reqid, name, 0, direction, &tv, sp); +} diff --git a/smtpd/lka_session.c b/smtpd/lka_session.c new file mode 100644 index 00000000..999e01d6 --- /dev/null +++ b/smtpd/lka_session.c @@ -0,0 +1,556 @@ +/* $OpenBSD: lka_session.c,v 1.93 2019/09/20 17:46:05 gilles Exp $ */ + +/* + * Copyright (c) 2011 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/wait.h> + +#include <netinet/in.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <resolv.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +#define EXPAND_DEPTH 10 + +#define F_WAITING 0x01 + +struct lka_session { + uint64_t id; /* given by smtp */ + + TAILQ_HEAD(, envelope) deliverylist; + struct expand expand; + + int flags; + int error; + const char *errormsg; + struct envelope envelope; + struct xnodes nodes; + /* waiting for fwdrq */ + struct rule *rule; + struct expandnode *node; +}; + +static void lka_expand(struct lka_session *, struct rule *, + struct expandnode *); +static void lka_submit(struct lka_session *, struct rule *, + struct expandnode *); +static void lka_resume(struct lka_session *); + +static int init; +static struct tree sessions; + +void +lka_session(uint64_t id, struct envelope *envelope) +{ + struct lka_session *lks; + struct expandnode xn; + + if (init == 0) { + init = 1; + tree_init(&sessions); + } + + lks = xcalloc(1, sizeof(*lks)); + lks->id = id; + RB_INIT(&lks->expand.tree); + TAILQ_INIT(&lks->deliverylist); + tree_xset(&sessions, lks->id, lks); + + lks->envelope = *envelope; + + TAILQ_INIT(&lks->nodes); + memset(&xn, 0, sizeof xn); + xn.type = EXPAND_ADDRESS; + xn.u.mailaddr = lks->envelope.rcpt; + lks->expand.parent = NULL; + lks->expand.rule = NULL; + lks->expand.queue = &lks->nodes; + expand_insert(&lks->expand, &xn); + lka_resume(lks); +} + +void +lka_session_forward_reply(struct forward_req *fwreq, int fd) +{ + struct lka_session *lks; + struct dispatcher *dsp; + struct rule *rule; + struct expandnode *xn; + int ret; + + lks = tree_xget(&sessions, fwreq->id); + xn = lks->node; + rule = lks->rule; + + lks->flags &= ~F_WAITING; + + switch (fwreq->status) { + case 0: + /* permanent failure while lookup ~/.forward */ + log_trace(TRACE_EXPAND, "expand: ~/.forward failed for user %s", + fwreq->user); + lks->error = LKA_PERMFAIL; + break; + case 1: + if (fd == -1) { + dsp = dict_get(env->sc_dispatchers, lks->rule->dispatcher); + if (dsp->u.local.forward_only) { + log_trace(TRACE_EXPAND, "expand: no .forward " + "for user %s on forward-only rule", fwreq->user); + lks->error = LKA_TEMPFAIL; + } + else if (dsp->u.local.expand_only) { + log_trace(TRACE_EXPAND, "expand: no .forward " + "for user %s and no default action on rule", fwreq->user); + lks->error = LKA_PERMFAIL; + } + else { + log_trace(TRACE_EXPAND, "expand: no .forward for " + "user %s, just deliver", fwreq->user); + lka_submit(lks, rule, xn); + } + } + else { + dsp = dict_get(env->sc_dispatchers, rule->dispatcher); + + /* expand for the current user and rule */ + lks->expand.rule = rule; + lks->expand.parent = xn; + + /* forwards_get() will close the descriptor no matter what */ + ret = forwards_get(fd, &lks->expand); + if (ret == -1) { + log_trace(TRACE_EXPAND, "expand: temporary " + "forward error for user %s", fwreq->user); + lks->error = LKA_TEMPFAIL; + } + else if (ret == 0) { + if (dsp->u.local.forward_only) { + log_trace(TRACE_EXPAND, "expand: empty .forward " + "for user %s on forward-only rule", fwreq->user); + lks->error = LKA_TEMPFAIL; + } + else if (dsp->u.local.expand_only) { + log_trace(TRACE_EXPAND, "expand: empty .forward " + "for user %s and no default action on rule", fwreq->user); + lks->error = LKA_PERMFAIL; + } + else { + log_trace(TRACE_EXPAND, "expand: empty .forward " + "for user %s, just deliver", fwreq->user); + lka_submit(lks, rule, xn); + } + } + } + break; + default: + /* temporary failure while looking up ~/.forward */ + lks->error = LKA_TEMPFAIL; + } + + if (lks->error == LKA_TEMPFAIL && lks->errormsg == NULL) + lks->errormsg = "424 4.2.4 Mailing list expansion problem"; + if (lks->error == LKA_PERMFAIL && lks->errormsg == NULL) + lks->errormsg = "524 5.2.4 Mailing list expansion problem"; + + lka_resume(lks); +} + +static void +lka_resume(struct lka_session *lks) +{ + struct envelope *ep; + struct expandnode *xn; + + if (lks->error) + goto error; + + /* pop next node and expand it */ + while ((xn = TAILQ_FIRST(&lks->nodes))) { + TAILQ_REMOVE(&lks->nodes, xn, tq_entry); + lka_expand(lks, xn->rule, xn); + if (lks->flags & F_WAITING) + return; + if (lks->error) + goto error; + } + + /* delivery list is empty, reject */ + if (TAILQ_FIRST(&lks->deliverylist) == NULL) { + log_trace(TRACE_EXPAND, "expand: lka_done: expanded to empty " + "delivery list"); + lks->error = LKA_PERMFAIL; + lks->errormsg = "524 5.2.4 Mailing list expansion problem"; + } + error: + if (lks->error) { + m_create(p_pony, IMSG_SMTP_EXPAND_RCPT, 0, 0, -1); + m_add_id(p_pony, lks->id); + m_add_int(p_pony, lks->error); + + if (lks->errormsg) + m_add_string(p_pony, lks->errormsg); + else { + if (lks->error == LKA_PERMFAIL) + m_add_string(p_pony, "550 Invalid recipient"); + else if (lks->error == LKA_TEMPFAIL) + m_add_string(p_pony, "451 Temporary failure"); + } + + m_close(p_pony); + while ((ep = TAILQ_FIRST(&lks->deliverylist)) != NULL) { + TAILQ_REMOVE(&lks->deliverylist, ep, entry); + free(ep); + } + } + else { + /* Process the delivery list and submit envelopes to queue */ + while ((ep = TAILQ_FIRST(&lks->deliverylist)) != NULL) { + TAILQ_REMOVE(&lks->deliverylist, ep, entry); + m_create(p_queue, IMSG_LKA_ENVELOPE_SUBMIT, 0, 0, -1); + m_add_id(p_queue, lks->id); + m_add_envelope(p_queue, ep); + m_close(p_queue); + free(ep); + } + + m_create(p_queue, IMSG_LKA_ENVELOPE_COMMIT, 0, 0, -1); + m_add_id(p_queue, lks->id); + m_close(p_queue); + } + + expand_clear(&lks->expand); + tree_xpop(&sessions, lks->id); + free(lks); +} + +static void +lka_expand(struct lka_session *lks, struct rule *rule, struct expandnode *xn) +{ + struct forward_req fwreq; + struct envelope ep; + struct expandnode node; + struct mailaddr maddr; + struct dispatcher *dsp; + struct table *userbase; + int r; + union lookup lk; + char *tag; + const char *srs_decoded; + + if (xn->depth >= EXPAND_DEPTH) { + log_trace(TRACE_EXPAND, "expand: lka_expand: node too deep."); + lks->error = LKA_PERMFAIL; + lks->errormsg = "524 5.2.4 Mailing list expansion problem"; + return; + } + + switch (xn->type) { + case EXPAND_INVALID: + case EXPAND_INCLUDE: + fatalx("lka_expand: unexpected type"); + break; + + case EXPAND_ADDRESS: + + log_trace(TRACE_EXPAND, "expand: lka_expand: address: %s@%s " + "[depth=%d]", + xn->u.mailaddr.user, xn->u.mailaddr.domain, xn->depth); + + + ep = lks->envelope; + ep.dest = xn->u.mailaddr; + if (xn->parent) /* nodes with parent are forward addresses */ + ep.flags |= EF_INTERNAL; + + /* handle SRS */ + if (env->sc_srs_key != NULL && + ep.sender.user[0] == '\0' && + (strncasecmp(ep.rcpt.user, "SRS0=", 5) == 0 || + strncasecmp(ep.rcpt.user, "SRS1=", 5) == 0)) { + srs_decoded = srs_decode(mailaddr_to_text(&ep.rcpt)); + if (srs_decoded && + text_to_mailaddr(&ep.rcpt, srs_decoded)) { + /* flag envelope internal and override rcpt */ + ep.flags |= EF_INTERNAL; + xn->u.mailaddr = ep.rcpt; + lks->envelope = ep; + } + else { + log_warn("SRS failed to decode: %s", + mailaddr_to_text(&ep.rcpt)); + } + } + + /* Pass the node through the ruleset */ + rule = ruleset_match(&ep); + if (rule == NULL || rule->reject) { + lks->error = (errno == EAGAIN) ? + LKA_TEMPFAIL : LKA_PERMFAIL; + break; + } + + dsp = dict_xget(env->sc_dispatchers, rule->dispatcher); + if (dsp->type == DISPATCHER_REMOTE) { + lka_submit(lks, rule, xn); + } + else if (dsp->u.local.table_virtual) { + /* expand */ + lks->expand.rule = rule; + lks->expand.parent = xn; + + /* temporary replace the mailaddr with a copy where + * we eventually strip the '+'-part before lookup. + */ + maddr = xn->u.mailaddr; + xlowercase(maddr.user, xn->u.mailaddr.user, + sizeof maddr.user); + r = aliases_virtual_get(&lks->expand, &maddr); + if (r == -1) { + lks->error = LKA_TEMPFAIL; + log_trace(TRACE_EXPAND, "expand: lka_expand: " + "error in virtual alias lookup"); + } + else if (r == 0) { + lks->error = LKA_PERMFAIL; + log_trace(TRACE_EXPAND, "expand: lka_expand: " + "no aliases for virtual"); + } + if (lks->error == LKA_TEMPFAIL && lks->errormsg == NULL) + lks->errormsg = "424 4.2.4 Mailing list expansion problem"; + if (lks->error == LKA_PERMFAIL && lks->errormsg == NULL) + lks->errormsg = "524 5.2.4 Mailing list expansion problem"; + } + else { + lks->expand.rule = rule; + lks->expand.parent = xn; + xn->rule = rule; + + memset(&node, 0, sizeof node); + node.type = EXPAND_USERNAME; + xlowercase(node.u.user, xn->u.mailaddr.user, + sizeof node.u.user); + expand_insert(&lks->expand, &node); + } + break; + + case EXPAND_USERNAME: + log_trace(TRACE_EXPAND, "expand: lka_expand: username: %s " + "[depth=%d, sameuser=%d]", + xn->u.user, xn->depth, xn->sameuser); + + /* expand aliases with the given rule */ + dsp = dict_xget(env->sc_dispatchers, rule->dispatcher); + + lks->expand.rule = rule; + lks->expand.parent = xn; + + if (!xn->sameuser && + (dsp->u.local.table_alias || dsp->u.local.table_virtual)) { + if (dsp->u.local.table_alias) + r = aliases_get(&lks->expand, xn->u.user); + if (dsp->u.local.table_virtual) + r = aliases_virtual_get(&lks->expand, &xn->u.mailaddr); + if (r == -1) { + log_trace(TRACE_EXPAND, "expand: lka_expand: " + "error in alias lookup"); + lks->error = LKA_TEMPFAIL; + if (lks->errormsg == NULL) + lks->errormsg = "424 4.2.4 Mailing list expansion problem"; + } + if (r) + break; + } + + /* gilles+hackers@ -> gilles@ */ + if ((tag = strchr(xn->u.user, *env->sc_subaddressing_delim)) != NULL) { + *tag++ = '\0'; + (void)strlcpy(xn->subaddress, tag, sizeof xn->subaddress); + } + + userbase = table_find(env, dsp->u.local.table_userbase); + r = table_lookup(userbase, K_USERINFO, xn->u.user, &lk); + if (r == -1) { + log_trace(TRACE_EXPAND, "expand: lka_expand: " + "backend error while searching user"); + lks->error = LKA_TEMPFAIL; + break; + } + if (r == 0) { + log_trace(TRACE_EXPAND, "expand: lka_expand: " + "user-part does not match system user"); + lks->error = LKA_PERMFAIL; + break; + } + xn->realuser = 1; + + if (xn->sameuser && xn->parent->forwarded) { + log_trace(TRACE_EXPAND, "expand: lka_expand: same " + "user, submitting"); + lka_submit(lks, rule, xn); + break; + } + + /* no aliases found, query forward file */ + lks->rule = rule; + lks->node = xn; + xn->forwarded = 1; + + memset(&fwreq, 0, sizeof(fwreq)); + fwreq.id = lks->id; + (void)strlcpy(fwreq.user, lk.userinfo.username, sizeof(fwreq.user)); + (void)strlcpy(fwreq.directory, lk.userinfo.directory, sizeof(fwreq.directory)); + fwreq.uid = lk.userinfo.uid; + fwreq.gid = lk.userinfo.gid; + + m_compose(p_parent, IMSG_LKA_OPEN_FORWARD, 0, 0, -1, + &fwreq, sizeof(fwreq)); + lks->flags |= F_WAITING; + break; + + case EXPAND_FILENAME: + dsp = dict_xget(env->sc_dispatchers, rule->dispatcher); + if (dsp->u.local.forward_only) { + log_trace(TRACE_EXPAND, "expand: filename matched on forward-only rule"); + lks->error = LKA_TEMPFAIL; + break; + } + log_trace(TRACE_EXPAND, "expand: lka_expand: filename: %s " + "[depth=%d]", xn->u.buffer, xn->depth); + lka_submit(lks, rule, xn); + break; + + case EXPAND_ERROR: + dsp = dict_xget(env->sc_dispatchers, rule->dispatcher); + if (dsp->u.local.forward_only) { + log_trace(TRACE_EXPAND, "expand: error matched on forward-only rule"); + lks->error = LKA_TEMPFAIL; + break; + } + log_trace(TRACE_EXPAND, "expand: lka_expand: error: %s " + "[depth=%d]", xn->u.buffer, xn->depth); + if (xn->u.buffer[0] == '4') + lks->error = LKA_TEMPFAIL; + else if (xn->u.buffer[0] == '5') + lks->error = LKA_PERMFAIL; + lks->errormsg = xn->u.buffer; + break; + + case EXPAND_FILTER: + dsp = dict_xget(env->sc_dispatchers, rule->dispatcher); + if (dsp->u.local.forward_only) { + log_trace(TRACE_EXPAND, "expand: filter matched on forward-only rule"); + lks->error = LKA_TEMPFAIL; + break; + } + log_trace(TRACE_EXPAND, "expand: lka_expand: filter: %s " + "[depth=%d]", xn->u.buffer, xn->depth); + lka_submit(lks, rule, xn); + break; + } +} + +static struct expandnode * +lka_find_ancestor(struct expandnode *xn, enum expand_type type) +{ + while (xn && (xn->type != type)) + xn = xn->parent; + if (xn == NULL) { + log_warnx("warn: lka_find_ancestor: no ancestors of type %d", + type); + fatalx(NULL); + } + return (xn); +} + +static void +lka_submit(struct lka_session *lks, struct rule *rule, struct expandnode *xn) +{ + struct envelope *ep; + struct dispatcher *dsp; + const char *user; + const char *format; + + ep = xmemdup(&lks->envelope, sizeof *ep); + (void)strlcpy(ep->dispatcher, rule->dispatcher, sizeof ep->dispatcher); + + dsp = dict_xget(env->sc_dispatchers, ep->dispatcher); + + switch (dsp->type) { + case DISPATCHER_REMOTE: + if (xn->type != EXPAND_ADDRESS) + fatalx("lka_deliver: expect address"); + ep->type = D_MTA; + ep->dest = xn->u.mailaddr; + break; + + case DISPATCHER_BOUNCE: + case DISPATCHER_LOCAL: + if (xn->type != EXPAND_USERNAME && + xn->type != EXPAND_FILENAME && + xn->type != EXPAND_FILTER) + fatalx("lka_deliver: wrong type: %d", xn->type); + + ep->type = D_MDA; + ep->dest = lka_find_ancestor(xn, EXPAND_ADDRESS)->u.mailaddr; + if (xn->type == EXPAND_USERNAME) { + (void)strlcpy(ep->mda_user, xn->u.user, sizeof(ep->mda_user)); + (void)strlcpy(ep->mda_subaddress, xn->subaddress, sizeof(ep->mda_subaddress)); + } + else { + user = !xn->parent->realuser ? + SMTPD_USER : + xn->parent->u.user; + (void)strlcpy(ep->mda_user, user, sizeof (ep->mda_user)); + + /* this battle needs to be fought ... */ + if (xn->type == EXPAND_FILTER && + strcmp(ep->mda_user, SMTPD_USER) == 0) + log_warnx("commands executed from aliases " + "run with %s privileges", SMTPD_USER); + + if (xn->type == EXPAND_FILENAME) + format = PATH_LIBEXEC"/mail.mboxfile -f %%{mbox.from} %s"; + else if (xn->type == EXPAND_FILTER) + format = "%s"; + (void)snprintf(ep->mda_exec, sizeof(ep->mda_exec), + format, xn->u.buffer); + } + break; + } + + TAILQ_INSERT_TAIL(&lks->deliverylist, ep, entry); +} diff --git a/smtpd/log.c b/smtpd/log.c new file mode 100644 index 00000000..14f681e3 --- /dev/null +++ b/smtpd/log.c @@ -0,0 +1,220 @@ +/* $OpenBSD: log.c,v 1.20 2017/03/21 12:06:56 bluhm 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 "includes.h" + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <syslog.h> +#include <errno.h> +#include <time.h> + +static int debug; +static int verbose; +const char *log_procname; + +void log_init(int, int); +void log_procinit(const char *); +void log_setverbose(int); +int log_getverbose(void); +void log_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +__dead void fatalx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + +void +log_init(int n_debug, int facility) +{ + extern char *__progname; + + debug = n_debug; + verbose = n_debug; + log_procinit(__progname); + + if (!debug) + openlog(__progname, LOG_PID | LOG_NDELAY, facility); + + tzset(); +} + +void +log_procinit(const char *procname) +{ + if (procname != NULL) + log_procname = procname; +} + +void +log_setverbose(int v) +{ + verbose = v; +} + +int +log_getverbose(void) +{ + return (verbose); +} + +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; + int saved_errno = errno; + + 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); + + errno = saved_errno; +} + +void +log_warn(const char *emsg, ...) +{ + char *nfmt; + va_list ap; + int saved_errno = errno; + + /* best effort to even work in out of memory situations */ + if (emsg == NULL) + logit(LOG_ERR, "%s", strerror(saved_errno)); + else { + va_start(ap, emsg); + + if (asprintf(&nfmt, "%s: %s", emsg, + strerror(saved_errno)) == -1) { + /* we tried it... */ + vlog(LOG_ERR, emsg, ap); + logit(LOG_ERR, "%s", strerror(saved_errno)); + } else { + vlog(LOG_ERR, nfmt, ap); + free(nfmt); + } + va_end(ap); + } + + errno = saved_errno; +} + +void +log_warnx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_ERR, 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 (verbose > 1) { + va_start(ap, emsg); + vlog(LOG_DEBUG, emsg, ap); + va_end(ap); + } +} + +static void +vfatalc(int code, const char *emsg, va_list ap) +{ + static char s[BUFSIZ]; + const char *sep; + + if (emsg != NULL) { + (void)vsnprintf(s, sizeof(s), emsg, ap); + sep = ": "; + } else { + s[0] = '\0'; + sep = ""; + } + if (code) + logit(LOG_CRIT, "%s: %s%s%s", + log_procname, s, sep, strerror(code)); + else + logit(LOG_CRIT, "%s%s%s", log_procname, sep, s); +} + +void +fatal(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vfatalc(errno, emsg, ap); + va_end(ap); + exit(1); +} + +void +fatalx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vfatalc(0, emsg, ap); + va_end(ap); + exit(1); +} diff --git a/smtpd/log.h b/smtpd/log.h new file mode 100644 index 00000000..81d0973c --- /dev/null +++ b/smtpd/log.h @@ -0,0 +1,52 @@ +/* $OpenBSD: log.h,v 1.8 2018/04/26 20:57:59 eric 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. + */ + +#ifndef LOG_H +#define LOG_H + +#include "openbsd-compat.h" + +#include <syslog.h> + +#include <stdarg.h> +#ifdef HAVE_SYS_CDEFS_H +#include <sys/cdefs.h> +#endif + +void log_init(int, int); +void log_procinit(const char *); +void log_setverbose(int); +int log_getverbose(void); +void log_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +__dead void fatalx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + +#endif /* LOG_H */ diff --git a/smtpd/mail.lmtp.8 b/smtpd/mail.lmtp.8 new file mode 100644 index 00000000..98dee00d --- /dev/null +++ b/smtpd/mail.lmtp.8 @@ -0,0 +1,55 @@ +.\" $OpenBSD: mail.lmtp.8,v 1.1 2017/02/14 15:16:34 gilles Exp $ +.\" +.\" Copyright (c) 2017 Gilles Chehade <gilles@poolp.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: February 14 2017 $ +.Dt MAIL.LMTP 8 +.Os +.Sh NAME +.Nm mail.lmtp +.Nd deliver mail through LMTP +.Sh SYNOPSIS +.Nm mail.lmtp +.Op Fl d Ar destination +.Op Fl f Ar from +.Op Fl l Ar lhlo +.Ar user ... +.Sh DESCRIPTION +.Nm +reads the standard input up to an end-of-file and delivers it to +an LMTP server for each +.Ar user Ns 's +address. +The +.Ar user +must be a valid user name or email address. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl d Ar destination +Specify the destination LMTP address. +.It Fl f Ar from +Specify the sender's name or email address. +.It Fl l Ar lhlo +Specify the LHLO argument used in the LMTP session. +By default, +.Nm mail.lmtp +will default to "localhost". +.El +.Sh EXIT STATUS +.Ex -std mail.lmtp +.Sh SEE ALSO +.Xr mail 1 , +.Xr smtpd 8 diff --git a/smtpd/mail.lmtp.c b/smtpd/mail.lmtp.c new file mode 100644 index 00000000..90b89990 --- /dev/null +++ b/smtpd/mail.lmtp.c @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2017 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/un.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +enum phase { + PHASE_BANNER, + PHASE_HELO, + PHASE_MAILFROM, + PHASE_RCPTTO, + PHASE_DATA, + PHASE_EOM, + PHASE_QUIT +}; + +struct session { + const char *lhlo; + const char *mailfrom; + char *rcptto; + + char **rcpts; + int n_rcpts; +}; + +static int lmtp_connect(const char *); +static void lmtp_engine(int, struct session *); +static void stream_file(FILE *); + +int +main(int argc, char *argv[]) +{ + int ch; + int conn; + const char *destination = "localhost"; + struct session session; + + if (! geteuid()) + errx(EX_TEMPFAIL, "mail.lmtp: may not be executed as root"); + + session.lhlo = "localhost"; + session.mailfrom = getenv("SENDER"); + session.rcptto = NULL; + + while ((ch = getopt(argc, argv, "d:l:f:ru")) != -1) { + switch (ch) { + case 'd': + destination = optarg; + break; + case 'l': + session.lhlo = optarg; + break; + case 'f': + session.mailfrom = optarg; + break; + + case 'r': + session.rcptto = getenv("RECIPIENT"); + break; + + case 'u': + session.rcptto = getenv("USER"); + break; + + default: + break; + } + } + argc -= optind; + argv += optind; + + if (session.mailfrom == NULL) + errx(EX_TEMPFAIL, "sender must be specified with -f"); + + if (argc == 0 && session.rcptto == NULL) + errx(EX_TEMPFAIL, "no recipient was specified"); + + if (session.rcptto) { + session.rcpts = &session.rcptto; + session.n_rcpts = 1; + } + else { + session.rcpts = argv; + session.n_rcpts = argc; + } + + conn = lmtp_connect(destination); + lmtp_engine(conn, &session); + + return (0); +} + +static int +lmtp_connect_inet(const char *destination) +{ + struct addrinfo hints, *res, *res0; + char *destcopy = NULL; + const char *hostname = NULL; + const char *servname = NULL; + const char *cause = NULL; + char *p; + int n, s = -1, save_errno; + + if ((destcopy = strdup(destination)) == NULL) + err(EX_TEMPFAIL, NULL); + + servname = "25"; + hostname = destcopy; + p = destcopy; + if (*p == '[') { + if ((p = strchr(destcopy, ']')) == NULL) + errx(EX_TEMPFAIL, "inet: invalid address syntax"); + + /* remove [ and ] */ + *p = '\0'; + hostname++; + if (strncasecmp(hostname, "IPv6:", 5) == 0) + hostname += 5; + + /* extract port if any */ + switch (*(p+1)) { + case ':': + servname = p+2; + break; + case '\0': + break; + default: + errx(EX_TEMPFAIL, "inet: invalid address syntax"); + } + } + else if ((p = strchr(destcopy, ':')) != NULL) { + *p++ = '\0'; + servname = p; + } + + memset(&hints, 0, sizeof hints); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_NUMERICSERV; + n = getaddrinfo(hostname, servname, &hints, &res0); + if (n) + errx(EX_TEMPFAIL, "inet: %s", gai_strerror(n)); + + for (res = res0; res; res = res->ai_next) { + s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (s == -1) { + cause = "socket"; + continue; + } + + if (connect(s, res->ai_addr, res->ai_addrlen) == -1) { + cause = "connect"; + save_errno = errno; + close(s); + errno = save_errno; + s = -1; + continue; + } + break; + } + + freeaddrinfo(res0); + if (s == -1) + errx(EX_TEMPFAIL, "%s", cause); + + free(destcopy); + return s; +} + +static int +lmtp_connect_unix(const char *destination) +{ + struct sockaddr_un addr; + int s; + + if (*destination != '/') + errx(EX_TEMPFAIL, "unix: path must be absolute"); + + if ((s = socket(PF_LOCAL, SOCK_STREAM, 0)) == -1) + err(EX_TEMPFAIL, NULL); + + memset(&addr, 0, sizeof addr); + addr.sun_family = AF_UNIX; + if (strlcpy(addr.sun_path, destination, sizeof addr.sun_path) + >= sizeof addr.sun_path) + errx(EX_TEMPFAIL, "unix: socket path is too long"); + + if (connect(s, (struct sockaddr *)&addr, sizeof addr) == -1) + err(EX_TEMPFAIL, "connect"); + + return s; +} + +static int +lmtp_connect(const char *destination) +{ + if (destination[0] == '/') + return lmtp_connect_unix(destination); + return lmtp_connect_inet(destination); +} + +static void +lmtp_engine(int fd_read, struct session *session) +{ + int fd_write = 0; + FILE *file_read = 0; + FILE *file_write = 0; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + enum phase phase = PHASE_BANNER; + + if ((fd_write = dup(fd_read)) == -1) + err(EX_TEMPFAIL, "dup"); + + if ((file_read = fdopen(fd_read, "r")) == NULL) + err(EX_TEMPFAIL, "fdopen"); + + if ((file_write = fdopen(fd_write, "w")) == NULL) + err(EX_TEMPFAIL, "fdopen"); + + do { + fflush(file_write); + + if ((linelen = getline(&line, &linesize, file_read)) == -1) { + if (ferror(file_read)) + err(EX_TEMPFAIL, "getline"); + else + errx(EX_TEMPFAIL, "unexpected EOF from LMTP server"); + } + line[strcspn(line, "\n")] = '\0'; + line[strcspn(line, "\r")] = '\0'; + + if (linelen < 4 || + !isdigit((unsigned char)line[0]) || + !isdigit((unsigned char)line[1]) || + !isdigit((unsigned char)line[2]) || + (line[3] != ' ' && line[3] != '-')) + errx(EX_TEMPFAIL, "LMTP server sent an invalid line"); + + if (line[0] != (phase == PHASE_DATA ? '3' : '2')) + errx(EX_TEMPFAIL, "LMTP server error: %s", line); + + if (line[3] == '-') + continue; + + switch (phase) { + + case PHASE_BANNER: + fprintf(file_write, "LHLO %s\r\n", session->lhlo); + phase++; + break; + + case PHASE_HELO: + fprintf(file_write, "MAIL FROM:<%s>\r\n", session->mailfrom); + phase++; + break; + + case PHASE_MAILFROM: + fprintf(file_write, "RCPT TO:<%s>\r\n", session->rcpts[session->n_rcpts - 1]); + if (session->n_rcpts - 1 == 0) { + phase++; + break; + } + session->n_rcpts--; + break; + + case PHASE_RCPTTO: + fprintf(file_write, "DATA\r\n"); + phase++; + break; + + case PHASE_DATA: + stream_file(file_write); + fprintf(file_write, ".\r\n"); + phase++; + break; + + case PHASE_EOM: + fprintf(file_write, "QUIT\r\n"); + phase++; + break; + + case PHASE_QUIT: + exit(0); + } + } while (1); +} + +static void +stream_file(FILE *conn) +{ + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + + while ((linelen = getline(&line, &linesize, stdin)) != -1) { + line[strcspn(line, "\n")] = '\0'; + if (line[0] == '.') + fprintf(conn, "."); + fprintf(conn, "%s\r\n", line); + } + free(line); + if (ferror(stdin)) + err(EX_TEMPFAIL, "getline"); +} diff --git a/smtpd/mail.maildir.8 b/smtpd/mail.maildir.8 new file mode 100644 index 00000000..ce822698 --- /dev/null +++ b/smtpd/mail.maildir.8 @@ -0,0 +1,45 @@ +.\" $OpenBSD: mail.maildir.8,v 1.5 2018/05/30 12:37:57 jmc Exp $ +.\" +.\" Copyright (c) 2017 Gilles Chehade <gilles@poolp.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: May 30 2018 $ +.Dt MAIL.MAILDIR 8 +.Os +.Sh NAME +.Nm mail.maildir +.Nd store mail in a maildir +.Sh SYNOPSIS +.Nm mail.maildir +.Op Fl j +.Op Ar pathname +.Sh DESCRIPTION +.Nm +reads the standard input up to an end-of-file and adds it to the +mail directory located in +.Ar pathname +or to the mail directory +.Pa Maildir +located in the user's home directory. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl j +Scan message for X-Spam and move to Junk folder if result is positive. +.El +.Sh EXIT STATUS +.Ex -std mail.maildir +.Sh SEE ALSO +.Xr mail 1 , +.Xr smtpd 8 diff --git a/smtpd/mail.maildir.c b/smtpd/mail.maildir.c new file mode 100644 index 00000000..fe6adba6 --- /dev/null +++ b/smtpd/mail.maildir.c @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2017 Gilles Chehade <gilles@poolp.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 "includes.h" + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +#include <sys/types.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <sysexits.h> +#include <unistd.h> + +#define MAILADDR_ESCAPE "!#$%&'*/?^`{|}~" + +static int maildir_subdir(const char *, char *, size_t); +static void maildir_mkdirs(const char *); +static void maildir_engine(const char *, int); +static int mkdirs_component(const char *, mode_t); +static int mkdirs(const char *, mode_t); + +int +main(int argc, char *argv[]) +{ + int ch; + int junk = 0; + + if (! geteuid()) + errx(1, "mail.maildir: may not be executed as root"); + + while ((ch = getopt(argc, argv, "j")) != -1) { + switch (ch) { + case 'j': + junk = 1; + break; + default: + break; + } + } + argc -= optind; + argv += optind; + + if (argc > 1) + errx(1, "mail.maildir: only one maildir is allowed"); + + maildir_engine(argv[0], junk); + + return (0); +} + +static int +maildir_subdir(const char *extension, char *dest, size_t len) +{ + char *sanitized; + + if (strlcpy(dest, extension, len) >= len) + return 0; + + for (sanitized = dest; *sanitized; sanitized++) + if (strchr(MAILADDR_ESCAPE, *sanitized)) + *sanitized = ':'; + + return 1; +} + +static void +maildir_mkdirs(const char *dirname) +{ + uint i; + int ret; + char pathname[PATH_MAX]; + char *subdirs[] = { "cur", "tmp", "new" }; + + if (mkdirs(dirname, 0700) == -1 && errno != EEXIST) { + if (errno == EINVAL || errno == ENAMETOOLONG) + err(1, NULL); + err(EX_TEMPFAIL, NULL); + } + + for (i = 0; i < nitems(subdirs); ++i) { + ret = snprintf(pathname, sizeof pathname, "%s/%s", dirname, + subdirs[i]); + if (ret < 0 || (size_t)ret >= sizeof pathname) + errc(1, ENAMETOOLONG, "%s/%s", dirname, subdirs[i]); + if (mkdir(pathname, 0700) == -1 && errno != EEXIST) + err(EX_TEMPFAIL, NULL); + } +} + +static void +maildir_engine(const char *dirname, int junk) +{ + char rootpath[PATH_MAX]; + char junkpath[PATH_MAX]; + char extpath[PATH_MAX]; + char subdir[PATH_MAX]; + char filename[PATH_MAX]; + char hostname[HOST_NAME_MAX+1]; + + char tmp[PATH_MAX]; + char new[PATH_MAX]; + + int ret; + + int fd; + FILE *fp; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + struct stat sb; + char *home; + char *extension; + + int is_junk = 0; + int in_hdr = 1; + + if (dirname == NULL) { + if ((home = getenv("HOME")) == NULL) + err(1, NULL); + ret = snprintf(rootpath, sizeof rootpath, "%s/Maildir", home); + if (ret < 0 || (size_t)ret >= sizeof rootpath) + errc(1, ENAMETOOLONG, "%s/Maildir", home); + dirname = rootpath; + } + maildir_mkdirs(dirname); + + if (junk) { + /* create Junk subdirectory */ + ret = snprintf(junkpath, sizeof junkpath, "%s/.Junk", dirname); + if (ret < 0 || (size_t)ret >= sizeof junkpath) + errc(1, ENAMETOOLONG, "%s/.Junk", dirname); + maildir_mkdirs(junkpath); + } + + if ((extension = getenv("EXTENSION")) != NULL) { + if (maildir_subdir(extension, subdir, sizeof(subdir)) && + subdir[0]) { + ret = snprintf(extpath, sizeof extpath, "%s/.%s", + dirname, subdir); + if (ret < 0 || (size_t)ret >= sizeof extpath) + errc(1, ENAMETOOLONG, "%s/.%s", + dirname, subdir); + if (stat(extpath, &sb) != -1) { + dirname = extpath; + maildir_mkdirs(dirname); + } + } + } + + if (gethostname(hostname, sizeof hostname) != 0) + (void)strlcpy(hostname, "localhost", sizeof hostname); + + (void)snprintf(filename, sizeof filename, "%lld.%08x.%s", + (long long int) time(NULL), + arc4random(), + hostname); + + (void)snprintf(tmp, sizeof tmp, "%s/tmp/%s", dirname, filename); + + fd = open(tmp, O_CREAT | O_EXCL | O_WRONLY, 0600); + if (fd == -1) + err(EX_TEMPFAIL, NULL); + if ((fp = fdopen(fd, "w")) == NULL) + err(EX_TEMPFAIL, NULL); + + while ((linelen = getline(&line, &linesize, stdin)) != -1) { + line[strcspn(line, "\n")] = '\0'; + if (line[0] == '\0') + in_hdr = 0; + if (junk && in_hdr && + (strcasecmp(line, "x-spam: yes") == 0 || + strcasecmp(line, "x-spam-flag: yes") == 0)) + is_junk = 1; + fprintf(fp, "%s\n", line); + } + free(line); + if (ferror(stdin)) + err(EX_TEMPFAIL, NULL); + + if (fflush(fp) == EOF || + ferror(fp) || + fsync(fd) == -1 || + fclose(fp) == EOF) + err(EX_TEMPFAIL, NULL); + + (void)snprintf(new, sizeof new, "%s/new/%s", + is_junk ? junkpath : dirname, filename); + + if (rename(tmp, new) == -1) + err(EX_TEMPFAIL, NULL); + + exit(0); +} + + +static int +mkdirs_component(const char *path, mode_t mode) +{ + struct stat sb; + + if (stat(path, &sb) == -1) { + if (errno != ENOENT) + return 0; + if (mkdir(path, mode | S_IWUSR | S_IXUSR) == -1) + return 0; + } + else if (!S_ISDIR(sb.st_mode)) { + errno = ENOTDIR; + return 0; + } + + return 1; +} + +static int +mkdirs(const char *path, mode_t mode) +{ + char buf[PATH_MAX]; + int i = 0; + int done = 0; + const char *p; + + /* absolute path required */ + if (*path != '/') { + errno = EINVAL; + return 0; + } + + /* make sure we don't exceed PATH_MAX */ + if (strlen(path) >= sizeof buf) { + errno = ENAMETOOLONG; + return 0; + } + + memset(buf, 0, sizeof buf); + for (p = path; *p; p++) { + if (*p == '/') { + if (buf[0] != '\0') + if (!mkdirs_component(buf, mode)) + return 0; + while (*p == '/') + p++; + buf[i++] = '/'; + buf[i++] = *p; + if (*p == '\0' && ++done) + break; + continue; + } + buf[i++] = *p; + } + if (!done) + if (!mkdirs_component(buf, mode)) + return 0; + + if (chmod(path, mode) == -1) + return 0; + + return 1; +} diff --git a/smtpd/mail.mboxfile.8 b/smtpd/mail.mboxfile.8 new file mode 100644 index 00000000..015adcb5 --- /dev/null +++ b/smtpd/mail.mboxfile.8 @@ -0,0 +1,34 @@ +.\" $OpenBSD: mail.mboxfile.8,v 1.1 2018/07/25 10:19:28 gilles Exp $ +.\" +.\" Copyright (c) 2017 Gilles Chehade <gilles@poolp.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: July 25 2018 $ +.Dt MAIL.MDA 8 +.Os +.Sh NAME +.Nm mail.mboxfile +.Nd deliver mail to a file in mbox format +.Sh SYNOPSIS +.Nm mail.mboxfile +.Ar file +.Sh DESCRIPTION +.Nm +appends mail to a file in mbox format and acknowledges delivery success or failure +with its exit status. +.Sh EXIT STATUS +.Ex -std mail.mboxfile +.Sh SEE ALSO +.Xr mail 1 , +.Xr smtpd 8 diff --git a/smtpd/mail.mboxfile.c b/smtpd/mail.mboxfile.c new file mode 100644 index 00000000..097a8d96 --- /dev/null +++ b/smtpd/mail.mboxfile.c @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <time.h> +#include <unistd.h> + +static void mboxfile_engine(const char *sender, const char *filename); + +int +main(int argc, char *argv[]) +{ + int ch; + char *sender = "<unknown>"; + + if (! geteuid()) + errx(1, "mail.mboxfile: may not be executed as root"); + + while ((ch = getopt(argc, argv, "f:")) != -1) { + switch (ch) { + case 'f': + sender = optarg; + break; + default: + break; + } + } + argc -= optind; + argv += optind; + + if (argc > 1) + errx(1, "mail.mboxfile: only one mboxfile is allowed"); + + mboxfile_engine(sender, argv[0]); + + return (0); +} + +static void +mboxfile_engine(const char *sender, const char *filename) +{ + int fd; + FILE *fp; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + time_t now; + + time(&now); + +#ifndef O_EXLOCK +#define O_EXLOCK 0 +#endif + fd = open(filename, O_CREAT | O_APPEND | O_WRONLY | O_EXLOCK, 0600); +#ifndef HAVE_O_EXLOCK + /* XXX : do something! */ +#endif + if (fd == -1) + err(EX_TEMPFAIL, NULL); + + if ((fp = fdopen(fd, "w")) == NULL) + err(EX_TEMPFAIL, NULL); + + fprintf(fp, "From %s %s", sender, ctime(&now)); + while ((linelen = getline(&line, &linesize, stdin)) != -1) { + line[strcspn(line, "\n")] = '\0'; + if (strncmp(line, "From ", 5) == 0) + fprintf(fp, ">%s\n", line); + else + fprintf(fp, "%s\n", line); + } + fprintf(fp, "\n"); + free(line); + if (ferror(stdin)) + err(EX_TEMPFAIL, NULL); + + if (fflush(fp) == EOF || + ferror(fp) || + (fsync(fd) == -1 && errno != EINVAL) || + fclose(fp) == EOF) + err(EX_TEMPFAIL, NULL); +} diff --git a/smtpd/mail.mda.8 b/smtpd/mail.mda.8 new file mode 100644 index 00000000..61fed733 --- /dev/null +++ b/smtpd/mail.mda.8 @@ -0,0 +1,35 @@ +.\" $OpenBSD: mail.mda.8,v 1.1 2017/08/09 07:56:10 gilles Exp $ +.\" +.\" Copyright (c) 2017 Gilles Chehade <gilles@poolp.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: August 9 2017 $ +.Dt MAIL.MDA 8 +.Os +.Sh NAME +.Nm mail.mda +.Nd deliver mail to a program +.Sh SYNOPSIS +.Nm mail.mda +.Ar program +.Sh DESCRIPTION +.Nm +executes the program and its parameters. +The program must read from the standard input up to an end-of-file +and acknowledge delivery success or failure with its exit status. +.Sh EXIT STATUS +.Ex -std mail.mda +.Sh SEE ALSO +.Xr mail 1 , +.Xr smtpd 8 diff --git a/smtpd/mail.mda.c b/smtpd/mail.mda.c new file mode 100644 index 00000000..23958071 --- /dev/null +++ b/smtpd/mail.mda.c @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2017 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/wait.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +int +main(int argc, char *argv[]) +{ + int ch; + int ret; + + if (! geteuid()) + errx(1, "mail.mda: may not be executed as root"); + + while ((ch = getopt(argc, argv, "")) != -1) { + switch (ch) { + default: + break; + } + } + argc -= optind; + argv += optind; + + if (argc == 0) + errx(1, "mail.mda: command required"); + + if (argc > 1) + errx(1, "mail.mda: only one command is supported"); + + /* could not obtain a shell or could not obtain wait status, + * tempfail */ + if ((ret = system(argv[0])) == -1) + errx(EX_TEMPFAIL, "%s", strerror(errno)); + + /* not exited properly but we have no details, + * tempfail */ + if (! WIFEXITED(ret)) + exit(EX_TEMPFAIL); + + exit(WEXITSTATUS(ret)); +} diff --git a/smtpd/mail/Makefile b/smtpd/mail/Makefile new file mode 100644 index 00000000..b2bc2a26 --- /dev/null +++ b/smtpd/mail/Makefile @@ -0,0 +1,20 @@ +# $OpenBSD: Makefile,v 1.8 2018/07/25 10:19:28 gilles Exp $ +.PATH: ${.CURDIR}/.. + +PROGS= mail.lmtp mail.maildir mail.mboxfile mail.mda + +MAN= mail.lmtp.8 mail.maildir.8 mail.mboxfile.8 mail.mda.8 + +BINOWN= root +BINGRP= wheel + +BINDIR= /usr/libexec + +CFLAGS+= -fstack-protector-all +CFLAGS+= -Wall -Wstrict-prototypes -Wmissing-prototypes +CFLAGS+= -Wmissing-declarations +CFLAGS+= -Wshadow -Wpointer-arith -Wcast-qual +CFLAGS+= -Wsign-compare +CFLAGS+= -Werror-implicit-function-declaration + +.include <bsd.prog.mk> diff --git a/smtpd/mailaddr.c b/smtpd/mailaddr.c new file mode 100644 index 00000000..4346e3dc --- /dev/null +++ b/smtpd/mailaddr.c @@ -0,0 +1,135 @@ +/* $OpenBSD: mailaddr.c,v 1.3 2018/05/31 21:06:12 gilles Exp $ */ + +/* + * Copyright (c) 2015 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <limits.h> +#include <stdlib.h> +#include <string.h> + +#include "smtpd.h" +#include "log.h" + +static int +mailaddr_line_split(char **line, char **ret) +{ + static char buffer[LINE_MAX]; + int esc, dq, sq; + size_t i; + char *s; + + memset(buffer, 0, sizeof buffer); + esc = dq = sq = 0; + i = 0; + for (s = *line; (*s) && (i < sizeof(buffer)); ++s) { + if (esc) { + buffer[i++] = *s; + esc = 0; + continue; + } + if (*s == '\\') { + esc = 1; + continue; + } + if (*s == ',' && !dq && !sq) { + *ret = buffer; + *line = s+1; + return (1); + } + + buffer[i++] = *s; + esc = 0; + + if (*s == '"' && !sq) + dq ^= 1; + if (*s == '\'' && !dq) + sq ^= 1; + } + + if (esc || dq || sq || i == sizeof(buffer)) + return (-1); + + *ret = buffer; + *line = s; + return (i ? 1 : 0); +} + +int +mailaddr_line(struct maddrmap *maddrmap, const char *s) +{ + struct maddrnode mn; + char buffer[LINE_MAX]; + char *p, *subrcpt; + int ret; + + memset(buffer, 0, sizeof buffer); + if (strlcpy(buffer, s, sizeof buffer) >= sizeof buffer) + return 0; + + p = buffer; + while ((ret = mailaddr_line_split(&p, &subrcpt)) > 0) { + subrcpt = strip(subrcpt); + if (subrcpt[0] == '\0') + continue; + if (!text_to_mailaddr(&mn.mailaddr, subrcpt)) + return 0; + log_debug("subrcpt: [%s]", subrcpt); + maddrmap_insert(maddrmap, &mn); + } + + if (ret >= 0) + return 1; + /* expand_line_split() returned < 0 */ + return 0; +} + +void +maddrmap_init(struct maddrmap *maddrmap) +{ + TAILQ_INIT(&maddrmap->queue); +} + +void +maddrmap_insert(struct maddrmap *maddrmap, struct maddrnode *maddrnode) +{ + struct maddrnode *mn; + + mn = xmemdup(maddrnode, sizeof *maddrnode); + TAILQ_INSERT_TAIL(&maddrmap->queue, mn, entries); +} + +void +maddrmap_free(struct maddrmap *maddrmap) +{ + struct maddrnode *mn; + + while ((mn = TAILQ_FIRST(&maddrmap->queue))) { + TAILQ_REMOVE(&maddrmap->queue, mn, entries); + free(mn); + } + free(maddrmap); +} diff --git a/smtpd/makemap.8 b/smtpd/makemap.8 new file mode 100644 index 00000000..674bef6f --- /dev/null +++ b/smtpd/makemap.8 @@ -0,0 +1,174 @@ +.\" $OpenBSD: makemap.8,v 1.30 2018/11/25 14:41:16 gilles Exp $ +.\" +.\" Copyright (c) 2009 Jacek Masiulaniec <jacekm@openbsd.org> +.\" Copyright (c) 2008-2009 Gilles Chehade <gilles@poolp.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 25 2018 $ +.Dt MAKEMAP 8 +.Os +.Sh NAME +.Nm makemap +.Nd create database maps for smtpd +.Sh SYNOPSIS +.Nm makemap +.Op Fl U +.Op Fl d Ar dbtype +.Op Fl o Ar dbfile +.Op Fl t Ar type +.Ar file +.Sh DESCRIPTION +Maps provide a generic interface for associating textual key to a value. +Such associations may be accessed through a plaintext file, database, or DNS. +The format of these file types is described below. +.Nm +itself creates the database maps used by keyed map lookups specified in +.Xr smtpd.conf 5 . +.Pp +.Nm +reads input from +.Ar file +and writes data to a file whose name is made by adding a +.Dq .db +suffix to +.Ar file . +The current line can be extended over multiple lines using a backslash +.Pq Sq \e . +Comments can be put anywhere in the file using a hash mark +.Pq Sq # , +and extend to the end of the current line. +Care should be taken when commenting out multi-line text: +the comment is effective until the end of the entire block. +In all cases, +.Nm +reads lines consisting of words separated by whitespace. +The first word of a line is the database key; +the remainder represents the mapped value. +The database key and value may optionally be separated +by the colon character. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl d Ar dbtype +Specify the format of the database. +Available formats are +.Ar hash +and +.Ar btree . +The default value is +.Ar hash . +.It Fl o Ar dbfile +Write the generated database to +.Ar dbfile . +.It Fl t Ar type +Specify the format of the resulting map file. +The default map format is suitable for storing simple, unstructured, +key-to-value string associations. +However, if the mapped value has special meaning, +as in the case of the virtual domains file, +a suitable +.Ar type +must be provided. +The available output types are: +.Bl -tag -width "aliases" +.It Cm aliases +The mapped value is a comma-separated list of mail destinations. +This format can be used for building user aliases and +user mappings for virtual domain files. +.It Cm set +There is no mapped value \(en a map of this type will only allow for +the lookup of keys. +This format can be used for building primary domain maps. +.El +.It Fl U +Instead of generating a database map from text input, +dump the contents of a database map as text +with the key and value separated with a tab. +.El +.Sh PRIMARY DOMAINS +Primary domains can be kept in tables. +To create a primary domain table, add each primary domain on a +single line by itself. +.Pp +In addition to adding an entry to the primary domain map, +one must add a filter rule that accepts mail for the domain +map, for example: +.Bd -literal -offset indent +table domains db:/etc/mail/domains.db + +action "local" mbox + +match for domain <domains> action "local" +.Ed +.Sh VIRTUAL DOMAINS +Virtual domains may also be kept in tables. +To create a virtual domain table, add each virtual domain on a +single line by itself. +.Pp +Virtual domains expect a mapping of virtual users to real users +in order to determine if a recipient is accepted or not. +The mapping format is an extension to +.Xr aliases 5 , +which allows the use of +.Dq user@domain.tld +to accept user only on the specified domain, +.Dq user +to accept the user for any of the virtual domains, +.Dq @domain.tld +to provide a catch-all for the specified domain and +.Dq @ +to provide a global catch-all for all domains. +.Xr smtpd 8 +will perform the lookups in that specific order. +.Pp +To create single virtual address, add +.Dq user@example.com user +to the users map. +To handle all mail destined to any user at example.com, add +.Dq @example.com user +to the virtual map. +.Pp +In addition to adding an entry to the virtual map, +one must add a filter rule that accepts mail for virtual domains, +for example: +.Bd -literal -offset indent +table vdomains db:/etc/mail/vdomains.db +table vusers db:/etc/mail/users.db + +action "local" mbox virtual <vusers> + +match for domain <vdomains> action "local" +match for domain "example.org" action "local" +.Ed +.Sh FILES +.Bl -tag -width "/etc/mail/aliasesXXX" -compact +.It Pa /etc/mail/aliases +List of user mail aliases. +.It Pa /etc/mail/secrets +List of remote host credentials. +.El +.Sh EXIT STATUS +.Ex -std makemap +.Sh SEE ALSO +.Xr aliases 5 , +.Xr smtpd.conf 5 , +.Xr table 5 , +.Xr newaliases 8 , +.Xr smtpd 8 +.Sh HISTORY +The +.Nm +command first appeared in +.Ox 4.6 +as a replacement for the equivalent command shipped with sendmail. diff --git a/smtpd/makemap.c b/smtpd/makemap.c new file mode 100644 index 00000000..10e3f555 --- /dev/null +++ b/smtpd/makemap.c @@ -0,0 +1,521 @@ +/* $OpenBSD: makemap.c,v 1.73 2020/02/24 16:16:07 millert Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008-2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#ifdef HAVE_SYS_FILE_H +#include <sys/file.h> /* Needed for flock */ +#endif +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/tree.h> +#include <sys/queue.h> +#include <sys/socket.h> + +#include <ctype.h> +#ifdef HAVE_DB_H +#include <db.h> +#elif defined(HAVE_DB1_DB_H) +#include <db1/db.h> +#elif defined(HAVE_DB_185_H) +#include <db_185.h> +#endif +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> +#include <limits.h> +#ifdef HAVE_UTIL_H +#include <util.h> +#endif +#ifdef HAVE_LIBUTIL_H +#include <libutil.h> +#endif + +#include "smtpd.h" +#include "log.h" + +#define PATH_ALIASES SMTPD_CONFDIR "/aliases" + +static void usage(void); +static int parse_map(DB *, int *, char *); +static int parse_entry(DB *, int *, char *, size_t, size_t); +static int parse_mapentry(DB *, int *, char *, size_t, size_t); +static int parse_setentry(DB *, int *, char *, size_t, size_t); +static int make_plain(DBT *, char *); +static int make_aliases(DBT *, char *); +static char *conf_aliases(char *); +static int dump_db(const char *, DBTYPE); + +char *source; +static int mode; + +enum output_type { + T_PLAIN, + T_ALIASES, + T_SET +} type; + +/* + * Stub functions so that makemap compiles using minimum object files. + */ +int +fork_proc_backend(const char *backend, const char *conf, const char *procname) +{ + return (-1); +} + +int +makemap(int prog_mode, int argc, char *argv[]) +{ + struct stat sb; + char dbname[PATH_MAX]; + DB *db; + const char *opts; + char *conf, *oflag = NULL; + int ch, dbputs = 0, Uflag = 0; + DBTYPE dbtype = DB_HASH; + char *p; + gid_t gid; + int fd = -1; + + gid = getgid(); + if (setresgid(gid, gid, gid) == -1) + err(1, "setresgid"); + + if ((env = config_default()) == NULL) + err(1, NULL); + + log_init(1, LOG_MAIL); + + mode = prog_mode; + conf = CONF_FILE; + type = T_PLAIN; + opts = "b:C:d:ho:O:t:U"; + if (mode == P_NEWALIASES) + opts = "f:h"; + + while ((ch = getopt(argc, argv, opts)) != -1) { + switch (ch) { + case 'b': + if (optarg && strcmp(optarg, "i") == 0) + mode = P_NEWALIASES; + break; + case 'C': + break; /* for compatibility */ + case 'd': + if (strcmp(optarg, "hash") == 0) + dbtype = DB_HASH; + else if (strcmp(optarg, "btree") == 0) + dbtype = DB_BTREE; + else + errx(1, "unsupported DB type '%s'", optarg); + break; + case 'f': + conf = optarg; + break; + case 'o': + oflag = optarg; + break; + case 'O': + if (strncmp(optarg, "AliasFile=", 10) != 0) + break; + type = T_ALIASES; + p = strchr(optarg, '='); + source = ++p; + break; + case 't': + if (strcmp(optarg, "aliases") == 0) + type = T_ALIASES; + else if (strcmp(optarg, "set") == 0) + type = T_SET; + else + errx(1, "unsupported type '%s'", optarg); + break; + case 'U': + Uflag = 1; + break; + default: + usage(); + } + } + argc -= optind; + argv += optind; + + /* sendmail-compat makemap ... re-execute using proper interface */ + if (argc == 2) { + if (oflag) + usage(); + + p = strstr(argv[1], ".db"); + if (p == NULL || strcmp(p, ".db") != 0) { + if (!bsnprintf(dbname, sizeof dbname, "%s.db", + argv[1])) + errx(1, "database name too long"); + } + else { + if (strlcpy(dbname, argv[1], sizeof dbname) + >= sizeof dbname) + errx(1, "database name too long"); + } + + execl(PATH_MAKEMAP, "makemap", "-d", argv[0], "-o", dbname, + "-", (char *)NULL); + err(1, "execl"); + } + + if (mode == P_NEWALIASES) { + if (geteuid()) + errx(1, "need root privileges"); + if (argc != 0) + usage(); + type = T_ALIASES; + if (source == NULL) + source = conf_aliases(conf); + } else { + if (argc != 1) + usage(); + source = argv[0]; + } + + if (Uflag) + return dump_db(source, dbtype); + + if (oflag == NULL && asprintf(&oflag, "%s.db", source) == -1) + err(1, "asprintf"); + + if (strcmp(source, "-") != 0) + if (stat(source, &sb) == -1) + err(1, "stat: %s", source); + + if (!bsnprintf(dbname, sizeof(dbname), "%s.XXXXXXXXXXX", oflag)) + errx(1, "path too long"); + if ((fd = mkstemp(dbname)) == -1) + err(1, "mkstemp"); + + db = dbopen(dbname, O_TRUNC|O_RDWR, 0644, dbtype, NULL); + if (db == NULL) { + warn("dbopen: %s", dbname); + goto bad; + } + + if (strcmp(source, "-") != 0) + if (fchmod(db->fd(db), sb.st_mode) == -1 || + fchown(db->fd(db), sb.st_uid, sb.st_gid) == -1) { + warn("couldn't carry ownership and perms to %s", + dbname); + goto bad; + } + + if (!parse_map(db, &dbputs, source)) + goto bad; + + if (db->close(db) == -1) { + warn("dbclose: %s", dbname); + goto bad; + } + + /* force to disk before renaming over an existing file */ + if (fsync(fd) == -1) { + warn("fsync: %s", dbname); + goto bad; + } + if (close(fd) == -1) { + fd = -1; + warn("close: %s", dbname); + goto bad; + } + fd = -1; + + if (rename(dbname, oflag) == -1) { + warn("rename"); + goto bad; + } + + if (mode == P_NEWALIASES) + printf("%s: %d aliases\n", source, dbputs); + else if (dbputs == 0) + warnx("warning: empty map created: %s", oflag); + + return 0; +bad: + if (fd != -1) + close(fd); + unlink(dbname); + return 1; +} + +static int +parse_map(DB *db, int *dbputs, char *filename) +{ + FILE *fp; + char *line; + size_t len; + size_t lineno = 0; + + if (strcmp(filename, "-") == 0) + fp = fdopen(0, "r"); + else + fp = fopen(filename, "r"); + if (fp == NULL) { + warn("%s", filename); + return 0; + } + + if (!isatty(fileno(fp)) && flock(fileno(fp), LOCK_SH|LOCK_NB) == -1) { + if (errno == EWOULDBLOCK) + warnx("%s is locked", filename); + else + warn("%s: flock", filename); + fclose(fp); + return 0; + } + + while ((line = fparseln(fp, &len, &lineno, + NULL, FPARSELN_UNESCCOMM)) != NULL) { + if (!parse_entry(db, dbputs, line, len, lineno)) { + free(line); + fclose(fp); + return 0; + } + free(line); + } + + fclose(fp); + return 1; +} + +static int +parse_entry(DB *db, int *dbputs, char *line, size_t len, size_t lineno) +{ + switch (type) { + case T_PLAIN: + case T_ALIASES: + return parse_mapentry(db, dbputs, line, len, lineno); + case T_SET: + return parse_setentry(db, dbputs, line, len, lineno); + } + return 0; +} + +static int +parse_mapentry(DB *db, int *dbputs, char *line, size_t len, size_t lineno) +{ + DBT key; + DBT val; + char *keyp; + char *valp; + + keyp = line; + while (isspace((unsigned char)*keyp)) + keyp++; + if (*keyp == '\0') + return 1; + + valp = keyp; + strsep(&valp, " \t:"); + if (valp == NULL || valp == keyp) + goto bad; + while (*valp == ':' || isspace((unsigned char)*valp)) + valp++; + if (*valp == '\0') + goto bad; + + /* Check for dups. */ + key.data = keyp; + key.size = strlen(keyp) + 1; + + xlowercase(key.data, key.data, strlen(key.data) + 1); + if (db->get(db, &key, &val, 0) == 0) { + warnx("%s:%zd: duplicate entry for %s", source, lineno, keyp); + return 0; + } + + if (type == T_PLAIN) { + if (!make_plain(&val, valp)) + goto bad; + } + else if (type == T_ALIASES) { + if (!make_aliases(&val, valp)) + goto bad; + } + + if (db->put(db, &key, &val, 0) == -1) { + warn("dbput"); + return 0; + } + + (*dbputs)++; + + free(val.data); + + return 1; + +bad: + warnx("%s:%zd: invalid entry", source, lineno); + return 0; +} + +static int +parse_setentry(DB *db, int *dbputs, char *line, size_t len, size_t lineno) +{ + DBT key; + DBT val; + char *keyp; + + keyp = line; + while (isspace((unsigned char)*keyp)) + keyp++; + if (*keyp == '\0') + return 1; + + val.data = "<set>"; + val.size = strlen(val.data) + 1; + + /* Check for dups. */ + key.data = keyp; + key.size = strlen(keyp) + 1; + xlowercase(key.data, key.data, strlen(key.data) + 1); + if (db->get(db, &key, &val, 0) == 0) { + warnx("%s:%zd: duplicate entry for %s", source, lineno, keyp); + return 0; + } + + if (db->put(db, &key, &val, 0) == -1) { + warn("dbput"); + return 0; + } + + (*dbputs)++; + + return 1; +} + +static int +make_plain(DBT *val, char *text) +{ + val->data = xstrdup(text); + val->size = strlen(text) + 1; + + return (val->size); +} + +static int +make_aliases(DBT *val, char *text) +{ + struct expandnode xn; + char *subrcpt; + char *origtext; + + val->data = NULL; + val->size = 0; + + origtext = xstrdup(text); + + while ((subrcpt = strsep(&text, ",")) != NULL) { + /* subrcpt: strip initial and trailing whitespace. */ + subrcpt = strip(subrcpt); + if (*subrcpt == '\0') + goto error; + + if (!text_to_expandnode(&xn, subrcpt)) + goto error; + } + + val->data = origtext; + val->size = strlen(origtext) + 1; + return (val->size); + +error: + free(origtext); + + return 0; +} + +static char * +conf_aliases(char *cfgpath) +{ + struct table *table; + char *path; + char *p; + + if (parse_config(env, cfgpath, 0)) + exit(1); + + table = table_find(env, "aliases"); + if (table == NULL) + return (PATH_ALIASES); + + path = xstrdup(table->t_config); + p = strstr(path, ".db"); + if (p == NULL || strcmp(p, ".db") != 0) { + return (path); + } + *p = '\0'; + return (path); +} + +static int +dump_db(const char *dbname, DBTYPE dbtype) +{ + DB *db; + DBT key, val; + char *keystr, *valstr; + int r; + + db = dbopen(dbname, O_RDONLY, 0644, dbtype, NULL); + if (db == NULL) + err(1, "dbopen: %s", dbname); + + for (r = db->seq(db, &key, &val, R_FIRST); r == 0; + r = db->seq(db, &key, &val, R_NEXT)) { + keystr = key.data; + valstr = val.data; + if (keystr[key.size - 1] == '\0') + key.size--; + if (valstr[val.size - 1] == '\0') + val.size--; + printf("%.*s\t%.*s\n", (int)key.size, keystr, + (int)val.size, valstr); + } + if (r == -1) + err(1, "db->seq: %s", dbname); + + if (db->close(db) == -1) + err(1, "dbclose: %s", dbname); + + return 0; +} + +static void +usage(void) +{ + if (mode == P_NEWALIASES) + fprintf(stderr, "usage: newaliases [-f file]\n"); + else + fprintf(stderr, "usage: makemap [-U] [-d dbtype] [-o dbfile] " + "[-t type] file\n"); + exit(1); +} diff --git a/smtpd/mda.c b/smtpd/mda.c new file mode 100644 index 00000000..5e8fec19 --- /dev/null +++ b/smtpd/mda.c @@ -0,0 +1,919 @@ +/* $OpenBSD: mda.c,v 1.141 2019/10/03 08:50:08 gilles Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <grp.h> /* needed for setgroups */ +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> +#if defined(HAVE_VIS_H) && !defined(BROKEN_STRNVIS) +#include <vis.h> +#else +#include "bsd-vis.h" +#endif + +#include "smtpd.h" +#include "log.h" + +#define MDA_HIWAT 65536 + +struct mda_envelope { + TAILQ_ENTRY(mda_envelope) entry; + uint64_t session_id; + uint64_t id; + time_t creation; + char *sender; + char *rcpt; + char *dest; + char *user; + char *dispatcher; + char *mda_subaddress; + char *mda_exec; +}; + +#define USER_WAITINFO 0x01 +#define USER_RUNNABLE 0x02 +#define USER_ONHOLD 0x04 +#define USER_HOLDQ 0x08 + +struct mda_user { + uint64_t id; + TAILQ_ENTRY(mda_user) entry; + TAILQ_ENTRY(mda_user) entry_runnable; + char name[LOGIN_NAME_MAX]; + char usertable[PATH_MAX]; + size_t evpcount; + TAILQ_HEAD(, mda_envelope) envelopes; + int flags; + size_t running; + struct userinfo userinfo; +}; + +struct mda_session { + uint64_t id; + struct mda_user *user; + struct mda_envelope *evp; + struct io *io; + FILE *datafp; +}; + +static void mda_io(struct io *, int, void *); +static int mda_check_loop(FILE *, struct mda_envelope *); +static int mda_getlastline(int, char *, size_t); +static void mda_done(struct mda_session *); +static void mda_fail(struct mda_user *, int, const char *, + enum enhanced_status_code); +static void mda_drain(void); +static void mda_log(const struct mda_envelope *, const char *, const char *); +static void mda_queue_ok(uint64_t); +static void mda_queue_tempfail(uint64_t, const char *, + enum enhanced_status_code); +static void mda_queue_permfail(uint64_t, const char *, enum enhanced_status_code); +static void mda_queue_loop(uint64_t); +static struct mda_user *mda_user(const struct envelope *); +static void mda_user_free(struct mda_user *); +static const char *mda_user_to_text(const struct mda_user *); +static struct mda_envelope *mda_envelope(uint64_t, const struct envelope *); +static void mda_envelope_free(struct mda_envelope *); +static struct mda_session * mda_session(struct mda_user *); +static const char *mda_sysexit_to_str(int); + +static struct tree sessions; +static struct tree users; + +static TAILQ_HEAD(, mda_user) runnable; + +void +mda_imsg(struct mproc *p, struct imsg *imsg) +{ + struct mda_session *s; + struct mda_user *u; + struct mda_envelope *e; + struct envelope evp; + struct deliver deliver; + struct msg m; + const void *data; + const char *error, *parent_error, *syserror; + uint64_t reqid; + size_t sz; + char out[256], buf[LINE_MAX]; + int n; + enum lka_resp_status status; + enum mda_resp_status mda_status; + int mda_sysexit; + + switch (imsg->hdr.type) { + case IMSG_MDA_LOOKUP_USERINFO: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, (int *)&status); + if (status == LKA_OK) + m_get_data(&m, &data, &sz); + m_end(&m); + + u = tree_xget(&users, reqid); + + if (status == LKA_TEMPFAIL) + mda_fail(u, 0, + "Temporary failure in user lookup", + ESC_OTHER_ADDRESS_STATUS); + else if (status == LKA_PERMFAIL) + mda_fail(u, 1, + "Permanent failure in user lookup", + ESC_DESTINATION_MAILBOX_HAS_MOVED); + else { + if (sz != sizeof(u->userinfo)) + fatalx("mda: userinfo size mismatch"); + memmove(&u->userinfo, data, sz); + u->flags &= ~USER_WAITINFO; + u->flags |= USER_RUNNABLE; + TAILQ_INSERT_TAIL(&runnable, u, entry_runnable); + mda_drain(); + } + return; + + case IMSG_QUEUE_DELIVER: + m_msg(&m, imsg); + m_get_envelope(&m, &evp); + m_end(&m); + + u = mda_user(&evp); + + if (u->evpcount >= env->sc_mda_task_hiwat) { + if (!(u->flags & USER_ONHOLD)) { + log_debug("debug: mda: hiwat reached for " + "user \"%s\": holding envelopes", + mda_user_to_text(u)); + u->flags |= USER_ONHOLD; + } + } + + if (u->flags & USER_ONHOLD) { + u->flags |= USER_HOLDQ; + m_create(p_queue, IMSG_MDA_DELIVERY_HOLD, + 0, 0, -1); + m_add_evpid(p_queue, evp.id); + m_add_id(p_queue, u->id); + m_close(p_queue); + return; + } + + e = mda_envelope(u->id, &evp); + TAILQ_INSERT_TAIL(&u->envelopes, e, entry); + u->evpcount += 1; + stat_increment("mda.pending", 1); + + if (!(u->flags & USER_RUNNABLE) && + !(u->flags & USER_WAITINFO)) { + u->flags |= USER_RUNNABLE; + TAILQ_INSERT_TAIL(&runnable, u, entry_runnable); + } + + mda_drain(); + return; + + case IMSG_MDA_OPEN_MESSAGE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_end(&m); + + s = tree_xget(&sessions, reqid); + e = s->evp; + + if (imsg->fd == -1) { + log_debug("debug: mda: cannot get message fd"); + mda_queue_tempfail(e->id, + "Cannot get message fd", + ESC_OTHER_MAIL_SYSTEM_STATUS); + mda_log(e, "TempFail", "Cannot get message fd"); + mda_done(s); + return; + } + + log_debug("debug: mda: got message fd %d " + "for session %016"PRIx64 " evpid %016"PRIx64, + imsg->fd, s->id, e->id); + + if ((s->datafp = fdopen(imsg->fd, "r")) == NULL) { + log_warn("warn: mda: fdopen"); + close(imsg->fd); + mda_queue_tempfail(e->id, "fdopen failed", + ESC_OTHER_MAIL_SYSTEM_STATUS); + mda_log(e, "TempFail", "fdopen failed"); + mda_done(s); + return; + } + + /* check delivery loop */ + if (mda_check_loop(s->datafp, e)) { + log_debug("debug: mda: loop detected"); + mda_queue_loop(e->id); + mda_log(e, "PermFail", "Loop detected"); + mda_done(s); + return; + } + + /* start queueing delivery headers */ + if (e->sender[0]) + /* + * XXX: remove existing Return-Path, + * if any + */ + n = io_printf(s->io, + "Return-Path: <%s>\n" + "Delivered-To: %s\n", + e->sender, + e->rcpt ? e->rcpt : e->dest); + else + n = io_printf(s->io, + "Delivered-To: %s\n", + e->rcpt ? e->rcpt : e->dest); + if (n == -1) { + log_warn("warn: mda: " + "fail to write delivery info"); + mda_queue_tempfail(e->id, "Out of memory", + ESC_OTHER_MAIL_SYSTEM_STATUS); + mda_log(e, "TempFail", "Out of memory"); + mda_done(s); + return; + } + + /* request parent to fork a helper process */ + memset(&deliver, 0, sizeof deliver); + (void)text_to_mailaddr(&deliver.sender, s->evp->sender); + (void)text_to_mailaddr(&deliver.rcpt, s->evp->rcpt); + (void)text_to_mailaddr(&deliver.dest, s->evp->dest); + if (s->evp->mda_exec) + (void)strlcpy(deliver.mda_exec, s->evp->mda_exec, sizeof deliver.mda_exec); + if (s->evp->mda_subaddress) + (void)strlcpy(deliver.mda_subaddress, s->evp->mda_subaddress, sizeof deliver.mda_subaddress); + (void)strlcpy(deliver.dispatcher, s->evp->dispatcher, sizeof deliver.dispatcher); + deliver.userinfo = s->user->userinfo; + + log_debug("debug: mda: querying mda fd " + "for session %016"PRIx64 " evpid %016"PRIx64, + s->id, s->evp->id); + + m_create(p_parent, IMSG_MDA_FORK, 0, 0, -1); + m_add_id(p_parent, reqid); + m_add_data(p_parent, &deliver, sizeof(deliver)); + m_close(p_parent); + return; + + case IMSG_MDA_FORK: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_end(&m); + + s = tree_xget(&sessions, reqid); + e = s->evp; + if (imsg->fd == -1) { + log_warn("warn: mda: fail to retrieve mda fd"); + mda_queue_tempfail(e->id, "Cannot get mda fd", + ESC_OTHER_MAIL_SYSTEM_STATUS); + mda_log(e, "TempFail", "Cannot get mda fd"); + mda_done(s); + return; + } + + log_debug("debug: mda: got mda fd %d " + "for session %016"PRIx64 " evpid %016"PRIx64, + imsg->fd, s->id, s->evp->id); + + io_set_nonblocking(imsg->fd); + io_set_fd(s->io, imsg->fd); + io_set_write(s->io); + return; + + case IMSG_MDA_DONE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, (int *)&mda_status); + m_get_int(&m, (int *)&mda_sysexit); + m_get_string(&m, &parent_error); + m_end(&m); + + s = tree_xget(&sessions, reqid); + e = s->evp; + /* + * Grab last line of mda stdout/stderr if available. + */ + out[0] = '\0'; + if (imsg->fd != -1) + mda_getlastline(imsg->fd, out, sizeof(out)); + + /* + * Choose between parent's description of error and + * child's output, the latter having preference over + * the former. + */ + error = NULL; + if (mda_status == MDA_OK) { + if (s->datafp || (s->io && io_queued(s->io))) { + error = "mda exited prematurely"; + mda_status = MDA_TEMPFAIL; + } + } else + error = out[0] ? out : parent_error; + + syserror = NULL; + if (mda_sysexit) + syserror = mda_sysexit_to_str(mda_sysexit); + + /* update queue entry */ + switch (mda_status) { + case MDA_TEMPFAIL: + mda_queue_tempfail(e->id, error, + ESC_OTHER_MAIL_SYSTEM_STATUS); + (void)snprintf(buf, sizeof buf, + "Error (%s%s%s)", + syserror ? syserror : "", + syserror ? ": " : "", + error); + mda_log(e, "TempFail", buf); + break; + case MDA_PERMFAIL: + mda_queue_permfail(e->id, error, + ESC_OTHER_MAIL_SYSTEM_STATUS); + (void)snprintf(buf, sizeof buf, + "Error (%s%s%s)", + syserror ? syserror : "", + syserror ? ": " : "", + error); + mda_log(e, "PermFail", buf); + break; + case MDA_OK: + mda_queue_ok(e->id); + mda_log(e, "Ok", "Delivered"); + break; + } + mda_done(s); + return; + } + + errx(1, "mda_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type)); +} + +void +mda_postfork() +{ +} + +void +mda_postprivdrop() +{ + tree_init(&sessions); + tree_init(&users); + TAILQ_INIT(&runnable); +} + +static void +mda_io(struct io *io, int evt, void *arg) +{ + struct mda_session *s = arg; + char *ln = NULL; + size_t sz = 0; + ssize_t len; + + log_trace(TRACE_IO, "mda: %p: %s %s", s, io_strevent(evt), + io_strio(io)); + + switch (evt) { + case IO_LOWAT: + + /* done */ + done: + if (s->datafp == NULL) { + log_debug("debug: mda: all data sent for session" + " %016"PRIx64 " evpid %016"PRIx64, + s->id, s->evp->id); + io_free(io); + s->io = NULL; + return; + } + + while (io_queued(s->io) < MDA_HIWAT) { + if ((len = getline(&ln, &sz, s->datafp)) == -1) + break; + if (io_write(s->io, ln, len) == -1) { + m_create(p_parent, IMSG_MDA_KILL, + 0, 0, -1); + m_add_id(p_parent, s->id); + m_add_string(p_parent, "Out of memory"); + m_close(p_parent); + io_pause(io, IO_OUT); + free(ln); + return; + } + } + + free(ln); + ln = NULL; + if (ferror(s->datafp)) { + log_debug("debug: mda: ferror on session %016"PRIx64, + s->id); + m_create(p_parent, IMSG_MDA_KILL, 0, 0, -1); + m_add_id(p_parent, s->id); + m_add_string(p_parent, "Error reading body"); + m_close(p_parent); + io_pause(io, IO_OUT); + return; + } + + if (feof(s->datafp)) { + log_debug("debug: mda: end-of-file for session" + " %016"PRIx64 " evpid %016"PRIx64, + s->id, s->evp->id); + fclose(s->datafp); + s->datafp = NULL; + if (io_queued(s->io) == 0) + goto done; + } + return; + + case IO_TIMEOUT: + log_debug("debug: mda: timeout on session %016"PRIx64, s->id); + io_pause(io, IO_OUT); + return; + + case IO_ERROR: + log_debug("debug: mda: io error on session %016"PRIx64": %s", + s->id, io_error(io)); + io_pause(io, IO_OUT); + return; + + case IO_DISCONNECTED: + log_debug("debug: mda: io disconnected on session %016"PRIx64, + s->id); + io_pause(io, IO_OUT); + return; + + default: + log_debug("debug: mda: unexpected event on session %016"PRIx64, + s->id); + io_pause(io, IO_OUT); + return; + } +} + +static int +mda_check_loop(FILE *fp, struct mda_envelope *e) +{ + char *buf = NULL; + size_t sz = 0; + ssize_t len; + int ret = 0; + + while ((len = getline(&buf, &sz, fp)) != -1) { + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + + if (strchr(buf, ':') == NULL && !isspace((unsigned char)*buf)) + break; + + if (strncasecmp("Delivered-To: ", buf, 14) == 0) { + if (strcasecmp(buf + 14, e->dest) == 0) { + ret = 1; + break; + } + } + } + + free(buf); + fseek(fp, SEEK_SET, 0); + return (ret); +} + +static int +mda_getlastline(int fd, char *dst, size_t dstsz) +{ + FILE *fp; + char *ln = NULL; + size_t sz = 0; + ssize_t len; + int out = 0; + + if (lseek(fd, 0, SEEK_SET) < 0) { + log_warn("warn: mda: lseek"); + close(fd); + return (-1); + } + fp = fdopen(fd, "r"); + if (fp == NULL) { + log_warn("warn: mda: fdopen"); + close(fd); + return (-1); + } + while ((len = getline(&ln, &sz, fp)) != -1) { + if (ln[len - 1] == '\n') + ln[len - 1] = '\0'; + out = 1; + } + fclose(fp); + + if (out) { + (void)strlcpy(dst, "\"", dstsz); + (void)strnvis(dst + 1, ln, dstsz - 2, VIS_SAFE | VIS_CSTYLE | VIS_NL); + (void)strlcat(dst, "\"", dstsz); + } + + free(ln); + return (0); +} + +static void +mda_fail(struct mda_user *user, int permfail, const char *error, + enum enhanced_status_code code) +{ + struct mda_envelope *e; + + while ((e = TAILQ_FIRST(&user->envelopes))) { + TAILQ_REMOVE(&user->envelopes, e, entry); + if (permfail) { + mda_log(e, "PermFail", error); + mda_queue_permfail(e->id, error, code); + } + else { + mda_log(e, "TempFail", error); + mda_queue_tempfail(e->id, error, code); + } + mda_envelope_free(e); + } + + mda_user_free(user); +} + +static void +mda_drain(void) +{ + struct mda_user *u; + + while ((u = (TAILQ_FIRST(&runnable)))) { + + TAILQ_REMOVE(&runnable, u, entry_runnable); + + if (u->evpcount == 0 && u->running == 0) { + log_debug("debug: mda: all done for user \"%s\"", + mda_user_to_text(u)); + mda_user_free(u); + continue; + } + + if (u->evpcount == 0) { + log_debug("debug: mda: no more envelope for \"%s\"", + mda_user_to_text(u)); + u->flags &= ~USER_RUNNABLE; + continue; + } + + if (u->running >= env->sc_mda_max_user_session) { + log_debug("debug: mda: " + "maximum number of session reached for user \"%s\"", + mda_user_to_text(u)); + u->flags &= ~USER_RUNNABLE; + continue; + } + + if (tree_count(&sessions) >= env->sc_mda_max_session) { + log_debug("debug: mda: " + "maximum number of session reached"); + TAILQ_INSERT_HEAD(&runnable, u, entry_runnable); + return; + } + + mda_session(u); + + if (u->evpcount == env->sc_mda_task_lowat) { + if (u->flags & USER_ONHOLD) { + log_debug("debug: mda: down to lowat for user " + "\"%s\": releasing", + mda_user_to_text(u)); + u->flags &= ~USER_ONHOLD; + } + if (u->flags & USER_HOLDQ) { + m_create(p_queue, IMSG_MDA_HOLDQ_RELEASE, + 0, 0, -1); + m_add_id(p_queue, u->id); + m_add_int(p_queue, env->sc_mda_task_release); + m_close(p_queue); + } + } + + /* re-add the user at the tail of the queue */ + TAILQ_INSERT_TAIL(&runnable, u, entry_runnable); + } +} + +static void +mda_done(struct mda_session *s) +{ + log_debug("debug: mda: session %016" PRIx64 " done", s->id); + + tree_xpop(&sessions, s->id); + + mda_envelope_free(s->evp); + + s->user->running--; + if (!(s->user->flags & USER_RUNNABLE)) { + log_debug("debug: mda: user \"%s\" becomes runnable", + s->user->name); + TAILQ_INSERT_TAIL(&runnable, s->user, entry_runnable); + s->user->flags |= USER_RUNNABLE; + } + + if (s->datafp) + fclose(s->datafp); + if (s->io) + io_free(s->io); + + free(s); + + stat_decrement("mda.running", 1); + + mda_drain(); +} + +static void +mda_log(const struct mda_envelope *evp, const char *prefix, const char *status) +{ + char rcpt[LINE_MAX]; + + rcpt[0] = '\0'; + if (evp->rcpt) + (void)snprintf(rcpt, sizeof rcpt, "rcpt=<%s> ", evp->rcpt); + + log_info("%016"PRIx64" mda delivery evpid=%016" PRIx64 " from=<%s> to=<%s> " + "%suser=%s delay=%s result=%s stat=%s", + evp->session_id, + evp->id, + evp->sender ? evp->sender : "", + evp->dest, + rcpt, + evp->user, + duration_to_text(time(NULL) - evp->creation), + prefix, + status); +} + +static void +mda_queue_ok(uint64_t evpid) +{ + m_create(p_queue, IMSG_MDA_DELIVERY_OK, 0, 0, -1); + m_add_evpid(p_queue, evpid); + m_close(p_queue); +} + +static void +mda_queue_tempfail(uint64_t evpid, const char *reason, + enum enhanced_status_code code) +{ + m_create(p_queue, IMSG_MDA_DELIVERY_TEMPFAIL, 0, 0, -1); + m_add_evpid(p_queue, evpid); + m_add_string(p_queue, reason); + m_add_int(p_queue, (int)code); + m_close(p_queue); +} + +static void +mda_queue_permfail(uint64_t evpid, const char *reason, + enum enhanced_status_code code) +{ + m_create(p_queue, IMSG_MDA_DELIVERY_PERMFAIL, 0, 0, -1); + m_add_evpid(p_queue, evpid); + m_add_string(p_queue, reason); + m_add_int(p_queue, (int)code); + m_close(p_queue); +} + +static void +mda_queue_loop(uint64_t evpid) +{ + m_create(p_queue, IMSG_MDA_DELIVERY_LOOP, 0, 0, -1); + m_add_evpid(p_queue, evpid); + m_close(p_queue); +} + +static struct mda_user * +mda_user(const struct envelope *evp) +{ + struct dispatcher *dsp; + struct mda_user *u; + void *i; + + i = NULL; + dsp = dict_xget(env->sc_dispatchers, evp->dispatcher); + while (tree_iter(&users, &i, NULL, (void**)(&u))) { + if (!strcmp(evp->mda_user, u->name) && + !strcmp(dsp->u.local.table_userbase, u->usertable)) + return (u); + } + + u = xcalloc(1, sizeof *u); + u->id = generate_uid(); + TAILQ_INIT(&u->envelopes); + (void)strlcpy(u->name, evp->mda_user, sizeof(u->name)); + (void)strlcpy(u->usertable, dsp->u.local.table_userbase, + sizeof(u->usertable)); + + tree_xset(&users, u->id, u); + + m_create(p_lka, IMSG_MDA_LOOKUP_USERINFO, 0, 0, -1); + m_add_id(p_lka, u->id); + m_add_string(p_lka, dsp->u.local.table_userbase); + m_add_string(p_lka, evp->mda_user); + m_close(p_lka); + u->flags |= USER_WAITINFO; + + stat_increment("mda.user", 1); + + if (dsp->u.local.user) + log_debug("mda: new user %016" PRIx64 + " for \"%s\" delivering as \"%s\"", + u->id, mda_user_to_text(u), dsp->u.local.user); + else + log_debug("mda: new user %016" PRIx64 + " for \"%s\"", u->id, mda_user_to_text(u)); + + return (u); +} + +static void +mda_user_free(struct mda_user *u) +{ + tree_xpop(&users, u->id); + + if (u->flags & USER_HOLDQ) { + m_create(p_queue, IMSG_MDA_HOLDQ_RELEASE, 0, 0, -1); + m_add_id(p_queue, u->id); + m_add_int(p_queue, 0); + m_close(p_queue); + } + + free(u); + stat_decrement("mda.user", 1); +} + +static const char * +mda_user_to_text(const struct mda_user *u) +{ + static char buf[1024]; + + (void)snprintf(buf, sizeof(buf), "%s:%s", u->usertable, u->name); + + return (buf); +} + +static struct mda_envelope * +mda_envelope(uint64_t session_id, const struct envelope *evp) +{ + struct mda_envelope *e; + char buf[LINE_MAX]; + + e = xcalloc(1, sizeof *e); + e->session_id = session_id; + e->id = evp->id; + e->creation = evp->creation; + buf[0] = '\0'; + if (evp->sender.user[0] && evp->sender.domain[0]) + (void)snprintf(buf, sizeof buf, "%s@%s", + evp->sender.user, evp->sender.domain); + e->sender = xstrdup(buf); + (void)snprintf(buf, sizeof buf, "%s@%s", evp->dest.user, + evp->dest.domain); + e->dest = xstrdup(buf); + (void)snprintf(buf, sizeof buf, "%s@%s", evp->rcpt.user, + evp->rcpt.domain); + e->rcpt = xstrdup(buf); + e->user = evp->mda_user[0] ? + xstrdup(evp->mda_user) : xstrdup(evp->dest.user); + e->dispatcher = xstrdup(evp->dispatcher); + if (evp->mda_exec[0]) + e->mda_exec = xstrdup(evp->mda_exec); + if (evp->mda_subaddress[0]) + e->mda_subaddress = xstrdup(evp->mda_subaddress); + stat_increment("mda.envelope", 1); + return (e); +} + +static void +mda_envelope_free(struct mda_envelope *e) +{ + free(e->sender); + free(e->dest); + free(e->rcpt); + free(e->user); + free(e->mda_exec); + free(e); + + stat_decrement("mda.envelope", 1); +} + +static struct mda_session * +mda_session(struct mda_user * u) +{ + struct mda_session *s; + + s = xcalloc(1, sizeof *s); + s->id = generate_uid(); + s->user = u; + s->io = io_new(); + io_set_callback(s->io, mda_io, s); + + tree_xset(&sessions, s->id, s); + + s->evp = TAILQ_FIRST(&u->envelopes); + TAILQ_REMOVE(&u->envelopes, s->evp, entry); + u->evpcount--; + u->running++; + + stat_decrement("mda.pending", 1); + stat_increment("mda.running", 1); + + log_debug("debug: mda: new session %016" PRIx64 + " for user \"%s\" evpid %016" PRIx64, s->id, + mda_user_to_text(u), s->evp->id); + + m_create(p_queue, IMSG_MDA_OPEN_MESSAGE, 0, 0, -1); + m_add_id(p_queue, s->id); + m_add_msgid(p_queue, evpid_to_msgid(s->evp->id)); + m_close(p_queue); + + return (s); +} + +static const char * +mda_sysexit_to_str(int sysexit) +{ + switch (sysexit) { + case EX_USAGE: + return "command line usage error"; + case EX_DATAERR: + return "data format error"; + case EX_NOINPUT: + return "cannot open input"; + case EX_NOUSER: + return "user unknown"; + case EX_NOHOST: + return "host name unknown"; + case EX_UNAVAILABLE: + return "service unavailable"; + case EX_SOFTWARE: + return "internal software error"; + case EX_OSERR: + return "system resource problem"; + case EX_OSFILE: + return "critical OS file missing"; + case EX_CANTCREAT: + return "can't create user output file"; + case EX_IOERR: + return "input/output error"; + case EX_TEMPFAIL: + return "temporary failure"; + case EX_PROTOCOL: + return "remote error in protocol"; + case EX_NOPERM: + return "permission denied"; + case EX_CONFIG: + return "local configuration error"; + default: + break; + } + return NULL; +} + diff --git a/smtpd/mda_mbox.c b/smtpd/mda_mbox.c new file mode 100644 index 00000000..8918e3ee --- /dev/null +++ b/smtpd/mda_mbox.c @@ -0,0 +1,94 @@ +/* $OpenBSD: mda_mbox.c,v 1.2 2020/02/03 15:41:22 gilles Exp $ */ + +/* + * Copyright (c) 2018 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <paths.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" + + +void +mda_mbox(struct deliver *deliver) +{ + int ret; + char sender[LINE_MAX]; + char *envp[] = { + "HOME=/", + "PATH=" _PATH_DEFPATH, + "LOGNAME=root", + "USER=root", + NULL, + }; + + if (deliver->sender.user[0] == '\0' && + deliver->sender.domain[0] == '\0') + ret = snprintf(sender, sizeof sender, "MAILER-DAEMON"); + else + ret = snprintf(sender, sizeof sender, "%s@%s", + deliver->sender.user, deliver->sender.domain); + if (ret < 0 || (size_t)ret >= sizeof sender) + errx(EX_TEMPFAIL, "sender address too long"); + + execle(PATH_MAILLOCAL, PATH_MAILLOCAL, "-f", + sender, deliver->userinfo.username, (char *)NULL, envp); + perror("execl"); + _exit(EX_TEMPFAIL); +} + +void +mda_mbox_init(struct deliver *deliver) +{ + int fd; + int ret; + char buffer[LINE_MAX]; + + ret = snprintf(buffer, sizeof buffer, "%s/%s", + _PATH_MAILDIR, deliver->userinfo.username); + if (ret < 0 || (size_t)ret >= sizeof buffer) + errx(EX_TEMPFAIL, "mailbox pathname too long"); + + if ((fd = open(buffer, O_CREAT|O_EXCL, 0)) == -1) { + if (errno == EEXIST) + return; + err(EX_TEMPFAIL, "open"); + } + + if (fchown(fd, deliver->userinfo.uid, deliver->userinfo.gid) == -1) + err(EX_TEMPFAIL, "fchown"); + + if (fchmod(fd, S_IRUSR|S_IWUSR) == -1) + err(EX_TEMPFAIL, "fchown"); +} diff --git a/smtpd/mda_unpriv.c b/smtpd/mda_unpriv.c new file mode 100644 index 00000000..2143b9a0 --- /dev/null +++ b/smtpd/mda_unpriv.c @@ -0,0 +1,110 @@ +/* $OpenBSD: mda_unpriv.c,v 1.6 2020/02/02 22:13:48 gilles Exp $ */ + +/* + * Copyright (c) 2018 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <paths.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" + + +void +mda_unpriv(struct dispatcher *dsp, struct deliver *deliver, + const char *pw_name, const char *pw_dir) +{ + int idx; + char *mda_environ[11]; + char mda_exec[LINE_MAX]; + char mda_wrapper[LINE_MAX]; + const char *mda_command; + const char *mda_command_wrap; + + if (deliver->mda_exec[0]) + mda_command = deliver->mda_exec; + else + mda_command = dsp->u.local.command; + + if (strlcpy(mda_exec, mda_command, sizeof (mda_exec)) + >= sizeof (mda_exec)) + errx(1, "mda command line too long"); + + if (mda_expand_format(mda_exec, sizeof mda_exec, deliver, + &deliver->userinfo, NULL) == -1) + errx(1, "mda command line could not be expanded"); + + mda_command = mda_exec; + + /* setup environment similar to other MTA */ + idx = 0; + xasprintf(&mda_environ[idx++], "PATH=%s", _PATH_DEFPATH); + xasprintf(&mda_environ[idx++], "DOMAIN=%s", deliver->rcpt.domain); + xasprintf(&mda_environ[idx++], "HOME=%s", pw_dir); + xasprintf(&mda_environ[idx++], "RECIPIENT=%s@%s", deliver->dest.user, deliver->dest.domain); + xasprintf(&mda_environ[idx++], "SHELL=/bin/sh"); + xasprintf(&mda_environ[idx++], "LOCAL=%s", deliver->rcpt.user); + xasprintf(&mda_environ[idx++], "LOGNAME=%s", pw_name); + xasprintf(&mda_environ[idx++], "USER=%s", pw_name); + + if (deliver->sender.user[0]) + xasprintf(&mda_environ[idx++], "SENDER=%s@%s", + deliver->sender.user, deliver->sender.domain); + else + xasprintf(&mda_environ[idx++], "SENDER="); + + if (deliver->mda_subaddress[0]) + xasprintf(&mda_environ[idx++], "EXTENSION=%s", deliver->mda_subaddress); + + mda_environ[idx++] = (char *)NULL; + + if (dsp->u.local.mda_wrapper) { + mda_command_wrap = dict_get(env->sc_mda_wrappers, + dsp->u.local.mda_wrapper); + if (mda_command_wrap == NULL) + errx(1, "could not find wrapper %s", + dsp->u.local.mda_wrapper); + + if (strlcpy(mda_wrapper, mda_command_wrap, sizeof (mda_wrapper)) + >= sizeof (mda_wrapper)) + errx(1, "mda command line too long"); + + if (mda_expand_format(mda_wrapper, sizeof mda_wrapper, deliver, + &deliver->userinfo, mda_command) == -1) + errx(1, "mda command line could not be expanded"); + mda_command = mda_wrapper; + } + execle("/bin/sh", "/bin/sh", "-c", mda_command, (char *)NULL, + mda_environ); + + perror("execle"); + _exit(1); +} + diff --git a/smtpd/mda_variables.c b/smtpd/mda_variables.c new file mode 100644 index 00000000..b672e492 --- /dev/null +++ b/smtpd/mda_variables.c @@ -0,0 +1,374 @@ +/* $OpenBSD: mda_variables.c,v 1.6 2019/09/19 07:35:36 gilles Exp $ */ + +/* + * Copyright (c) 2011-2017 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <netinet/in.h> + +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +#define EXPAND_DEPTH 10 + +ssize_t mda_expand_format(char *, size_t, const struct deliver *, + const struct userinfo *, const char *); +static ssize_t mda_expand_token(char *, size_t, const char *, + const struct deliver *, const struct userinfo *, const char *); +static int mod_lowercase(char *, size_t); +static int mod_uppercase(char *, size_t); +static int mod_strip(char *, size_t); + +static struct modifiers { + char *name; + int (*f)(char *buf, size_t len); +} token_modifiers[] = { + { "lowercase", mod_lowercase }, + { "uppercase", mod_uppercase }, + { "strip", mod_strip }, + { "raw", NULL }, /* special case, must stay last */ +}; + +#define MAXTOKENLEN 128 + +static ssize_t +mda_expand_token(char *dest, size_t len, const char *token, + const struct deliver *dlv, const struct userinfo *ui, const char *mda_command) +{ + char rtoken[MAXTOKENLEN]; + char tmp[EXPAND_BUFFER]; + const char *string; + char *lbracket, *rbracket, *content, *sep, *mods; + ssize_t i; + ssize_t begoff, endoff; + const char *errstr = NULL; + int replace = 1; + int raw = 0; + + begoff = 0; + endoff = EXPAND_BUFFER; + mods = NULL; + + if (strlcpy(rtoken, token, sizeof rtoken) >= sizeof rtoken) + return -1; + + /* token[x[:y]] -> extracts optional x and y, converts into offsets */ + if ((lbracket = strchr(rtoken, '[')) && + (rbracket = strchr(rtoken, ']'))) { + /* ] before [ ... or empty */ + if (rbracket < lbracket || rbracket - lbracket <= 1) + return -1; + + *lbracket = *rbracket = '\0'; + content = lbracket + 1; + + if ((sep = strchr(content, ':')) == NULL) + endoff = begoff = strtonum(content, -EXPAND_BUFFER, + EXPAND_BUFFER, &errstr); + else { + *sep = '\0'; + if (content != sep) + begoff = strtonum(content, -EXPAND_BUFFER, + EXPAND_BUFFER, &errstr); + if (*(++sep)) { + if (errstr == NULL) + endoff = strtonum(sep, -EXPAND_BUFFER, + EXPAND_BUFFER, &errstr); + } + } + if (errstr) + return -1; + + /* token:mod_1,mod_2,mod_n -> extract modifiers */ + mods = strchr(rbracket + 1, ':'); + } else { + if ((mods = strchr(rtoken, ':')) != NULL) + *mods++ = '\0'; + } + + /* token -> expanded token */ + if (!strcasecmp("sender", rtoken)) { + if (snprintf(tmp, sizeof tmp, "%s@%s", + dlv->sender.user, dlv->sender.domain) >= (int)sizeof tmp) + return -1; + if (strcmp(tmp, "@") == 0) + (void)strlcpy(tmp, "", sizeof tmp); + string = tmp; + } + else if (!strcasecmp("rcpt", rtoken)) { + if (snprintf(tmp, sizeof tmp, "%s@%s", + dlv->rcpt.user, dlv->rcpt.domain) >= (int)sizeof tmp) + return -1; + if (strcmp(tmp, "@") == 0) + (void)strlcpy(tmp, "", sizeof tmp); + string = tmp; + } + else if (!strcasecmp("dest", rtoken)) { + if (snprintf(tmp, sizeof tmp, "%s@%s", + dlv->dest.user, dlv->dest.domain) >= (int)sizeof tmp) + return -1; + if (strcmp(tmp, "@") == 0) + (void)strlcpy(tmp, "", sizeof tmp); + string = tmp; + } + else if (!strcasecmp("sender.user", rtoken)) + string = dlv->sender.user; + else if (!strcasecmp("sender.domain", rtoken)) + string = dlv->sender.domain; + else if (!strcasecmp("user.username", rtoken)) + string = ui->username; + else if (!strcasecmp("user.directory", rtoken)) { + string = ui->directory; + replace = 0; + } + else if (!strcasecmp("rcpt.user", rtoken)) + string = dlv->rcpt.user; + else if (!strcasecmp("rcpt.domain", rtoken)) + string = dlv->rcpt.domain; + else if (!strcasecmp("dest.user", rtoken)) + string = dlv->dest.user; + else if (!strcasecmp("dest.domain", rtoken)) + string = dlv->dest.domain; + else if (!strcasecmp("mda", rtoken)) { + string = mda_command; + replace = 0; + } + else if (!strcasecmp("mbox.from", rtoken)) { + if (snprintf(tmp, sizeof tmp, "%s@%s", + dlv->sender.user, dlv->sender.domain) >= (int)sizeof tmp) + return -1; + if (strcmp(tmp, "@") == 0) + (void)strlcpy(tmp, "MAILER-DAEMON", sizeof tmp); + string = tmp; + } + else + return -1; + + if (string != tmp) { + if (strlcpy(tmp, string, sizeof tmp) >= sizeof tmp) + return -1; + string = tmp; + } + + /* apply modifiers */ + if (mods != NULL) { + do { + if ((sep = strchr(mods, '|')) != NULL) + *sep++ = '\0'; + for (i = 0; (size_t)i < nitems(token_modifiers); ++i) { + if (!strcasecmp(token_modifiers[i].name, mods)) { + if (token_modifiers[i].f == NULL) { + raw = 1; + break; + } + if (!token_modifiers[i].f(tmp, sizeof tmp)) + return -1; /* modifier error */ + break; + } + } + if ((size_t)i == nitems(token_modifiers)) + return -1; /* modifier not found */ + } while ((mods = sep) != NULL); + } + + if (!raw && replace) + for (i = 0; (size_t)i < strlen(tmp); ++i) + if (strchr(MAILADDR_ESCAPE, tmp[i])) + tmp[i] = ':'; + + /* expanded string is empty */ + i = strlen(string); + if (i == 0) + return 0; + + /* begin offset beyond end of string */ + if (begoff >= i) + return -1; + + /* end offset beyond end of string, make it end of string */ + if (endoff >= i) + endoff = i - 1; + + /* negative begin offset, make it relative to end of string */ + if (begoff < 0) + begoff += i; + /* negative end offset, make it relative to end of string, + * note that end offset is inclusive. + */ + if (endoff < 0) + endoff += i - 1; + + /* check that final offsets are valid */ + if (begoff < 0 || endoff < 0 || endoff < begoff) + return -1; + endoff += 1; /* end offset is inclusive */ + + /* check that substring does not exceed destination buffer length */ + i = endoff - begoff; + if ((size_t)i + 1 >= len) + return -1; + + string += begoff; + for (; i; i--) { + *dest = *string; + dest++; + string++; + } + + return endoff - begoff; +} + + +ssize_t +mda_expand_format(char *buf, size_t len, const struct deliver *dlv, + const struct userinfo *ui, const char *mda_command) +{ + char tmpbuf[EXPAND_BUFFER], *ptmp, *pbuf, *ebuf; + char exptok[EXPAND_BUFFER]; + ssize_t exptoklen; + char token[MAXTOKENLEN]; + size_t ret, tmpret; + + if (len < sizeof tmpbuf) { + log_warnx("mda_expand_format: tmp buffer < rule buffer"); + return -1; + } + + memset(tmpbuf, 0, sizeof tmpbuf); + pbuf = buf; + ptmp = tmpbuf; + ret = tmpret = 0; + + /* special case: ~/ only allowed expanded at the beginning */ + if (strncmp(pbuf, "~/", 2) == 0) { + tmpret = snprintf(ptmp, sizeof tmpbuf, "%s/", ui->directory); + if (tmpret >= sizeof tmpbuf) { + log_warnx("warn: user directory for %s too large", + ui->directory); + return 0; + } + ret += tmpret; + ptmp += tmpret; + pbuf += 2; + } + + + /* expansion loop */ + for (; *pbuf && ret < sizeof tmpbuf; ret += tmpret) { + if (*pbuf == '%' && *(pbuf + 1) == '%') { + *ptmp++ = *pbuf++; + pbuf += 1; + tmpret = 1; + continue; + } + + if (*pbuf != '%' || *(pbuf + 1) != '{') { + *ptmp++ = *pbuf++; + tmpret = 1; + continue; + } + + /* %{...} otherwise fail */ + if (*(pbuf+1) != '{' || (ebuf = strchr(pbuf+1, '}')) == NULL) + return 0; + + /* extract token from %{token} */ + if ((size_t)(ebuf - pbuf) - 1 >= sizeof token) + return 0; + + memcpy(token, pbuf+2, ebuf-pbuf-1); + if (strchr(token, '}') == NULL) + return 0; + *strchr(token, '}') = '\0'; + + exptoklen = mda_expand_token(exptok, sizeof exptok, token, dlv, + ui, mda_command); + if (exptoklen == -1) + return -1; + + /* writing expanded token at ptmp will overflow tmpbuf */ + if (sizeof (tmpbuf) - (ptmp - tmpbuf) <= (size_t)exptoklen) + return -1; + + memcpy(ptmp, exptok, exptoklen); + pbuf = ebuf + 1; + ptmp += exptoklen; + tmpret = exptoklen; + } + if (ret >= sizeof tmpbuf) + return -1; + + if ((ret = strlcpy(buf, tmpbuf, len)) >= len) + return -1; + + return ret; +} + +static int +mod_lowercase(char *buf, size_t len) +{ + char tmp[EXPAND_BUFFER]; + + if (!lowercase(tmp, buf, sizeof tmp)) + return 0; + if (strlcpy(buf, tmp, len) >= len) + return 0; + return 1; +} + +static int +mod_uppercase(char *buf, size_t len) +{ + char tmp[EXPAND_BUFFER]; + + if (!uppercase(tmp, buf, sizeof tmp)) + return 0; + if (strlcpy(buf, tmp, len) >= len) + return 0; + return 1; +} + +static int +mod_strip(char *buf, size_t len) +{ + char *tag, *at; + unsigned int i; + + /* gilles+hackers -> gilles */ + if ((tag = strchr(buf, *env->sc_subaddressing_delim)) != NULL) { + /* gilles+hackers@poolp.org -> gilles@poolp.org */ + if ((at = strchr(tag, '@')) != NULL) { + for (i = 0; i <= strlen(at); ++i) + tag[i] = at[i]; + } else + *tag = '\0'; + } + return 1; +} diff --git a/smtpd/mproc.c b/smtpd/mproc.c new file mode 100644 index 00000000..bde229e1 --- /dev/null +++ b/smtpd/mproc.c @@ -0,0 +1,676 @@ +/* $OpenBSD: mproc.c,v 1.36 2020/03/17 09:01:53 tobhe Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot <eric@faurot.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/tree.h> +#include <sys/queue.h> +#include <sys/uio.h> + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <arpa/nameser.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <limits.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +static void mproc_dispatch(int, short, void *); + +static ssize_t imsg_read_nofd(struct imsgbuf *); + +int +mproc_fork(struct mproc *p, const char *path, char *argv[]) +{ + int sp[2]; + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) + return (-1); + + io_set_nonblocking(sp[0]); + io_set_nonblocking(sp[1]); + + if ((p->pid = fork()) == -1) + goto err; + + if (p->pid == 0) { + /* child process */ + dup2(sp[0], STDIN_FILENO); + closefrom(STDERR_FILENO + 1); + execv(path, argv); + err(1, "execv: %s", path); + } + + /* parent process */ + close(sp[0]); + mproc_init(p, sp[1]); + return (0); + +err: + log_warn("warn: Failed to start process %s, instance of %s", argv[0], path); + close(sp[0]); + close(sp[1]); + return (-1); +} + +void +mproc_init(struct mproc *p, int fd) +{ + imsg_init(&p->imsgbuf, fd); +} + +void +mproc_clear(struct mproc *p) +{ + log_debug("debug: clearing p=%s, fd=%d, pid=%d", p->name, p->imsgbuf.fd, p->pid); + + event_del(&p->ev); + close(p->imsgbuf.fd); + imsg_clear(&p->imsgbuf); +} + +void +mproc_enable(struct mproc *p) +{ + if (p->enable == 0) { + log_trace(TRACE_MPROC, "mproc: %s -> %s: enabled", + proc_name(smtpd_process), + proc_name(p->proc)); + p->enable = 1; + } + mproc_event_add(p); +} + +void +mproc_disable(struct mproc *p) +{ + if (p->enable == 1) { + log_trace(TRACE_MPROC, "mproc: %s -> %s: disabled", + proc_name(smtpd_process), + proc_name(p->proc)); + p->enable = 0; + } + mproc_event_add(p); +} + +void +mproc_event_add(struct mproc *p) +{ + short events; + + if (p->enable) + events = EV_READ; + else + events = 0; + + if (p->imsgbuf.w.queued) + events |= EV_WRITE; + + if (p->events) + event_del(&p->ev); + + p->events = events; + if (events) { + event_set(&p->ev, p->imsgbuf.fd, events, mproc_dispatch, p); + event_add(&p->ev, NULL); + } +} + +static void +mproc_dispatch(int fd, short event, void *arg) +{ + struct mproc *p = arg; + struct imsg imsg; + ssize_t n; + + p->events = 0; + + if (event & EV_READ) { + + if (p->proc == PROC_CLIENT) + n = imsg_read_nofd(&p->imsgbuf); + else + n = imsg_read(&p->imsgbuf); + + switch (n) { + case -1: + if (errno == EAGAIN) + break; + log_warn("warn: %s -> %s: imsg_read", + proc_name(smtpd_process), p->name); + fatal("exiting"); + /* NOTREACHED */ + case 0: + /* this pipe is dead, so remove the event handler */ + log_debug("debug: %s -> %s: pipe closed", + proc_name(smtpd_process), p->name); + p->handler(p, NULL); + return; + default: + break; + } + } + + if (event & EV_WRITE) { + n = msgbuf_write(&p->imsgbuf.w); + if (n == 0 || (n == -1 && errno != EAGAIN)) { + /* this pipe is dead, so remove the event handler */ + log_debug("debug: %s -> %s: pipe closed", + proc_name(smtpd_process), p->name); + p->handler(p, NULL); + return; + } + } + + for (;;) { + if ((n = imsg_get(&p->imsgbuf, &imsg)) == -1) { + + if (smtpd_process == PROC_CONTROL && + p->proc == PROC_CLIENT) { + log_warnx("warn: client sent invalid imsg " + "over control socket"); + p->handler(p, NULL); + return; + } + log_warn("fatal: %s: error in imsg_get for %s", + proc_name(smtpd_process), p->name); + fatalx(NULL); + } + if (n == 0) + break; + + p->handler(p, &imsg); + + imsg_free(&imsg); + } + + mproc_event_add(p); +} + +/* This should go into libutil */ +static ssize_t +imsg_read_nofd(struct imsgbuf *ibuf) +{ + ssize_t n; + char *buf; + size_t len; + + buf = ibuf->r.buf + ibuf->r.wpos; + len = sizeof(ibuf->r.buf) - ibuf->r.wpos; + + while ((n = recv(ibuf->fd, buf, len, 0)) == -1) { + if (errno != EINTR) + return (n); + } + + ibuf->r.wpos += n; + return (n); +} + +void +m_forward(struct mproc *p, struct imsg *imsg) +{ + imsg_compose(&p->imsgbuf, imsg->hdr.type, imsg->hdr.peerid, + imsg->hdr.pid, imsg->fd, imsg->data, + imsg->hdr.len - sizeof(imsg->hdr)); + + if (imsg->hdr.type != IMSG_STAT_DECREMENT && + imsg->hdr.type != IMSG_STAT_INCREMENT) + log_trace(TRACE_MPROC, "mproc: %s -> %s : %zu %s (forward)", + proc_name(smtpd_process), + proc_name(p->proc), + imsg->hdr.len - sizeof(imsg->hdr), + imsg_to_str(imsg->hdr.type)); + + mproc_event_add(p); +} + +void +m_compose(struct mproc *p, uint32_t type, uint32_t peerid, pid_t pid, int fd, + void *data, size_t len) +{ + imsg_compose(&p->imsgbuf, type, peerid, pid, fd, data, len); + + if (type != IMSG_STAT_DECREMENT && + type != IMSG_STAT_INCREMENT) + log_trace(TRACE_MPROC, "mproc: %s -> %s : %zu %s", + proc_name(smtpd_process), + proc_name(p->proc), + len, + imsg_to_str(type)); + + mproc_event_add(p); +} + +void +m_composev(struct mproc *p, uint32_t type, uint32_t peerid, pid_t pid, + int fd, const struct iovec *iov, int n) +{ + size_t len; + int i; + + imsg_composev(&p->imsgbuf, type, peerid, pid, fd, iov, n); + + len = 0; + for (i = 0; i < n; i++) + len += iov[i].iov_len; + + if (type != IMSG_STAT_DECREMENT && + type != IMSG_STAT_INCREMENT) + log_trace(TRACE_MPROC, "mproc: %s -> %s : %zu %s", + proc_name(smtpd_process), + proc_name(p->proc), + len, + imsg_to_str(type)); + + mproc_event_add(p); +} + +void +m_create(struct mproc *p, uint32_t type, uint32_t peerid, pid_t pid, int fd) +{ + p->m_pos = 0; + p->m_type = type; + p->m_peerid = peerid; + p->m_pid = pid; + p->m_fd = fd; +} + +void +m_add(struct mproc *p, const void *data, size_t len) +{ + size_t alloc; + void *tmp; + + if (p->m_pos + len + IMSG_HEADER_SIZE > MAX_IMSGSIZE) { + log_warnx("warn: message too large"); + fatal(NULL); + } + + alloc = p->m_alloc ? p->m_alloc : 128; + while (p->m_pos + len > alloc) + alloc *= 2; + if (alloc != p->m_alloc) { + log_trace(TRACE_MPROC, "mproc: %s -> %s: realloc %zu -> %zu", + proc_name(smtpd_process), + proc_name(p->proc), + p->m_alloc, + alloc); + + tmp = recallocarray(p->m_buf, p->m_alloc, alloc, 1); + if (tmp == NULL) + fatal("realloc"); + p->m_alloc = alloc; + p->m_buf = tmp; + } + + memmove(p->m_buf + p->m_pos, data, len); + p->m_pos += len; +} + +void +m_close(struct mproc *p) +{ + if (imsg_compose(&p->imsgbuf, p->m_type, p->m_peerid, p->m_pid, p->m_fd, + p->m_buf, p->m_pos) == -1) + fatal("imsg_compose"); + + log_trace(TRACE_MPROC, "mproc: %s -> %s : %zu %s", + proc_name(smtpd_process), + proc_name(p->proc), + p->m_pos, + imsg_to_str(p->m_type)); + + mproc_event_add(p); +} + +void +m_flush(struct mproc *p) +{ + if (imsg_compose(&p->imsgbuf, p->m_type, p->m_peerid, p->m_pid, p->m_fd, + p->m_buf, p->m_pos) == -1) + fatal("imsg_compose"); + + log_trace(TRACE_MPROC, "mproc: %s -> %s : %zu %s (flush)", + proc_name(smtpd_process), + proc_name(p->proc), + p->m_pos, + imsg_to_str(p->m_type)); + + p->m_pos = 0; + + if (imsg_flush(&p->imsgbuf) == -1) + fatal("imsg_flush"); +} + +static struct imsg * current; + +static void +m_error(const char *error) +{ + char buf[512]; + + (void)snprintf(buf, sizeof buf, "%s: %s: %s", + proc_name(smtpd_process), + imsg_to_str(current->hdr.type), + error); + fatalx("%s", buf); +} + +void +m_msg(struct msg *m, struct imsg *imsg) +{ + current = imsg; + m->pos = imsg->data; + m->end = m->pos + (imsg->hdr.len - sizeof(imsg->hdr)); +} + +void +m_end(struct msg *m) +{ + if (m->pos != m->end) + m_error("not at msg end"); +} + +int +m_is_eom(struct msg *m) +{ + return (m->pos == m->end); +} + +static inline void +m_get(struct msg *m, void *dst, size_t sz) +{ + if (sz > MAX_IMSGSIZE || + m->end - m->pos < (ssize_t)sz) + fatalx("msg too short"); + + memmove(dst, m->pos, sz); + m->pos += sz; +} + +void +m_add_int(struct mproc *m, int v) +{ + m_add(m, &v, sizeof(v)); +}; + +void +m_add_u32(struct mproc *m, uint32_t u32) +{ + m_add(m, &u32, sizeof(u32)); +}; + +void +m_add_size(struct mproc *m, size_t sz) +{ + m_add(m, &sz, sizeof(sz)); +}; + +void +m_add_time(struct mproc *m, time_t v) +{ + m_add(m, &v, sizeof(v)); +}; + +void +m_add_timeval(struct mproc *m, struct timeval *tv) +{ + m_add(m, tv, sizeof(*tv)); +} + + +void +m_add_string(struct mproc *m, const char *v) +{ + if (v) { + m_add(m, "s", 1); + m_add(m, v, strlen(v) + 1); + } + else + m_add(m, "\0", 1); +}; + +void +m_add_data(struct mproc *m, const void *v, size_t len) +{ + m_add_size(m, len); + m_add(m, v, len); +}; + +void +m_add_id(struct mproc *m, uint64_t v) +{ + m_add(m, &v, sizeof(v)); +} + +void +m_add_evpid(struct mproc *m, uint64_t v) +{ + m_add(m, &v, sizeof(v)); +} + +void +m_add_msgid(struct mproc *m, uint32_t v) +{ + m_add(m, &v, sizeof(v)); +} + +void +m_add_sockaddr(struct mproc *m, const struct sockaddr *sa) +{ + m_add_size(m, SA_LEN(sa)); + m_add(m, sa, SA_LEN(sa)); +} + +void +m_add_mailaddr(struct mproc *m, const struct mailaddr *maddr) +{ + m_add(m, maddr, sizeof(*maddr)); +} + +void +m_add_envelope(struct mproc *m, const struct envelope *evp) +{ + char buf[sizeof(*evp)]; + + envelope_dump_buffer(evp, buf, sizeof(buf)); + m_add_evpid(m, evp->id); + m_add_string(m, buf); +} + +void +m_add_params(struct mproc *m, struct dict *d) +{ + const char *key; + char *value; + void *iter; + + if (d == NULL) { + m_add_size(m, 0); + return; + } + m_add_size(m, dict_count(d)); + iter = NULL; + while (dict_iter(d, &iter, &key, (void **)&value)) { + m_add_string(m, key); + m_add_string(m, value); + } +} + +void +m_get_int(struct msg *m, int *i) +{ + m_get(m, i, sizeof(*i)); +} + +void +m_get_u32(struct msg *m, uint32_t *u32) +{ + m_get(m, u32, sizeof(*u32)); +} + +void +m_get_size(struct msg *m, size_t *sz) +{ + m_get(m, sz, sizeof(*sz)); +} + +void +m_get_time(struct msg *m, time_t *t) +{ + m_get(m, t, sizeof(*t)); +} + +void +m_get_timeval(struct msg *m, struct timeval *tv) +{ + m_get(m, tv, sizeof(*tv)); +} + +void +m_get_string(struct msg *m, const char **s) +{ + uint8_t *end; + char c; + + if (m->pos >= m->end) + m_error("msg too short"); + + c = *m->pos++; + if (c == '\0') { + *s = NULL; + return; + } + + if (m->pos >= m->end) + m_error("msg too short"); + end = memchr(m->pos, 0, m->end - m->pos); + if (end == NULL) + m_error("unterminated string"); + + *s = m->pos; + m->pos = end + 1; +} + +void +m_get_data(struct msg *m, const void **data, size_t *sz) +{ + m_get_size(m, sz); + + if (*sz == 0) { + *data = NULL; + return; + } + + if (m->pos + *sz > m->end) + m_error("msg too short"); + + *data = m->pos; + m->pos += *sz; +} + +void +m_get_evpid(struct msg *m, uint64_t *evpid) +{ + m_get(m, evpid, sizeof(*evpid)); +} + +void +m_get_msgid(struct msg *m, uint32_t *msgid) +{ + m_get(m, msgid, sizeof(*msgid)); +} + +void +m_get_id(struct msg *m, uint64_t *id) +{ + m_get(m, id, sizeof(*id)); +} + +void +m_get_sockaddr(struct msg *m, struct sockaddr *sa) +{ + size_t len; + + m_get_size(m, &len); + m_get(m, sa, len); +} + +void +m_get_mailaddr(struct msg *m, struct mailaddr *maddr) +{ + m_get(m, maddr, sizeof(*maddr)); +} + +void +m_get_envelope(struct msg *m, struct envelope *evp) +{ + uint64_t evpid; + const char *buf; + + m_get_evpid(m, &evpid); + m_get_string(m, &buf); + if (buf == NULL) + fatalx("empty envelope buffer"); + + if (!envelope_load_buffer(evp, buf, strlen(buf))) + fatalx("failed to retrieve envelope"); + evp->id = evpid; +} + +void +m_get_params(struct msg *m, struct dict *d) +{ + size_t c; + const char *key; + const char *value; + char *tmp; + + dict_init(d); + + m_get_size(m, &c); + + for (; c; c--) { + m_get_string(m, &key); + m_get_string(m, &value); + if ((tmp = strdup(value)) == NULL) + fatal("m_get_params"); + dict_set(d, key, tmp); + } +} + +void +m_clear_params(struct dict *d) +{ + char *value; + + while (dict_poproot(d, (void **)&value)) + free(value); +} diff --git a/smtpd/mta.c b/smtpd/mta.c new file mode 100644 index 00000000..922170ae --- /dev/null +++ b/smtpd/mta.c @@ -0,0 +1,2647 @@ +/* $OpenBSD: mta.c,v 1.234 2019/12/21 10:34:07 gilles Exp $ */ + +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <netdb.h> +#include <grp.h> /* needed for setgroups */ +#include <limits.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" +#include "log.h" + +#define MAXERROR_PER_ROUTE 4 + +#define DELAY_CHECK_SOURCE 1 +#define DELAY_CHECK_SOURCE_SLOW 10 +#define DELAY_CHECK_SOURCE_FAST 0 +#define DELAY_CHECK_LIMIT 5 + +#define DELAY_QUADRATIC 1 +#define DELAY_ROUTE_BASE 15 +#define DELAY_ROUTE_MAX 3600 + +#define RELAY_ONHOLD 0x01 +#define RELAY_HOLDQ 0x02 + +static void mta_handle_envelope(struct envelope *, const char *); +static void mta_query_smarthost(struct envelope *); +static void mta_on_smarthost(struct envelope *, const char *); +static void mta_query_mx(struct mta_relay *); +static void mta_query_secret(struct mta_relay *); +static void mta_query_preference(struct mta_relay *); +static void mta_query_source(struct mta_relay *); +static void mta_on_mx(void *, void *, void *); +static void mta_on_secret(struct mta_relay *, const char *); +static void mta_on_preference(struct mta_relay *, int); +static void mta_on_source(struct mta_relay *, struct mta_source *); +static void mta_on_timeout(struct runq *, void *); +static void mta_connect(struct mta_connector *); +static void mta_route_enable(struct mta_route *); +static void mta_route_disable(struct mta_route *, int, int); +static void mta_drain(struct mta_relay *); +static void mta_delivery_flush_event(int, short, void *); +static void mta_flush(struct mta_relay *, int, const char *); +static struct mta_route *mta_find_route(struct mta_connector *, time_t, int*, + time_t*, struct mta_mx **); +static void mta_log(const struct mta_envelope *, const char *, const char *, + const char *, const char *); + +SPLAY_HEAD(mta_relay_tree, mta_relay); +static struct mta_relay *mta_relay(struct envelope *, struct relayhost *); +static void mta_relay_ref(struct mta_relay *); +static void mta_relay_unref(struct mta_relay *); +static void mta_relay_show(struct mta_relay *, struct mproc *, uint32_t, time_t); +static int mta_relay_cmp(const struct mta_relay *, const struct mta_relay *); +SPLAY_PROTOTYPE(mta_relay_tree, mta_relay, entry, mta_relay_cmp); + +SPLAY_HEAD(mta_host_tree, mta_host); +static struct mta_host *mta_host(const struct sockaddr *); +static void mta_host_ref(struct mta_host *); +static void mta_host_unref(struct mta_host *); +static int mta_host_cmp(const struct mta_host *, const struct mta_host *); +SPLAY_PROTOTYPE(mta_host_tree, mta_host, entry, mta_host_cmp); + +SPLAY_HEAD(mta_domain_tree, mta_domain); +static struct mta_domain *mta_domain(char *, int); +#if 0 +static void mta_domain_ref(struct mta_domain *); +#endif +static void mta_domain_unref(struct mta_domain *); +static int mta_domain_cmp(const struct mta_domain *, const struct mta_domain *); +SPLAY_PROTOTYPE(mta_domain_tree, mta_domain, entry, mta_domain_cmp); + +SPLAY_HEAD(mta_source_tree, mta_source); +static struct mta_source *mta_source(const struct sockaddr *); +static void mta_source_ref(struct mta_source *); +static void mta_source_unref(struct mta_source *); +static const char *mta_source_to_text(struct mta_source *); +static int mta_source_cmp(const struct mta_source *, const struct mta_source *); +SPLAY_PROTOTYPE(mta_source_tree, mta_source, entry, mta_source_cmp); + +static struct mta_connector *mta_connector(struct mta_relay *, + struct mta_source *); +static void mta_connector_free(struct mta_connector *); +static const char *mta_connector_to_text(struct mta_connector *); + +SPLAY_HEAD(mta_route_tree, mta_route); +static struct mta_route *mta_route(struct mta_source *, struct mta_host *); +static void mta_route_ref(struct mta_route *); +static void mta_route_unref(struct mta_route *); +static const char *mta_route_to_text(struct mta_route *); +static int mta_route_cmp(const struct mta_route *, const struct mta_route *); +SPLAY_PROTOTYPE(mta_route_tree, mta_route, entry, mta_route_cmp); + +struct mta_block { + SPLAY_ENTRY(mta_block) entry; + struct mta_source *source; + char *domain; +}; + +SPLAY_HEAD(mta_block_tree, mta_block); +void mta_block(struct mta_source *, char *); +void mta_unblock(struct mta_source *, char *); +int mta_is_blocked(struct mta_source *, char *); +static int mta_block_cmp(const struct mta_block *, const struct mta_block *); +SPLAY_PROTOTYPE(mta_block_tree, mta_block, entry, mta_block_cmp); + +static struct mta_relay_tree relays; +static struct mta_domain_tree domains; +static struct mta_host_tree hosts; +static struct mta_source_tree sources; +static struct mta_route_tree routes; +static struct mta_block_tree blocks; + +static struct tree wait_mx; +static struct tree wait_preference; +static struct tree wait_secret; +static struct tree wait_smarthost; +static struct tree wait_source; +static struct tree flush_evp; +static struct event ev_flush_evp; + +static struct runq *runq_relay; +static struct runq *runq_connector; +static struct runq *runq_route; +static struct runq *runq_hoststat; + +static time_t max_seen_conndelay_route; +static time_t max_seen_discdelay_route; + +#define HOSTSTAT_EXPIRE_DELAY (4 * 3600) +struct hoststat { + char name[HOST_NAME_MAX+1]; + time_t tm; + char error[LINE_MAX]; + struct tree deferred; +}; +static struct dict hoststat; + +void mta_hoststat_update(const char *, const char *); +void mta_hoststat_cache(const char *, uint64_t); +void mta_hoststat_uncache(const char *, uint64_t); +void mta_hoststat_reschedule(const char *); +static void mta_hoststat_remove_entry(struct hoststat *); + +void +mta_imsg(struct mproc *p, struct imsg *imsg) +{ + struct mta_relay *relay; + struct mta_domain *domain; + struct mta_host *host; + struct mta_route *route; + struct mta_block *block; + struct mta_mx *mx, *imx; + struct mta_source *source; + struct hoststat *hs; + struct sockaddr_storage ss; + struct envelope evp, *e; + struct msg m; + const char *secret; + const char *hostname; + const char *dom; + const char *smarthost; + uint64_t reqid; + time_t t; + char buf[LINE_MAX]; + int dnserror, preference, v, status; + void *iter; + uint64_t u64; + + switch (imsg->hdr.type) { + case IMSG_QUEUE_TRANSFER: + m_msg(&m, imsg); + m_get_envelope(&m, &evp); + m_end(&m); + mta_handle_envelope(&evp, NULL); + return; + + case IMSG_MTA_OPEN_MESSAGE: + mta_session_imsg(p, imsg); + return; + + case IMSG_MTA_LOOKUP_CREDENTIALS: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &secret); + m_end(&m); + relay = tree_xpop(&wait_secret, reqid); + mta_on_secret(relay, secret[0] ? secret : NULL); + return; + + case IMSG_MTA_LOOKUP_SOURCE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &status); + if (status == LKA_OK) + m_get_sockaddr(&m, (struct sockaddr*)&ss); + m_end(&m); + + relay = tree_xpop(&wait_source, reqid); + mta_on_source(relay, (status == LKA_OK) ? + mta_source((struct sockaddr *)&ss) : NULL); + return; + + case IMSG_MTA_LOOKUP_SMARTHOST: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &status); + smarthost = NULL; + if (status == LKA_OK) + m_get_string(&m, &smarthost); + m_end(&m); + + e = tree_xpop(&wait_smarthost, reqid); + mta_on_smarthost(e, smarthost); + return; + + case IMSG_MTA_LOOKUP_HELO: + mta_session_imsg(p, imsg); + return; + + case IMSG_MTA_DNS_HOST: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &hostname); + m_get_sockaddr(&m, (struct sockaddr*)&ss); + m_get_int(&m, &preference); + m_end(&m); + domain = tree_xget(&wait_mx, reqid); + mx = xcalloc(1, sizeof *mx); + mx->mxname = xstrdup(hostname); + mx->host = mta_host((struct sockaddr*)&ss); + mx->preference = preference; + TAILQ_FOREACH(imx, &domain->mxs, entry) { + if (imx->preference > mx->preference) { + TAILQ_INSERT_BEFORE(imx, mx, entry); + return; + } + } + TAILQ_INSERT_TAIL(&domain->mxs, mx, entry); + return; + + case IMSG_MTA_DNS_HOST_END: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &dnserror); + m_end(&m); + domain = tree_xpop(&wait_mx, reqid); + domain->mxstatus = dnserror; + if (domain->mxstatus == DNS_OK) { + log_debug("debug: MXs for domain %s:", + domain->name); + TAILQ_FOREACH(mx, &domain->mxs, entry) + log_debug(" %s preference %d", + sa_to_text(mx->host->sa), + mx->preference); + } + else { + log_debug("debug: Failed MX query for %s:", + domain->name); + } + domain->lastmxquery = time(NULL); + waitq_run(&domain->mxs, domain); + return; + + case IMSG_MTA_DNS_MX_PREFERENCE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &dnserror); + if (dnserror == 0) + m_get_int(&m, &preference); + m_end(&m); + + relay = tree_xpop(&wait_preference, reqid); + if (dnserror) { + log_warnx("warn: Couldn't find backup " + "preference for %s: error %d", + mta_relay_to_text(relay), dnserror); + preference = INT_MAX; + } + mta_on_preference(relay, preference); + return; + + case IMSG_CTL_RESUME_ROUTE: + u64 = *((uint64_t *)imsg->data); + if (u64) + log_debug("resuming route: %llu", + (unsigned long long)u64); + else + log_debug("resuming all routes"); + SPLAY_FOREACH(route, mta_route_tree, &routes) { + if (u64 && route->id != u64) + continue; + + if (route->flags & ROUTE_DISABLED) { + log_info("smtp-out: Enabling route %s per admin request", + mta_route_to_text(route)); + if (!runq_cancel(runq_route, route)) { + log_warnx("warn: route not on runq"); + fatalx("exiting"); + } + route->flags &= ~ROUTE_DISABLED; + route->flags |= ROUTE_NEW; + route->nerror = 0; + route->penalty = 0; + mta_route_unref(route); /* from mta_route_disable */ + } + + if (u64) + break; + } + return; + + case IMSG_CTL_MTA_SHOW_HOSTS: + t = time(NULL); + SPLAY_FOREACH(host, mta_host_tree, &hosts) { + (void)snprintf(buf, sizeof(buf), + "%s %s refcount=%d nconn=%zu lastconn=%s", + sockaddr_to_text(host->sa), + host->ptrname, + host->refcount, + host->nconn, + host->lastconn ? duration_to_text(t - host->lastconn) : "-"); + m_compose(p, IMSG_CTL_MTA_SHOW_HOSTS, + imsg->hdr.peerid, 0, -1, + buf, strlen(buf) + 1); + } + m_compose(p, IMSG_CTL_MTA_SHOW_HOSTS, imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + + case IMSG_CTL_MTA_SHOW_RELAYS: + t = time(NULL); + SPLAY_FOREACH(relay, mta_relay_tree, &relays) + mta_relay_show(relay, p, imsg->hdr.peerid, t); + m_compose(p, IMSG_CTL_MTA_SHOW_RELAYS, imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + + case IMSG_CTL_MTA_SHOW_ROUTES: + SPLAY_FOREACH(route, mta_route_tree, &routes) { + v = runq_pending(runq_route, route, &t); + (void)snprintf(buf, sizeof(buf), + "%llu. %s %c%c%c%c nconn=%zu nerror=%d penalty=%d timeout=%s", + (unsigned long long)route->id, + mta_route_to_text(route), + route->flags & ROUTE_NEW ? 'N' : '-', + route->flags & ROUTE_DISABLED ? 'D' : '-', + route->flags & ROUTE_RUNQ ? 'Q' : '-', + route->flags & ROUTE_KEEPALIVE ? 'K' : '-', + route->nconn, + route->nerror, + route->penalty, + v ? duration_to_text(t - time(NULL)) : "-"); + m_compose(p, IMSG_CTL_MTA_SHOW_ROUTES, + imsg->hdr.peerid, 0, -1, + buf, strlen(buf) + 1); + } + m_compose(p, IMSG_CTL_MTA_SHOW_ROUTES, imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + + case IMSG_CTL_MTA_SHOW_HOSTSTATS: + iter = NULL; + while (dict_iter(&hoststat, &iter, &hostname, + (void **)&hs)) { + (void)snprintf(buf, sizeof(buf), + "%s|%llu|%s", + hostname, (unsigned long long) hs->tm, + hs->error); + m_compose(p, IMSG_CTL_MTA_SHOW_HOSTSTATS, + imsg->hdr.peerid, 0, -1, + buf, strlen(buf) + 1); + } + m_compose(p, IMSG_CTL_MTA_SHOW_HOSTSTATS, + imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + + case IMSG_CTL_MTA_BLOCK: + m_msg(&m, imsg); + m_get_sockaddr(&m, (struct sockaddr*)&ss); + m_get_string(&m, &dom); + m_end(&m); + source = mta_source((struct sockaddr*)&ss); + if (*dom != '\0') { + if (!(strlcpy(buf, dom, sizeof(buf)) + >= sizeof(buf))) + mta_block(source, buf); + } + else + mta_block(source, NULL); + mta_source_unref(source); + m_compose(p, IMSG_CTL_OK, imsg->hdr.peerid, 0, -1, NULL, 0); + return; + + case IMSG_CTL_MTA_UNBLOCK: + m_msg(&m, imsg); + m_get_sockaddr(&m, (struct sockaddr*)&ss); + m_get_string(&m, &dom); + m_end(&m); + source = mta_source((struct sockaddr*)&ss); + if (*dom != '\0') { + if (!(strlcpy(buf, dom, sizeof(buf)) + >= sizeof(buf))) + mta_unblock(source, buf); + } + else + mta_unblock(source, NULL); + mta_source_unref(source); + m_compose(p, IMSG_CTL_OK, imsg->hdr.peerid, 0, -1, NULL, 0); + return; + + case IMSG_CTL_MTA_SHOW_BLOCK: + SPLAY_FOREACH(block, mta_block_tree, &blocks) { + (void)snprintf(buf, sizeof(buf), "%s -> %s", + mta_source_to_text(block->source), + block->domain ? block->domain : "*"); + m_compose(p, IMSG_CTL_MTA_SHOW_BLOCK, + imsg->hdr.peerid, 0, -1, buf, strlen(buf) + 1); + } + m_compose(p, IMSG_CTL_MTA_SHOW_BLOCK, imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + } + + errx(1, "mta_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type)); +} + +void +mta_postfork(void) +{ +} + +void +mta_postprivdrop(void) +{ + SPLAY_INIT(&relays); + SPLAY_INIT(&domains); + SPLAY_INIT(&hosts); + SPLAY_INIT(&sources); + SPLAY_INIT(&routes); + SPLAY_INIT(&blocks); + + tree_init(&wait_secret); + tree_init(&wait_smarthost); + tree_init(&wait_mx); + tree_init(&wait_preference); + tree_init(&wait_source); + tree_init(&flush_evp); + dict_init(&hoststat); + + evtimer_set(&ev_flush_evp, mta_delivery_flush_event, NULL); + + runq_init(&runq_relay, mta_on_timeout); + runq_init(&runq_connector, mta_on_timeout); + runq_init(&runq_route, mta_on_timeout); + runq_init(&runq_hoststat, mta_on_timeout); +} + + +/* + * Local error on the given source. + */ +void +mta_source_error(struct mta_relay *relay, struct mta_route *route, const char *e) +{ + struct mta_connector *c; + + /* + * Remember the source as broken for this connector. + */ + c = mta_connector(relay, route->src); + if (!(c->flags & CONNECTOR_ERROR_SOURCE)) + log_info("smtp-out: Error on %s: %s", + mta_route_to_text(route), e); + c->flags |= CONNECTOR_ERROR_SOURCE; +} + +void +mta_route_error(struct mta_relay *relay, struct mta_route *route) +{ +#if 0 + route->nerror += 1; + + if (route->nerror > MAXERROR_PER_ROUTE) { + log_info("smtp-out: Too many errors on %s: " + "disabling for a while", mta_route_to_text(route)); + mta_route_disable(route, 2, ROUTE_DISABLED_SMTP); + } +#endif +} + +void +mta_route_ok(struct mta_relay *relay, struct mta_route *route) +{ + struct mta_connector *c; + + if (!(route->flags & ROUTE_NEW)) + return; + + log_debug("debug: mta-routing: route %s is now valid.", + mta_route_to_text(route)); + + route->nerror = 0; + route->flags &= ~ROUTE_NEW; + + c = mta_connector(relay, route->src); + mta_connect(c); +} + +void +mta_route_down(struct mta_relay *relay, struct mta_route *route) +{ +#if 0 + mta_route_disable(route, 2, ROUTE_DISABLED_SMTP); +#endif +} + +void +mta_route_collect(struct mta_relay *relay, struct mta_route *route) +{ + struct mta_connector *c; + + log_debug("debug: mta_route_collect(%s)", + mta_route_to_text(route)); + + relay->nconn -= 1; + relay->domain->nconn -= 1; + route->nconn -= 1; + route->src->nconn -= 1; + route->dst->nconn -= 1; + route->lastdisc = time(NULL); + + /* First connection failed */ + if (route->flags & ROUTE_NEW) + mta_route_disable(route, 1, ROUTE_DISABLED_NET); + + c = mta_connector(relay, route->src); + c->nconn -= 1; + mta_connect(c); + mta_route_unref(route); /* from mta_find_route() */ + mta_relay_unref(relay); /* from mta_connect() */ +} + +struct mta_task * +mta_route_next_task(struct mta_relay *relay, struct mta_route *route) +{ + struct mta_task *task; + + if ((task = TAILQ_FIRST(&relay->tasks))) { + TAILQ_REMOVE(&relay->tasks, task, entry); + relay->ntask -= 1; + task->relay = NULL; + + /* When the number of tasks is down to lowat, query some evp */ + if (relay->ntask == (size_t)relay->limits->task_lowat) { + if (relay->state & RELAY_ONHOLD) { + log_info("smtp-out: back to lowat on %s: releasing", + mta_relay_to_text(relay)); + relay->state &= ~RELAY_ONHOLD; + } + if (relay->state & RELAY_HOLDQ) { + m_create(p_queue, IMSG_MTA_HOLDQ_RELEASE, 0, 0, -1); + m_add_id(p_queue, relay->id); + m_add_int(p_queue, relay->limits->task_release); + m_close(p_queue); + } + } + else if (relay->ntask == 0 && relay->state & RELAY_HOLDQ) { + m_create(p_queue, IMSG_MTA_HOLDQ_RELEASE, 0, 0, -1); + m_add_id(p_queue, relay->id); + m_add_int(p_queue, 0); + m_close(p_queue); + } + } + + return (task); +} + +static void +mta_handle_envelope(struct envelope *evp, const char *smarthost) +{ + struct mta_relay *relay; + struct mta_task *task; + struct mta_envelope *e; + struct dispatcher *dispatcher; + struct mailaddr maddr; + struct relayhost relayh; + char buf[LINE_MAX]; + + dispatcher = dict_xget(env->sc_dispatchers, evp->dispatcher); + if (dispatcher->u.remote.smarthost && smarthost == NULL) { + mta_query_smarthost(evp); + return; + } + + memset(&relayh, 0, sizeof(relayh)); + relayh.tls = RELAY_TLS_OPPORTUNISTIC; + if (smarthost && !text_to_relayhost(&relayh, smarthost)) { + log_warnx("warn: Failed to parse smarthost %s", smarthost); + m_create(p_queue, IMSG_MTA_DELIVERY_TEMPFAIL, 0, 0, -1); + m_add_evpid(p_queue, evp->id); + m_add_string(p_queue, "Cannot parse smarthost"); + m_add_int(p_queue, ESC_OTHER_STATUS); + m_close(p_queue); + return; + } + + if (relayh.flags & RELAY_AUTH && dispatcher->u.remote.auth == NULL) { + log_warnx("warn: No auth table on action \"%s\" for relay %s", + evp->dispatcher, smarthost); + m_create(p_queue, IMSG_MTA_DELIVERY_TEMPFAIL, 0, 0, -1); + m_add_evpid(p_queue, evp->id); + m_add_string(p_queue, "No auth table for relaying"); + m_add_int(p_queue, ESC_OTHER_STATUS); + m_close(p_queue); + return; + } + + if (dispatcher->u.remote.tls_required) { + /* Reject relay if smtp+notls:// is requested */ + if (relayh.tls == RELAY_TLS_NO) { + log_warnx("warn: TLS required for action \"%s\"", + evp->dispatcher); + m_create(p_queue, IMSG_MTA_DELIVERY_TEMPFAIL, 0, 0, -1); + m_add_evpid(p_queue, evp->id); + m_add_string(p_queue, "TLS required for relaying"); + m_add_int(p_queue, ESC_OTHER_STATUS); + m_close(p_queue); + return; + } + /* Update smtp:// to smtp+tls:// */ + if (relayh.tls == RELAY_TLS_OPPORTUNISTIC) + relayh.tls = RELAY_TLS_STARTTLS; + } + + relay = mta_relay(evp, &relayh); + /* ignore if we don't know the limits yet */ + if (relay->limits && + relay->ntask >= (size_t)relay->limits->task_hiwat) { + if (!(relay->state & RELAY_ONHOLD)) { + log_info("smtp-out: hiwat reached on %s: holding envelopes", + mta_relay_to_text(relay)); + relay->state |= RELAY_ONHOLD; + } + } + + /* + * If the relay has too many pending tasks, tell the + * scheduler to hold it until further notice + */ + if (relay->state & RELAY_ONHOLD) { + relay->state |= RELAY_HOLDQ; + m_create(p_queue, IMSG_MTA_DELIVERY_HOLD, 0, 0, -1); + m_add_evpid(p_queue, evp->id); + m_add_id(p_queue, relay->id); + m_close(p_queue); + mta_relay_unref(relay); /* from here */ + return; + } + + task = NULL; + TAILQ_FOREACH(task, &relay->tasks, entry) + if (task->msgid == evpid_to_msgid(evp->id)) + break; + + if (task == NULL) { + task = xmalloc(sizeof *task); + TAILQ_INIT(&task->envelopes); + task->relay = relay; + relay->ntask += 1; + TAILQ_INSERT_TAIL(&relay->tasks, task, entry); + task->msgid = evpid_to_msgid(evp->id); + if (evp->sender.user[0] || evp->sender.domain[0]) + (void)snprintf(buf, sizeof buf, "%s@%s", + evp->sender.user, evp->sender.domain); + else + buf[0] = '\0'; + + if (dispatcher->u.remote.mail_from && evp->sender.user[0]) { + memset(&maddr, 0, sizeof (maddr)); + if (text_to_mailaddr(&maddr, + dispatcher->u.remote.mail_from)) { + (void)snprintf(buf, sizeof buf, "%s@%s", + maddr.user[0] ? maddr.user : evp->sender.user, + maddr.domain[0] ? maddr.domain : evp->sender.domain); + } + } + + task->sender = xstrdup(buf); + stat_increment("mta.task", 1); + } + + e = xcalloc(1, sizeof *e); + e->id = evp->id; + e->creation = evp->creation; + e->smtpname = xstrdup(evp->smtpname); + (void)snprintf(buf, sizeof buf, "%s@%s", + evp->dest.user, evp->dest.domain); + e->dest = xstrdup(buf); + (void)snprintf(buf, sizeof buf, "%s@%s", + evp->rcpt.user, evp->rcpt.domain); + if (strcmp(buf, e->dest)) + e->rcpt = xstrdup(buf); + e->task = task; + if (evp->dsn_orcpt.user[0] && evp->dsn_orcpt.domain[0]) { + (void)snprintf(buf, sizeof buf, "%s@%s", + evp->dsn_orcpt.user, evp->dsn_orcpt.domain); + e->dsn_orcpt = xstrdup(buf); + } + (void)strlcpy(e->dsn_envid, evp->dsn_envid, + sizeof e->dsn_envid); + e->dsn_notify = evp->dsn_notify; + e->dsn_ret = evp->dsn_ret; + + TAILQ_INSERT_TAIL(&task->envelopes, e, entry); + log_debug("debug: mta: received evp:%016" PRIx64 + " for <%s>", e->id, e->dest); + + stat_increment("mta.envelope", 1); + + mta_drain(relay); + mta_relay_unref(relay); /* from here */ +} + +static void +mta_delivery_flush_event(int fd, short event, void *arg) +{ + struct mta_envelope *e; + struct timeval tv; + + if (tree_poproot(&flush_evp, NULL, (void**)(&e))) { + + if (e->delivery == IMSG_MTA_DELIVERY_OK) { + m_create(p_queue, IMSG_MTA_DELIVERY_OK, 0, 0, -1); + m_add_evpid(p_queue, e->id); + m_add_int(p_queue, e->ext); + m_close(p_queue); + } else if (e->delivery == IMSG_MTA_DELIVERY_TEMPFAIL) { + m_create(p_queue, IMSG_MTA_DELIVERY_TEMPFAIL, 0, 0, -1); + m_add_evpid(p_queue, e->id); + m_add_string(p_queue, e->status); + m_add_int(p_queue, ESC_OTHER_STATUS); + m_close(p_queue); + } + else if (e->delivery == IMSG_MTA_DELIVERY_PERMFAIL) { + m_create(p_queue, IMSG_MTA_DELIVERY_PERMFAIL, 0, 0, -1); + m_add_evpid(p_queue, e->id); + m_add_string(p_queue, e->status); + m_add_int(p_queue, ESC_OTHER_STATUS); + m_close(p_queue); + } + else if (e->delivery == IMSG_MTA_DELIVERY_LOOP) { + m_create(p_queue, IMSG_MTA_DELIVERY_LOOP, 0, 0, -1); + m_add_evpid(p_queue, e->id); + m_close(p_queue); + } + else { + log_warnx("warn: bad delivery type %d for %016" PRIx64, + e->delivery, e->id); + fatalx("aborting"); + } + + log_debug("debug: mta: flush for %016"PRIx64" (-> %s)", e->id, e->dest); + + free(e->smtpname); + free(e->dest); + free(e->rcpt); + free(e->dsn_orcpt); + free(e); + + tv.tv_sec = 0; + tv.tv_usec = 0; + evtimer_add(&ev_flush_evp, &tv); + } +} + +void +mta_delivery_log(struct mta_envelope *e, const char *source, const char *relay, + int delivery, const char *status) +{ + if (delivery == IMSG_MTA_DELIVERY_OK) + mta_log(e, "Ok", source, relay, status); + else if (delivery == IMSG_MTA_DELIVERY_TEMPFAIL) + mta_log(e, "TempFail", source, relay, status); + else if (delivery == IMSG_MTA_DELIVERY_PERMFAIL) + mta_log(e, "PermFail", source, relay, status); + else if (delivery == IMSG_MTA_DELIVERY_LOOP) + mta_log(e, "PermFail", source, relay, "Loop detected"); + else { + log_warnx("warn: bad delivery type %d for %016" PRIx64, + delivery, e->id); + fatalx("aborting"); + } + + e->delivery = delivery; + if (status) + (void)strlcpy(e->status, status, sizeof(e->status)); +} + +void +mta_delivery_notify(struct mta_envelope *e) +{ + struct timeval tv; + + tree_xset(&flush_evp, e->id, e); + if (tree_count(&flush_evp) == 1) { + tv.tv_sec = 0; + tv.tv_usec = 0; + evtimer_add(&ev_flush_evp, &tv); + } +} + +static void +mta_query_mx(struct mta_relay *relay) +{ + uint64_t id; + + if (relay->status & RELAY_WAIT_MX) + return; + + log_debug("debug: mta: querying MX for %s...", + mta_relay_to_text(relay)); + + if (waitq_wait(&relay->domain->mxs, mta_on_mx, relay)) { + id = generate_uid(); + tree_xset(&wait_mx, id, relay->domain); + if (relay->domain->as_host) + m_create(p_lka, IMSG_MTA_DNS_HOST, 0, 0, -1); + else + m_create(p_lka, IMSG_MTA_DNS_MX, 0, 0, -1); + m_add_id(p_lka, id); + m_add_string(p_lka, relay->domain->name); + m_close(p_lka); + } + relay->status |= RELAY_WAIT_MX; + mta_relay_ref(relay); +} + +static void +mta_query_limits(struct mta_relay *relay) +{ + if (relay->status & RELAY_WAIT_LIMITS) + return; + + relay->limits = dict_get(env->sc_limits_dict, relay->domain->name); + if (relay->limits == NULL) + relay->limits = dict_get(env->sc_limits_dict, "default"); + + if (max_seen_conndelay_route < relay->limits->conndelay_route) + max_seen_conndelay_route = relay->limits->conndelay_route; + if (max_seen_discdelay_route < relay->limits->discdelay_route) + max_seen_discdelay_route = relay->limits->discdelay_route; +} + +static void +mta_query_secret(struct mta_relay *relay) +{ + if (relay->status & RELAY_WAIT_SECRET) + return; + + log_debug("debug: mta: querying secret for %s...", + mta_relay_to_text(relay)); + + tree_xset(&wait_secret, relay->id, relay); + relay->status |= RELAY_WAIT_SECRET; + + m_create(p_lka, IMSG_MTA_LOOKUP_CREDENTIALS, 0, 0, -1); + m_add_id(p_lka, relay->id); + m_add_string(p_lka, relay->authtable); + m_add_string(p_lka, relay->authlabel); + m_close(p_lka); + + mta_relay_ref(relay); +} + +static void +mta_query_smarthost(struct envelope *evp0) +{ + struct dispatcher *dispatcher; + struct envelope *evp; + + evp = malloc(sizeof(*evp)); + memmove(evp, evp0, sizeof(*evp)); + + dispatcher = dict_xget(env->sc_dispatchers, evp->dispatcher); + + log_debug("debug: mta: querying smarthost for %s:%s...", + evp->dispatcher, dispatcher->u.remote.smarthost); + + tree_xset(&wait_smarthost, evp->id, evp); + + m_create(p_lka, IMSG_MTA_LOOKUP_SMARTHOST, 0, 0, -1); + m_add_id(p_lka, evp->id); + if (dispatcher->u.remote.smarthost_domain) + m_add_string(p_lka, evp->dest.domain); + else + m_add_string(p_lka, NULL); + m_add_string(p_lka, dispatcher->u.remote.smarthost); + m_close(p_lka); + + log_debug("debug: mta: querying smarthost"); +} + +static void +mta_query_preference(struct mta_relay *relay) +{ + if (relay->status & RELAY_WAIT_PREFERENCE) + return; + + log_debug("debug: mta: querying preference for %s...", + mta_relay_to_text(relay)); + + tree_xset(&wait_preference, relay->id, relay); + relay->status |= RELAY_WAIT_PREFERENCE; + + m_create(p_lka, IMSG_MTA_DNS_MX_PREFERENCE, 0, 0, -1); + m_add_id(p_lka, relay->id); + m_add_string(p_lka, relay->domain->name); + m_add_string(p_lka, relay->backupname); + m_close(p_lka); + + mta_relay_ref(relay); +} + +static void +mta_query_source(struct mta_relay *relay) +{ + log_debug("debug: mta: querying source for %s...", + mta_relay_to_text(relay)); + + relay->sourceloop += 1; + + if (relay->sourcetable == NULL) { + /* + * This is a recursive call, but it only happens once, since + * another source will not be queried immediately. + */ + mta_relay_ref(relay); + mta_on_source(relay, mta_source(NULL)); + return; + } + + m_create(p_lka, IMSG_MTA_LOOKUP_SOURCE, 0, 0, -1); + m_add_id(p_lka, relay->id); + m_add_string(p_lka, relay->sourcetable); + m_close(p_lka); + + tree_xset(&wait_source, relay->id, relay); + relay->status |= RELAY_WAIT_SOURCE; + mta_relay_ref(relay); +} + +static void +mta_on_mx(void *tag, void *arg, void *data) +{ + struct mta_domain *domain = data; + struct mta_relay *relay = arg; + + log_debug("debug: mta: ... got mx (%p, %s, %s)", + tag, domain->name, mta_relay_to_text(relay)); + + switch (domain->mxstatus) { + case DNS_OK: + break; + case DNS_RETRY: + relay->fail = IMSG_MTA_DELIVERY_TEMPFAIL; + relay->failstr = "Temporary failure in MX lookup"; + break; + case DNS_EINVAL: + relay->fail = IMSG_MTA_DELIVERY_PERMFAIL; + relay->failstr = "Invalid domain name"; + break; + case DNS_ENONAME: + relay->fail = IMSG_MTA_DELIVERY_PERMFAIL; + relay->failstr = "Domain does not exist"; + break; + case DNS_ENOTFOUND: + relay->fail = IMSG_MTA_DELIVERY_TEMPFAIL; + if (relay->domain->as_host) + relay->failstr = "Host not found"; + else + relay->failstr = "No MX found for domain"; + break; + default: + fatalx("bad DNS lookup error code"); + break; + } + + if (domain->mxstatus) + log_info("smtp-out: Failed to resolve MX for %s: %s", + mta_relay_to_text(relay), relay->failstr); + + relay->status &= ~RELAY_WAIT_MX; + mta_drain(relay); + mta_relay_unref(relay); /* from mta_drain() */ +} + +static void +mta_on_secret(struct mta_relay *relay, const char *secret) +{ + log_debug("debug: mta: ... got secret for %s: %s", + mta_relay_to_text(relay), secret); + + if (secret) + relay->secret = strdup(secret); + + if (relay->secret == NULL) { + log_warnx("warn: Failed to retrieve secret " + "for %s", mta_relay_to_text(relay)); + relay->fail = IMSG_MTA_DELIVERY_TEMPFAIL; + relay->failstr = "Could not retrieve credentials"; + } + + relay->status &= ~RELAY_WAIT_SECRET; + mta_drain(relay); + mta_relay_unref(relay); /* from mta_query_secret() */ +} + +static void +mta_on_smarthost(struct envelope *evp, const char *smarthost) +{ + if (smarthost == NULL) { + log_warnx("warn: Failed to retrieve smarthost " + "for envelope %"PRIx64, evp->id); + m_create(p_queue, IMSG_MTA_DELIVERY_TEMPFAIL, 0, 0, -1); + m_add_evpid(p_queue, evp->id); + m_add_string(p_queue, "Cannot retrieve smarthost"); + m_add_int(p_queue, ESC_OTHER_STATUS); + m_close(p_queue); + return; + } + + log_debug("debug: mta: ... got smarthost for %016"PRIx64": %s", + evp->id, smarthost); + mta_handle_envelope(evp, smarthost); + free(evp); +} + +static void +mta_on_preference(struct mta_relay *relay, int preference) +{ + log_debug("debug: mta: ... got preference for %s: %d", + mta_relay_to_text(relay), preference); + + relay->backuppref = preference; + + relay->status &= ~RELAY_WAIT_PREFERENCE; + mta_drain(relay); + mta_relay_unref(relay); /* from mta_query_preference() */ +} + +static void +mta_on_source(struct mta_relay *relay, struct mta_source *source) +{ + struct mta_connector *c; + void *iter; + int delay, errmask; + + log_debug("debug: mta: ... got source for %s: %s", + mta_relay_to_text(relay), source ? mta_source_to_text(source) : "NULL"); + + relay->lastsource = time(NULL); + delay = DELAY_CHECK_SOURCE_SLOW; + + if (source) { + c = mta_connector(relay, source); + if (c->flags & CONNECTOR_NEW) { + c->flags &= ~CONNECTOR_NEW; + delay = DELAY_CHECK_SOURCE; + } + mta_connect(c); + if ((c->flags & CONNECTOR_ERROR) == 0) + relay->sourceloop = 0; + else + delay = DELAY_CHECK_SOURCE_FAST; + mta_source_unref(source); /* from constructor */ + } + else { + log_warnx("warn: Failed to get source address for %s", + mta_relay_to_text(relay)); + } + + if (tree_count(&relay->connectors) == 0) { + relay->fail = IMSG_MTA_DELIVERY_TEMPFAIL; + relay->failstr = "Could not retrieve source address"; + } + if (tree_count(&relay->connectors) < relay->sourceloop) { + relay->fail = IMSG_MTA_DELIVERY_TEMPFAIL; + relay->failstr = "No valid route to remote MX"; + + errmask = 0; + iter = NULL; + while (tree_iter(&relay->connectors, &iter, NULL, (void **)&c)) + errmask |= c->flags; + + if (errmask & CONNECTOR_ERROR_ROUTE_SMTP) + relay->failstr = "Destination seem to reject all mails"; + else if (errmask & CONNECTOR_ERROR_ROUTE_NET) + relay->failstr = "Network error on destination MXs"; + else if (errmask & CONNECTOR_ERROR_MX) + relay->failstr = "No MX found for destination"; + else if (errmask & CONNECTOR_ERROR_FAMILY) + relay->failstr = "Address family mismatch on destination MXs"; + else if (errmask & CONNECTOR_ERROR_BLOCKED) + relay->failstr = "All routes to destination blocked"; + else + relay->failstr = "No valid route to destination"; + } + + relay->nextsource = relay->lastsource + delay; + relay->status &= ~RELAY_WAIT_SOURCE; + mta_drain(relay); + mta_relay_unref(relay); /* from mta_query_source() */ +} + +static void +mta_connect(struct mta_connector *c) +{ + struct mta_route *route; + struct mta_mx *mx; + struct mta_limits *l = c->relay->limits; + int limits; + time_t nextconn, now; + + /* toggle the block flag */ + if (mta_is_blocked(c->source, c->relay->domain->name)) + c->flags |= CONNECTOR_ERROR_BLOCKED; + else + c->flags &= ~CONNECTOR_ERROR_BLOCKED; + + again: + + log_debug("debug: mta: connecting with %s", mta_connector_to_text(c)); + + /* Do not connect if this connector has an error. */ + if (c->flags & CONNECTOR_ERROR) { + log_debug("debug: mta: connector error"); + return; + } + + if (c->flags & CONNECTOR_WAIT) { + log_debug("debug: mta: cancelling connector timeout"); + runq_cancel(runq_connector, c); + c->flags &= ~CONNECTOR_WAIT; + } + + /* No job. */ + if (c->relay->ntask == 0) { + log_debug("debug: mta: no task for connector"); + return; + } + + /* Do not create more connections than necessary */ + if ((c->relay->nconn_ready >= c->relay->ntask) || + (c->relay->nconn > 2 && c->relay->nconn >= c->relay->ntask / 2)) { + log_debug("debug: mta: enough connections already"); + return; + } + + limits = 0; + nextconn = now = time(NULL); + + if (c->relay->domain->lastconn + l->conndelay_domain > nextconn) { + log_debug("debug: mta: cannot use domain %s before %llus", + c->relay->domain->name, + (unsigned long long) c->relay->domain->lastconn + l->conndelay_domain - now); + nextconn = c->relay->domain->lastconn + l->conndelay_domain; + } + if (c->relay->domain->nconn >= l->maxconn_per_domain) { + log_debug("debug: mta: hit domain limit"); + limits |= CONNECTOR_LIMIT_DOMAIN; + } + + if (c->source->lastconn + l->conndelay_source > nextconn) { + log_debug("debug: mta: cannot use source %s before %llus", + mta_source_to_text(c->source), + (unsigned long long) c->source->lastconn + l->conndelay_source - now); + nextconn = c->source->lastconn + l->conndelay_source; + } + if (c->source->nconn >= l->maxconn_per_source) { + log_debug("debug: mta: hit source limit"); + limits |= CONNECTOR_LIMIT_SOURCE; + } + + if (c->lastconn + l->conndelay_connector > nextconn) { + log_debug("debug: mta: cannot use %s before %llus", + mta_connector_to_text(c), + (unsigned long long) c->lastconn + l->conndelay_connector - now); + nextconn = c->lastconn + l->conndelay_connector; + } + if (c->nconn >= l->maxconn_per_connector) { + log_debug("debug: mta: hit connector limit"); + limits |= CONNECTOR_LIMIT_CONN; + } + + if (c->relay->lastconn + l->conndelay_relay > nextconn) { + log_debug("debug: mta: cannot use %s before %llus", + mta_relay_to_text(c->relay), + (unsigned long long) c->relay->lastconn + l->conndelay_relay - now); + nextconn = c->relay->lastconn + l->conndelay_relay; + } + if (c->relay->nconn >= l->maxconn_per_relay) { + log_debug("debug: mta: hit relay limit"); + limits |= CONNECTOR_LIMIT_RELAY; + } + + /* We can connect now, find a route */ + if (!limits && nextconn <= now) + route = mta_find_route(c, now, &limits, &nextconn, &mx); + else + route = NULL; + + /* No route */ + if (route == NULL) { + + if (c->flags & CONNECTOR_ERROR) { + /* XXX we might want to clear this flag later */ + log_debug("debug: mta-routing: no route available for %s: errors on connector", + mta_connector_to_text(c)); + return; + } + else if (limits) { + log_debug("debug: mta-routing: no route available for %s: limits reached", + mta_connector_to_text(c)); + nextconn = now + DELAY_CHECK_LIMIT; + } + else { + log_debug("debug: mta-routing: no route available for %s: must wait a bit", + mta_connector_to_text(c)); + } + log_debug("debug: mta: retrying to connect on %s in %llus...", + mta_connector_to_text(c), + (unsigned long long) nextconn - time(NULL)); + c->flags |= CONNECTOR_WAIT; + runq_schedule_at(runq_connector, nextconn, c); + return; + } + + log_debug("debug: mta-routing: spawning new connection on %s", + mta_route_to_text(route)); + + c->nconn += 1; + c->lastconn = time(NULL); + + c->relay->nconn += 1; + c->relay->lastconn = c->lastconn; + c->relay->domain->nconn += 1; + c->relay->domain->lastconn = c->lastconn; + route->nconn += 1; + route->lastconn = c->lastconn; + route->src->nconn += 1; + route->src->lastconn = c->lastconn; + route->dst->nconn += 1; + route->dst->lastconn = c->lastconn; + + mta_session(c->relay, route, mx->mxname); /* this never fails synchronously */ + mta_relay_ref(c->relay); + + goto again; +} + +static void +mta_on_timeout(struct runq *runq, void *arg) +{ + struct mta_connector *connector = arg; + struct mta_relay *relay = arg; + struct mta_route *route = arg; + struct hoststat *hs = arg; + + if (runq == runq_relay) { + log_debug("debug: mta: ... timeout for %s", + mta_relay_to_text(relay)); + relay->status &= ~RELAY_WAIT_CONNECTOR; + mta_drain(relay); + mta_relay_unref(relay); /* from mta_drain() */ + } + else if (runq == runq_connector) { + log_debug("debug: mta: ... timeout for %s", + mta_connector_to_text(connector)); + connector->flags &= ~CONNECTOR_WAIT; + mta_connect(connector); + } + else if (runq == runq_route) { + route->flags &= ~ROUTE_RUNQ; + mta_route_enable(route); + mta_route_unref(route); + } + else if (runq == runq_hoststat) { + log_debug("debug: mta: ... timeout for hoststat %s", + hs->name); + mta_hoststat_remove_entry(hs); + free(hs); + } +} + +static void +mta_route_disable(struct mta_route *route, int penalty, int reason) +{ + unsigned long long delay; + + route->penalty += penalty; + route->lastpenalty = time(NULL); + delay = (unsigned long long)DELAY_ROUTE_BASE * route->penalty * route->penalty; + if (delay > DELAY_ROUTE_MAX) + delay = DELAY_ROUTE_MAX; +#if 0 + delay = 60; +#endif + + log_info("smtp-out: Disabling route %s for %llus", + mta_route_to_text(route), delay); + + if (route->flags & ROUTE_DISABLED) + runq_cancel(runq_route, route); + else + mta_route_ref(route); + + route->flags |= reason & ROUTE_DISABLED; + runq_schedule(runq_route, delay, route); +} + +static void +mta_route_enable(struct mta_route *route) +{ + if (route->flags & ROUTE_DISABLED) { + log_info("smtp-out: Enabling route %s", + mta_route_to_text(route)); + route->flags &= ~ROUTE_DISABLED; + route->flags |= ROUTE_NEW; + route->nerror = 0; + } + + if (route->penalty) { +#if DELAY_QUADRATIC + route->penalty -= 1; + route->lastpenalty = time(NULL); +#else + route->penalty = 0; +#endif + } +} + +static void +mta_drain(struct mta_relay *r) +{ + char buf[64]; + + log_debug("debug: mta: draining %s " + "refcount=%d, ntask=%zu, nconnector=%zu, nconn=%zu", + mta_relay_to_text(r), + r->refcount, r->ntask, tree_count(&r->connectors), r->nconn); + + /* + * All done. + */ + if (r->ntask == 0) { + log_debug("debug: mta: all done for %s", mta_relay_to_text(r)); + return; + } + + /* + * If we know that this relay is failing flush the tasks. + */ + if (r->fail) { + mta_flush(r, r->fail, r->failstr); + return; + } + + /* Query secret if needed. */ + if (r->flags & RELAY_AUTH && r->secret == NULL) + mta_query_secret(r); + + /* Query our preference if needed. */ + if (r->backupname && r->backuppref == -1) + mta_query_preference(r); + + /* Query the domain MXs if needed. */ + if (r->domain->lastmxquery == 0) + mta_query_mx(r); + + /* Query the limits if needed. */ + if (r->limits == NULL) + mta_query_limits(r); + + /* Wait until we are ready to proceed. */ + if (r->status & RELAY_WAITMASK) { + buf[0] = '\0'; + if (r->status & RELAY_WAIT_MX) + (void)strlcat(buf, " MX", sizeof buf); + if (r->status & RELAY_WAIT_PREFERENCE) + (void)strlcat(buf, " preference", sizeof buf); + if (r->status & RELAY_WAIT_SECRET) + (void)strlcat(buf, " secret", sizeof buf); + if (r->status & RELAY_WAIT_SOURCE) + (void)strlcat(buf, " source", sizeof buf); + if (r->status & RELAY_WAIT_CONNECTOR) + (void)strlcat(buf, " connector", sizeof buf); + log_debug("debug: mta: %s waiting for%s", + mta_relay_to_text(r), buf); + return; + } + + /* + * We have pending task, and it's maybe time too try a new source. + */ + if (r->nextsource <= time(NULL)) + mta_query_source(r); + else { + log_debug("debug: mta: scheduling relay %s in %llus...", + mta_relay_to_text(r), + (unsigned long long) r->nextsource - time(NULL)); + runq_schedule_at(runq_relay, r->nextsource, r); + r->status |= RELAY_WAIT_CONNECTOR; + mta_relay_ref(r); + } +} + +static void +mta_flush(struct mta_relay *relay, int fail, const char *error) +{ + struct mta_envelope *e; + struct mta_task *task; + const char *domain; + void *iter; + struct mta_connector *c; + size_t n, r; + + log_debug("debug: mta_flush(%s, %d, \"%s\")", + mta_relay_to_text(relay), fail, error); + + if (fail != IMSG_MTA_DELIVERY_TEMPFAIL && fail != IMSG_MTA_DELIVERY_PERMFAIL) + errx(1, "unexpected delivery status %d", fail); + + n = 0; + while ((task = TAILQ_FIRST(&relay->tasks))) { + TAILQ_REMOVE(&relay->tasks, task, entry); + while ((e = TAILQ_FIRST(&task->envelopes))) { + TAILQ_REMOVE(&task->envelopes, e, entry); + + /* + * host was suspended, cache envelope id in hoststat tree + * so that it can be retried when a delivery succeeds for + * that domain. + */ + domain = strchr(e->dest, '@'); + if (fail == IMSG_MTA_DELIVERY_TEMPFAIL && domain) { + r = 0; + iter = NULL; + while (tree_iter(&relay->connectors, &iter, + NULL, (void **)&c)) { + if (c->flags & CONNECTOR_ERROR_ROUTE) + r++; + } + if (tree_count(&relay->connectors) == r) + mta_hoststat_cache(domain+1, e->id); + } + + mta_delivery_log(e, NULL, relay->domain->name, fail, error); + mta_delivery_notify(e); + + n++; + } + free(task->sender); + free(task); + } + + stat_decrement("mta.task", relay->ntask); + stat_decrement("mta.envelope", n); + relay->ntask = 0; + + /* release all waiting envelopes for the relay */ + if (relay->state & RELAY_HOLDQ) { + m_create(p_queue, IMSG_MTA_HOLDQ_RELEASE, 0, 0, -1); + m_add_id(p_queue, relay->id); + m_add_int(p_queue, -1); + m_close(p_queue); + } +} + +/* + * Find a route to use for this connector + */ +static struct mta_route * +mta_find_route(struct mta_connector *c, time_t now, int *limits, + time_t *nextconn, struct mta_mx **pmx) +{ + struct mta_route *route, *best; + struct mta_limits *l = c->relay->limits; + struct mta_mx *mx; + int level, limit_host, limit_route; + int family_mismatch, seen, suspended_route; + time_t tm; + + log_debug("debug: mta-routing: searching new route for %s...", + mta_connector_to_text(c)); + + tm = 0; + limit_host = 0; + limit_route = 0; + suspended_route = 0; + family_mismatch = 0; + level = -1; + best = NULL; + seen = 0; + + TAILQ_FOREACH(mx, &c->relay->domain->mxs, entry) { + /* + * New preference level + */ + if (mx->preference > level) { +#ifndef IGNORE_MX_PREFERENCE + /* + * Use the current best MX if found. + */ + if (best) + break; + + /* + * No candidate found. There are valid MXs at this + * preference level but they reached their limit, or + * we can't connect yet. + */ + if (limit_host || limit_route || tm) + break; + + /* + * If we are a backup MX, do not relay to MXs with + * a greater preference value. + */ + if (c->relay->backuppref >= 0 && + mx->preference >= c->relay->backuppref) + break; + + /* + * Start looking at MXs on this preference level. + */ +#endif + level = mx->preference; + } + + if (mx->host->flags & HOST_IGNORE) + continue; + + /* Found a possibly valid mx */ + seen++; + + if ((c->source->sa && + c->source->sa->sa_family != mx->host->sa->sa_family) || + (l->family && l->family != mx->host->sa->sa_family)) { + log_debug("debug: mta-routing: skipping host %s: AF mismatch", + mta_host_to_text(mx->host)); + family_mismatch = 1; + continue; + } + + if (mx->host->nconn >= l->maxconn_per_host) { + log_debug("debug: mta-routing: skipping host %s: too many connections", + mta_host_to_text(mx->host)); + limit_host = 1; + continue; + } + + if (mx->host->lastconn + l->conndelay_host > now) { + log_debug("debug: mta-routing: skipping host %s: cannot use before %llus", + mta_host_to_text(mx->host), + (unsigned long long) mx->host->lastconn + l->conndelay_host - now); + if (tm == 0 || mx->host->lastconn + l->conndelay_host < tm) + tm = mx->host->lastconn + l->conndelay_host; + continue; + } + + route = mta_route(c->source, mx->host); + + if (route->flags & ROUTE_DISABLED) { + log_debug("debug: mta-routing: skipping route %s: suspend", + mta_route_to_text(route)); + suspended_route |= route->flags & ROUTE_DISABLED; + mta_route_unref(route); /* from here */ + continue; + } + + if (route->nconn && (route->flags & ROUTE_NEW)) { + log_debug("debug: mta-routing: skipping route %s: not validated yet", + mta_route_to_text(route)); + limit_route = 1; + mta_route_unref(route); /* from here */ + continue; + } + + if (route->nconn >= l->maxconn_per_route) { + log_debug("debug: mta-routing: skipping route %s: too many connections", + mta_route_to_text(route)); + limit_route = 1; + mta_route_unref(route); /* from here */ + continue; + } + + if (route->lastconn + l->conndelay_route > now) { + log_debug("debug: mta-routing: skipping route %s: cannot use before %llus (delay after connect)", + mta_route_to_text(route), + (unsigned long long) route->lastconn + l->conndelay_route - now); + if (tm == 0 || route->lastconn + l->conndelay_route < tm) + tm = route->lastconn + l->conndelay_route; + mta_route_unref(route); /* from here */ + continue; + } + + if (route->lastdisc + l->discdelay_route > now) { + log_debug("debug: mta-routing: skipping route %s: cannot use before %llus (delay after disconnect)", + mta_route_to_text(route), + (unsigned long long) route->lastdisc + l->discdelay_route - now); + if (tm == 0 || route->lastdisc + l->discdelay_route < tm) + tm = route->lastdisc + l->discdelay_route; + mta_route_unref(route); /* from here */ + continue; + } + + /* Use the route with the lowest number of connections. */ + if (best && route->nconn >= best->nconn) { + log_debug("debug: mta-routing: skipping route %s: current one is better", + mta_route_to_text(route)); + mta_route_unref(route); /* from here */ + continue; + } + + if (best) + mta_route_unref(best); /* from here */ + best = route; + *pmx = mx; + log_debug("debug: mta-routing: selecting candidate route %s", + mta_route_to_text(route)); + } + + if (best) + return (best); + + /* Order is important */ + if (seen == 0) { + log_info("smtp-out: No MX found for %s", + mta_connector_to_text(c)); + c->flags |= CONNECTOR_ERROR_MX; + } + else if (limit_route) { + log_debug("debug: mta: hit route limit"); + *limits |= CONNECTOR_LIMIT_ROUTE; + } + else if (limit_host) { + log_debug("debug: mta: hit host limit"); + *limits |= CONNECTOR_LIMIT_HOST; + } + else if (tm) { + if (tm > *nextconn) + *nextconn = tm; + } + else if (family_mismatch) { + log_info("smtp-out: Address family mismatch on %s", + mta_connector_to_text(c)); + c->flags |= CONNECTOR_ERROR_FAMILY; + } + else if (suspended_route) { + log_info("smtp-out: No valid route for %s", + mta_connector_to_text(c)); + if (suspended_route & ROUTE_DISABLED_NET) + c->flags |= CONNECTOR_ERROR_ROUTE_NET; + if (suspended_route & ROUTE_DISABLED_SMTP) + c->flags |= CONNECTOR_ERROR_ROUTE_SMTP; + } + + return (NULL); +} + +static void +mta_log(const struct mta_envelope *evp, const char *prefix, const char *source, + const char *relay, const char *status) +{ + log_info("%016"PRIx64" mta delivery evpid=%016"PRIx64" " + "from=<%s> to=<%s> rcpt=<%s> source=\"%s\" " + "relay=\"%s\" delay=%s result=\"%s\" stat=\"%s\"", + evp->session, + evp->id, + evp->task->sender, + evp->dest, + evp->rcpt ? evp->rcpt : "-", + source ? source : "-", + relay, + duration_to_text(time(NULL) - evp->creation), + prefix, + status); +} + +static struct mta_relay * +mta_relay(struct envelope *e, struct relayhost *relayh) +{ + struct dispatcher *dispatcher; + struct mta_relay key, *r; + + dispatcher = dict_xget(env->sc_dispatchers, e->dispatcher); + + memset(&key, 0, sizeof key); + + key.pki_name = dispatcher->u.remote.pki; + key.ca_name = dispatcher->u.remote.ca; + key.authtable = dispatcher->u.remote.auth; + key.sourcetable = dispatcher->u.remote.source; + key.helotable = dispatcher->u.remote.helo_source; + key.heloname = dispatcher->u.remote.helo; + key.srs = dispatcher->u.remote.srs; + + if (relayh->hostname[0]) { + key.domain = mta_domain(relayh->hostname, 1); + } + else { + key.domain = mta_domain(e->dest.domain, 0); + if (dispatcher->u.remote.backup) { + key.backupname = dispatcher->u.remote.backupmx; + if (key.backupname == NULL) + key.backupname = e->smtpname; + } + } + + key.tls = relayh->tls; + key.flags |= relayh->flags; + key.port = relayh->port; + key.authlabel = relayh->authlabel; + if (!key.authlabel[0]) + key.authlabel = NULL; + + if ((key.tls == RELAY_TLS_STARTTLS || key.tls == RELAY_TLS_SMTPS) && + dispatcher->u.remote.tls_noverify == 0) + key.flags |= RELAY_TLS_VERIFY; + + if ((r = SPLAY_FIND(mta_relay_tree, &relays, &key)) == NULL) { + r = xcalloc(1, sizeof *r); + TAILQ_INIT(&r->tasks); + r->id = generate_uid(); + r->dispatcher = dispatcher; + r->tls = key.tls; + r->flags = key.flags; + r->domain = key.domain; + r->backupname = key.backupname ? + xstrdup(key.backupname) : NULL; + r->backuppref = -1; + r->port = key.port; + r->pki_name = key.pki_name ? xstrdup(key.pki_name) : NULL; + r->ca_name = key.ca_name ? xstrdup(key.ca_name) : NULL; + if (key.authtable) + r->authtable = xstrdup(key.authtable); + if (key.authlabel) + r->authlabel = xstrdup(key.authlabel); + if (key.sourcetable) + r->sourcetable = xstrdup(key.sourcetable); + if (key.helotable) + r->helotable = xstrdup(key.helotable); + if (key.heloname) + r->heloname = xstrdup(key.heloname); + r->srs = key.srs; + SPLAY_INSERT(mta_relay_tree, &relays, r); + stat_increment("mta.relay", 1); + } else { + mta_domain_unref(key.domain); /* from here */ + } + + r->refcount++; + return (r); +} + +static void +mta_relay_ref(struct mta_relay *r) +{ + r->refcount++; +} + +static void +mta_relay_unref(struct mta_relay *relay) +{ + struct mta_connector *c; + + if (--relay->refcount) + return; + + /* Make sure they are no envelopes held for this relay */ + if (relay->state & RELAY_HOLDQ) { + m_create(p_queue, IMSG_MTA_HOLDQ_RELEASE, 0, 0, -1); + m_add_id(p_queue, relay->id); + m_add_int(p_queue, 0); + m_close(p_queue); + } + + log_debug("debug: mta: freeing %s", mta_relay_to_text(relay)); + SPLAY_REMOVE(mta_relay_tree, &relays, relay); + + while ((tree_poproot(&relay->connectors, NULL, (void**)&c))) + mta_connector_free(c); + + free(relay->authlabel); + free(relay->authtable); + free(relay->backupname); + free(relay->pki_name); + free(relay->ca_name); + free(relay->helotable); + free(relay->heloname); + free(relay->secret); + free(relay->sourcetable); + + mta_domain_unref(relay->domain); /* from constructor */ + free(relay); + stat_decrement("mta.relay", 1); +} + +const char * +mta_relay_to_text(struct mta_relay *relay) +{ + static char buf[1024]; + char tmp[32]; + const char *sep = ","; + + (void)snprintf(buf, sizeof buf, "[relay:%s", relay->domain->name); + + if (relay->port) { + (void)strlcat(buf, sep, sizeof buf); + (void)snprintf(tmp, sizeof tmp, "port=%d", (int)relay->port); + (void)strlcat(buf, tmp, sizeof buf); + } + + (void)strlcat(buf, sep, sizeof buf); + switch(relay->tls) { + case RELAY_TLS_OPPORTUNISTIC: + (void)strlcat(buf, "smtp", sizeof buf); + break; + case RELAY_TLS_STARTTLS: + (void)strlcat(buf, "smtp+tls", sizeof buf); + break; + case RELAY_TLS_SMTPS: + (void)strlcat(buf, "smtps", sizeof buf); + break; + case RELAY_TLS_NO: + if (relay->flags & RELAY_LMTP) + (void)strlcat(buf, "lmtp", sizeof buf); + else + (void)strlcat(buf, "smtp+notls", sizeof buf); + break; + default: + (void)strlcat(buf, "???", sizeof buf); + } + + if (relay->flags & RELAY_AUTH) { + (void)strlcat(buf, sep, sizeof buf); + (void)strlcat(buf, "auth=", sizeof buf); + (void)strlcat(buf, relay->authtable, sizeof buf); + (void)strlcat(buf, ":", sizeof buf); + (void)strlcat(buf, relay->authlabel, sizeof buf); + } + + if (relay->pki_name) { + (void)strlcat(buf, sep, sizeof buf); + (void)strlcat(buf, "pki_name=", sizeof buf); + (void)strlcat(buf, relay->pki_name, sizeof buf); + } + + if (relay->domain->as_host) { + (void)strlcat(buf, sep, sizeof buf); + (void)strlcat(buf, "mx", sizeof buf); + } + + if (relay->backupname) { + (void)strlcat(buf, sep, sizeof buf); + (void)strlcat(buf, "backup=", sizeof buf); + (void)strlcat(buf, relay->backupname, sizeof buf); + } + + if (relay->sourcetable) { + (void)strlcat(buf, sep, sizeof buf); + (void)strlcat(buf, "sourcetable=", sizeof buf); + (void)strlcat(buf, relay->sourcetable, sizeof buf); + } + + if (relay->helotable) { + (void)strlcat(buf, sep, sizeof buf); + (void)strlcat(buf, "helotable=", sizeof buf); + (void)strlcat(buf, relay->helotable, sizeof buf); + } + + if (relay->heloname) { + (void)strlcat(buf, sep, sizeof buf); + (void)strlcat(buf, "heloname=", sizeof buf); + (void)strlcat(buf, relay->heloname, sizeof buf); + } + + (void)strlcat(buf, "]", sizeof buf); + + return (buf); +} + +static void +mta_relay_show(struct mta_relay *r, struct mproc *p, uint32_t id, time_t t) +{ + struct mta_connector *c; + void *iter; + char buf[1024], flags[1024], dur[64]; + time_t to; + + flags[0] = '\0'; + +#define SHOWSTATUS(f, n) do { \ + if (r->status & (f)) { \ + if (flags[0]) \ + (void)strlcat(flags, ",", sizeof(flags)); \ + (void)strlcat(flags, (n), sizeof(flags)); \ + } \ + } while(0) + + SHOWSTATUS(RELAY_WAIT_MX, "MX"); + SHOWSTATUS(RELAY_WAIT_PREFERENCE, "preference"); + SHOWSTATUS(RELAY_WAIT_SECRET, "secret"); + SHOWSTATUS(RELAY_WAIT_LIMITS, "limits"); + SHOWSTATUS(RELAY_WAIT_SOURCE, "source"); + SHOWSTATUS(RELAY_WAIT_CONNECTOR, "connector"); +#undef SHOWSTATUS + + if (runq_pending(runq_relay, r, &to)) + (void)snprintf(dur, sizeof(dur), "%s", duration_to_text(to - t)); + else + (void)strlcpy(dur, "-", sizeof(dur)); + + (void)snprintf(buf, sizeof(buf), "%s refcount=%d ntask=%zu nconn=%zu lastconn=%s timeout=%s wait=%s%s", + mta_relay_to_text(r), + r->refcount, + r->ntask, + r->nconn, + r->lastconn ? duration_to_text(t - r->lastconn) : "-", + dur, + flags, + (r->state & RELAY_ONHOLD) ? "ONHOLD" : ""); + m_compose(p, IMSG_CTL_MTA_SHOW_RELAYS, id, 0, -1, buf, strlen(buf) + 1); + + iter = NULL; + while (tree_iter(&r->connectors, &iter, NULL, (void **)&c)) { + + if (runq_pending(runq_connector, c, &to)) + (void)snprintf(dur, sizeof(dur), "%s", duration_to_text(to - t)); + else + (void)strlcpy(dur, "-", sizeof(dur)); + + flags[0] = '\0'; + +#define SHOWFLAG(f, n) do { \ + if (c->flags & (f)) { \ + if (flags[0]) \ + (void)strlcat(flags, ",", sizeof(flags)); \ + (void)strlcat(flags, (n), sizeof(flags)); \ + } \ + } while(0) + + SHOWFLAG(CONNECTOR_NEW, "NEW"); + SHOWFLAG(CONNECTOR_WAIT, "WAIT"); + + SHOWFLAG(CONNECTOR_ERROR_FAMILY, "ERROR_FAMILY"); + SHOWFLAG(CONNECTOR_ERROR_SOURCE, "ERROR_SOURCE"); + SHOWFLAG(CONNECTOR_ERROR_MX, "ERROR_MX"); + SHOWFLAG(CONNECTOR_ERROR_ROUTE_NET, "ERROR_ROUTE_NET"); + SHOWFLAG(CONNECTOR_ERROR_ROUTE_SMTP, "ERROR_ROUTE_SMTP"); + SHOWFLAG(CONNECTOR_ERROR_BLOCKED, "ERROR_BLOCKED"); + + SHOWFLAG(CONNECTOR_LIMIT_HOST, "LIMIT_HOST"); + SHOWFLAG(CONNECTOR_LIMIT_ROUTE, "LIMIT_ROUTE"); + SHOWFLAG(CONNECTOR_LIMIT_SOURCE, "LIMIT_SOURCE"); + SHOWFLAG(CONNECTOR_LIMIT_RELAY, "LIMIT_RELAY"); + SHOWFLAG(CONNECTOR_LIMIT_CONN, "LIMIT_CONN"); + SHOWFLAG(CONNECTOR_LIMIT_DOMAIN, "LIMIT_DOMAIN"); +#undef SHOWFLAG + + (void)snprintf(buf, sizeof(buf), + " connector %s refcount=%d nconn=%zu lastconn=%s timeout=%s flags=%s", + mta_source_to_text(c->source), + c->refcount, + c->nconn, + c->lastconn ? duration_to_text(t - c->lastconn) : "-", + dur, + flags); + m_compose(p, IMSG_CTL_MTA_SHOW_RELAYS, id, 0, -1, buf, + strlen(buf) + 1); + + + } +} + +static int +mta_relay_cmp(const struct mta_relay *a, const struct mta_relay *b) +{ + int r; + + if (a->domain < b->domain) + return (-1); + if (a->domain > b->domain) + return (1); + + if (a->tls < b->tls) + return (-1); + if (a->tls > b->tls) + return (1); + + if (a->flags < b->flags) + return (-1); + if (a->flags > b->flags) + return (1); + + if (a->port < b->port) + return (-1); + if (a->port > b->port) + return (1); + + if (a->authtable == NULL && b->authtable) + return (-1); + if (a->authtable && b->authtable == NULL) + return (1); + if (a->authtable && ((r = strcmp(a->authtable, b->authtable)))) + return (r); + if (a->authlabel == NULL && b->authlabel) + return (-1); + if (a->authlabel && b->authlabel == NULL) + return (1); + if (a->authlabel && ((r = strcmp(a->authlabel, b->authlabel)))) + return (r); + if (a->sourcetable == NULL && b->sourcetable) + return (-1); + if (a->sourcetable && b->sourcetable == NULL) + return (1); + if (a->sourcetable && ((r = strcmp(a->sourcetable, b->sourcetable)))) + return (r); + if (a->helotable == NULL && b->helotable) + return (-1); + if (a->helotable && b->helotable == NULL) + return (1); + if (a->helotable && ((r = strcmp(a->helotable, b->helotable)))) + return (r); + if (a->heloname == NULL && b->heloname) + return (-1); + if (a->heloname && b->heloname == NULL) + return (1); + if (a->heloname && ((r = strcmp(a->heloname, b->heloname)))) + return (r); + + if (a->pki_name == NULL && b->pki_name) + return (-1); + if (a->pki_name && b->pki_name == NULL) + return (1); + if (a->pki_name && ((r = strcmp(a->pki_name, b->pki_name)))) + return (r); + + if (a->ca_name == NULL && b->ca_name) + return (-1); + if (a->ca_name && b->ca_name == NULL) + return (1); + if (a->ca_name && ((r = strcmp(a->ca_name, b->ca_name)))) + return (r); + + if (a->backupname == NULL && b->backupname) + return (-1); + if (a->backupname && b->backupname == NULL) + return (1); + if (a->backupname && ((r = strcmp(a->backupname, b->backupname)))) + return (r); + + if (a->srs < b->srs) + return (-1); + if (a->srs > b->srs) + return (1); + + return (0); +} + +SPLAY_GENERATE(mta_relay_tree, mta_relay, entry, mta_relay_cmp); + +static struct mta_host * +mta_host(const struct sockaddr *sa) +{ + struct mta_host key, *h; + struct sockaddr_storage ss; + + memmove(&ss, sa, SA_LEN(sa)); + key.sa = (struct sockaddr*)&ss; + h = SPLAY_FIND(mta_host_tree, &hosts, &key); + + if (h == NULL) { + h = xcalloc(1, sizeof(*h)); + h->sa = xmemdup(sa, SA_LEN(sa)); + SPLAY_INSERT(mta_host_tree, &hosts, h); + stat_increment("mta.host", 1); + } + + h->refcount++; + return (h); +} + +static void +mta_host_ref(struct mta_host *h) +{ + h->refcount++; +} + +static void +mta_host_unref(struct mta_host *h) +{ + if (--h->refcount) + return; + + SPLAY_REMOVE(mta_host_tree, &hosts, h); + free(h->sa); + free(h->ptrname); + free(h); + stat_decrement("mta.host", 1); +} + +const char * +mta_host_to_text(struct mta_host *h) +{ + static char buf[1024]; + + if (h->ptrname) + (void)snprintf(buf, sizeof buf, "%s (%s)", + sa_to_text(h->sa), h->ptrname); + else + (void)snprintf(buf, sizeof buf, "%s", sa_to_text(h->sa)); + + return (buf); +} + +static int +mta_host_cmp(const struct mta_host *a, const struct mta_host *b) +{ + if (SA_LEN(a->sa) < SA_LEN(b->sa)) + return (-1); + if (SA_LEN(a->sa) > SA_LEN(b->sa)) + return (1); + return (memcmp(a->sa, b->sa, SA_LEN(a->sa))); +} + +SPLAY_GENERATE(mta_host_tree, mta_host, entry, mta_host_cmp); + +static struct mta_domain * +mta_domain(char *name, int as_host) +{ + struct mta_domain key, *d; + + key.name = name; + key.as_host = as_host; + d = SPLAY_FIND(mta_domain_tree, &domains, &key); + + if (d == NULL) { + d = xcalloc(1, sizeof(*d)); + d->name = xstrdup(name); + d->as_host = as_host; + TAILQ_INIT(&d->mxs); + SPLAY_INSERT(mta_domain_tree, &domains, d); + stat_increment("mta.domain", 1); + } + + d->refcount++; + return (d); +} + +#if 0 +static void +mta_domain_ref(struct mta_domain *d) +{ + d->refcount++; +} +#endif + +static void +mta_domain_unref(struct mta_domain *d) +{ + struct mta_mx *mx; + + if (--d->refcount) + return; + + while ((mx = TAILQ_FIRST(&d->mxs))) { + TAILQ_REMOVE(&d->mxs, mx, entry); + mta_host_unref(mx->host); /* from IMSG_DNS_HOST */ + free(mx->mxname); + free(mx); + } + + SPLAY_REMOVE(mta_domain_tree, &domains, d); + free(d->name); + free(d); + stat_decrement("mta.domain", 1); +} + +static int +mta_domain_cmp(const struct mta_domain *a, const struct mta_domain *b) +{ + if (a->as_host < b->as_host) + return (-1); + if (a->as_host > b->as_host) + return (1); + return (strcasecmp(a->name, b->name)); +} + +SPLAY_GENERATE(mta_domain_tree, mta_domain, entry, mta_domain_cmp); + +static struct mta_source * +mta_source(const struct sockaddr *sa) +{ + struct mta_source key, *s; + struct sockaddr_storage ss; + + if (sa) { + memmove(&ss, sa, SA_LEN(sa)); + key.sa = (struct sockaddr*)&ss; + } else + key.sa = NULL; + s = SPLAY_FIND(mta_source_tree, &sources, &key); + + if (s == NULL) { + s = xcalloc(1, sizeof(*s)); + if (sa) + s->sa = xmemdup(sa, SA_LEN(sa)); + SPLAY_INSERT(mta_source_tree, &sources, s); + stat_increment("mta.source", 1); + } + + s->refcount++; + return (s); +} + +static void +mta_source_ref(struct mta_source *s) +{ + s->refcount++; +} + +static void +mta_source_unref(struct mta_source *s) +{ + if (--s->refcount) + return; + + SPLAY_REMOVE(mta_source_tree, &sources, s); + free(s->sa); + free(s); + stat_decrement("mta.source", 1); +} + +static const char * +mta_source_to_text(struct mta_source *s) +{ + static char buf[1024]; + + if (s->sa == NULL) + return "[]"; + (void)snprintf(buf, sizeof buf, "%s", sa_to_text(s->sa)); + return (buf); +} + +static int +mta_source_cmp(const struct mta_source *a, const struct mta_source *b) +{ + if (a->sa == NULL) + return ((b->sa == NULL) ? 0 : -1); + if (b->sa == NULL) + return (1); + if (SA_LEN(a->sa) < SA_LEN(b->sa)) + return (-1); + if (SA_LEN(a->sa) > SA_LEN(b->sa)) + return (1); + return (memcmp(a->sa, b->sa, SA_LEN(a->sa))); +} + +SPLAY_GENERATE(mta_source_tree, mta_source, entry, mta_source_cmp); + +static struct mta_connector * +mta_connector(struct mta_relay *relay, struct mta_source *source) +{ + struct mta_connector *c; + + c = tree_get(&relay->connectors, (uintptr_t)(source)); + if (c == NULL) { + c = xcalloc(1, sizeof(*c)); + c->relay = relay; + c->source = source; + c->flags |= CONNECTOR_NEW; + mta_source_ref(source); + tree_xset(&relay->connectors, (uintptr_t)(source), c); + stat_increment("mta.connector", 1); + log_debug("debug: mta: new %s", mta_connector_to_text(c)); + } + + return (c); +} + +static void +mta_connector_free(struct mta_connector *c) +{ + log_debug("debug: mta: freeing %s", + mta_connector_to_text(c)); + + if (c->flags & CONNECTOR_WAIT) { + log_debug("debug: mta: cancelling timeout for %s", + mta_connector_to_text(c)); + runq_cancel(runq_connector, c); + } + mta_source_unref(c->source); /* from constructor */ + free(c); + + stat_decrement("mta.connector", 1); +} + +static const char * +mta_connector_to_text(struct mta_connector *c) +{ + static char buf[1024]; + + (void)snprintf(buf, sizeof buf, "[connector:%s->%s,0x%x]", + mta_source_to_text(c->source), + mta_relay_to_text(c->relay), + c->flags); + return (buf); +} + +static struct mta_route * +mta_route(struct mta_source *src, struct mta_host *dst) +{ + struct mta_route key, *r; + static uint64_t rid = 0; + + key.src = src; + key.dst = dst; + r = SPLAY_FIND(mta_route_tree, &routes, &key); + + if (r == NULL) { + r = xcalloc(1, sizeof(*r)); + r->src = src; + r->dst = dst; + r->flags |= ROUTE_NEW; + r->id = ++rid; + SPLAY_INSERT(mta_route_tree, &routes, r); + mta_source_ref(src); + mta_host_ref(dst); + stat_increment("mta.route", 1); + } + else if (r->flags & ROUTE_RUNQ) { + log_debug("debug: mta: mta_route_ref(): cancelling runq for route %s", + mta_route_to_text(r)); + r->flags &= ~(ROUTE_RUNQ | ROUTE_KEEPALIVE); + runq_cancel(runq_route, r); + r->refcount--; /* from mta_route_unref() */ + } + + r->refcount++; + return (r); +} + +static void +mta_route_ref(struct mta_route *r) +{ + r->refcount++; +} + +static void +mta_route_unref(struct mta_route *r) +{ + time_t sched, now; + int delay; + + if (--r->refcount) + return; + + /* + * Nothing references this route, but we might want to keep it alive + * for a while. + */ + now = time(NULL); + sched = 0; + + if (r->penalty) { +#if DELAY_QUADRATIC + delay = DELAY_ROUTE_BASE * r->penalty * r->penalty; +#else + delay = 15 * 60; +#endif + if (delay > DELAY_ROUTE_MAX) + delay = DELAY_ROUTE_MAX; + sched = r->lastpenalty + delay; + log_debug("debug: mta: mta_route_unref(): keeping route %s alive for %llus (penalty %d)", + mta_route_to_text(r), (unsigned long long) sched - now, r->penalty); + } else if (!(r->flags & ROUTE_KEEPALIVE)) { + if (r->lastconn + max_seen_conndelay_route > now) + sched = r->lastconn + max_seen_conndelay_route; + if (r->lastdisc + max_seen_discdelay_route > now && + r->lastdisc + max_seen_discdelay_route < sched) + sched = r->lastdisc + max_seen_discdelay_route; + + if (sched > now) + log_debug("debug: mta: mta_route_unref(): keeping route %s alive for %llus (imposed delay)", + mta_route_to_text(r), (unsigned long long) sched - now); + } + + if (sched > now) { + r->flags |= ROUTE_RUNQ; + runq_schedule_at(runq_route, sched, r); + r->refcount++; + return; + } + + log_debug("debug: mta: mta_route_unref(): really discarding route %s", + mta_route_to_text(r)); + + SPLAY_REMOVE(mta_route_tree, &routes, r); + mta_source_unref(r->src); /* from constructor */ + mta_host_unref(r->dst); /* from constructor */ + free(r); + stat_decrement("mta.route", 1); +} + +static const char * +mta_route_to_text(struct mta_route *r) +{ + static char buf[1024]; + + (void)snprintf(buf, sizeof buf, "%s <-> %s", + mta_source_to_text(r->src), + mta_host_to_text(r->dst)); + + return (buf); +} + +static int +mta_route_cmp(const struct mta_route *a, const struct mta_route *b) +{ + if (a->src < b->src) + return (-1); + if (a->src > b->src) + return (1); + + if (a->dst < b->dst) + return (-1); + if (a->dst > b->dst) + return (1); + + return (0); +} + +SPLAY_GENERATE(mta_route_tree, mta_route, entry, mta_route_cmp); + +void +mta_block(struct mta_source *src, char *dom) +{ + struct mta_block key, *b; + + key.source = src; + key.domain = dom; + + b = SPLAY_FIND(mta_block_tree, &blocks, &key); + if (b != NULL) + return; + + b = xcalloc(1, sizeof(*b)); + if (dom) + b->domain = xstrdup(dom); + b->source = src; + mta_source_ref(src); + SPLAY_INSERT(mta_block_tree, &blocks, b); +} + +void +mta_unblock(struct mta_source *src, char *dom) +{ + struct mta_block key, *b; + + key.source = src; + key.domain = dom; + + b = SPLAY_FIND(mta_block_tree, &blocks, &key); + if (b == NULL) + return; + + SPLAY_REMOVE(mta_block_tree, &blocks, b); + + mta_source_unref(b->source); + free(b->domain); + free(b); +} + +int +mta_is_blocked(struct mta_source *src, char *dom) +{ + struct mta_block key; + + key.source = src; + key.domain = dom; + + if (SPLAY_FIND(mta_block_tree, &blocks, &key)) + return (1); + + return (0); +} + +static +int +mta_block_cmp(const struct mta_block *a, const struct mta_block *b) +{ + if (a->source < b->source) + return (-1); + if (a->source > b->source) + return (1); + if (!a->domain && b->domain) + return (-1); + if (a->domain && !b->domain) + return (1); + if (a->domain == b->domain) + return (0); + return (strcasecmp(a->domain, b->domain)); +} + +SPLAY_GENERATE(mta_block_tree, mta_block, entry, mta_block_cmp); + + + +/* hoststat errors are not critical, we do best effort */ +void +mta_hoststat_update(const char *host, const char *error) +{ + struct hoststat *hs = NULL; + char buf[HOST_NAME_MAX+1]; + + if (!lowercase(buf, host, sizeof buf)) + return; + + hs = dict_get(&hoststat, buf); + if (hs == NULL) { + if ((hs = calloc(1, sizeof *hs)) == NULL) + return; + tree_init(&hs->deferred); + runq_schedule(runq_hoststat, HOSTSTAT_EXPIRE_DELAY, hs); + } + (void)strlcpy(hs->name, buf, sizeof hs->name); + (void)strlcpy(hs->error, error, sizeof hs->error); + hs->tm = time(NULL); + dict_set(&hoststat, buf, hs); + + runq_cancel(runq_hoststat, hs); + runq_schedule(runq_hoststat, HOSTSTAT_EXPIRE_DELAY, hs); +} + +void +mta_hoststat_cache(const char *host, uint64_t evpid) +{ + struct hoststat *hs = NULL; + char buf[HOST_NAME_MAX+1]; + + if (!lowercase(buf, host, sizeof buf)) + return; + + hs = dict_get(&hoststat, buf); + if (hs == NULL) + return; + + if (tree_count(&hs->deferred) >= env->sc_mta_max_deferred) + return; + + tree_set(&hs->deferred, evpid, NULL); +} + +void +mta_hoststat_uncache(const char *host, uint64_t evpid) +{ + struct hoststat *hs = NULL; + char buf[HOST_NAME_MAX+1]; + + if (!lowercase(buf, host, sizeof buf)) + return; + + hs = dict_get(&hoststat, buf); + if (hs == NULL) + return; + + tree_pop(&hs->deferred, evpid); +} + +void +mta_hoststat_reschedule(const char *host) +{ + struct hoststat *hs = NULL; + char buf[HOST_NAME_MAX+1]; + uint64_t evpid; + + if (!lowercase(buf, host, sizeof buf)) + return; + + hs = dict_get(&hoststat, buf); + if (hs == NULL) + return; + + while (tree_poproot(&hs->deferred, &evpid, NULL)) { + m_compose(p_queue, IMSG_MTA_SCHEDULE, 0, 0, -1, + &evpid, sizeof evpid); + } +} + +static void +mta_hoststat_remove_entry(struct hoststat *hs) +{ + while (tree_poproot(&hs->deferred, NULL, NULL)) + ; + dict_pop(&hoststat, hs->name); + runq_cancel(runq_hoststat, hs); +} diff --git a/smtpd/mta_session.c b/smtpd/mta_session.c new file mode 100644 index 00000000..ad1c3c84 --- /dev/null +++ b/smtpd/mta_session.c @@ -0,0 +1,2008 @@ +/* $OpenBSD: mta_session.c,v 1.136 2020/05/21 15:38:05 millert Exp $ */ + +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/uio.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <netdb.h> +#include <openssl/ssl.h> +#include <pwd.h> +#include <resolv.h> +#include <limits.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" + +#define MAX_TRYBEFOREDISABLE 10 + +#define MTA_HIWAT 65535 + +enum mta_state { + MTA_INIT, + MTA_BANNER, + MTA_EHLO, + MTA_HELO, + MTA_LHLO, + MTA_STARTTLS, + MTA_AUTH, + MTA_AUTH_PLAIN, + MTA_AUTH_LOGIN, + MTA_AUTH_LOGIN_USER, + MTA_AUTH_LOGIN_PASS, + MTA_READY, + MTA_MAIL, + MTA_RCPT, + MTA_DATA, + MTA_BODY, + MTA_EOM, + MTA_LMTP_EOM, + MTA_RSET, + MTA_QUIT, +}; + +#define MTA_FORCE_ANYSSL 0x0001 +#define MTA_FORCE_SMTPS 0x0002 +#define MTA_FORCE_TLS 0x0004 +#define MTA_FORCE_PLAIN 0x0008 +#define MTA_WANT_SECURE 0x0010 +#define MTA_DOWNGRADE_PLAIN 0x0080 + +#define MTA_TLS 0x0100 +#define MTA_TLS_VERIFIED 0x0200 + +#define MTA_FREE 0x0400 +#define MTA_LMTP 0x0800 +#define MTA_WAIT 0x1000 +#define MTA_HANGON 0x2000 +#define MTA_RECONN 0x4000 + +#define MTA_EXT_STARTTLS 0x01 +#define MTA_EXT_PIPELINING 0x02 +#define MTA_EXT_AUTH 0x04 +#define MTA_EXT_AUTH_PLAIN 0x08 +#define MTA_EXT_AUTH_LOGIN 0x10 +#define MTA_EXT_SIZE 0x20 + +struct mta_session { + uint64_t id; + struct mta_relay *relay; + struct mta_route *route; + char *helo; + char *mxname; + + char *username; + + int flags; + + int attempt; + int use_smtps; + int use_starttls; + int use_smtp_tls; + int ready; + + struct event ev; + struct io *io; + int ext; + + size_t ext_size; + + size_t msgtried; + size_t msgcount; + size_t rcptcount; + int hangon; + + enum mta_state state; + struct mta_task *task; + struct mta_envelope *currevp; + FILE *datafp; + size_t datalen; + + size_t failures; + + char replybuf[2048]; +}; + +static void mta_session_init(void); +static void mta_start(int fd, short ev, void *arg); +static void mta_io(struct io *, int, void *); +static void mta_free(struct mta_session *); +static void mta_getnameinfo_cb(void *, int, const char *, const char *); +static void mta_on_ptr(void *, void *, void *); +static void mta_on_timeout(struct runq *, void *); +static void mta_connect(struct mta_session *); +static void mta_enter_state(struct mta_session *, int); +static void mta_flush_task(struct mta_session *, int, const char *, size_t, int); +static void mta_error(struct mta_session *, const char *, ...); +static void mta_send(struct mta_session *, char *, ...); +static ssize_t mta_queue_data(struct mta_session *); +static void mta_response(struct mta_session *, char *); +static const char * mta_strstate(int); +static void mta_cert_init(struct mta_session *); +static void mta_cert_init_cb(void *, int, const char *, const void *, size_t); +static void mta_cert_verify(struct mta_session *); +static void mta_cert_verify_cb(void *, int); +static void mta_tls_verified(struct mta_session *); +static struct mta_session *mta_tree_pop(struct tree *, uint64_t); +static const char * dsn_strret(enum dsn_ret); +static const char * dsn_strnotify(uint8_t); + +void mta_hoststat_update(const char *, const char *); +void mta_hoststat_reschedule(const char *); +void mta_hoststat_cache(const char *, uint64_t); +void mta_hoststat_uncache(const char *, uint64_t); + + +static void mta_filter_begin(struct mta_session *); +static void mta_filter_end(struct mta_session *); +static void mta_connected(struct mta_session *); +static void mta_disconnected(struct mta_session *); + +static void mta_report_link_connect(struct mta_session *, const char *, int, + const struct sockaddr_storage *, + const struct sockaddr_storage *); +static void mta_report_link_greeting(struct mta_session *, const char *); +static void mta_report_link_identify(struct mta_session *, const char *, const char *); +static void mta_report_link_tls(struct mta_session *, const char *); +static void mta_report_link_disconnect(struct mta_session *); +static void mta_report_link_auth(struct mta_session *, const char *, const char *); +static void mta_report_tx_reset(struct mta_session *, uint32_t); +static void mta_report_tx_begin(struct mta_session *, uint32_t); +static void mta_report_tx_mail(struct mta_session *, uint32_t, const char *, int); +static void mta_report_tx_rcpt(struct mta_session *, uint32_t, const char *, int); +static void mta_report_tx_envelope(struct mta_session *, uint32_t, uint64_t); +static void mta_report_tx_data(struct mta_session *, uint32_t, int); +static void mta_report_tx_commit(struct mta_session *, uint32_t, size_t); +static void mta_report_tx_rollback(struct mta_session *, uint32_t); +static void mta_report_protocol_client(struct mta_session *, const char *); +static void mta_report_protocol_server(struct mta_session *, const char *); +#if 0 +static void mta_report_filter_response(struct mta_session *, int, int, const char *); +#endif +static void mta_report_timeout(struct mta_session *); + + +static struct tree wait_helo; +static struct tree wait_ptr; +static struct tree wait_fd; +static struct tree wait_tls_init; +static struct tree wait_tls_verify; + +static struct runq *hangon; + +#define SESSION_FILTERED(s) \ + ((s)->relay->dispatcher->u.remote.filtername) + +static void +mta_session_init(void) +{ + static int init = 0; + + if (!init) { + tree_init(&wait_helo); + tree_init(&wait_ptr); + tree_init(&wait_fd); + tree_init(&wait_tls_init); + tree_init(&wait_tls_verify); + runq_init(&hangon, mta_on_timeout); + init = 1; + } +} + +void +mta_session(struct mta_relay *relay, struct mta_route *route, const char *mxname) +{ + struct mta_session *s; + struct timeval tv; + + mta_session_init(); + + s = xcalloc(1, sizeof *s); + s->id = generate_uid(); + s->relay = relay; + s->route = route; + s->mxname = xstrdup(mxname); + + mta_filter_begin(s); + + if (relay->flags & RELAY_LMTP) + s->flags |= MTA_LMTP; + switch (relay->tls) { + case RELAY_TLS_SMTPS: + s->flags |= MTA_FORCE_SMTPS; + s->flags |= MTA_WANT_SECURE; + break; + case RELAY_TLS_STARTTLS: + s->flags |= MTA_FORCE_TLS; + s->flags |= MTA_WANT_SECURE; + break; + case RELAY_TLS_OPPORTUNISTIC: + /* do not force anything, try tls then smtp */ + break; + case RELAY_TLS_NO: + s->flags |= MTA_FORCE_PLAIN; + break; + default: + fatalx("bad value for relay->tls: %d", relay->tls); + } + + log_debug("debug: mta: %p: spawned for relay %s", s, + mta_relay_to_text(relay)); + stat_increment("mta.session", 1); + + if (route->dst->ptrname || route->dst->lastptrquery) { + /* We want to delay the connection since to always notify + * the relay asynchronously. + */ + tv.tv_sec = 0; + tv.tv_usec = 0; + evtimer_set(&s->ev, mta_start, s); + evtimer_add(&s->ev, &tv); + } else if (waitq_wait(&route->dst->ptrname, mta_on_ptr, s)) { + resolver_getnameinfo(s->route->dst->sa, 0, mta_getnameinfo_cb, s); + } +} + +void +mta_session_imsg(struct mproc *p, struct imsg *imsg) +{ + struct mta_session *s; + struct msg m; + uint64_t reqid; + const char *name; + int status; + struct stat sb; + + switch (imsg->hdr.type) { + + case IMSG_MTA_OPEN_MESSAGE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_end(&m); + + s = mta_tree_pop(&wait_fd, reqid); + if (s == NULL) { + if (imsg->fd != -1) + close(imsg->fd); + return; + } + + if (imsg->fd == -1) { + log_debug("debug: mta: failed to obtain msg fd"); + mta_flush_task(s, IMSG_MTA_DELIVERY_TEMPFAIL, + "Could not get message fd", 0, 0); + mta_enter_state(s, MTA_READY); + return; + } + + if ((s->ext & MTA_EXT_SIZE) && s->ext_size != 0) { + if (fstat(imsg->fd, &sb) == -1) { + log_debug("debug: mta: failed to stat msg fd"); + mta_flush_task(s, IMSG_MTA_DELIVERY_TEMPFAIL, + "Could not stat message fd", 0, 0); + mta_enter_state(s, MTA_READY); + close(imsg->fd); + return; + } + if (sb.st_size > (off_t)s->ext_size) { + log_debug("debug: mta: message too large for peer"); + mta_flush_task(s, IMSG_MTA_DELIVERY_PERMFAIL, + "message too large for peer", 0, 0); + mta_enter_state(s, MTA_READY); + close(imsg->fd); + return; + } + } + + s->datafp = fdopen(imsg->fd, "r"); + if (s->datafp == NULL) + fatal("mta: fdopen"); + + mta_enter_state(s, MTA_MAIL); + return; + + case IMSG_MTA_LOOKUP_HELO: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &status); + if (status == LKA_OK) + m_get_string(&m, &name); + m_end(&m); + + s = mta_tree_pop(&wait_helo, reqid); + if (s == NULL) + return; + + if (status == LKA_OK) { + s->helo = xstrdup(name); + mta_connect(s); + } else { + mta_source_error(s->relay, s->route, + "Failed to retrieve helo string"); + mta_free(s); + } + return; + + default: + errx(1, "mta_session_imsg: unexpected %s imsg", + imsg_to_str(imsg->hdr.type)); + } +} + +static struct mta_session * +mta_tree_pop(struct tree *wait, uint64_t reqid) +{ + struct mta_session *s; + + s = tree_xpop(wait, reqid); + if (s->flags & MTA_FREE) { + log_debug("debug: mta: %p: zombie session", s); + mta_free(s); + return (NULL); + } + s->flags &= ~MTA_WAIT; + + return (s); +} + +static void +mta_free(struct mta_session *s) +{ + struct mta_relay *relay; + struct mta_route *route; + + log_debug("debug: mta: %p: session done", s); + + mta_disconnected(s); + + if (s->ready) + s->relay->nconn_ready -= 1; + + if (s->flags & MTA_HANGON) { + log_debug("debug: mta: %p: cancelling hangon timer", s); + runq_cancel(hangon, s); + } + + if (s->io) + io_free(s->io); + + if (s->task) + fatalx("current task should have been deleted already"); + if (s->datafp) { + fclose(s->datafp); + s->datalen = 0; + } + free(s->helo); + + relay = s->relay; + route = s->route; + free(s->username); + free(s->mxname); + free(s); + stat_decrement("mta.session", 1); + mta_route_collect(relay, route); +} + +static void +mta_getnameinfo_cb(void *arg, int gaierrno, const char *host, const char *serv) +{ + struct mta_session *s = arg; + struct mta_host *h; + + h = s->route->dst; + h->lastptrquery = time(NULL); + if (host) + h->ptrname = xstrdup(host); + waitq_run(&h->ptrname, h->ptrname); +} + +static void +mta_on_timeout(struct runq *runq, void *arg) +{ + struct mta_session *s = arg; + + log_debug("mta: timeout for session hangon"); + + s->flags &= ~MTA_HANGON; + s->hangon++; + + mta_enter_state(s, MTA_READY); +} + +static void +mta_on_ptr(void *tag, void *arg, void *data) +{ + struct mta_session *s = arg; + + mta_connect(s); +} + +static void +mta_start(int fd, short ev, void *arg) +{ + struct mta_session *s = arg; + + mta_connect(s); +} + +static void +mta_connect(struct mta_session *s) +{ + struct sockaddr_storage ss; + struct sockaddr *sa; + int portno; + const char *schema; + + if (s->helo == NULL) { + if (s->relay->helotable && s->route->src->sa) { + m_create(p_lka, IMSG_MTA_LOOKUP_HELO, 0, 0, -1); + m_add_id(p_lka, s->id); + m_add_string(p_lka, s->relay->helotable); + m_add_sockaddr(p_lka, s->route->src->sa); + m_close(p_lka); + tree_xset(&wait_helo, s->id, s); + s->flags |= MTA_WAIT; + return; + } + else if (s->relay->heloname) + s->helo = xstrdup(s->relay->heloname); + else + s->helo = xstrdup(env->sc_hostname); + } + + if (s->io) { + io_free(s->io); + s->io = NULL; + } + + s->use_smtps = s->use_starttls = s->use_smtp_tls = 0; + + switch (s->attempt) { + case 0: + if (s->flags & MTA_FORCE_SMTPS) + s->use_smtps = 1; /* smtps */ + else if (s->flags & (MTA_FORCE_TLS|MTA_FORCE_ANYSSL)) + s->use_starttls = 1; /* tls, tls+smtps */ + else if (!(s->flags & MTA_FORCE_PLAIN)) + s->use_smtp_tls = 1; + break; + case 1: + if (s->flags & MTA_FORCE_ANYSSL) { + s->use_smtps = 1; /* tls+smtps */ + break; + } + else if (s->flags & MTA_DOWNGRADE_PLAIN) { + /* smtp, with tls failure */ + break; + } + default: + mta_free(s); + return; + } + portno = s->use_smtps ? 465 : 25; + + /* Override with relay-specified port */ + if (s->relay->port) + portno = s->relay->port; + + memmove(&ss, s->route->dst->sa, SA_LEN(s->route->dst->sa)); + sa = (struct sockaddr *)&ss; + + if (sa->sa_family == AF_INET) + ((struct sockaddr_in *)sa)->sin_port = htons(portno); + else if (sa->sa_family == AF_INET6) + ((struct sockaddr_in6 *)sa)->sin6_port = htons(portno); + + s->attempt += 1; + if (s->use_smtp_tls) + schema = "smtp://"; + else if (s->use_starttls) + schema = "smtp+tls://"; + else if (s->use_smtps) + schema = "smtps://"; + else if (s->flags & MTA_LMTP) + schema = "lmtp://"; + else + schema = "smtp+notls://"; + + log_info("%016"PRIx64" mta " + "connecting address=%s%s:%d host=%s", + s->id, schema, sa_to_text(s->route->dst->sa), + portno, s->route->dst->ptrname); + + mta_enter_state(s, MTA_INIT); + s->io = io_new(); + io_set_callback(s->io, mta_io, s); + io_set_timeout(s->io, 300000); + if (io_connect(s->io, sa, s->route->src->sa) == -1) { + /* + * This error is most likely a "no route", + * so there is no need to try again. + */ + log_debug("debug: mta: io_connect failed: %s", io_error(s->io)); + if (errno == EADDRNOTAVAIL) + mta_source_error(s->relay, s->route, io_error(s->io)); + else + mta_error(s, "Connection failed: %s", io_error(s->io)); + mta_free(s); + } +} + +static void +mta_enter_state(struct mta_session *s, int newstate) +{ + struct mta_envelope *e; + size_t envid_sz; + int oldstate; + ssize_t q; + char ibuf[LINE_MAX]; + char obuf[LINE_MAX]; + int offset; + const char *srs_sender; + +again: + oldstate = s->state; + + log_trace(TRACE_MTA, "mta: %p: %s -> %s", s, + mta_strstate(oldstate), + mta_strstate(newstate)); + + s->state = newstate; + + memset(s->replybuf, 0, sizeof s->replybuf); + + /* don't try this at home! */ +#define mta_enter_state(_s, _st) do { newstate = _st; goto again; } while (0) + + switch (s->state) { + case MTA_INIT: + case MTA_BANNER: + break; + + case MTA_EHLO: + s->ext = 0; + mta_send(s, "EHLO %s", s->helo); + mta_report_link_identify(s, "EHLO", s->helo); + break; + + case MTA_HELO: + s->ext = 0; + mta_send(s, "HELO %s", s->helo); + mta_report_link_identify(s, "HELO", s->helo); + break; + + case MTA_LHLO: + s->ext = 0; + mta_send(s, "LHLO %s", s->helo); + mta_report_link_identify(s, "LHLO", s->helo); + break; + + case MTA_STARTTLS: + if (s->flags & MTA_DOWNGRADE_PLAIN) + mta_enter_state(s, MTA_AUTH); + if (s->flags & MTA_TLS) /* already started */ + mta_enter_state(s, MTA_AUTH); + else if ((s->ext & MTA_EXT_STARTTLS) == 0) { + if (s->flags & MTA_FORCE_TLS || s->flags & MTA_WANT_SECURE) { + mta_error(s, "TLS required but not supported by remote host"); + s->flags |= MTA_RECONN; + } + else + /* server doesn't support starttls, do not use it */ + mta_enter_state(s, MTA_AUTH); + } + else + mta_send(s, "STARTTLS"); + break; + + case MTA_AUTH: + if (s->relay->secret && s->flags & MTA_TLS) { + if (s->ext & MTA_EXT_AUTH) { + if (s->ext & MTA_EXT_AUTH_PLAIN) { + mta_enter_state(s, MTA_AUTH_PLAIN); + break; + } + if (s->ext & MTA_EXT_AUTH_LOGIN) { + mta_enter_state(s, MTA_AUTH_LOGIN); + break; + } + log_debug("debug: mta: %p: no supported AUTH method on session", s); + mta_error(s, "no supported AUTH method"); + } + else { + log_debug("debug: mta: %p: AUTH not advertised on session", s); + mta_error(s, "AUTH not advertised"); + } + } + else if (s->relay->secret) { + log_debug("debug: mta: %p: not using AUTH on non-TLS " + "session", s); + mta_error(s, "Refuse to AUTH over unsecure channel"); + mta_connect(s); + } else { + mta_enter_state(s, MTA_READY); + } + break; + + case MTA_AUTH_PLAIN: + memset(ibuf, 0, sizeof ibuf); + if (base64_decode(s->relay->secret, (unsigned char *)ibuf, + sizeof(ibuf)-1) == -1) { + log_debug("debug: mta: %p: credentials too large on session", s); + mta_error(s, "Credentials too large"); + break; + } + s->username = xstrdup(ibuf+1); + mta_send(s, "AUTH PLAIN %s", s->relay->secret); + break; + + case MTA_AUTH_LOGIN: + mta_send(s, "AUTH LOGIN"); + break; + + case MTA_AUTH_LOGIN_USER: + memset(ibuf, 0, sizeof ibuf); + if (base64_decode(s->relay->secret, (unsigned char *)ibuf, + sizeof(ibuf)-1) == -1) { + log_debug("debug: mta: %p: credentials too large on session", s); + mta_error(s, "Credentials too large"); + break; + } + s->username = xstrdup(ibuf+1); + + memset(obuf, 0, sizeof obuf); + base64_encode((unsigned char *)ibuf + 1, strlen(ibuf + 1), obuf, sizeof obuf); + mta_send(s, "%s", obuf); + + memset(ibuf, 0, sizeof ibuf); + memset(obuf, 0, sizeof obuf); + break; + + case MTA_AUTH_LOGIN_PASS: + memset(ibuf, 0, sizeof ibuf); + if (base64_decode(s->relay->secret, (unsigned char *)ibuf, + sizeof(ibuf)-1) == -1) { + log_debug("debug: mta: %p: credentials too large on session", s); + mta_error(s, "Credentials too large"); + break; + } + + offset = strlen(ibuf+1)+2; + memset(obuf, 0, sizeof obuf); + base64_encode((unsigned char *)ibuf + offset, strlen(ibuf + offset), obuf, sizeof obuf); + mta_send(s, "%s", obuf); + + memset(ibuf, 0, sizeof ibuf); + memset(obuf, 0, sizeof obuf); + break; + + case MTA_READY: + /* Ready to send a new mail */ + if (s->ready == 0) { + s->ready = 1; + s->relay->nconn_ready += 1; + mta_route_ok(s->relay, s->route); + } + + if (s->msgtried >= MAX_TRYBEFOREDISABLE) { + log_info("%016"PRIx64" mta host-rejects-all-mails", + s->id); + mta_route_down(s->relay, s->route); + mta_enter_state(s, MTA_QUIT); + break; + } + + if (s->msgcount >= s->relay->limits->max_mail_per_session) { + log_debug("debug: mta: " + "%p: cannot send more message to relay %s", s, + mta_relay_to_text(s->relay)); + mta_enter_state(s, MTA_QUIT); + break; + } + + /* + * When downgrading from opportunistic TLS, clear flag and + * possibly reuse the same task (forbidden in other cases). + */ + if (s->flags & MTA_DOWNGRADE_PLAIN) + s->flags &= ~MTA_DOWNGRADE_PLAIN; + else if (s->task) + fatalx("task should be NULL at this point"); + + if (s->task == NULL) + s->task = mta_route_next_task(s->relay, s->route); + if (s->task == NULL) { + log_debug("debug: mta: %p: no task for relay %s", + s, mta_relay_to_text(s->relay)); + + if (s->relay->nconn > 1 || + s->hangon >= s->relay->limits->sessdelay_keepalive) { + mta_enter_state(s, MTA_QUIT); + break; + } + + log_debug("mta: debug: last connection: hanging on for %llds", + (long long)(s->relay->limits->sessdelay_keepalive - + s->hangon)); + s->flags |= MTA_HANGON; + runq_schedule(hangon, 1, s); + break; + } + + log_debug("debug: mta: %p: handling next task for relay %s", s, + mta_relay_to_text(s->relay)); + + stat_increment("mta.task.running", 1); + + m_create(p_queue, IMSG_MTA_OPEN_MESSAGE, 0, 0, -1); + m_add_id(p_queue, s->id); + m_add_msgid(p_queue, s->task->msgid); + m_close(p_queue); + + tree_xset(&wait_fd, s->id, s); + s->flags |= MTA_WAIT; + break; + + case MTA_MAIL: + s->currevp = TAILQ_FIRST(&s->task->envelopes); + + e = s->currevp; + s->hangon = 0; + s->msgtried++; + envid_sz = strlen(e->dsn_envid); + + /* SRS-encode if requested for the relay action, AND we're not + * bouncing, AND we have an RCPT which means we are forwarded, + * AND the RCPT has a '@' just for sanity check (will always). + */ + if (env->sc_srs_key != NULL && + s->relay->srs && + strchr(s->task->sender, '@') && + e->rcpt && + strchr(e->rcpt, '@')) { + /* encode and replace task sender with new SRS-sender */ + srs_sender = srs_encode(s->task->sender, + strchr(e->rcpt, '@') + 1); + if (srs_sender) { + free(s->task->sender); + s->task->sender = xstrdup(srs_sender); + } + } + + if (s->ext & MTA_EXT_DSN) { + mta_send(s, "MAIL FROM:<%s>%s%s%s%s", + s->task->sender, + e->dsn_ret ? " RET=" : "", + e->dsn_ret ? dsn_strret(e->dsn_ret) : "", + envid_sz ? " ENVID=" : "", + envid_sz ? e->dsn_envid : ""); + } else + mta_send(s, "MAIL FROM:<%s>", s->task->sender); + break; + + case MTA_RCPT: + if (s->currevp == NULL) + s->currevp = TAILQ_FIRST(&s->task->envelopes); + + e = s->currevp; + if (s->ext & MTA_EXT_DSN) { + mta_send(s, "RCPT TO:<%s>%s%s%s%s", + e->dest, + e->dsn_notify ? " NOTIFY=" : "", + e->dsn_notify ? dsn_strnotify(e->dsn_notify) : "", + e->dsn_orcpt ? " ORCPT=rfc822;" : "", + e->dsn_orcpt ? e->dsn_orcpt : ""); + } else + mta_send(s, "RCPT TO:<%s>", e->dest); + + mta_report_tx_envelope(s, s->task->msgid, e->id); + s->rcptcount++; + break; + + case MTA_DATA: + fseek(s->datafp, 0, SEEK_SET); + mta_send(s, "DATA"); + break; + + case MTA_BODY: + if (s->datafp == NULL) { + log_trace(TRACE_MTA, "mta: %p: end-of-file", s); + mta_enter_state(s, MTA_EOM); + break; + } + + if ((q = mta_queue_data(s)) == -1) { + s->flags |= MTA_FREE; + break; + } + if (q == 0) { + mta_enter_state(s, MTA_BODY); + break; + } + + log_trace(TRACE_MTA, "mta: %p: >>> [...%zd bytes...]", s, q); + break; + + case MTA_EOM: + mta_send(s, "."); + break; + + case MTA_LMTP_EOM: + /* LMTP reports status of each delivery, so enable read */ + io_set_read(s->io); + break; + + case MTA_RSET: + if (s->datafp) { + fclose(s->datafp); + s->datafp = NULL; + s->datalen = 0; + } + mta_send(s, "RSET"); + break; + + case MTA_QUIT: + mta_send(s, "QUIT"); + break; + + default: + fatalx("mta_enter_state: unknown state"); + } +#undef mta_enter_state +} + +/* + * Handle a response to an SMTP command + */ +static void +mta_response(struct mta_session *s, char *line) +{ + struct mta_envelope *e; + struct sockaddr_storage ss; + struct sockaddr *sa; + const char *domain; + char *pbuf; + socklen_t sa_len; + char buf[LINE_MAX]; + int delivery; + + switch (s->state) { + + case MTA_BANNER: + if (line[0] != '2') { + mta_error(s, "BANNER rejected: %s", line); + s->flags |= MTA_FREE; + return; + } + + pbuf = ""; + if (strlen(line) > 4) { + (void)strlcpy(buf, line + 4, sizeof buf); + if ((pbuf = strchr(buf, ' '))) + *pbuf = '\0'; + pbuf = valid_domainpart(buf) ? buf : ""; + } + mta_report_link_greeting(s, pbuf); + + if (s->flags & MTA_LMTP) + mta_enter_state(s, MTA_LHLO); + else + mta_enter_state(s, MTA_EHLO); + break; + + case MTA_EHLO: + if (line[0] != '2') { + /* rejected at ehlo state */ + if ((s->relay->flags & RELAY_AUTH) || + (s->flags & MTA_WANT_SECURE)) { + mta_error(s, "EHLO rejected: %s", line); + s->flags |= MTA_FREE; + return; + } + mta_enter_state(s, MTA_HELO); + return; + } + if (!(s->flags & MTA_FORCE_PLAIN)) + mta_enter_state(s, MTA_STARTTLS); + else + mta_enter_state(s, MTA_READY); + break; + + case MTA_HELO: + if (line[0] != '2') { + mta_error(s, "HELO rejected: %s", line); + s->flags |= MTA_FREE; + return; + } + mta_enter_state(s, MTA_READY); + break; + + case MTA_LHLO: + if (line[0] != '2') { + mta_error(s, "LHLO rejected: %s", line); + s->flags |= MTA_FREE; + return; + } + mta_enter_state(s, MTA_READY); + break; + + case MTA_STARTTLS: + if (line[0] != '2') { + if (!(s->flags & MTA_WANT_SECURE)) { + mta_enter_state(s, MTA_AUTH); + return; + } + /* XXX mark that the MX doesn't support STARTTLS */ + mta_error(s, "STARTTLS rejected: %s", line); + s->flags |= MTA_FREE; + return; + } + + mta_cert_init(s); + break; + + case MTA_AUTH_PLAIN: + if (line[0] != '2') { + mta_error(s, "AUTH rejected: %s", line); + mta_report_link_auth(s, s->username, "fail"); + s->flags |= MTA_FREE; + return; + } + mta_report_link_auth(s, s->username, "pass"); + mta_enter_state(s, MTA_READY); + break; + + case MTA_AUTH_LOGIN: + if (strncmp(line, "334 ", 4) != 0) { + mta_error(s, "AUTH rejected: %s", line); + mta_report_link_auth(s, s->username, "fail"); + s->flags |= MTA_FREE; + return; + } + mta_enter_state(s, MTA_AUTH_LOGIN_USER); + break; + + case MTA_AUTH_LOGIN_USER: + if (strncmp(line, "334 ", 4) != 0) { + mta_error(s, "AUTH rejected: %s", line); + mta_report_link_auth(s, s->username, "fail"); + s->flags |= MTA_FREE; + return; + } + mta_enter_state(s, MTA_AUTH_LOGIN_PASS); + break; + + case MTA_AUTH_LOGIN_PASS: + if (line[0] != '2') { + mta_error(s, "AUTH rejected: %s", line); + mta_report_link_auth(s, s->username, "fail"); + s->flags |= MTA_FREE; + return; + } + mta_report_link_auth(s, s->username, "pass"); + mta_enter_state(s, MTA_READY); + break; + + case MTA_MAIL: + if (line[0] != '2') { + if (line[0] == '5') + delivery = IMSG_MTA_DELIVERY_PERMFAIL; + else + delivery = IMSG_MTA_DELIVERY_TEMPFAIL; + + mta_flush_task(s, delivery, line, 0, 0); + mta_enter_state(s, MTA_RSET); + return; + } + mta_report_tx_begin(s, s->task->msgid); + mta_report_tx_mail(s, s->task->msgid, s->task->sender, 1); + mta_enter_state(s, MTA_RCPT); + break; + + case MTA_RCPT: + e = s->currevp; + + /* remove envelope from hosttat cache if there */ + if ((domain = strchr(e->dest, '@')) != NULL) { + domain++; + mta_hoststat_uncache(domain, e->id); + } + + s->currevp = TAILQ_NEXT(s->currevp, entry); + if (line[0] == '2') { + s->failures = 0; + /* + * this host is up, reschedule envelopes that + * were cached for reschedule. + */ + if (domain) + mta_hoststat_reschedule(domain); + } + else { + mta_report_tx_rollback(s, s->task->msgid); + mta_report_tx_reset(s, s->task->msgid); + if (line[0] == '5') + delivery = IMSG_MTA_DELIVERY_PERMFAIL; + else + delivery = IMSG_MTA_DELIVERY_TEMPFAIL; + s->failures++; + + /* remove failed envelope from task list */ + TAILQ_REMOVE(&s->task->envelopes, e, entry); + stat_decrement("mta.envelope", 1); + + /* log right away */ + (void)snprintf(buf, sizeof(buf), "%s", + mta_host_to_text(s->route->dst)); + + e->session = s->id; + /* XXX */ + /* + * getsockname() can only fail with ENOBUFS here + * best effort, don't log source ... + */ + sa_len = sizeof(ss); + sa = (struct sockaddr *)&ss; + if (getsockname(io_fileno(s->io), sa, &sa_len) == -1) + mta_delivery_log(e, NULL, buf, delivery, line); + else + mta_delivery_log(e, sa_to_text(sa), + buf, delivery, line); + + if (domain) + mta_hoststat_update(domain, e->status); + mta_delivery_notify(e); + + if (s->relay->limits->max_failures_per_session && + s->failures == s->relay->limits->max_failures_per_session) { + mta_flush_task(s, IMSG_MTA_DELIVERY_TEMPFAIL, + "Too many consecutive errors, closing connection", 0, 1); + mta_enter_state(s, MTA_QUIT); + break; + } + + /* + * if no more envelopes, flush failed queue + */ + if (TAILQ_EMPTY(&s->task->envelopes)) { + mta_flush_task(s, IMSG_MTA_DELIVERY_OK, + "No envelope", 0, 0); + mta_enter_state(s, MTA_RSET); + break; + } + } + + switch (line[0]) { + case '2': + mta_report_tx_rcpt(s, + s->task->msgid, e->dest, 1); + break; + case '4': + mta_report_tx_rcpt(s, + s->task->msgid, e->dest, -1); + break; + case '5': + mta_report_tx_rcpt(s, + s->task->msgid, e->dest, 0); + break; + } + + if (s->currevp == NULL) + mta_enter_state(s, MTA_DATA); + else + mta_enter_state(s, MTA_RCPT); + break; + + case MTA_DATA: + if (line[0] == '2' || line[0] == '3') { + mta_report_tx_data(s, s->task->msgid, 1); + mta_enter_state(s, MTA_BODY); + break; + } + + if (line[0] == '5') + delivery = IMSG_MTA_DELIVERY_PERMFAIL; + else + delivery = IMSG_MTA_DELIVERY_TEMPFAIL; + mta_report_tx_data(s, s->task->msgid, + delivery == IMSG_MTA_DELIVERY_TEMPFAIL ? -1 : 0); + mta_report_tx_rollback(s, s->task->msgid); + mta_report_tx_reset(s, s->task->msgid); + mta_flush_task(s, delivery, line, 0, 0); + mta_enter_state(s, MTA_RSET); + break; + + case MTA_LMTP_EOM: + case MTA_EOM: + if (line[0] == '2') { + delivery = IMSG_MTA_DELIVERY_OK; + s->msgtried = 0; + s->msgcount++; + } + else if (line[0] == '5') + delivery = IMSG_MTA_DELIVERY_PERMFAIL; + else + delivery = IMSG_MTA_DELIVERY_TEMPFAIL; + if (delivery != IMSG_MTA_DELIVERY_OK) { + mta_report_tx_rollback(s, s->task->msgid); + mta_report_tx_reset(s, s->task->msgid); + } + else { + mta_report_tx_commit(s, s->task->msgid, s->datalen); + mta_report_tx_reset(s, s->task->msgid); + } + mta_flush_task(s, delivery, line, (s->flags & MTA_LMTP) ? 1 : 0, 0); + if (s->task) { + s->rcptcount--; + mta_enter_state(s, MTA_LMTP_EOM); + } else { + s->rcptcount = 0; + if (s->relay->limits->sessdelay_transaction) { + log_debug("debug: mta: waiting for %llds before next transaction", + (long long int)s->relay->limits->sessdelay_transaction); + s->hangon = s->relay->limits->sessdelay_transaction -1; + s->flags |= MTA_HANGON; + runq_schedule(hangon, + s->relay->limits->sessdelay_transaction, s); + } + else + mta_enter_state(s, MTA_READY); + } + break; + + case MTA_RSET: + s->rcptcount = 0; + + if (s->task) { + mta_report_tx_rollback(s, s->task->msgid); + mta_report_tx_reset(s, s->task->msgid); + } + if (s->relay->limits->sessdelay_transaction) { + log_debug("debug: mta: waiting for %llds after reset", + (long long int)s->relay->limits->sessdelay_transaction); + s->hangon = s->relay->limits->sessdelay_transaction -1; + s->flags |= MTA_HANGON; + runq_schedule(hangon, + s->relay->limits->sessdelay_transaction, s); + } + else + mta_enter_state(s, MTA_READY); + break; + + default: + fatalx("mta_response() bad state"); + } +} + +static void +mta_io(struct io *io, int evt, void *arg) +{ + struct mta_session *s = arg; + char *line, *msg, *p; + size_t len; + const char *error; + int cont; + + log_trace(TRACE_IO, "mta: %p: %s %s", s, io_strevent(evt), + io_strio(io)); + + switch (evt) { + + case IO_CONNECTED: + mta_connected(s); + + if (s->use_smtps) { + io_set_write(io); + mta_cert_init(s); + } + else { + mta_enter_state(s, MTA_BANNER); + io_set_read(io); + } + break; + + case IO_TLSREADY: + log_info("%016"PRIx64" mta tls ciphers=%s", + s->id, ssl_to_text(io_tls(s->io))); + s->flags |= MTA_TLS; + + mta_report_link_tls(s, + ssl_to_text(io_tls(s->io))); + + mta_cert_verify(s); + break; + + case IO_DATAIN: + nextline: + line = io_getline(s->io, &len); + if (line == NULL) { + if (io_datalen(s->io) >= LINE_MAX) { + mta_error(s, "Input too long"); + mta_free(s); + } + return; + } + + /* Strip trailing '\r' */ + if (len && line[len - 1] == '\r') + line[--len] = '\0'; + + log_trace(TRACE_MTA, "mta: %p: <<< %s", s, line); + mta_report_protocol_server(s, line); + + if ((error = parse_smtp_response(line, len, &msg, &cont))) { + mta_error(s, "Bad response: %s", error); + mta_free(s); + return; + } + + /* read extensions */ + if (s->state == MTA_EHLO) { + if (strcmp(msg, "STARTTLS") == 0) + s->ext |= MTA_EXT_STARTTLS; + else if (strncmp(msg, "AUTH ", 5) == 0) { + s->ext |= MTA_EXT_AUTH; + if ((p = strstr(msg, " PLAIN")) && + (*(p+6) == '\0' || *(p+6) == ' ')) + s->ext |= MTA_EXT_AUTH_PLAIN; + if ((p = strstr(msg, " LOGIN")) && + (*(p+6) == '\0' || *(p+6) == ' ')) + s->ext |= MTA_EXT_AUTH_LOGIN; + } + else if (strcmp(msg, "PIPELINING") == 0) + s->ext |= MTA_EXT_PIPELINING; + else if (strcmp(msg, "DSN") == 0) + s->ext |= MTA_EXT_DSN; + else if (strncmp(msg, "SIZE ", 5) == 0) { + s->ext_size = strtonum(msg+5, 0, UINT32_MAX, &error); + if (error == NULL) + s->ext |= MTA_EXT_SIZE; + } + } + + /* continuation reply, we parse out the repeating statuses and ESC */ + if (cont) { + if (s->replybuf[0] == '\0') + (void)strlcat(s->replybuf, line, sizeof s->replybuf); + else if (len > 4) { + p = line + 4; + if (isdigit((unsigned char)p[0]) && p[1] == '.' && + isdigit((unsigned char)p[2]) && p[3] == '.' && + isdigit((unsigned char)p[4]) && isspace((unsigned char)p[5])) + p += 5; + (void)strlcat(s->replybuf, p, sizeof s->replybuf); + } + goto nextline; + } + + /* last line of a reply, check if we're on a continuation to parse out status and ESC. + * if we overflow reply buffer or are not on continuation, log entire last line. + */ + if (s->replybuf[0] == '\0') + (void)strlcat(s->replybuf, line, sizeof s->replybuf); + else if (len > 4) { + p = line + 4; + if (isdigit((unsigned char)p[0]) && p[1] == '.' && + isdigit((unsigned char)p[2]) && p[3] == '.' && + isdigit((unsigned char)p[4]) && isspace((unsigned char)p[5])) + p += 5; + if (strlcat(s->replybuf, p, sizeof s->replybuf) >= sizeof s->replybuf) + (void)strlcpy(s->replybuf, line, sizeof s->replybuf); + } + + if (s->state == MTA_QUIT) { + log_info("%016"PRIx64" mta disconnected reason=quit messages=%zu", + s->id, s->msgcount); + mta_free(s); + return; + } + io_set_write(io); + mta_response(s, s->replybuf); + if (s->flags & MTA_FREE) { + mta_free(s); + return; + } + if (s->flags & MTA_RECONN) { + s->flags &= ~MTA_RECONN; + mta_connect(s); + return; + } + + if (io_datalen(s->io)) { + log_debug("debug: mta: remaining data in input buffer"); + mta_error(s, "Remote host sent too much data"); + if (s->flags & MTA_WAIT) + s->flags |= MTA_FREE; + else + mta_free(s); + } + break; + + case IO_LOWAT: + if (s->state == MTA_BODY) { + mta_enter_state(s, MTA_BODY); + if (s->flags & MTA_FREE) { + mta_free(s); + return; + } + } + + if (io_queued(s->io) == 0) + io_set_read(io); + break; + + case IO_TIMEOUT: + log_debug("debug: mta: %p: connection timeout", s); + mta_error(s, "Connection timeout"); + mta_report_timeout(s); + if (!s->ready) + mta_connect(s); + else + mta_free(s); + break; + + case IO_ERROR: + case IO_TLSERROR: + log_debug("debug: mta: %p: IO error: %s", s, io_error(io)); + + if (s->state == MTA_STARTTLS && s->use_smtp_tls) { + /* error in non-strict SSL negotiation, downgrade to plain */ + log_info("smtp-out: Error on session %016"PRIx64 + ": opportunistic TLS failed, " + "downgrading to plain", s->id); + s->flags &= ~MTA_TLS; + s->flags |= MTA_DOWNGRADE_PLAIN; + mta_connect(s); + break; + } + + mta_error(s, "IO Error: %s", io_error(io)); + mta_free(s); + break; + + case IO_DISCONNECTED: + log_debug("debug: mta: %p: disconnected in state %s", + s, mta_strstate(s->state)); + mta_error(s, "Connection closed unexpectedly"); + if (!s->ready) + mta_connect(s); + else + mta_free(s); + break; + + default: + fatalx("mta_io() bad event"); + } +} + +static void +mta_send(struct mta_session *s, char *fmt, ...) +{ + va_list ap; + char *p; + int len; + + va_start(ap, fmt); + if ((len = vasprintf(&p, fmt, ap)) == -1) + fatal("mta: vasprintf"); + va_end(ap); + + log_trace(TRACE_MTA, "mta: %p: >>> %s", s, p); + + if (strncasecmp(p, "AUTH PLAIN ", 11) == 0) + mta_report_protocol_client(s, "AUTH PLAIN ********"); + else if (s->state == MTA_AUTH_LOGIN_USER || s->state == MTA_AUTH_LOGIN_PASS) + mta_report_protocol_client(s, "********"); + else + mta_report_protocol_client(s, p); + + io_xprintf(s->io, "%s\r\n", p); + + free(p); +} + +/* + * Queue some data into the input buffer + */ +static ssize_t +mta_queue_data(struct mta_session *s) +{ + char *ln = NULL; + size_t sz = 0, q; + ssize_t len; + + q = io_queued(s->io); + + while (io_queued(s->io) < MTA_HIWAT) { + if ((len = getline(&ln, &sz, s->datafp)) == -1) + break; + if (ln[len - 1] == '\n') + ln[len - 1] = '\0'; + s->datalen += io_xprintf(s->io, "%s%s\r\n", *ln == '.' ? "." : "", ln); + } + + free(ln); + if (ferror(s->datafp)) { + mta_flush_task(s, IMSG_MTA_DELIVERY_TEMPFAIL, + "Error reading content file", 0, 0); + return (-1); + } + + if (feof(s->datafp)) { + fclose(s->datafp); + s->datafp = NULL; + } + + return (io_queued(s->io) - q); +} + +static void +mta_flush_task(struct mta_session *s, int delivery, const char *error, size_t count, + int cache) +{ + struct mta_envelope *e; + char relay[LINE_MAX]; + size_t n; + struct sockaddr_storage ss; + struct sockaddr *sa; + socklen_t sa_len; + const char *domain; + + (void)snprintf(relay, sizeof relay, "%s", mta_host_to_text(s->route->dst)); + n = 0; + while ((e = TAILQ_FIRST(&s->task->envelopes))) { + + if (count && n == count) { + stat_decrement("mta.envelope", n); + return; + } + + TAILQ_REMOVE(&s->task->envelopes, e, entry); + + /* we're about to log, associate session to envelope */ + e->session = s->id; + e->ext = s->ext; + + /* XXX */ + /* + * getsockname() can only fail with ENOBUFS here + * best effort, don't log source ... + */ + sa = (struct sockaddr *)&ss; + sa_len = sizeof(ss); + if (getsockname(io_fileno(s->io), sa, &sa_len) == -1) + mta_delivery_log(e, NULL, relay, delivery, error); + else + mta_delivery_log(e, sa_to_text(sa), + relay, delivery, error); + + mta_delivery_notify(e); + + domain = strchr(e->dest, '@'); + if (domain) { + domain++; + mta_hoststat_update(domain, error); + if (cache) + mta_hoststat_cache(domain, e->id); + } + + n++; + } + + free(s->task->sender); + free(s->task); + s->task = NULL; + + if (s->datafp) { + fclose(s->datafp); + s->datafp = NULL; + } + + stat_decrement("mta.envelope", n); + stat_decrement("mta.task.running", 1); + stat_decrement("mta.task", 1); +} + +static void +mta_error(struct mta_session *s, const char *fmt, ...) +{ + va_list ap; + char *error; + int len; + + va_start(ap, fmt); + if ((len = vasprintf(&error, fmt, ap)) == -1) + fatal("mta: vasprintf"); + va_end(ap); + + if (s->msgcount) + log_info("smtp-out: Error on session %016"PRIx64 + " after %zu message%s sent: %s", s->id, s->msgcount, + (s->msgcount > 1) ? "s" : "", error); + else + log_info("%016"PRIx64" mta error reason=%s", + s->id, error); + + /* + * If not connected yet, and the error is not local, just ignore it + * and try to reconnect. + */ + if (s->state == MTA_INIT && + (errno == ETIMEDOUT || errno == ECONNREFUSED)) { + log_debug("debug: mta: not reporting route error yet"); + free(error); + return; + } + + mta_route_error(s->relay, s->route); + + if (s->task) + mta_flush_task(s, IMSG_MTA_DELIVERY_TEMPFAIL, error, 0, 0); + + free(error); +} + +static void +mta_cert_init(struct mta_session *s) +{ + const char *name; + int fallback; + + if (s->relay->pki_name) { + name = s->relay->pki_name; + fallback = 0; + } + else { + name = s->helo; + fallback = 1; + } + + if (cert_init(name, fallback, mta_cert_init_cb, s)) { + tree_xset(&wait_tls_init, s->id, s); + s->flags |= MTA_WAIT; + } +} + +static void +mta_cert_init_cb(void *arg, int status, const char *name, const void *cert, + size_t cert_len) +{ + struct mta_session *s = arg; + void *ssl; + char *xname = NULL, *xcert = NULL; + + if (s->flags & MTA_WAIT) + mta_tree_pop(&wait_tls_init, s->id); + + if (status == CA_FAIL && s->relay->pki_name) { + log_info("%016"PRIx64" mta closing reason=ca-failure", s->id); + mta_free(s); + return; + } + + if (name) + xname = xstrdup(name); + if (cert) + xcert = xmemdup(cert, cert_len); + ssl = ssl_mta_init(xname, xcert, cert_len, env->sc_tls_ciphers); + free(xname); + free(xcert); + if (ssl == NULL) + fatal("mta: ssl_mta_init"); + io_start_tls(s->io, ssl); +} + +static void +mta_cert_verify(struct mta_session *s) +{ + const char *name; + int fallback; + + if (s->relay->ca_name) { + name = s->relay->ca_name; + fallback = 0; + } + else { + name = s->helo; + fallback = 1; + } + + if (cert_verify(io_tls(s->io), name, fallback, mta_cert_verify_cb, s)) { + tree_xset(&wait_tls_verify, s->id, s); + io_pause(s->io, IO_IN); + s->flags |= MTA_WAIT; + } +} + +static void +mta_cert_verify_cb(void *arg, int status) +{ + struct mta_session *s = arg; + int match, resume = 0; + X509 *cert; + + if (s->flags & MTA_WAIT) { + mta_tree_pop(&wait_tls_verify, s->id); + resume = 1; + } + + if (status == CERT_OK) { + cert = SSL_get_peer_certificate(io_tls(s->io)); + if (!cert) + status = CERT_NOCERT; + else { + match = 0; + (void)ssl_check_name(cert, s->mxname, &match); + X509_free(cert); + if (!match) { + log_info("%016"PRIx64" mta " + "ssl_check_name: no match for '%s' in cert", + s->id, s->mxname); + status = CERT_INVALID; + } + } + } + + if (status == CERT_OK) + s->flags |= MTA_TLS_VERIFIED; + else if (s->relay->flags & RELAY_TLS_VERIFY) { + errno = 0; + mta_error(s, "SSL certificate check failed"); + mta_free(s); + return; + } + + mta_tls_verified(s); + if (resume) + io_resume(s->io, IO_IN); +} + +static void +mta_tls_verified(struct mta_session *s) +{ + X509 *x; + + x = SSL_get_peer_certificate(io_tls(s->io)); + if (x) { + log_info("%016"PRIx64" mta " + "server-cert-check result=\"%s\"", + s->id, + (s->flags & MTA_TLS_VERIFIED) ? "success" : "failure"); + X509_free(x); + } + + if (s->use_smtps) { + mta_enter_state(s, MTA_BANNER); + io_set_read(s->io); + } + else + mta_enter_state(s, MTA_EHLO); +} + +static const char * +dsn_strret(enum dsn_ret ret) +{ + if (ret == DSN_RETHDRS) + return "HDRS"; + else if (ret == DSN_RETFULL) + return "FULL"; + else { + log_debug("mta: invalid ret %d", ret); + return "???"; + } +} + +static const char * +dsn_strnotify(uint8_t arg) +{ + static char buf[32]; + size_t sz; + + buf[0] = '\0'; + if (arg & DSN_SUCCESS) + (void)strlcat(buf, "SUCCESS,", sizeof(buf)); + + if (arg & DSN_FAILURE) + (void)strlcat(buf, "FAILURE,", sizeof(buf)); + + if (arg & DSN_DELAY) + (void)strlcat(buf, "DELAY,", sizeof(buf)); + + if (arg & DSN_NEVER) + (void)strlcat(buf, "NEVER,", sizeof(buf)); + + /* trim trailing comma */ + sz = strlen(buf); + if (sz) + buf[sz - 1] = '\0'; + + return (buf); +} + +#define CASE(x) case x : return #x + +static const char * +mta_strstate(int state) +{ + switch (state) { + CASE(MTA_INIT); + CASE(MTA_BANNER); + CASE(MTA_EHLO); + CASE(MTA_HELO); + CASE(MTA_STARTTLS); + CASE(MTA_AUTH); + CASE(MTA_AUTH_PLAIN); + CASE(MTA_AUTH_LOGIN); + CASE(MTA_AUTH_LOGIN_USER); + CASE(MTA_AUTH_LOGIN_PASS); + CASE(MTA_READY); + CASE(MTA_MAIL); + CASE(MTA_RCPT); + CASE(MTA_DATA); + CASE(MTA_BODY); + CASE(MTA_EOM); + CASE(MTA_LMTP_EOM); + CASE(MTA_RSET); + CASE(MTA_QUIT); + default: + return "MTA_???"; + } +} + +static void +mta_filter_begin(struct mta_session *s) +{ + if (!SESSION_FILTERED(s)) + return; + + m_create(p_lka, IMSG_FILTER_SMTP_BEGIN, 0, 0, -1); + m_add_id(p_lka, s->id); + m_add_string(p_lka, s->relay->dispatcher->u.remote.filtername); + m_close(p_lka); +} + +static void +mta_filter_end(struct mta_session *s) +{ + if (!SESSION_FILTERED(s)) + return; + + m_create(p_lka, IMSG_FILTER_SMTP_END, 0, 0, -1); + m_add_id(p_lka, s->id); + m_close(p_lka); +} + +static void +mta_connected(struct mta_session *s) +{ + struct sockaddr_storage sa_src; + struct sockaddr_storage sa_dest; + int sa_len; + + log_info("%016"PRIx64" mta connected", s->id); + + sa_len = sizeof sa_src; + if (getsockname(io_fileno(s->io), + (struct sockaddr *)&sa_src, &sa_len) == -1) + bzero(&sa_src, sizeof sa_src); + sa_len = sizeof sa_dest; + if (getpeername(io_fileno(s->io), + (struct sockaddr *)&sa_dest, &sa_len) == -1) + bzero(&sa_dest, sizeof sa_dest); + + mta_report_link_connect(s, + s->route->dst->ptrname, 1, + &sa_src, + &sa_dest); +} + +static void +mta_disconnected(struct mta_session *s) +{ + mta_report_link_disconnect(s); + mta_filter_end(s); +} + + +static void +mta_report_link_connect(struct mta_session *s, const char *rdns, int fcrdns, + const struct sockaddr_storage *ss_src, + const struct sockaddr_storage *ss_dest) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_connect("smtp-out", s->id, rdns, fcrdns, ss_src, ss_dest); +} + +static void +mta_report_link_greeting(struct mta_session *s, + const char *domain) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_greeting("smtp-out", s->id, domain); +} + +static void +mta_report_link_identify(struct mta_session *s, const char *method, const char *identity) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_identify("smtp-out", s->id, method, identity); +} + +static void +mta_report_link_tls(struct mta_session *s, const char *ssl) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_tls("smtp-out", s->id, ssl); +} + +static void +mta_report_link_disconnect(struct mta_session *s) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_disconnect("smtp-out", s->id); +} + +static void +mta_report_link_auth(struct mta_session *s, const char *user, const char *result) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_auth("smtp-out", s->id, user, result); +} + +static void +mta_report_tx_reset(struct mta_session *s, uint32_t msgid) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_reset("smtp-out", s->id, msgid); +} + +static void +mta_report_tx_begin(struct mta_session *s, uint32_t msgid) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_begin("smtp-out", s->id, msgid); +} + +static void +mta_report_tx_mail(struct mta_session *s, uint32_t msgid, const char *address, int ok) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_mail("smtp-out", s->id, msgid, address, ok); +} + +static void +mta_report_tx_rcpt(struct mta_session *s, uint32_t msgid, const char *address, int ok) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_rcpt("smtp-out", s->id, msgid, address, ok); +} + +static void +mta_report_tx_envelope(struct mta_session *s, uint32_t msgid, uint64_t evpid) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_envelope("smtp-out", s->id, msgid, evpid); +} + +static void +mta_report_tx_data(struct mta_session *s, uint32_t msgid, int ok) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_data("smtp-out", s->id, msgid, ok); +} + +static void +mta_report_tx_commit(struct mta_session *s, uint32_t msgid, size_t msgsz) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_commit("smtp-out", s->id, msgid, msgsz); +} + +static void +mta_report_tx_rollback(struct mta_session *s, uint32_t msgid) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_rollback("smtp-out", s->id, msgid); +} + +static void +mta_report_protocol_client(struct mta_session *s, const char *command) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_protocol_client("smtp-out", s->id, command); +} + +static void +mta_report_protocol_server(struct mta_session *s, const char *response) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_protocol_server("smtp-out", s->id, response); +} + +#if 0 +static void +mta_report_filter_response(struct mta_session *s, int phase, int response, const char *param) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_filter_response("smtp-out", s->id, phase, response, param); +} +#endif + +static void +mta_report_timeout(struct mta_session *s) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_timeout("smtp-out", s->id); +} diff --git a/smtpd/newaliases.8 b/smtpd/newaliases.8 new file mode 100644 index 00000000..b82e4515 --- /dev/null +++ b/smtpd/newaliases.8 @@ -0,0 +1,86 @@ +.\" $OpenBSD: newaliases.8,v 1.12 2018/07/20 15:35:33 millert Exp $ +.\" +.\" Copyright (c) 2009 Jacek Masiulaniec <jacekm@openbsd.org> +.\" Copyright (c) 2008-2009 Gilles Chehade <gilles@poolp.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: July 20 2018 $ +.Dt NEWALIASES 8 +.Os +.Sh NAME +.Nm newaliases +.Nd rebuild mail aliases +.Sh SYNOPSIS +.Nm newaliases +.Op Fl f Ar file +.Sh DESCRIPTION +The +.Nm +utility makes changes to the mail aliases file visible to +.Xr smtpd 8 . +It should be run every time the +.Xr aliases 5 +file is changed. +The location of the alias file is defined in +.Xr smtpd.conf 5 , +and defaults to +.Pa /etc/mail/aliases . +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl f Ar file +Use +.Ar file +as the configuration file, +instead of the default +.Pa /etc/mail/smtpd.conf . +.El +.Pp +If using database (db) files, +.Nm +is equivalent to running +.Xr makemap 8 +as follows: +.Bd -literal -offset indent +# makemap -t aliases /etc/mail/aliases +.Ed +.Pp +If using plain text files, +.Nm +is equivalent to running +.Xr smtpctl 8 +as follows: +.Bd -literal -offset indent +# smtpctl update table aliases +.Ed +.Sh FILES +.Bl -tag -width "/etc/mail/aliasesXXX" -compact +.It Pa /etc/mail/aliases +List of local user mail aliases. +.It Pa /etc/mail/virtual +List of virtual host aliases. +.El +.Sh EXIT STATUS +.Ex -std newaliases +.Sh SEE ALSO +.Xr smtpd.conf 5 , +.Xr makemap 8 , +.Xr smtpctl 8 , +.Xr smtpd 8 +.Sh HISTORY +The +.Nm +command first appeared in +.Ox 4.6 +as a replacement for the equivalent command shipped with sendmail. diff --git a/smtpd/parse.y b/smtpd/parse.y new file mode 100644 index 00000000..db5be747 --- /dev/null +++ b/smtpd/parse.y @@ -0,0 +1,3598 @@ +/* $OpenBSD: parse.y,v 1.277 2020/02/24 23:54:27 millert Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org> + * Copyright (c) 2001 Markus Friedl. All rights reserved. + * Copyright (c) 2001 Daniel Hartmeier. All rights reserved. + * Copyright (c) 2001 Theo de Raadt. All rights reserved. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +%{ +#include "includes.h" + +#include <sys/types.h> +#include <sys/time.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/ioctl.h> + +#include <net/if.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <ifaddrs.h> +#include <imsg.h> +#include <inttypes.h> +#include <limits.h> +#include <netdb.h> +#include <pwd.h> +#include <stdarg.h> +#include <resolv.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> +#ifdef HAVE_UTIL_H +#include <util.h> +#endif + +#include <openssl/ssl.h> + +#include "smtpd.h" +#include "ssl.h" +#include "log.h" + +TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files); +static struct file { + TAILQ_ENTRY(file) entry; + FILE *stream; + char *name; + size_t ungetpos; + size_t ungetsize; + u_char *ungetbuf; + int eof_reached; + 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 kw_cmp(const void *, const void *); +int lookup(char *); +int igetc(void); +int lgetc(int); +void lungetc(int); +int findeol(void); +int yyerror(const char *, ...) + __attribute__((__format__ (printf, 1, 2))) + __attribute__((__nonnull__ (1))); + +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; + +struct table *table = NULL; +struct mta_limits *limits; +static struct pki *pki; +static struct ca *sca; + +struct dispatcher *dispatcher; +struct rule *rule; +struct filter_proc *processor; +struct filter_config *filter_config; +static uint32_t last_dynchain_id = 1; + +enum listen_options { + LO_FAMILY = 0x000001, + LO_PORT = 0x000002, + LO_SSL = 0x000004, + LO_FILTER = 0x000008, + LO_PKI = 0x000010, + LO_AUTH = 0x000020, + LO_TAG = 0x000040, + LO_HOSTNAME = 0x000080, + LO_HOSTNAMES = 0x000100, + LO_MASKSOURCE = 0x000200, + LO_NODSN = 0x000400, + LO_SENDERS = 0x000800, + LO_RECEIVEDAUTH = 0x001000, + LO_MASQUERADE = 0x002000, + LO_CA = 0x004000, + LO_PROXY = 0x008000, +}; + +static struct listen_opts { + char *ifx; + int family; + in_port_t port; + uint16_t ssl; + char *filtername; + char *pki; + char *ca; + uint16_t auth; + struct table *authtable; + char *tag; + char *hostname; + struct table *hostnametable; + struct table *sendertable; + uint16_t flags; + + uint32_t options; +} listen_opts; + +static void create_sock_listener(struct listen_opts *); +static void create_if_listener(struct listen_opts *); +static void config_listener(struct listener *, struct listen_opts *); +static int host_v4(struct listen_opts *); +static int host_v6(struct listen_opts *); +static int host_dns(struct listen_opts *); +static int interface(struct listen_opts *); + +int delaytonum(char *); +int is_if_in_group(const char *, const char *); + +static int config_lo_mask_source(struct listen_opts *); + +typedef struct { + union { + int64_t number; + struct table *table; + char *string; + struct host *host; + struct mailaddr *maddr; + } v; + int lineno; +} YYSTYPE; + +%} + +%token ACTION ALIAS ANY ARROW AUTH AUTH_OPTIONAL +%token BACKUP BOUNCE BYPASS +%token CA CERT CHAIN CHROOT CIPHERS COMMIT COMPRESSION CONNECT +%token DATA DATA_LINE DHE DISCONNECT DOMAIN +%token EHLO ENABLE ENCRYPTION ERROR EXPAND_ONLY +%token FCRDNS FILTER FOR FORWARD_ONLY FROM +%token GROUP +%token HELO HELO_SRC HOST HOSTNAME HOSTNAMES +%token INCLUDE INET4 INET6 +%token JUNK +%token KEY +%token LIMIT LISTEN LMTP LOCAL +%token MAIL_FROM MAILDIR MASK_SRC MASQUERADE MATCH MAX_MESSAGE_SIZE MAX_DEFERRED MBOX MDA MTA MX +%token NO_DSN NO_VERIFY NOOP +%token ON +%token PHASE PKI PORT PROC PROC_EXEC PROXY_V2 +%token QUEUE QUIT +%token RCPT_TO RDNS RECIPIENT RECEIVEDAUTH REGEX RELAY REJECT REPORT REWRITE RSET +%token SCHEDULER SENDER SENDERS SMTP SMTP_IN SMTP_OUT SMTPS SOCKET SRC SRS SUB_ADDR_DELIM +%token TABLE TAG TAGGED TLS TLS_REQUIRE TTL +%token USER USERBASE +%token VERIFY VIRTUAL +%token WARN_INTERVAL WRAPPER + +%token <v.string> STRING +%token <v.number> NUMBER +%type <v.table> table +%type <v.number> size negation +%type <v.table> tables tablenew tableref +%% + +grammar : /* empty */ + | grammar '\n' + | grammar include '\n' + | grammar varset '\n' + | grammar bounce '\n' + | grammar ca '\n' + | grammar mda '\n' + | grammar mta '\n' + | grammar pki '\n' + | grammar proc '\n' + | grammar queue '\n' + | grammar scheduler '\n' + | grammar smtp '\n' + | grammar srs '\n' + | grammar listen '\n' + | grammar table '\n' + | grammar dispatcher '\n' + | grammar match '\n' + | grammar filter '\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 { + char *s = $1; + while (*s++) { + if (isspace((unsigned char)*s)) { + yyerror("macro name cannot contain " + "whitespace"); + free($1); + free($3); + YYERROR; + } + } + if (symset($1, $3, 0) == -1) + fatal("cannot store variable"); + free($1); + free($3); + } + ; + +comma : ',' + | nl + | /* empty */ + ; + +optnl : '\n' optnl + | + ; + +nl : '\n' optnl + ; + +negation : '!' { $$ = 1; } + | /* empty */ { $$ = 0; } + ; + +assign : '=' | ARROW; + + +keyval : STRING assign STRING { + table_add(table, $1, $3); + free($1); + free($3); + } + ; + +keyval_list : keyval + | keyval comma keyval_list + ; + +stringel : STRING { + table_add(table, $1, NULL); + free($1); + } + ; + +string_list : stringel + | stringel comma string_list + ; + +tableval_list : string_list { } + | keyval_list { } + ; + +bounce: +BOUNCE WARN_INTERVAL { + memset(conf->sc_bounce_warn, 0, sizeof conf->sc_bounce_warn); +} bouncedelays +; + + +ca: +CA STRING { + char buf[HOST_NAME_MAX+1]; + + /* if not catchall, check that it is a valid domain */ + if (strcmp($2, "*") != 0) { + if (!res_hnok($2)) { + yyerror("not a valid domain name: %s", $2); + free($2); + YYERROR; + } + } + xlowercase(buf, $2, sizeof(buf)); + free($2); + sca = dict_get(conf->sc_ca_dict, buf); + if (sca == NULL) { + sca = xcalloc(1, sizeof *sca); + (void)strlcpy(sca->ca_name, buf, sizeof(sca->ca_name)); + dict_set(conf->sc_ca_dict, sca->ca_name, sca); + } +} ca_params +; + + +ca_params_opt: +CERT STRING { + sca->ca_cert_file = $2; +} +; + +ca_params: +ca_params_opt +; + + +mda: +MDA LIMIT limits_mda +| MDA WRAPPER STRING STRING { + if (dict_get(conf->sc_mda_wrappers, $3)) { + yyerror("mda wrapper already declared with that name: %s", $3); + YYERROR; + } + dict_set(conf->sc_mda_wrappers, $3, $4); +} +; + + +mta: +MTA MAX_DEFERRED NUMBER { + conf->sc_mta_max_deferred = $3; +} +| MTA LIMIT FOR DOMAIN STRING { + struct mta_limits *d; + + limits = dict_get(conf->sc_limits_dict, $5); + if (limits == NULL) { + limits = xcalloc(1, sizeof(*limits)); + dict_xset(conf->sc_limits_dict, $5, limits); + d = dict_xget(conf->sc_limits_dict, "default"); + memmove(limits, d, sizeof(*limits)); + } + free($5); +} limits_mta +| MTA LIMIT { + limits = dict_get(conf->sc_limits_dict, "default"); +} limits_mta +; + + +pki: +PKI STRING { + char buf[HOST_NAME_MAX+1]; + + /* if not catchall, check that it is a valid domain */ + if (strcmp($2, "*") != 0) { + if (!res_hnok($2)) { + yyerror("not a valid domain name: %s", $2); + free($2); + YYERROR; + } + } + xlowercase(buf, $2, sizeof(buf)); + free($2); + pki = dict_get(conf->sc_pki_dict, buf); + if (pki == NULL) { + pki = xcalloc(1, sizeof *pki); + (void)strlcpy(pki->pki_name, buf, sizeof(pki->pki_name)); + dict_set(conf->sc_pki_dict, pki->pki_name, pki); + } +} pki_params +; + +pki_params_opt: +CERT STRING { + pki->pki_cert_file = $2; +} +| KEY STRING { + pki->pki_key_file = $2; +} +| DHE STRING { + if (strcasecmp($2, "none") == 0) + pki->pki_dhe = 0; + else if (strcasecmp($2, "auto") == 0) + pki->pki_dhe = 1; + else if (strcasecmp($2, "legacy") == 0) + pki->pki_dhe = 2; + else { + yyerror("invalid DHE keyword: %s", $2); + free($2); + YYERROR; + } + free($2); +} +; + + +pki_params: +pki_params_opt pki_params +| /* empty */ +; + + +proc: +PROC STRING STRING { + if (dict_get(conf->sc_filter_processes_dict, $2)) { + yyerror("processor already exists with that name: %s", $2); + free($2); + free($3); + YYERROR; + } + processor = xcalloc(1, sizeof *processor); + processor->command = $3; +} proc_params { + dict_set(conf->sc_filter_processes_dict, $2, processor); + processor = NULL; +} +; + + +proc_params_opt: +USER STRING { + if (processor->user) { + yyerror("user already specified for this processor"); + free($2); + YYERROR; + } + processor->user = $2; +} +| GROUP STRING { + if (processor->group) { + yyerror("group already specified for this processor"); + free($2); + YYERROR; + } + processor->group = $2; +} +| CHROOT STRING { + if (processor->chroot) { + yyerror("chroot already specified for this processor"); + free($2); + YYERROR; + } + processor->chroot = $2; +} +; + +proc_params: +proc_params_opt proc_params +| /* empty */ +; + + +queue: +QUEUE COMPRESSION { + conf->sc_queue_flags |= QUEUE_COMPRESSION; +} +| QUEUE ENCRYPTION { + conf->sc_queue_flags |= QUEUE_ENCRYPTION; +} +| QUEUE ENCRYPTION STRING { + if (strcasecmp($3, "stdin") == 0 || strcasecmp($3, "-") == 0) { + conf->sc_queue_key = "stdin"; + free($3); + } + else + conf->sc_queue_key = $3; + conf->sc_queue_flags |= QUEUE_ENCRYPTION; +} +| QUEUE TTL STRING { + conf->sc_ttl = delaytonum($3); + if (conf->sc_ttl == -1) { + yyerror("invalid ttl delay: %s", $3); + free($3); + YYERROR; + } + free($3); +} +; + + +scheduler: +SCHEDULER LIMIT limits_scheduler +; + + +smtp: +SMTP LIMIT limits_smtp +| SMTP CIPHERS STRING { + conf->sc_tls_ciphers = $3; +} +| SMTP MAX_MESSAGE_SIZE size { + conf->sc_maxsize = $3; +} +| SMTP SUB_ADDR_DELIM STRING { + if (strlen($3) != 1) { + yyerror("subaddressing-delimiter must be one character"); + free($3); + YYERROR; + } + if (isspace((unsigned char)*$3) || !isprint((unsigned char)*$3) || *$3 == '@') { + yyerror("sub-addr-delim uses invalid character"); + free($3); + YYERROR; + } + conf->sc_subaddressing_delim = $3; +} +; + +srs: +SRS KEY STRING { + conf->sc_srs_key = $3; +} +| SRS KEY BACKUP STRING { + conf->sc_srs_key_backup = $4; +} +| SRS TTL STRING { + conf->sc_srs_ttl = delaytonum($3); + if (conf->sc_srs_ttl == -1) { + yyerror("ttl delay \"%s\" is invalid", $3); + free($3); + YYERROR; + } + + conf->sc_srs_ttl /= 86400; + if (conf->sc_srs_ttl == 0) { + yyerror("ttl delay \"%s\" is too short", $3); + free($3); + YYERROR; + } + free($3); +} +; + + +dispatcher_local_option: +USER STRING { + if (dispatcher->u.local.is_mbox) { + yyerror("user may not be specified for this dispatcher"); + YYERROR; + } + + if (dispatcher->u.local.forward_only) { + yyerror("user may not be specified for forward-only"); + YYERROR; + } + + if (dispatcher->u.local.expand_only) { + yyerror("user may not be specified for expand-only"); + YYERROR; + } + + if (dispatcher->u.local.user) { + yyerror("user already specified for this dispatcher"); + YYERROR; + } + + dispatcher->u.local.user = $2; +} +| ALIAS tables { + struct table *t = $2; + + if (dispatcher->u.local.table_alias) { + yyerror("alias mapping already specified for this dispatcher"); + YYERROR; + } + + if (dispatcher->u.local.table_virtual) { + yyerror("virtual mapping already specified for this dispatcher"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_ALIAS)) { + yyerror("table \"%s\" may not be used for alias lookups", + t->t_name); + YYERROR; + } + + dispatcher->u.local.table_alias = strdup(t->t_name); +} +| VIRTUAL tables { + struct table *t = $2; + + if (dispatcher->u.local.table_virtual) { + yyerror("virtual mapping already specified for this dispatcher"); + YYERROR; + } + + if (dispatcher->u.local.table_alias) { + yyerror("alias mapping already specified for this dispatcher"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_ALIAS)) { + yyerror("table \"%s\" may not be used for virtual lookups", + t->t_name); + YYERROR; + } + + dispatcher->u.local.table_virtual = strdup(t->t_name); +} +| USERBASE tables { + struct table *t = $2; + + if (dispatcher->u.local.table_userbase) { + yyerror("userbase mapping already specified for this dispatcher"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_USERINFO)) { + yyerror("table \"%s\" may not be used for userbase lookups", + t->t_name); + YYERROR; + } + + dispatcher->u.local.table_userbase = strdup(t->t_name); +} +| WRAPPER STRING { + if (! dict_get(conf->sc_mda_wrappers, $2)) { + yyerror("no mda wrapper with that name: %s", $2); + YYERROR; + } + dispatcher->u.local.mda_wrapper = $2; +} +; + +dispatcher_local_options: +dispatcher_local_option dispatcher_local_options +| /* empty */ +; + +dispatcher_local: +MBOX { + dispatcher->u.local.is_mbox = 1; + asprintf(&dispatcher->u.local.command, PATH_LIBEXEC"/mail.local -f %%{mbox.from} -- %%{user.username}"); +} dispatcher_local_options +| MAILDIR { + asprintf(&dispatcher->u.local.command, PATH_LIBEXEC"/mail.maildir"); +} dispatcher_local_options +| MAILDIR JUNK { + asprintf(&dispatcher->u.local.command, PATH_LIBEXEC"/mail.maildir -j"); +} dispatcher_local_options +| MAILDIR STRING { + if (strncmp($2, "~/", 2) == 0) + asprintf(&dispatcher->u.local.command, + PATH_LIBEXEC"/mail.maildir \"%%{user.directory}/%s\"", $2+2); + else + asprintf(&dispatcher->u.local.command, + PATH_LIBEXEC"/mail.maildir \"%s\"", $2); +} dispatcher_local_options +| MAILDIR STRING JUNK { + if (strncmp($2, "~/", 2) == 0) + asprintf(&dispatcher->u.local.command, + PATH_LIBEXEC"/mail.maildir -j \"%%{user.directory}/%s\"", $2+2); + else + asprintf(&dispatcher->u.local.command, + PATH_LIBEXEC"/mail.maildir -j \"%s\"", $2); +} dispatcher_local_options +| LMTP STRING { + asprintf(&dispatcher->u.local.command, + PATH_LIBEXEC"/mail.lmtp -d \"%s\" -u", $2); +} dispatcher_local_options +| LMTP STRING RCPT_TO { + asprintf(&dispatcher->u.local.command, + PATH_LIBEXEC"/mail.lmtp -d \"%s\" -r", $2); +} dispatcher_local_options +| MDA STRING { + asprintf(&dispatcher->u.local.command, + PATH_LIBEXEC"/mail.mda \"%s\"", $2); +} dispatcher_local_options +| FORWARD_ONLY { + dispatcher->u.local.forward_only = 1; +} dispatcher_local_options +| EXPAND_ONLY { + dispatcher->u.local.expand_only = 1; +} dispatcher_local_options + +; + +dispatcher_remote_option: +HELO STRING { + if (dispatcher->u.remote.helo) { + yyerror("helo already specified for this dispatcher"); + YYERROR; + } + + dispatcher->u.remote.helo = $2; +} +| HELO_SRC tables { + struct table *t = $2; + + if (dispatcher->u.remote.helo_source) { + yyerror("helo-source mapping already specified for this dispatcher"); + YYERROR; + } + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_ADDRNAME)) { + yyerror("table \"%s\" may not be used for helo-source lookups", + t->t_name); + YYERROR; + } + + dispatcher->u.remote.helo_source = strdup(t->t_name); +} +| PKI STRING { + if (dispatcher->u.remote.pki) { + yyerror("pki already specified for this dispatcher"); + YYERROR; + } + + dispatcher->u.remote.pki = $2; +} +| CA STRING { + if (dispatcher->u.remote.ca) { + yyerror("ca already specified for this dispatcher"); + YYERROR; + } + + dispatcher->u.remote.ca = $2; +} +| SRC tables { + struct table *t = $2; + + if (dispatcher->u.remote.source) { + yyerror("source mapping already specified for this dispatcher"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_SOURCE)) { + yyerror("table \"%s\" may not be used for source lookups", + t->t_name); + YYERROR; + } + + dispatcher->u.remote.source = strdup(t->t_name); +} +| MAIL_FROM STRING { + if (dispatcher->u.remote.mail_from) { + yyerror("mail-from already specified for this dispatcher"); + YYERROR; + } + + dispatcher->u.remote.mail_from = $2; +} +| BACKUP MX STRING { + if (dispatcher->u.remote.backup) { + yyerror("backup already specified for this dispatcher"); + YYERROR; + } + if (dispatcher->u.remote.smarthost) { + yyerror("backup and host are mutually exclusive"); + YYERROR; + } + + dispatcher->u.remote.backup = 1; + dispatcher->u.remote.backupmx = $3; +} +| BACKUP { + if (dispatcher->u.remote.backup) { + yyerror("backup already specified for this dispatcher"); + YYERROR; + } + if (dispatcher->u.remote.smarthost) { + yyerror("backup and host are mutually exclusive"); + YYERROR; + } + + dispatcher->u.remote.backup = 1; +} +| HOST tables { + struct table *t = $2; + + if (dispatcher->u.remote.smarthost) { + yyerror("host mapping already specified for this dispatcher"); + YYERROR; + } + if (dispatcher->u.remote.backup) { + yyerror("backup and host are mutually exclusive"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_RELAYHOST)) { + yyerror("table \"%s\" may not be used for host lookups", + t->t_name); + YYERROR; + } + + dispatcher->u.remote.smarthost = strdup(t->t_name); +} +| DOMAIN tables { + struct table *t = $2; + + if (dispatcher->u.remote.smarthost) { + yyerror("host mapping already specified for this dispatcher"); + YYERROR; + } + if (dispatcher->u.remote.backup) { + yyerror("backup and domain are mutually exclusive"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_RELAYHOST)) { + yyerror("table \"%s\" may not be used for host lookups", + t->t_name); + YYERROR; + } + + dispatcher->u.remote.smarthost = strdup(t->t_name); + dispatcher->u.remote.smarthost_domain = 1; +} +| TLS { + if (dispatcher->u.remote.tls_required == 1) { + yyerror("tls already specified for this dispatcher"); + YYERROR; + } + + dispatcher->u.remote.tls_required = 1; +} +| TLS NO_VERIFY { + if (dispatcher->u.remote.tls_required == 1) { + yyerror("tls already specified for this dispatcher"); + YYERROR; + } + + dispatcher->u.remote.tls_required = 1; + dispatcher->u.remote.tls_noverify = 1; +} +| AUTH tables { + struct table *t = $2; + + if (dispatcher->u.remote.smarthost == NULL) { + yyerror("auth may not be specified without host on a dispatcher"); + YYERROR; + } + + if (dispatcher->u.remote.auth) { + yyerror("auth mapping already specified for this dispatcher"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_CREDENTIALS)) { + yyerror("table \"%s\" may not be used for auth lookups", + t->t_name); + YYERROR; + } + + dispatcher->u.remote.auth = strdup(t->t_name); +} +| FILTER STRING { + struct filter_config *fc; + + if (dispatcher->u.remote.filtername) { + yyerror("filter already specified for this dispatcher"); + YYERROR; + } + + if ((fc = dict_get(conf->sc_filters_dict, $2)) == NULL) { + yyerror("no filter exist with that name: %s", $2); + free($2); + YYERROR; + } + fc->filter_subsystem |= FILTER_SUBSYSTEM_SMTP_OUT; + dispatcher->u.remote.filtername = $2; +} +| FILTER { + char buffer[128]; + char *filtername; + + if (dispatcher->u.remote.filtername) { + yyerror("filter already specified for this dispatcher"); + YYERROR; + } + + do { + (void)snprintf(buffer, sizeof buffer, "<dynchain:%08x>", last_dynchain_id++); + } while (dict_check(conf->sc_filters_dict, buffer)); + + filtername = xstrdup(buffer); + filter_config = xcalloc(1, sizeof *filter_config); + filter_config->filter_type = FILTER_TYPE_CHAIN; + filter_config->filter_subsystem |= FILTER_SUBSYSTEM_SMTP_OUT; + dict_init(&filter_config->chain_procs); + dispatcher->u.remote.filtername = filtername; +} '{' filter_list '}' { + dict_set(conf->sc_filters_dict, dispatcher->u.remote.filtername, filter_config); + filter_config = NULL; +} +| SRS { + if (conf->sc_srs_key == NULL) { + yyerror("an srs key is required for srs to be specified in an action"); + YYERROR; + } + if (dispatcher->u.remote.srs == 1) { + yyerror("srs already specified for this dispatcher"); + YYERROR; + } + + dispatcher->u.remote.srs = 1; +} +; + +dispatcher_remote_options: +dispatcher_remote_option dispatcher_remote_options +| /* empty */ +; + +dispatcher_remote : +RELAY dispatcher_remote_options +; + +dispatcher_type: +dispatcher_local { + dispatcher->type = DISPATCHER_LOCAL; +} +| dispatcher_remote { + dispatcher->type = DISPATCHER_REMOTE; +} +; + +dispatcher_option: +TTL STRING { + if (dispatcher->ttl) { + yyerror("ttl already specified for this dispatcher"); + YYERROR; + } + + dispatcher->ttl = delaytonum($2); + if (dispatcher->ttl == -1) { + yyerror("ttl delay \"%s\" is invalid", $2); + free($2); + YYERROR; + } + free($2); +} +; + +dispatcher_options: +dispatcher_option dispatcher_options +| /* empty */ +; + +dispatcher: +ACTION STRING { + if (dict_get(conf->sc_dispatchers, $2)) { + yyerror("dispatcher already declared with that name: %s", $2); + YYERROR; + } + dispatcher = xcalloc(1, sizeof *dispatcher); +} dispatcher_type dispatcher_options { + if (dispatcher->type == DISPATCHER_LOCAL) + if (dispatcher->u.local.table_userbase == NULL) + dispatcher->u.local.table_userbase = "<getpwnam>"; + dict_set(conf->sc_dispatchers, $2, dispatcher); + dispatcher = NULL; +} +; + +match_option: +negation TAG tables { + struct table *t = $3; + + if (rule->flag_tag) { + yyerror("tag already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_STRING)) { + yyerror("table \"%s\" may not be used for tag lookups", + t->t_name); + YYERROR; + } + + rule->flag_tag = $1 ? -1 : 1; + rule->table_tag = strdup(t->t_name); +} +| +negation TAG REGEX tables { + struct table *t = $4; + + if (rule->flag_tag) { + yyerror("tag already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for tag lookups", + t->t_name); + YYERROR; + } + + rule->flag_tag = $1 ? -1 : 1; + rule->flag_tag_regex = 1; + rule->table_tag = strdup(t->t_name); +} + +| negation HELO tables { + struct table *t = $3; + + if (rule->flag_smtp_helo) { + yyerror("helo already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_DOMAIN)) { + yyerror("table \"%s\" may not be used for helo lookups", + t->t_name); + YYERROR; + } + + rule->flag_smtp_helo = $1 ? -1 : 1; + rule->table_smtp_helo = strdup(t->t_name); +} +| negation HELO REGEX tables { + struct table *t = $4; + + if (rule->flag_smtp_helo) { + yyerror("helo already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for helo lookups", + t->t_name); + YYERROR; + } + + rule->flag_smtp_helo = $1 ? -1 : 1; + rule->flag_smtp_helo_regex = 1; + rule->table_smtp_helo = strdup(t->t_name); +} +| negation TLS { + if (rule->flag_smtp_starttls) { + yyerror("tls already specified for this rule"); + YYERROR; + } + rule->flag_smtp_starttls = $1 ? -1 : 1; +} +| negation AUTH { + if (rule->flag_smtp_auth) { + yyerror("auth already specified for this rule"); + YYERROR; + } + rule->flag_smtp_auth = $1 ? -1 : 1; +} +| negation AUTH tables { + struct table *t = $3; + + if (rule->flag_smtp_auth) { + yyerror("auth already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_STRING|K_CREDENTIALS)) { + yyerror("table \"%s\" may not be used for auth lookups", + t->t_name); + YYERROR; + } + + rule->flag_smtp_auth = $1 ? -1 : 1; + rule->table_smtp_auth = strdup(t->t_name); +} +| negation AUTH REGEX tables { + struct table *t = $4; + + if (rule->flag_smtp_auth) { + yyerror("auth already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for auth lookups", + t->t_name); + YYERROR; + } + + rule->flag_smtp_auth = $1 ? -1 : 1; + rule->flag_smtp_auth_regex = 1; + rule->table_smtp_auth = strdup(t->t_name); +} +| negation MAIL_FROM tables { + struct table *t = $3; + + if (rule->flag_smtp_mail_from) { + yyerror("mail-from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_MAILADDR)) { + yyerror("table \"%s\" may not be used for mail-from lookups", + t->t_name); + YYERROR; + } + + rule->flag_smtp_mail_from = $1 ? -1 : 1; + rule->table_smtp_mail_from = strdup(t->t_name); +} +| negation MAIL_FROM REGEX tables { + struct table *t = $4; + + if (rule->flag_smtp_mail_from) { + yyerror("mail-from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for mail-from lookups", + t->t_name); + YYERROR; + } + + rule->flag_smtp_mail_from = $1 ? -1 : 1; + rule->flag_smtp_mail_from_regex = 1; + rule->table_smtp_mail_from = strdup(t->t_name); +} +| negation RCPT_TO tables { + struct table *t = $3; + + if (rule->flag_smtp_rcpt_to) { + yyerror("rcpt-to already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_MAILADDR)) { + yyerror("table \"%s\" may not be used for rcpt-to lookups", + t->t_name); + YYERROR; + } + + rule->flag_smtp_rcpt_to = $1 ? -1 : 1; + rule->table_smtp_rcpt_to = strdup(t->t_name); +} +| negation RCPT_TO REGEX tables { + struct table *t = $4; + + if (rule->flag_smtp_rcpt_to) { + yyerror("rcpt-to already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for rcpt-to lookups", + t->t_name); + YYERROR; + } + + rule->flag_smtp_rcpt_to = $1 ? -1 : 1; + rule->flag_smtp_rcpt_to_regex = 1; + rule->table_smtp_rcpt_to = strdup(t->t_name); +} + +| negation FROM SOCKET { + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + rule->flag_from = $1 ? -1 : 1; + rule->flag_from_socket = 1; +} +| negation FROM LOCAL { + struct table *t = table_find(conf, "<localhost>"); + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + rule->flag_from = $1 ? -1 : 1; + rule->table_from = strdup(t->t_name); +} +| negation FROM ANY { + struct table *t = table_find(conf, "<anyhost>"); + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + rule->flag_from = $1 ? -1 : 1; + rule->table_from = strdup(t->t_name); +} +| negation FROM SRC tables { + struct table *t = $4; + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_NETADDR)) { + yyerror("table \"%s\" may not be used for from lookups", + t->t_name); + YYERROR; + } + + rule->flag_from = $1 ? -1 : 1; + rule->table_from = strdup(t->t_name); +} +| negation FROM SRC REGEX tables { + struct table *t = $5; + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for from lookups", + t->t_name); + YYERROR; + } + + rule->flag_from = $1 ? -1 : 1; + rule->flag_from_regex = 1; + rule->table_from = strdup(t->t_name); +} +| negation FROM RDNS { + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + rule->flag_from = $1 ? -1 : 1; + rule->flag_from_rdns = 1; +} +| negation FROM RDNS tables { + struct table *t = $4; + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_DOMAIN)) { + yyerror("table \"%s\" may not be used for rdns lookups", + t->t_name); + YYERROR; + } + + rule->flag_from = $1 ? -1 : 1; + rule->flag_from_rdns = 1; + rule->table_from = strdup(t->t_name); +} +| negation FROM RDNS REGEX tables { + struct table *t = $5; + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_DOMAIN)) { + yyerror("table \"%s\" may not be used for rdns lookups", + t->t_name); + YYERROR; + } + + rule->flag_from = $1 ? -1 : 1; + rule->flag_from_regex = 1; + rule->flag_from_rdns = 1; + rule->table_from = strdup(t->t_name); +} + +| negation FROM AUTH { + struct table *anyhost = table_find(conf, "<anyhost>"); + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + rule->flag_from = 1; + rule->table_from = strdup(anyhost->t_name); + rule->flag_smtp_auth = $1 ? -1 : 1; +} +| negation FROM AUTH tables { + struct table *anyhost = table_find(conf, "<anyhost>"); + struct table *t = $4; + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_STRING|K_CREDENTIALS)) { + yyerror("table \"%s\" may not be used for from lookups", + t->t_name); + YYERROR; + } + + rule->flag_from = 1; + rule->table_from = strdup(anyhost->t_name); + rule->flag_smtp_auth = $1 ? -1 : 1; + rule->table_smtp_auth = strdup(t->t_name); +} +| negation FROM AUTH REGEX tables { + struct table *anyhost = table_find(conf, "<anyhost>"); + struct table *t = $5; + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for from lookups", + t->t_name); + YYERROR; + } + + rule->flag_from = 1; + rule->table_from = strdup(anyhost->t_name); + rule->flag_smtp_auth = $1 ? -1 : 1; + rule->flag_smtp_auth_regex = 1; + rule->table_smtp_auth = strdup(t->t_name); +} + +| negation FROM MAIL_FROM tables { + struct table *anyhost = table_find(conf, "<anyhost>"); + struct table *t = $4; + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_MAILADDR)) { + yyerror("table \"%s\" may not be used for from lookups", + t->t_name); + YYERROR; + } + + rule->flag_from = 1; + rule->table_from = strdup(anyhost->t_name); + rule->flag_smtp_mail_from = $1 ? -1 : 1; + rule->table_smtp_mail_from = strdup(t->t_name); +} +| negation FROM MAIL_FROM REGEX tables { + struct table *anyhost = table_find(conf, "<anyhost>"); + struct table *t = $5; + + if (rule->flag_from) { + yyerror("from already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for from lookups", + t->t_name); + YYERROR; + } + + rule->flag_from = 1; + rule->table_from = strdup(anyhost->t_name); + rule->flag_smtp_mail_from = $1 ? -1 : 1; + rule->flag_smtp_mail_from_regex = 1; + rule->table_smtp_mail_from = strdup(t->t_name); +} + +| negation FOR LOCAL { + struct table *t = table_find(conf, "<localnames>"); + + if (rule->flag_for) { + yyerror("for already specified for this rule"); + YYERROR; + } + rule->flag_for = $1 ? -1 : 1; + rule->table_for = strdup(t->t_name); +} +| negation FOR ANY { + struct table *t = table_find(conf, "<anydestination>"); + + if (rule->flag_for) { + yyerror("for already specified for this rule"); + YYERROR; + } + rule->flag_for = $1 ? -1 : 1; + rule->table_for = strdup(t->t_name); +} +| negation FOR DOMAIN tables { + struct table *t = $4; + + if (rule->flag_for) { + yyerror("for already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_DOMAIN)) { + yyerror("table \"%s\" may not be used for 'for' lookups", + t->t_name); + YYERROR; + } + + rule->flag_for = $1 ? -1 : 1; + rule->table_for = strdup(t->t_name); +} +| negation FOR DOMAIN REGEX tables { + struct table *t = $5; + + if (rule->flag_for) { + yyerror("for already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for 'for' lookups", + t->t_name); + YYERROR; + } + + rule->flag_for = $1 ? -1 : 1; + rule->flag_for_regex = 1; + rule->table_for = strdup(t->t_name); +} +| negation FOR RCPT_TO tables { + struct table *anyhost = table_find(conf, "<anydestination>"); + struct table *t = $4; + + if (rule->flag_for) { + yyerror("for already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_MAILADDR)) { + yyerror("table \"%s\" may not be used for for lookups", + t->t_name); + YYERROR; + } + + rule->flag_for = 1; + rule->table_for = strdup(anyhost->t_name); + rule->flag_smtp_rcpt_to = $1 ? -1 : 1; + rule->table_smtp_rcpt_to = strdup(t->t_name); +} +| negation FOR RCPT_TO REGEX tables { + struct table *anyhost = table_find(conf, "<anydestination>"); + struct table *t = $5; + + if (rule->flag_for) { + yyerror("for already specified for this rule"); + YYERROR; + } + + if (!table_check_use(t, T_DYNAMIC|T_LIST, K_REGEX)) { + yyerror("table \"%s\" may not be used for for lookups", + t->t_name); + YYERROR; + } + + rule->flag_for = 1; + rule->table_for = strdup(anyhost->t_name); + rule->flag_smtp_rcpt_to = $1 ? -1 : 1; + rule->flag_smtp_rcpt_to_regex = 1; + rule->table_smtp_rcpt_to = strdup(t->t_name); +} +; + +match_options: +match_option match_options +| /* empty */ +; + +match_dispatcher: +STRING { + if (dict_get(conf->sc_dispatchers, $1) == NULL) { + yyerror("no such dispatcher: %s", $1); + YYERROR; + } + rule->dispatcher = $1; +} +; + +action: +REJECT { + rule->reject = 1; +} +| ACTION match_dispatcher +; + +match: +MATCH { + rule = xcalloc(1, sizeof *rule); +} match_options action { + if (!rule->flag_from) { + rule->table_from = strdup("<localhost>"); + rule->flag_from = 1; + } + if (!rule->flag_for) { + rule->table_for = strdup("<localnames>"); + rule->flag_for = 1; + } + TAILQ_INSERT_TAIL(conf->sc_rules, rule, r_entry); + rule = NULL; +} +; + +filter_action_builtin: +filter_action_builtin_nojunk +| JUNK { + filter_config->junk = 1; +} +| BYPASS { + filter_config->bypass = 1; +} +; + +filter_action_builtin_nojunk: +REJECT STRING { + filter_config->reject = $2; +} +| DISCONNECT STRING { + filter_config->disconnect = $2; +} +| REWRITE STRING { + filter_config->rewrite = $2; +} +| REPORT STRING { + filter_config->report = $2; +} +; + +filter_phase_check_fcrdns: +negation FCRDNS { + filter_config->not_fcrdns = $1 ? -1 : 1; + filter_config->fcrdns = 1; +} +; + +filter_phase_check_rdns: +negation RDNS { + filter_config->not_rdns = $1 ? -1 : 1; + filter_config->rdns = 1; +} +; + +filter_phase_check_rdns_table: +negation RDNS tables { + filter_config->not_rdns_table = $1 ? -1 : 1; + filter_config->rdns_table = $3; +} +; +filter_phase_check_rdns_regex: +negation RDNS REGEX tables { + filter_config->not_rdns_regex = $1 ? -1 : 1; + filter_config->rdns_regex = $4; +} +; + +filter_phase_check_src_table: +negation SRC tables { + filter_config->not_src_table = $1 ? -1 : 1; + filter_config->src_table = $3; +} +; +filter_phase_check_src_regex: +negation SRC REGEX tables { + filter_config->not_src_regex = $1 ? -1 : 1; + filter_config->src_regex = $4; +} +; + +filter_phase_check_helo_table: +negation HELO tables { + filter_config->not_helo_table = $1 ? -1 : 1; + filter_config->helo_table = $3; +} +; +filter_phase_check_helo_regex: +negation HELO REGEX tables { + filter_config->not_helo_regex = $1 ? -1 : 1; + filter_config->helo_regex = $4; +} +; + +filter_phase_check_auth: +negation AUTH { + filter_config->not_auth = $1 ? -1 : 1; + filter_config->auth = 1; +} +; +filter_phase_check_auth_table: +negation AUTH tables { + filter_config->not_auth_table = $1 ? -1 : 1; + filter_config->auth_table = $3; +} +; +filter_phase_check_auth_regex: +negation AUTH REGEX tables { + filter_config->not_auth_regex = $1 ? -1 : 1; + filter_config->auth_regex = $4; +} +; + +filter_phase_check_mail_from_table: +negation MAIL_FROM tables { + filter_config->not_mail_from_table = $1 ? -1 : 1; + filter_config->mail_from_table = $3; +} +; +filter_phase_check_mail_from_regex: +negation MAIL_FROM REGEX tables { + filter_config->not_mail_from_regex = $1 ? -1 : 1; + filter_config->mail_from_regex = $4; +} +; + +filter_phase_check_rcpt_to_table: +negation RCPT_TO tables { + filter_config->not_rcpt_to_table = $1 ? -1 : 1; + filter_config->rcpt_to_table = $3; +} +; +filter_phase_check_rcpt_to_regex: +negation RCPT_TO REGEX tables { + filter_config->not_rcpt_to_regex = $1 ? -1 : 1; + filter_config->rcpt_to_regex = $4; +} +; + +filter_phase_global_options: +filter_phase_check_fcrdns | +filter_phase_check_rdns | +filter_phase_check_rdns_regex | +filter_phase_check_rdns_table | +filter_phase_check_src_regex | +filter_phase_check_src_table; + +filter_phase_connect_options: +filter_phase_global_options; + +filter_phase_helo_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_global_options; + +filter_phase_auth_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_check_auth | +filter_phase_check_auth_table | +filter_phase_check_auth_regex | +filter_phase_global_options; + +filter_phase_mail_from_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_check_auth | +filter_phase_check_auth_table | +filter_phase_check_auth_regex | +filter_phase_check_mail_from_table | +filter_phase_check_mail_from_regex | +filter_phase_global_options; + +filter_phase_rcpt_to_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_check_auth | +filter_phase_check_auth_table | +filter_phase_check_auth_regex | +filter_phase_check_mail_from_table | +filter_phase_check_mail_from_regex | +filter_phase_check_rcpt_to_table | +filter_phase_check_rcpt_to_regex | +filter_phase_global_options; + +filter_phase_data_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_check_auth | +filter_phase_check_auth_table | +filter_phase_check_auth_regex | +filter_phase_check_mail_from_table | +filter_phase_check_mail_from_regex | +filter_phase_global_options; + +/* +filter_phase_quit_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_global_options; + +filter_phase_rset_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_global_options; + +filter_phase_noop_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_global_options; +*/ + +filter_phase_commit_options: +filter_phase_check_helo_table | +filter_phase_check_helo_regex | +filter_phase_check_auth | +filter_phase_check_auth_table | +filter_phase_check_auth_regex | +filter_phase_check_mail_from_table | +filter_phase_check_mail_from_regex | +filter_phase_global_options; + + +filter_phase_connect: +CONNECT { + filter_config->phase = FILTER_CONNECT; +} MATCH filter_phase_connect_options filter_action_builtin +; + + +filter_phase_helo: +HELO { + filter_config->phase = FILTER_HELO; +} MATCH filter_phase_helo_options filter_action_builtin +; + +filter_phase_ehlo: +EHLO { + filter_config->phase = FILTER_EHLO; +} MATCH filter_phase_helo_options filter_action_builtin +; + +filter_phase_auth: +AUTH { +} MATCH filter_phase_auth_options filter_action_builtin +; + +filter_phase_mail_from: +MAIL_FROM { + filter_config->phase = FILTER_MAIL_FROM; +} MATCH filter_phase_mail_from_options filter_action_builtin +; + +filter_phase_rcpt_to: +RCPT_TO { + filter_config->phase = FILTER_RCPT_TO; +} MATCH filter_phase_rcpt_to_options filter_action_builtin +; + +filter_phase_data: +DATA { + filter_config->phase = FILTER_DATA; +} MATCH filter_phase_data_options filter_action_builtin +; + +/* +filter_phase_data_line: +DATA_LINE { + filter_config->phase = FILTER_DATA_LINE; +} MATCH filter_action_builtin +; + +filter_phase_quit: +QUIT { + filter_config->phase = FILTER_QUIT; +} filter_phase_quit_options filter_action_builtin +; + +filter_phase_rset: +RSET { + filter_config->phase = FILTER_RSET; +} MATCH filter_phase_rset_options filter_action_builtin +; + +filter_phase_noop: +NOOP { + filter_config->phase = FILTER_NOOP; +} MATCH filter_phase_noop_options filter_action_builtin +; +*/ + +filter_phase_commit: +COMMIT { + filter_config->phase = FILTER_COMMIT; +} MATCH filter_phase_commit_options filter_action_builtin_nojunk +; + + + +filter_phase: +filter_phase_connect +| filter_phase_helo +| filter_phase_ehlo +| filter_phase_auth +| filter_phase_mail_from +| filter_phase_rcpt_to +| filter_phase_data +/*| filter_phase_data_line*/ +/*| filter_phase_quit*/ +/*| filter_phase_noop*/ +/*| filter_phase_rset*/ +| filter_phase_commit +; + + +filterel: +STRING { + struct filter_config *fr; + struct filter_proc *fp; + size_t i; + + if ((fr = dict_get(conf->sc_filters_dict, $1)) == NULL) { + yyerror("no filter exist with that name: %s", $1); + free($1); + YYERROR; + } + if (fr->filter_type == FILTER_TYPE_CHAIN) { + yyerror("no filter chain allowed within a filter chain: %s", $1); + free($1); + YYERROR; + } + + for (i = 0; i < filter_config->chain_size; i++) { + if (strcmp(filter_config->chain[i], $1) == 0) { + yyerror("no filter allowed twice within a filter chain: %s", $1); + free($1); + YYERROR; + } + } + + if (fr->proc) { + if ((fp = dict_get(&filter_config->chain_procs, fr->proc))) { + yyerror("no proc allowed twice within a filter chain: %s", fr->proc); + free($1); + YYERROR; + } + dict_set(&filter_config->chain_procs, fr->proc, NULL); + } + + fr->filter_subsystem |= filter_config->filter_subsystem; + filter_config->chain_size += 1; + filter_config->chain = reallocarray(filter_config->chain, filter_config->chain_size, sizeof(char *)); + if (filter_config->chain == NULL) + err(1, NULL); + filter_config->chain[filter_config->chain_size - 1] = $1; +} +; + +filter_list: +filterel +| filterel comma filter_list +; + +filter: +FILTER STRING PROC STRING { + struct filter_proc *fp; + + if (dict_get(conf->sc_filters_dict, $2)) { + yyerror("filter already exists with that name: %s", $2); + free($2); + free($4); + YYERROR; + } + if ((fp = dict_get(conf->sc_filter_processes_dict, $4)) == NULL) { + yyerror("no processor exist with that name: %s", $4); + free($4); + YYERROR; + } + + filter_config = xcalloc(1, sizeof *filter_config); + filter_config->filter_type = FILTER_TYPE_PROC; + filter_config->name = $2; + filter_config->proc = $4; + dict_set(conf->sc_filters_dict, $2, filter_config); + filter_config = NULL; +} +| +FILTER STRING PROC_EXEC STRING { + if (dict_get(conf->sc_filters_dict, $2)) { + yyerror("filter already exists with that name: %s", $2); + free($2); + free($4); + YYERROR; + } + + processor = xcalloc(1, sizeof *processor); + processor->command = $4; + + filter_config = xcalloc(1, sizeof *filter_config); + filter_config->filter_type = FILTER_TYPE_PROC; + filter_config->name = $2; + filter_config->proc = xstrdup($2); + dict_set(conf->sc_filters_dict, $2, filter_config); +} proc_params { + dict_set(conf->sc_filter_processes_dict, filter_config->proc, processor); + processor = NULL; + filter_config = NULL; +} +| +FILTER STRING PHASE { + if (dict_get(conf->sc_filters_dict, $2)) { + yyerror("filter already exists with that name: %s", $2); + free($2); + YYERROR; + } + filter_config = xcalloc(1, sizeof *filter_config); + filter_config->name = $2; + filter_config->filter_type = FILTER_TYPE_BUILTIN; + dict_set(conf->sc_filters_dict, $2, filter_config); +} filter_phase { + filter_config = NULL; +} +| +FILTER STRING CHAIN { + if (dict_get(conf->sc_filters_dict, $2)) { + yyerror("filter already exists with that name: %s", $2); + free($2); + YYERROR; + } + filter_config = xcalloc(1, sizeof *filter_config); + filter_config->filter_type = FILTER_TYPE_CHAIN; + dict_init(&filter_config->chain_procs); +} '{' filter_list '}' { + dict_set(conf->sc_filters_dict, $2, filter_config); + filter_config = NULL; +} +; + +size : NUMBER { + if ($1 < 0) { + yyerror("invalid size: %" PRId64, $1); + YYERROR; + } + $$ = $1; + } + | STRING { + long long result; + + if (scan_scaled($1, &result) == -1 || result < 0) { + yyerror("invalid size: %s", $1); + free($1); + YYERROR; + } + free($1); + $$ = result; + } + ; + +bouncedelay : STRING { + time_t d; + int i; + + d = delaytonum($1); + if (d < 0) { + yyerror("invalid bounce delay: %s", $1); + free($1); + YYERROR; + } + free($1); + for (i = 0; i < MAX_BOUNCE_WARN; i++) { + if (conf->sc_bounce_warn[i] != 0) + continue; + conf->sc_bounce_warn[i] = d; + break; + } + } + ; + +bouncedelays : bouncedelays ',' bouncedelay + | bouncedelay + ; + +opt_limit_mda : STRING NUMBER { + if (!strcmp($1, "max-session")) { + conf->sc_mda_max_session = $2; + } + else if (!strcmp($1, "max-session-per-user")) { + conf->sc_mda_max_user_session = $2; + } + else if (!strcmp($1, "task-lowat")) { + conf->sc_mda_task_lowat = $2; + } + else if (!strcmp($1, "task-hiwat")) { + conf->sc_mda_task_hiwat = $2; + } + else if (!strcmp($1, "task-release")) { + conf->sc_mda_task_release = $2; + } + else { + yyerror("invalid scheduler limit keyword: %s", $1); + free($1); + YYERROR; + } + free($1); + } + ; + +limits_smtp : opt_limit_smtp limits_smtp + | /* empty */ + ; + +opt_limit_smtp : STRING NUMBER { + if (!strcmp($1, "max-rcpt")) { + conf->sc_session_max_rcpt = $2; + } + else if (!strcmp($1, "max-mails")) { + conf->sc_session_max_mails = $2; + } + else { + yyerror("invalid session limit keyword: %s", $1); + free($1); + YYERROR; + } + free($1); + } + ; + +limits_mda : opt_limit_mda limits_mda + | /* empty */ + ; + +opt_limit_mta : INET4 { + limits->family = AF_INET; + } + | INET6 { + limits->family = AF_INET6; + } + | STRING NUMBER { + if (!limit_mta_set(limits, $1, $2)) { + yyerror("invalid mta limit keyword: %s", $1); + free($1); + YYERROR; + } + free($1); + } + ; + +limits_mta : opt_limit_mta limits_mta + | /* empty */ + ; + +opt_limit_scheduler : STRING NUMBER { + if (!strcmp($1, "max-inflight")) { + conf->sc_scheduler_max_inflight = $2; + } + else if (!strcmp($1, "max-evp-batch-size")) { + conf->sc_scheduler_max_evp_batch_size = $2; + } + else if (!strcmp($1, "max-msg-batch-size")) { + conf->sc_scheduler_max_msg_batch_size = $2; + } + else if (!strcmp($1, "max-schedule")) { + conf->sc_scheduler_max_schedule = $2; + } + else { + yyerror("invalid scheduler limit keyword: %s", $1); + free($1); + YYERROR; + } + free($1); + } + ; + +limits_scheduler: opt_limit_scheduler limits_scheduler + | /* empty */ + ; + + +opt_sock_listen : FILTER STRING { + struct filter_config *fc; + + if (listen_opts.options & LO_FILTER) { + yyerror("filter already specified"); + free($2); + YYERROR; + } + if ((fc = dict_get(conf->sc_filters_dict, $2)) == NULL) { + yyerror("no filter exist with that name: %s", $2); + free($2); + YYERROR; + } + fc->filter_subsystem |= FILTER_SUBSYSTEM_SMTP_IN; + listen_opts.options |= LO_FILTER; + listen_opts.filtername = $2; + } + | FILTER { + char buffer[128]; + + if (listen_opts.options & LO_FILTER) { + yyerror("filter already specified"); + YYERROR; + } + + do { + (void)snprintf(buffer, sizeof buffer, "<dynchain:%08x>", last_dynchain_id++); + } while (dict_check(conf->sc_filters_dict, buffer)); + + listen_opts.options |= LO_FILTER; + listen_opts.filtername = xstrdup(buffer); + filter_config = xcalloc(1, sizeof *filter_config); + filter_config->filter_type = FILTER_TYPE_CHAIN; + filter_config->filter_subsystem |= FILTER_SUBSYSTEM_SMTP_IN; + dict_init(&filter_config->chain_procs); + } '{' filter_list '}' { + dict_set(conf->sc_filters_dict, listen_opts.filtername, filter_config); + filter_config = NULL; + } + | MASK_SRC { + if (config_lo_mask_source(&listen_opts)) { + YYERROR; + } + } + | TAG STRING { + if (listen_opts.options & LO_TAG) { + yyerror("tag already specified"); + YYERROR; + } + listen_opts.options |= LO_TAG; + + if (strlen($2) >= SMTPD_TAG_SIZE) { + yyerror("tag name too long"); + free($2); + YYERROR; + } + listen_opts.tag = $2; + } + ; + +opt_if_listen : INET4 { + if (listen_opts.options & LO_FAMILY) { + yyerror("address family already specified"); + YYERROR; + } + listen_opts.options |= LO_FAMILY; + listen_opts.family = AF_INET; + } + | INET6 { + if (listen_opts.options & LO_FAMILY) { + yyerror("address family already specified"); + YYERROR; + } + listen_opts.options |= LO_FAMILY; + listen_opts.family = AF_INET6; + } + | PORT STRING { + struct servent *servent; + + if (listen_opts.options & LO_PORT) { + yyerror("port already specified"); + YYERROR; + } + listen_opts.options |= LO_PORT; + + servent = getservbyname($2, "tcp"); + if (servent == NULL) { + yyerror("invalid port: %s", $2); + free($2); + YYERROR; + } + free($2); + listen_opts.port = ntohs(servent->s_port); + } + | PORT SMTP { + struct servent *servent; + + if (listen_opts.options & LO_PORT) { + yyerror("port already specified"); + YYERROR; + } + listen_opts.options |= LO_PORT; + + servent = getservbyname("smtp", "tcp"); + if (servent == NULL) { + yyerror("invalid port: smtp"); + YYERROR; + } + listen_opts.port = ntohs(servent->s_port); + } + | PORT SMTPS { + struct servent *servent; + + if (listen_opts.options & LO_PORT) { + yyerror("port already specified"); + YYERROR; + } + listen_opts.options |= LO_PORT; + + servent = getservbyname("smtps", "tcp"); + if (servent == NULL) { + yyerror("invalid port: smtps"); + YYERROR; + } + listen_opts.port = ntohs(servent->s_port); + } + | PORT NUMBER { + if (listen_opts.options & LO_PORT) { + yyerror("port already specified"); + YYERROR; + } + listen_opts.options |= LO_PORT; + + if ($2 <= 0 || $2 > (int)USHRT_MAX) { + yyerror("invalid port: %" PRId64, $2); + YYERROR; + } + listen_opts.port = $2; + } + | FILTER STRING { + struct filter_config *fc; + + if (listen_opts.options & LO_FILTER) { + yyerror("filter already specified"); + YYERROR; + } + if ((fc = dict_get(conf->sc_filters_dict, $2)) == NULL) { + yyerror("no filter exist with that name: %s", $2); + free($2); + YYERROR; + } + fc->filter_subsystem |= FILTER_SUBSYSTEM_SMTP_IN; + listen_opts.options |= LO_FILTER; + listen_opts.filtername = $2; + } + | FILTER { + char buffer[128]; + + if (listen_opts.options & LO_FILTER) { + yyerror("filter already specified"); + YYERROR; + } + + do { + (void)snprintf(buffer, sizeof buffer, "<dynchain:%08x>", last_dynchain_id++); + } while (dict_check(conf->sc_filters_dict, buffer)); + + listen_opts.options |= LO_FILTER; + listen_opts.filtername = xstrdup(buffer); + filter_config = xcalloc(1, sizeof *filter_config); + filter_config->filter_type = FILTER_TYPE_CHAIN; + filter_config->filter_subsystem |= FILTER_SUBSYSTEM_SMTP_IN; + dict_init(&filter_config->chain_procs); + } '{' filter_list '}' { + dict_set(conf->sc_filters_dict, listen_opts.filtername, filter_config); + filter_config = NULL; + } + | SMTPS { + if (listen_opts.options & LO_SSL) { + yyerror("TLS mode already specified"); + YYERROR; + } + listen_opts.options |= LO_SSL; + listen_opts.ssl = F_SMTPS; + } + | SMTPS VERIFY { + if (listen_opts.options & LO_SSL) { + yyerror("TLS mode already specified"); + YYERROR; + } + listen_opts.options |= LO_SSL; + listen_opts.ssl = F_SMTPS|F_TLS_VERIFY; + } + | TLS { + if (listen_opts.options & LO_SSL) { + yyerror("TLS mode already specified"); + YYERROR; + } + listen_opts.options |= LO_SSL; + listen_opts.ssl = F_STARTTLS; + } + | TLS_REQUIRE { + if (listen_opts.options & LO_SSL) { + yyerror("TLS mode already specified"); + YYERROR; + } + listen_opts.options |= LO_SSL; + listen_opts.ssl = F_STARTTLS|F_STARTTLS_REQUIRE; + } + | TLS_REQUIRE VERIFY { + if (listen_opts.options & LO_SSL) { + yyerror("TLS mode already specified"); + YYERROR; + } + listen_opts.options |= LO_SSL; + listen_opts.ssl = F_STARTTLS|F_STARTTLS_REQUIRE|F_TLS_VERIFY; + } + | PKI STRING { + if (listen_opts.options & LO_PKI) { + yyerror("pki already specified"); + YYERROR; + } + listen_opts.options |= LO_PKI; + listen_opts.pki = $2; + } + | CA STRING { + if (listen_opts.options & LO_CA) { + yyerror("ca already specified"); + YYERROR; + } + listen_opts.options |= LO_CA; + listen_opts.ca = $2; + } + | AUTH { + if (listen_opts.options & LO_AUTH) { + yyerror("auth already specified"); + YYERROR; + } + listen_opts.options |= LO_AUTH; + listen_opts.auth = F_AUTH|F_AUTH_REQUIRE; + } + | AUTH_OPTIONAL { + if (listen_opts.options & LO_AUTH) { + yyerror("auth already specified"); + YYERROR; + } + listen_opts.options |= LO_AUTH; + listen_opts.auth = F_AUTH; + } + | AUTH tables { + if (listen_opts.options & LO_AUTH) { + yyerror("auth already specified"); + YYERROR; + } + listen_opts.options |= LO_AUTH; + listen_opts.authtable = $2; + listen_opts.auth = F_AUTH|F_AUTH_REQUIRE; + } + | AUTH_OPTIONAL tables { + if (listen_opts.options & LO_AUTH) { + yyerror("auth already specified"); + YYERROR; + } + listen_opts.options |= LO_AUTH; + listen_opts.authtable = $2; + listen_opts.auth = F_AUTH; + } + | TAG STRING { + if (listen_opts.options & LO_TAG) { + yyerror("tag already specified"); + YYERROR; + } + listen_opts.options |= LO_TAG; + + if (strlen($2) >= SMTPD_TAG_SIZE) { + yyerror("tag name too long"); + free($2); + YYERROR; + } + listen_opts.tag = $2; + } + | HOSTNAME STRING { + if (listen_opts.options & LO_HOSTNAME) { + yyerror("hostname already specified"); + YYERROR; + } + listen_opts.options |= LO_HOSTNAME; + + listen_opts.hostname = $2; + } + | HOSTNAMES tables { + struct table *t = $2; + + if (listen_opts.options & LO_HOSTNAMES) { + yyerror("hostnames already specified"); + YYERROR; + } + listen_opts.options |= LO_HOSTNAMES; + + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_ADDRNAME)) { + yyerror("invalid use of table \"%s\" as " + "HOSTNAMES parameter", t->t_name); + YYERROR; + } + listen_opts.hostnametable = t; + } + | MASK_SRC { + if (config_lo_mask_source(&listen_opts)) { + YYERROR; + } + } + | RECEIVEDAUTH { + if (listen_opts.options & LO_RECEIVEDAUTH) { + yyerror("received-auth already specified"); + YYERROR; + } + listen_opts.options |= LO_RECEIVEDAUTH; + listen_opts.flags |= F_RECEIVEDAUTH; + } + | NO_DSN { + if (listen_opts.options & LO_NODSN) { + yyerror("no-dsn already specified"); + YYERROR; + } + listen_opts.options |= LO_NODSN; + listen_opts.flags &= ~F_EXT_DSN; + } + | PROXY_V2 { + if (listen_opts.options & LO_PROXY) { + yyerror("proxy-v2 already specified"); + YYERROR; + } + listen_opts.options |= LO_PROXY; + listen_opts.flags |= F_PROXY; + } + | SENDERS tables { + struct table *t = $2; + + if (listen_opts.options & LO_SENDERS) { + yyerror("senders already specified"); + YYERROR; + } + listen_opts.options |= LO_SENDERS; + + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_MAILADDRMAP)) { + yyerror("invalid use of table \"%s\" as " + "SENDERS parameter", t->t_name); + YYERROR; + } + listen_opts.sendertable = t; + } + | SENDERS tables MASQUERADE { + struct table *t = $2; + + if (listen_opts.options & LO_SENDERS) { + yyerror("senders already specified"); + YYERROR; + } + listen_opts.options |= LO_SENDERS|LO_MASQUERADE; + + if (!table_check_use(t, T_DYNAMIC|T_HASH, K_MAILADDRMAP)) { + yyerror("invalid use of table \"%s\" as " + "SENDERS parameter", t->t_name); + YYERROR; + } + listen_opts.sendertable = t; + } + ; + +listener_type : socket_listener + | if_listener + ; + +socket_listener : SOCKET sock_listen { + if (conf->sc_sock_listener) { + yyerror("socket listener already configured"); + YYERROR; + } + create_sock_listener(&listen_opts); + } + ; + +if_listener : STRING if_listen { + listen_opts.ifx = $1; + create_if_listener(&listen_opts); + } + ; + +sock_listen : opt_sock_listen sock_listen + | /* empty */ + ; + +if_listen : opt_if_listen if_listen + | /* empty */ + ; + + +listen : LISTEN { + memset(&listen_opts, 0, sizeof listen_opts); + listen_opts.family = AF_UNSPEC; + listen_opts.flags |= F_EXT_DSN; + } ON listener_type + ; + +table : TABLE STRING STRING { + char *p, *backend, *config; + + p = $3; + if (*p == '/') { + backend = "static"; + config = $3; + } + else { + backend = $3; + config = NULL; + for (p = $3; *p && *p != ':'; p++) + ; + if (*p == ':') { + *p = '\0'; + backend = $3; + config = p+1; + } + } + if (config != NULL && *config != '/') { + yyerror("invalid backend parameter for table: %s", + $2); + free($2); + free($3); + YYERROR; + } + table = table_create(conf, backend, $2, config); + if (!table_config(table)) { + yyerror("invalid configuration file %s for table %s", + config, table->t_name); + free($2); + free($3); + YYERROR; + } + table = NULL; + free($2); + free($3); + } + | TABLE STRING { + table = table_create(conf, "static", $2, NULL); + free($2); + } '{' tableval_list '}' { + table = NULL; + } + ; + +tablenew : STRING { + struct table *t; + + t = table_create(conf, "static", NULL, NULL); + table_add(t, $1, NULL); + free($1); + $$ = t; + } + | '{' { + table = table_create(conf, "static", NULL, NULL); + } tableval_list '}' { + $$ = table; + table = NULL; + } + ; + +tableref : '<' STRING '>' { + struct table *t; + + if ((t = table_find(conf, $2)) == NULL) { + yyerror("no such table: %s", $2); + free($2); + YYERROR; + } + free($2); + $$ = t; + } + ; + +tables : tablenew { $$ = $1; } + | tableref { $$ = $1; } + ; + + +%% + +struct keywords { + const char *k_name; + int k_val; +}; + +int +yyerror(const char *fmt, ...) +{ + va_list ap; + char *msg; + + file->errors++; + va_start(ap, fmt); + if (vasprintf(&msg, fmt, ap) == -1) + fatalx("yyerror vasprintf"); + va_end(ap); + logit(LOG_CRIT, "%s:%d: %s", file->name, yylval.lineno, msg); + free(msg); + 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[] = { + { "action", ACTION }, + { "alias", ALIAS }, + { "any", ANY }, + { "auth", AUTH }, + { "auth-optional", AUTH_OPTIONAL }, + { "backup", BACKUP }, + { "bounce", BOUNCE }, + { "bypass", BYPASS }, + { "ca", CA }, + { "cert", CERT }, + { "chain", CHAIN }, + { "chroot", CHROOT }, + { "ciphers", CIPHERS }, + { "commit", COMMIT }, + { "compression", COMPRESSION }, + { "connect", CONNECT }, + { "data", DATA }, + { "data-line", DATA_LINE }, + { "dhe", DHE }, + { "disconnect", DISCONNECT }, + { "domain", DOMAIN }, + { "ehlo", EHLO }, + { "encryption", ENCRYPTION }, + { "expand-only", EXPAND_ONLY }, + { "fcrdns", FCRDNS }, + { "filter", FILTER }, + { "for", FOR }, + { "forward-only", FORWARD_ONLY }, + { "from", FROM }, + { "group", GROUP }, + { "helo", HELO }, + { "helo-src", HELO_SRC }, + { "host", HOST }, + { "hostname", HOSTNAME }, + { "hostnames", HOSTNAMES }, + { "include", INCLUDE }, + { "inet4", INET4 }, + { "inet6", INET6 }, + { "junk", JUNK }, + { "key", KEY }, + { "limit", LIMIT }, + { "listen", LISTEN }, + { "lmtp", LMTP }, + { "local", LOCAL }, + { "mail-from", MAIL_FROM }, + { "maildir", MAILDIR }, + { "mask-src", MASK_SRC }, + { "masquerade", MASQUERADE }, + { "match", MATCH }, + { "max-deferred", MAX_DEFERRED }, + { "max-message-size", MAX_MESSAGE_SIZE }, + { "mbox", MBOX }, + { "mda", MDA }, + { "mta", MTA }, + { "mx", MX }, + { "no-dsn", NO_DSN }, + { "no-verify", NO_VERIFY }, + { "noop", NOOP }, + { "on", ON }, + { "phase", PHASE }, + { "pki", PKI }, + { "port", PORT }, + { "proc", PROC }, + { "proc-exec", PROC_EXEC }, + { "proxy-v2", PROXY_V2 }, + { "queue", QUEUE }, + { "quit", QUIT }, + { "rcpt-to", RCPT_TO }, + { "rdns", RDNS }, + { "received-auth", RECEIVEDAUTH }, + { "recipient", RECIPIENT }, + { "regex", REGEX }, + { "reject", REJECT }, + { "relay", RELAY }, + { "report", REPORT }, + { "rewrite", REWRITE }, + { "rset", RSET }, + { "scheduler", SCHEDULER }, + { "senders", SENDERS }, + { "smtp", SMTP }, + { "smtp-in", SMTP_IN }, + { "smtp-out", SMTP_OUT }, + { "smtps", SMTPS }, + { "socket", SOCKET }, + { "src", SRC }, + { "srs", SRS }, + { "sub-addr-delim", SUB_ADDR_DELIM }, + { "table", TABLE }, + { "tag", TAG }, + { "tagged", TAGGED }, + { "tls", TLS }, + { "tls-require", TLS_REQUIRE }, + { "ttl", TTL }, + { "user", USER }, + { "userbase", USERBASE }, + { "verify", VERIFY }, + { "virtual", VIRTUAL }, + { "warn-interval", WARN_INTERVAL }, + { "wrapper", WRAPPER }, + }; + 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 START_EXPAND 1 +#define DONE_EXPAND 2 + +static int expanding; + +int +igetc(void) +{ + int c; + + while (1) { + if (file->ungetpos > 0) + c = file->ungetbuf[--file->ungetpos]; + else + c = getc(file->stream); + + if (c == START_EXPAND) + expanding = 1; + else if (c == DONE_EXPAND) + expanding = 0; + else + break; + } + return (c); +} + +int +lgetc(int quotec) +{ + int c, next; + + if (quotec) { + if ((c = igetc()) == EOF) { + yyerror("reached end of file while parsing " + "quoted string"); + if (file == topfile || popfile() == EOF) + return (EOF); + return (quotec); + } + return (c); + } + + while ((c = igetc()) == '\\') { + next = igetc(); + if (next != '\n') { + c = next; + break; + } + yylval.lineno = file->lineno; + file->lineno++; + } + + if (c == EOF) { + /* + * Fake EOL when hit EOF for the first time. This gets line + * count right if last line in included file is syntactically + * invalid and has no newline. + */ + if (file->eof_reached == 0) { + file->eof_reached = 1; + return ('\n'); + } + while (c == EOF) { + if (file == topfile || popfile() == EOF) + return (EOF); + c = igetc(); + } + } + return (c); +} + +void +lungetc(int c) +{ + if (c == EOF) + return; + + if (file->ungetpos >= file->ungetsize) { + void *p = reallocarray(file->ungetbuf, file->ungetsize, 2); + if (p == NULL) + err(1, "%s", __func__); + file->ungetbuf = p; + file->ungetsize *= 2; + } + file->ungetbuf[file->ungetpos++] = c; +} + +int +findeol(void) +{ + int c; + + /* 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) +{ + unsigned char buf[8096]; + unsigned 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 == '$' && !expanding) { + 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++ = c; + continue; + } + *p = '\0'; + lungetc(c); + break; + } + val = symget(buf); + if (val == NULL) { + yyerror("macro '%s' not defined", buf); + return (findeol()); + } + p = val + strlen(val) - 1; + lungetc(DONE_EXPAND); + while (p >= val) { + lungetc(*p); + p--; + } + lungetc(START_EXPAND); + 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 || next == ' ' || + next == '\t') + c = next; + else if (next == '\n') { + file->lineno++; + continue; + } else + lungetc(next); + } else if (c == quotec) { + *p = '\0'; + break; + } else if (c == '\0') { + yyerror("syntax error"); + return (findeol()); + } + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + *p++ = c; + } + yylval.v.string = strdup(buf); + if (yylval.v.string == NULL) + err(1, "%s", __func__); + return (STRING); + } + +#define allowed_to_end_number(x) \ + (isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=') + + if (c == '-' || isdigit(c)) { + do { + *p++ = c; + if ((size_t)(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); + } + } + + if (c == '=') { + if ((c = lgetc(0)) != EOF && c == '>') + return (ARROW); + lungetc(c); + 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 ((size_t)(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, "%s", __func__); + 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("warn: cannot stat %s", fname); + return (-1); + } + if (st.st_uid != 0 && st.st_uid != getuid()) { + log_warnx("warn: %s: owner not root or current user", fname); + return (-1); + } + if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) { + log_warnx("warn: %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) { + log_warn("%s", __func__); + return (NULL); + } + if ((nfile->name = strdup(name)) == NULL) { + log_warn("%s", __func__); + free(nfile); + return (NULL); + } + if ((nfile->stream = fopen(nfile->name, "r")) == NULL) { + log_warn("%s: %s", __func__, 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 = TAILQ_EMPTY(&files) ? 1 : 0; + nfile->ungetsize = 16; + nfile->ungetbuf = malloc(nfile->ungetsize); + if (nfile->ungetbuf == NULL) { + log_warn("%s", __func__); + fclose(nfile->stream); + free(nfile->name); + free(nfile); + return (NULL); + } + 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->ungetbuf); + 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; + errors = 0; + + if ((file = pushfile(filename, 0)) == NULL) { + purge_config(PURGE_EVERYTHING); + return (-1); + } + topfile = file; + + /* + * parse configuration + */ + setservent(1); + yyparse(); + errors = file->errors; + popfile(); + endservent(); + + /* If the socket listener was not configured, create a default one. */ + if (!conf->sc_sock_listener) { + memset(&listen_opts, 0, sizeof listen_opts); + create_sock_listener(&listen_opts); + } + + /* Free macros and check which have not been used. */ + TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) { + 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("warn: no rules, nothing to do"); + errors++; + } + + if (errors) { + purge_config(PURGE_EVERYTHING); + return (-1); + } + + return (0); +} + +int +symset(const char *nam, const char *val, int persist) +{ + struct sym *sym; + + TAILQ_FOREACH(sym, &symhead, entry) { + if (strcmp(nam, sym->nam) == 0) + break; + } + + 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; + + if ((val = strrchr(s, '=')) == NULL) + return (-1); + sym = strndup(s, val - s); + if (sym == NULL) + errx(1, "%s: strndup", __func__); + 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); +} + +static void +create_sock_listener(struct listen_opts *lo) +{ + struct listener *l = xcalloc(1, sizeof(*l)); + lo->hostname = conf->sc_hostname; + l->ss.ss_family = AF_LOCAL; +#ifdef HAVE_STRUCT_SOCKADDR_STORAGE_SS_LEN + l->ss.ss_len = sizeof(struct sockaddr *); +#endif + l->local = 1; + conf->sc_sock_listener = l; + config_listener(l, lo); +} + +static void +create_if_listener(struct listen_opts *lo) +{ + uint16_t flags; + + if (lo->port != 0 && lo->ssl == F_SSL) + errx(1, "invalid listen option: tls/smtps on same port"); + + if (lo->auth != 0 && !lo->ssl) + errx(1, "invalid listen option: auth requires tls/smtps"); + + if (lo->pki && !lo->ssl) + errx(1, "invalid listen option: pki requires tls/smtps"); + + flags = lo->flags; + + if (lo->port) { + lo->flags = lo->ssl|lo->auth|flags; + lo->port = htons(lo->port); + } + else { + if (lo->ssl & F_SMTPS) { + lo->port = htons(465); + lo->flags = F_SMTPS|lo->auth|flags; + } + + if (!lo->ssl || (lo->ssl & F_STARTTLS)) { + lo->port = htons(25); + lo->flags = lo->auth|flags; + if (lo->ssl & F_STARTTLS) + lo->flags |= F_STARTTLS; + } + } + + if (interface(lo)) + return; + if (host_v4(lo)) + return; + if (host_v6(lo)) + return; + if (host_dns(lo)) + return; + + errx(1, "invalid virtual ip or interface: %s", lo->ifx); +} + +static void +config_listener(struct listener *h, struct listen_opts *lo) +{ + h->fd = -1; + h->port = lo->port; + h->flags = lo->flags; + + if (lo->hostname == NULL) + lo->hostname = conf->sc_hostname; + + if (lo->options & LO_FILTER) { + h->flags |= F_FILTERED; + (void)strlcpy(h->filter_name, + lo->filtername, + sizeof(h->filter_name)); + } + + h->pki_name[0] = '\0'; + + if (lo->authtable != NULL) + (void)strlcpy(h->authtable, lo->authtable->t_name, sizeof(h->authtable)); + if (lo->pki != NULL) { + if (!lowercase(h->pki_name, lo->pki, sizeof(h->pki_name))) { + log_warnx("pki name too long: %s", lo->pki); + fatalx(NULL); + } + if (dict_get(conf->sc_pki_dict, h->pki_name) == NULL) { + log_warnx("pki name not found: %s", lo->pki); + fatalx(NULL); + } + } + + if (lo->ca != NULL) { + if (!lowercase(h->ca_name, lo->ca, sizeof(h->ca_name))) { + log_warnx("ca name too long: %s", lo->ca); + fatalx(NULL); + } + if (dict_get(conf->sc_ca_dict, h->ca_name) == NULL) { + log_warnx("ca name not found: %s", lo->ca); + fatalx(NULL); + } + } + if (lo->tag != NULL) + (void)strlcpy(h->tag, lo->tag, sizeof(h->tag)); + + (void)strlcpy(h->hostname, lo->hostname, sizeof(h->hostname)); + if (lo->hostnametable) + (void)strlcpy(h->hostnametable, lo->hostnametable->t_name, sizeof(h->hostnametable)); + if (lo->sendertable) { + (void)strlcpy(h->sendertable, lo->sendertable->t_name, sizeof(h->sendertable)); + if (lo->options & LO_MASQUERADE) + h->flags |= F_MASQUERADE; + } + + if (lo->ssl & F_TLS_VERIFY) + h->flags |= F_TLS_VERIFY; + + if (lo->ssl & F_STARTTLS_REQUIRE) + h->flags |= F_STARTTLS_REQUIRE; + + if (h != conf->sc_sock_listener) + TAILQ_INSERT_TAIL(conf->sc_listeners, h, entry); +} + +static int +host_v4(struct listen_opts *lo) +{ + struct in_addr ina; + struct sockaddr_in *sain; + struct listener *h; + + if (lo->family != AF_UNSPEC && lo->family != AF_INET) + return (0); + + memset(&ina, 0, sizeof(ina)); + if (inet_pton(AF_INET, lo->ifx, &ina) != 1) + return (0); + + h = xcalloc(1, sizeof(*h)); + sain = (struct sockaddr_in *)&h->ss; +#ifdef HAVE_STRUCT_SOCKADDR_IN_SIN_LEN + sain->sin_len = sizeof(struct sockaddr_in); +#endif + sain->sin_family = AF_INET; + sain->sin_addr.s_addr = ina.s_addr; + sain->sin_port = lo->port; + + if (sain->sin_addr.s_addr == htonl(INADDR_LOOPBACK)) + h->local = 1; + config_listener(h, lo); + + return (1); +} + +static int +host_v6(struct listen_opts *lo) +{ + struct in6_addr ina6; + struct sockaddr_in6 *sin6; + struct listener *h; + + if (lo->family != AF_UNSPEC && lo->family != AF_INET6) + return (0); + + memset(&ina6, 0, sizeof(ina6)); + if (inet_pton(AF_INET6, lo->ifx, &ina6) != 1) + return (0); + + h = xcalloc(1, sizeof(*h)); + sin6 = (struct sockaddr_in6 *)&h->ss; +#ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_LEN + sin6->sin6_len = sizeof(struct sockaddr_in6); +#endif + sin6->sin6_family = AF_INET6; + sin6->sin6_port = lo->port; + memcpy(&sin6->sin6_addr, &ina6, sizeof(ina6)); + + if (IN6_IS_ADDR_LOOPBACK(&sin6->sin6_addr)) + h->local = 1; + config_listener(h, lo); + + return (1); +} + +static int +host_dns(struct listen_opts *lo) +{ + struct addrinfo hints, *res0, *res; + int error, cnt = 0; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct listener *h; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = lo->family; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_ADDRCONFIG; + error = getaddrinfo(lo->ifx, NULL, &hints, &res0); + if (error == EAI_AGAIN || error == EAI_NODATA || error == EAI_NONAME) + return (0); + if (error) { + log_warnx("warn: host_dns: could not parse \"%s\": %s", lo->ifx, + gai_strerror(error)); + return (-1); + } + + for (res = res0; res; res = res->ai_next) { + if (res->ai_family != AF_INET && + res->ai_family != AF_INET6) + continue; + h = xcalloc(1, sizeof(*h)); + + h->ss.ss_family = res->ai_family; + if (res->ai_family == AF_INET) { + sain = (struct sockaddr_in *)&h->ss; +#ifdef HAVE_STRUCT_SOCKADDR_IN_SIN_LEN + sain->sin_len = sizeof(struct sockaddr_in); +#endif + sain->sin_addr.s_addr = ((struct sockaddr_in *) + res->ai_addr)->sin_addr.s_addr; + sain->sin_port = lo->port; + if (sain->sin_addr.s_addr == htonl(INADDR_LOOPBACK)) + h->local = 1; + } else { + sin6 = (struct sockaddr_in6 *)&h->ss; +#ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_LEN + sin6->sin6_len = sizeof(struct sockaddr_in6); +#endif + memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *) + res->ai_addr)->sin6_addr, sizeof(struct in6_addr)); + sin6->sin6_port = lo->port; + if (IN6_IS_ADDR_LOOPBACK(&sin6->sin6_addr)) + h->local = 1; + } + + config_listener(h, lo); + + cnt++; + } + + freeaddrinfo(res0); + return (cnt); +} + +static int +interface(struct listen_opts *lo) +{ + struct ifaddrs *ifap, *p; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct listener *h; + int ret = 0; + + if (getifaddrs(&ifap) == -1) + fatal("getifaddrs"); + + for (p = ifap; p != NULL; p = p->ifa_next) { + if (p->ifa_addr == NULL) + continue; + if (strcmp(p->ifa_name, lo->ifx) != 0 && + !is_if_in_group(p->ifa_name, lo->ifx)) + continue; + if (lo->family != AF_UNSPEC && lo->family != p->ifa_addr->sa_family) + continue; + + h = xcalloc(1, sizeof(*h)); + + switch (p->ifa_addr->sa_family) { + case AF_INET: + sain = (struct sockaddr_in *)&h->ss; + *sain = *(struct sockaddr_in *)p->ifa_addr; +#ifdef HAVE_STRUCT_SOCKADDR_IN_SIN_LEN + sain->sin_len = sizeof(struct sockaddr_in); +#endif + sain->sin_port = lo->port; + if (sain->sin_addr.s_addr == htonl(INADDR_LOOPBACK)) + h->local = 1; + break; + + case AF_INET6: + sin6 = (struct sockaddr_in6 *)&h->ss; + *sin6 = *(struct sockaddr_in6 *)p->ifa_addr; +#ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_LEN + sin6->sin6_len = sizeof(struct sockaddr_in6); +#endif + sin6->sin6_port = lo->port; + if (IN6_IS_ADDR_LOOPBACK(&sin6->sin6_addr)) + h->local = 1; + break; + + default: + free(h); + continue; + } + + config_listener(h, lo); + ret = 1; + } + + freeifaddrs(ifap); + + return ret; +} + +int +delaytonum(char *str) +{ + unsigned int factor; + size_t len; + const char *errstr = NULL; + int delay; + + /* we need at least 1 digit and 1 unit */ + len = strlen(str); + if (len < 2) + goto bad; + + switch(str[len - 1]) { + + case 's': + factor = 1; + break; + + case 'm': + factor = 60; + break; + + case 'h': + factor = 60 * 60; + break; + + case 'd': + factor = 24 * 60 * 60; + break; + + default: + goto bad; + } + + str[len - 1] = '\0'; + delay = strtonum(str, 1, INT_MAX / factor, &errstr); + if (errstr) + goto bad; + + return (delay * factor); + +bad: + return (-1); +} + +int +is_if_in_group(const char *ifname, const char *groupname) +{ +#ifdef HAVE_STRUCT_IFGROUPREQ + unsigned int len; + struct ifgroupreq ifgr; + struct ifg_req *ifg; + int s; + int ret = 0; + + if ((s = socket(AF_INET, SOCK_DGRAM, 0)) == -1) + err(1, "socket"); + + memset(&ifgr, 0, sizeof(ifgr)); + if (strlcpy(ifgr.ifgr_name, ifname, IFNAMSIZ) >= IFNAMSIZ) + errx(1, "interface name too large"); + + if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1) { + if (errno == EINVAL || errno == ENOTTY) + goto end; + err(1, "SIOCGIFGROUP"); + } + + len = ifgr.ifgr_len; + ifgr.ifgr_groups = xcalloc(len/sizeof(struct ifg_req), + sizeof(struct ifg_req)); + if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1) + err(1, "SIOCGIFGROUP"); + + ifg = ifgr.ifgr_groups; + for (; ifg && len >= sizeof(struct ifg_req); ifg++) { + len -= sizeof(struct ifg_req); + if (strcmp(ifg->ifgrq_group, groupname) == 0) { + ret = 1; + break; + } + } + free(ifgr.ifgr_groups); + +end: + close(s); + return ret; +#else + return (0); +#endif +} + +static int +config_lo_mask_source(struct listen_opts *lo) { + if (lo->options & LO_MASKSOURCE) { + yyerror("mask-source already specified"); + return -1; + } + lo->options |= LO_MASKSOURCE; + lo->flags |= F_MASK_SOURCE; + + return 0; +} + diff --git a/smtpd/parser.c b/smtpd/parser.c new file mode 100644 index 00000000..4c2321ec --- /dev/null +++ b/smtpd/parser.c @@ -0,0 +1,341 @@ +/* $OpenBSD: parser.c,v 1.42 2020/01/06 11:02:38 gilles Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/socket.h> + +#include <netinet/in.h> +#include <net/if.h> +#include <arpa/inet.h> + +#include <err.h> +#include <inttypes.h> +#include <limits.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "parser.h" + +uint64_t text_to_evpid(const char *); +uint32_t text_to_msgid(const char *); + +struct node { + int type; + const char *token; + struct node *parent; + TAILQ_ENTRY(node) entry; + TAILQ_HEAD(, node) children; + int (*cmd)(int, struct parameter*); +}; + +static struct node *root; + +static int text_to_sockaddr(struct sockaddr *, int, const char *); + +#define ARGVMAX 64 + +int +cmd_install(const char *pattern, int (*cmd)(int, struct parameter*)) +{ + struct node *node, *tmp; + char *s, *str, *argv[ARGVMAX], **ap; + int i, n; + + /* Tokenize */ + str = s = strdup(pattern); + if (str == NULL) + err(1, "strdup"); + n = 0; + for (ap = argv; n < ARGVMAX && (*ap = strsep(&str, " \t")) != NULL;) { + if (**ap != '\0') { + ap++; + n++; + } + } + *ap = NULL; + + if (root == NULL) { + root = calloc(1, sizeof (*root)); + TAILQ_INIT(&root->children); + } + node = root; + + for (i = 0; i < n; i++) { + TAILQ_FOREACH(tmp, &node->children, entry) { + if (!strcmp(tmp->token, argv[i])) { + node = tmp; + break; + } + } + if (tmp == NULL) { + tmp = calloc(1, sizeof (*tmp)); + TAILQ_INIT(&tmp->children); + if (!strcmp(argv[i], "<str>")) + tmp->type = P_STR; + else if (!strcmp(argv[i], "<int>")) + tmp->type = P_INT; + else if (!strcmp(argv[i], "<msgid>")) + tmp->type = P_MSGID; + else if (!strcmp(argv[i], "<evpid>")) + tmp->type = P_EVPID; + else if (!strcmp(argv[i], "<routeid>")) + tmp->type = P_ROUTEID; + else if (!strcmp(argv[i], "<addr>")) + tmp->type = P_ADDR; + else + tmp->type = P_TOKEN; + tmp->token = strdup(argv[i]); + tmp->parent = node; + TAILQ_INSERT_TAIL(&node->children, tmp, entry); + node = tmp; + } + } + + if (node->cmd) + errx(1, "duplicate pattern: %s", pattern); + node->cmd = cmd; + + free(s); + return (n); +} + +static int +cmd_check(const char *str, struct node *node, struct parameter *res) +{ + const char *e; + + switch (node->type) { + case P_TOKEN: + if (!strcmp(str, node->token)) + return (1); + return (0); + + case P_STR: + res->u.u_str = str; + return (1); + + case P_INT: + res->u.u_int = strtonum(str, INT_MIN, INT_MAX, &e); + if (e) + return (0); + return (1); + + case P_MSGID: + if (strlen(str) != 8) + return (0); + res->u.u_msgid = text_to_msgid(str); + if (res->u.u_msgid == 0) + return (0); + return (1); + + case P_EVPID: + if (strlen(str) != 16) + return (0); + res->u.u_evpid = text_to_evpid(str); + if (res->u.u_evpid == 0) + return (0); + return (1); + + case P_ROUTEID: + res->u.u_routeid = strtonum(str, 1, LLONG_MAX, &e); + if (e) + return (0); + return (1); + + case P_ADDR: + if (text_to_sockaddr((struct sockaddr *)&res->u.u_ss, PF_UNSPEC, str) == 0) + return (1); + return (0); + + default: + errx(1, "bad token type: %d", node->type); + return (0); + } +} + +int +cmd_run(int argc, char **argv) +{ + struct parameter param[ARGVMAX]; + struct node *node, *tmp, *stack[ARGVMAX], *best; + int i, j, np; + + node = root; + np = 0; + + for (i = 0; i < argc; i++) { + TAILQ_FOREACH(tmp, &node->children, entry) { + if (cmd_check(argv[i], tmp, ¶m[np])) { + stack[i] = tmp; + node = tmp; + param[np].type = node->type; + if (node->type != P_TOKEN) + np++; + break; + } + } + if (tmp == NULL) { + best = NULL; + TAILQ_FOREACH(tmp, &node->children, entry) { + if (tmp->type != P_TOKEN) + continue; + if (strstr(tmp->token, argv[i]) != tmp->token) + continue; + if (best) + goto fail; + best = tmp; + } + if (best == NULL) + goto fail; + stack[i] = best; + node = best; + param[np].type = node->type; + if (node->type != P_TOKEN) + np++; + } + } + + if (node->cmd == NULL) + goto fail; + + return (node->cmd(np, np ? param : NULL)); + +fail: + if (TAILQ_FIRST(&node->children) == NULL) { + fprintf(stderr, "invalid command\n"); + return (-1); + } + + fprintf(stderr, "possibilities are:\n"); + TAILQ_FOREACH(tmp, &node->children, entry) { + for (j = 0; j < i; j++) + fprintf(stderr, "%s%s", j?" ":"", stack[j]->token); + fprintf(stderr, "%s%s\n", i?" ":"", tmp->token); + } + + return (-1); +} + +int +cmd_show_params(int argc, struct parameter *argv) +{ + int i; + + for (i = 0; i < argc; i++) { + switch(argv[i].type) { + case P_STR: + printf(" str:\"%s\"", argv[i].u.u_str); + break; + case P_INT: + printf(" int:%d", argv[i].u.u_int); + break; + case P_MSGID: + printf(" msgid:%08"PRIx32, argv[i].u.u_msgid); + break; + case P_EVPID: + printf(" evpid:%016"PRIx64, argv[i].u.u_evpid); + break; + case P_ROUTEID: + printf(" routeid:%016"PRIx64, argv[i].u.u_routeid); + break; + default: + printf(" ???:%d", argv[i].type); + } + } + printf ("\n"); + return (1); +} + +static int +text_to_sockaddr(struct sockaddr *sa, int family, const char *str) +{ + struct in_addr ina; + struct in6_addr in6a; + struct sockaddr_in *in; + struct sockaddr_in6 *in6; + char *cp, *str2; + const char *errstr; + + switch (family) { + case PF_UNSPEC: + if (text_to_sockaddr(sa, PF_INET, str) == 0) + return (0); + return text_to_sockaddr(sa, PF_INET6, str); + + case PF_INET: + if (inet_pton(PF_INET, str, &ina) != 1) + return (-1); + + in = (struct sockaddr_in *)sa; + memset(in, 0, sizeof *in); +#ifdef HAVE_STRUCT_SOCKADDR_IN_SIN_LEN + in->sin_len = sizeof(struct sockaddr_in); +#endif + in->sin_family = PF_INET; + in->sin_addr.s_addr = ina.s_addr; + return (0); + + case PF_INET6: + cp = strchr(str, SCOPE_DELIMITER); + if (cp) { + str2 = strdup(str); + if (str2 == NULL) + return (-1); + str2[cp - str] = '\0'; + if (inet_pton(PF_INET6, str2, &in6a) != 1) { + free(str2); + return (-1); + } + cp++; + free(str2); + } else if (inet_pton(PF_INET6, str, &in6a) != 1) + return (-1); + + in6 = (struct sockaddr_in6 *)sa; + memset(in6, 0, sizeof *in6); +#ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_LEN + in6->sin6_len = sizeof(struct sockaddr_in6); +#endif + in6->sin6_family = PF_INET6; + in6->sin6_addr = in6a; + + if (cp == NULL) + return (0); + + if (IN6_IS_ADDR_LINKLOCAL(&in6a) || + IN6_IS_ADDR_MC_LINKLOCAL(&in6a) || + IN6_IS_ADDR_MC_NODELOCAL(&in6a)) + if ((in6->sin6_scope_id = if_nametoindex(cp))) + return (0); + + in6->sin6_scope_id = strtonum(cp, 0, UINT32_MAX, &errstr); + if (errstr) + return (-1); + return (0); + + default: + break; + } + + return (-1); +} diff --git a/smtpd/parser.h b/smtpd/parser.h new file mode 100644 index 00000000..f0114e9e --- /dev/null +++ b/smtpd/parser.h @@ -0,0 +1,43 @@ +/* $OpenBSD: parser.h,v 1.29 2014/02/04 15:22:39 eric Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@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. + */ + +enum { + P_TOKEN, + P_STR, + P_INT, + P_MSGID, + P_EVPID, + P_ROUTEID, + P_ADDR, +}; + +struct parameter { + int type; + union { + const char *u_str; + int u_int; + uint32_t u_msgid; + uint64_t u_evpid; + uint64_t u_routeid; + struct sockaddr_storage u_ss; + } u; +}; + +int cmd_install(const char *, int (*)(int, struct parameter *)); +int cmd_run(int, char **); +int cmd_show_params(int argc, struct parameter *argv); diff --git a/smtpd/pony.c b/smtpd/pony.c new file mode 100644 index 00000000..1865b339 --- /dev/null +++ b/smtpd/pony.c @@ -0,0 +1,212 @@ +/* $OpenBSD: pony.c,v 1.27 2019/06/13 11:45:35 eric Exp $ */ + +/* + * Copyright (c) 2014 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> +#include <grp.h> + +#include "smtpd.h" +#include "log.h" + +void mda_imsg(struct mproc *, struct imsg *); +void mta_imsg(struct mproc *, struct imsg *); +void smtp_imsg(struct mproc *, struct imsg *); + +static void pony_shutdown(void); + +void +pony_imsg(struct mproc *p, struct imsg *imsg) +{ + struct msg m; + int v; + + if (imsg == NULL) + pony_shutdown(); + + switch (imsg->hdr.type) { + + case IMSG_GETADDRINFO: + case IMSG_GETADDRINFO_END: + case IMSG_GETNAMEINFO: + case IMSG_RES_QUERY: + resolver_dispatch_result(p, imsg); + return; + + case IMSG_CERT_INIT: + case IMSG_CERT_VERIFY: + cert_dispatch_result(p, imsg); + return; + + case IMSG_CONF_START: + return; + case IMSG_CONF_END: + smtp_configure(); + return; + case IMSG_CTL_VERBOSE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + log_trace_verbose(v); + return; + case IMSG_CTL_PROFILE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + profiling = v; + return; + + /* smtp imsg */ + case IMSG_SMTP_CHECK_SENDER: + case IMSG_SMTP_EXPAND_RCPT: + case IMSG_SMTP_LOOKUP_HELO: + case IMSG_SMTP_AUTHENTICATE: + case IMSG_SMTP_MESSAGE_COMMIT: + case IMSG_SMTP_MESSAGE_CREATE: + case IMSG_SMTP_MESSAGE_OPEN: + case IMSG_FILTER_SMTP_PROTOCOL: + case IMSG_FILTER_SMTP_DATA_BEGIN: + case IMSG_QUEUE_ENVELOPE_SUBMIT: + case IMSG_QUEUE_ENVELOPE_COMMIT: + case IMSG_QUEUE_SMTP_SESSION: + case IMSG_CTL_SMTP_SESSION: + case IMSG_CTL_PAUSE_SMTP: + case IMSG_CTL_RESUME_SMTP: + smtp_imsg(p, imsg); + return; + + /* mta imsg */ + case IMSG_QUEUE_TRANSFER: + case IMSG_MTA_OPEN_MESSAGE: + case IMSG_MTA_LOOKUP_CREDENTIALS: + case IMSG_MTA_LOOKUP_SMARTHOST: + case IMSG_MTA_LOOKUP_SOURCE: + case IMSG_MTA_LOOKUP_HELO: + case IMSG_MTA_DNS_HOST: + case IMSG_MTA_DNS_HOST_END: + case IMSG_MTA_DNS_MX_PREFERENCE: + case IMSG_CTL_RESUME_ROUTE: + case IMSG_CTL_MTA_SHOW_HOSTS: + case IMSG_CTL_MTA_SHOW_RELAYS: + case IMSG_CTL_MTA_SHOW_ROUTES: + case IMSG_CTL_MTA_SHOW_HOSTSTATS: + case IMSG_CTL_MTA_BLOCK: + case IMSG_CTL_MTA_UNBLOCK: + case IMSG_CTL_MTA_SHOW_BLOCK: + mta_imsg(p, imsg); + return; + + /* mda imsg */ + case IMSG_MDA_LOOKUP_USERINFO: + case IMSG_QUEUE_DELIVER: + case IMSG_MDA_OPEN_MESSAGE: + case IMSG_MDA_FORK: + case IMSG_MDA_DONE: + mda_imsg(p, imsg); + return; + default: + break; + } + + errx(1, "session_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type)); +} + +static void +pony_shutdown(void) +{ + log_debug("debug: pony agent exiting"); + _exit(0); +} + +int +pony(void) +{ + struct passwd *pw; + + mda_postfork(); + mta_postfork(); + smtp_postfork(); + + /* do not purge listeners and pki, they are purged + * in smtp_configure() + */ + purge_config(PURGE_TABLES|PURGE_RULES); + + if ((pw = getpwnam(SMTPD_USER)) == NULL) + fatalx("unknown user " SMTPD_USER); + + if (chroot(PATH_CHROOT) == -1) + fatal("pony: chroot"); + if (chdir("/") == -1) + fatal("pony: chdir(\"/\")"); + + config_process(PROC_PONY); + + 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("pony: cannot drop privileges"); + + imsg_callback = pony_imsg; + event_init(); + + mda_postprivdrop(); + mta_postprivdrop(); + smtp_postprivdrop(); + + signal(SIGINT, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peer(PROC_PARENT); + config_peer(PROC_QUEUE); + config_peer(PROC_LKA); + config_peer(PROC_CONTROL); + config_peer(PROC_CA); + + ca_engine_init(); + +#if HAVE_PLEDGE + if (pledge("stdio inet unix recvfd sendfd", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + fatalx("exited event loop"); + + return (0); +} diff --git a/smtpd/proxy.c b/smtpd/proxy.c new file mode 100644 index 00000000..fdaf4f27 --- /dev/null +++ b/smtpd/proxy.c @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2017 Antoine Kaufmann <toni@famkaufmann.info> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/tree.h> +#include <sys/un.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "log.h" +#include "smtpd.h" + + +/* + * The PROXYv2 protocol is described here: + * http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + */ + +#define PROXY_CLOCAL 0x0 +#define PROXY_CPROXY 0x1 +#define PROXY_AF_UNSPEC 0x0 +#define PROXY_AF_INET 0x1 +#define PROXY_AF_INET6 0x2 +#define PROXY_AF_UNIX 0x3 +#define PROXY_TF_UNSPEC 0x0 +#define PROXY_TF_STREAM 0x1 +#define PROXY_TF_DGRAM 0x2 + +#define PROXY_SESSION_TIMEOUT 300 + +static const uint8_t pv2_signature[] = { + 0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, + 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A +}; + +struct proxy_hdr_v2 { + uint8_t sig[12]; + uint8_t ver_cmd; + uint8_t fam; + uint16_t len; +} __attribute__((packed)); + + +struct proxy_addr_ipv4 { + uint32_t src_addr; + uint32_t dst_addr; + uint16_t src_port; + uint16_t dst_port; +} __attribute__((packed)); + +struct proxy_addr_ipv6 { + uint8_t src_addr[16]; + uint8_t dst_addr[16]; + uint16_t src_port; + uint16_t dst_port; +} __attribute__((packed)); + +struct proxy_addr_unix { + uint8_t src_addr[108]; + uint8_t dst_addr[108]; +} __attribute__((packed)); + +union proxy_addr { + struct proxy_addr_ipv4 ipv4; + struct proxy_addr_ipv6 ipv6; + struct proxy_addr_unix un; +} __attribute__((packed)); + +struct proxy_session { + struct listener *l; + struct io *io; + + uint64_t id; + int fd; + uint16_t header_len; + uint16_t header_total; + uint16_t addr_len; + uint16_t addr_total; + + struct sockaddr_storage ss; + struct proxy_hdr_v2 hdr; + union proxy_addr addr; + + void (*cb_accepted)(struct listener *, int, + const struct sockaddr_storage *, struct io *); + void (*cb_dropped)(struct listener *, int, + const struct sockaddr_storage *); +}; + +static void proxy_io(struct io *, int, void *); +static void proxy_error(struct proxy_session *, const char *, const char *); +static int proxy_header_validate(struct proxy_session *); +static int proxy_translate_ss(struct proxy_session *); + +int +proxy_session(struct listener *listener, int sock, + const struct sockaddr_storage *ss, + void (*accepted)(struct listener *, int, + const struct sockaddr_storage *, struct io *), + void (*dropped)(struct listener *, int, + const struct sockaddr_storage *)); + +int +proxy_session(struct listener *listener, int sock, + const struct sockaddr_storage *ss, + void (*accepted)(struct listener *, int, + const struct sockaddr_storage *, struct io *), + void (*dropped)(struct listener *, int, + const struct sockaddr_storage *)) +{ + struct proxy_session *s; + + if ((s = calloc(1, sizeof(*s))) == NULL) + return (-1); + + if ((s->io = io_new()) == NULL) { + free(s); + return (-1); + } + + s->id = generate_uid(); + s->l = listener; + s->fd = sock; + s->header_len = 0; + s->addr_len = 0; + s->ss = *ss; + s->cb_accepted = accepted; + s->cb_dropped = dropped; + + io_set_callback(s->io, proxy_io, s); + io_set_fd(s->io, sock); + io_set_timeout(s->io, PROXY_SESSION_TIMEOUT * 1000); + io_set_read(s->io); + + log_info("%016"PRIx64" smtp event=proxy address=%s", + s->id, ss_to_text(&s->ss)); + + return 0; +} + +static void +proxy_io(struct io *io, int evt, void *arg) +{ + struct proxy_session *s = arg; + struct proxy_hdr_v2 *h = &s->hdr; + uint8_t *buf; + size_t len, off; + + switch (evt) { + + case IO_DATAIN: + buf = io_data(io); + len = io_datalen(io); + + if (s->header_len < sizeof(s->hdr)) { + /* header is incomplete */ + off = sizeof(s->hdr) - s->header_len; + off = (len < off ? len : off); + memcpy((uint8_t *) &s->hdr + s->header_len, buf, off); + + s->header_len += off; + buf += off; + len -= off; + io_drop(s->io, off); + + if (s->header_len < sizeof(s->hdr)) { + /* header is still not complete */ + return; + } + + if (proxy_header_validate(s) != 0) + return; + } + + if (s->addr_len < s->addr_total) { + /* address is incomplete */ + off = s->addr_total - s->addr_len; + off = (len < off ? len : off); + memcpy((uint8_t *) &s->addr + s->addr_len, buf, off); + + s->header_len += off; + s->addr_len += off; + buf += off; + len -= off; + io_drop(s->io, off); + + if (s->addr_len < s->addr_total) { + /* address is still not complete */ + return; + } + } + + if (s->header_len < s->header_total) { + /* additional parameters not complete */ + /* these are ignored for now, but we still need to drop + * the bytes from the buffer */ + off = s->header_total - s->header_len; + off = (len < off ? len : off); + + s->header_len += off; + io_drop(s->io, off); + + if (s->header_len < s->header_total) + /* not complete yet */ + return; + } + + switch(h->ver_cmd & 0xF) { + case PROXY_CLOCAL: + /* local address, no need to modify ss */ + break; + + case PROXY_CPROXY: + if (proxy_translate_ss(s) != 0) + return; + break; + + default: + proxy_error(s, "protocol error", "unknown command"); + return; + } + + s->cb_accepted(s->l, s->fd, &s->ss, s->io); + /* we passed off s->io, so it does not need to be freed here */ + free(s); + break; + + case IO_TIMEOUT: + proxy_error(s, "timeout", NULL); + break; + + case IO_DISCONNECTED: + proxy_error(s, "disconnected", NULL); + break; + + case IO_ERROR: + proxy_error(s, "IO error", io_error(io)); + break; + + default: + fatalx("proxy_io()"); + } + +} + +static void +proxy_error(struct proxy_session *s, const char *reason, const char *extra) +{ + if (extra) + log_info("proxy %p event=closed address=%s reason=\"%s (%s)\"", + s, ss_to_text(&s->ss), reason, extra); + else + log_info("proxy %p event=closed address=%s reason=\"%s\"", + s, ss_to_text(&s->ss), reason); + + s->cb_dropped(s->l, s->fd, &s->ss); + io_free(s->io); + free(s); +} + +static int +proxy_header_validate(struct proxy_session *s) +{ + struct proxy_hdr_v2 *h = &s->hdr; + + if (memcmp(h->sig, pv2_signature, + sizeof(pv2_signature)) != 0) { + proxy_error(s, "protocol error", "invalid signature"); + return (-1); + } + + if ((h->ver_cmd >> 4) != 2) { + proxy_error(s, "protocol error", "invalid version"); + return (-1); + } + + switch (h->fam) { + case (PROXY_AF_UNSPEC << 4 | PROXY_TF_UNSPEC): + s->addr_total = 0; + break; + + case (PROXY_AF_INET << 4 | PROXY_TF_STREAM): + s->addr_total = sizeof(s->addr.ipv4); + break; + + case (PROXY_AF_INET6 << 4 | PROXY_TF_STREAM): + s->addr_total = sizeof(s->addr.ipv6); + break; + + case (PROXY_AF_UNIX << 4 | PROXY_TF_STREAM): + s->addr_total = sizeof(s->addr.un); + break; + + default: + proxy_error(s, "protocol error", "unsupported address family"); + return (-1); + } + + s->header_total = ntohs(h->len); + if (s->header_total > UINT16_MAX - sizeof(struct proxy_hdr_v2)) { + proxy_error(s, "protocol error", "header too long"); + return (-1); + } + s->header_total += sizeof(struct proxy_hdr_v2); + + if (s->header_total < sizeof(struct proxy_hdr_v2) + s->addr_total) { + proxy_error(s, "protocol error", "address info too short"); + return (-1); + } + + return 0; +} + +static int +proxy_translate_ss(struct proxy_session *s) +{ + struct sockaddr_in *sin = (struct sockaddr_in *) &s->ss; + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) &s->ss; + struct sockaddr_un *sun = (struct sockaddr_un *) &s->ss; + size_t sun_len; + + switch (s->hdr.fam) { + case (PROXY_AF_UNSPEC << 4 | PROXY_TF_UNSPEC): + /* unspec: only supported for local */ + proxy_error(s, "address translation", "UNSPEC family not " + "supported for PROXYing"); + return (-1); + + case (PROXY_AF_INET << 4 | PROXY_TF_STREAM): + memset(&s->ss, 0, sizeof(s->ss)); + sin->sin_family = AF_INET; + sin->sin_port = s->addr.ipv4.src_port; + sin->sin_addr.s_addr = s->addr.ipv4.src_addr; + break; + + case (PROXY_AF_INET6 << 4 | PROXY_TF_STREAM): + memset(&s->ss, 0, sizeof(s->ss)); + sin6->sin6_family = AF_INET6; + sin6->sin6_port = s->addr.ipv6.src_port; + memcpy(sin6->sin6_addr.s6_addr, s->addr.ipv6.src_addr, + sizeof(s->addr.ipv6.src_addr)); + break; + + case (PROXY_AF_UNIX << 4 | PROXY_TF_STREAM): + memset(&s->ss, 0, sizeof(s->ss)); + sun_len = strnlen(s->addr.un.src_addr, + sizeof(s->addr.un.src_addr)); + if (sun_len > sizeof(sun->sun_path)) { + proxy_error(s, "address translation", "Unix socket path" + " longer than supported"); + return (-1); + } + sun->sun_family = AF_UNIX; + memcpy(sun->sun_path, s->addr.un.src_addr, sun_len); + break; + + default: + fatalx("proxy_translate_ss()"); + } + + return 0; +} diff --git a/smtpd/queue.c b/smtpd/queue.c new file mode 100644 index 00000000..434e3647 --- /dev/null +++ b/smtpd/queue.c @@ -0,0 +1,750 @@ +/* $OpenBSD: queue.c,v 1.190 2020/04/22 11:35:34 eric Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <err.h> +#include <event.h> +#include <fcntl.h> +#include <grp.h> /* needed for setgroups */ +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +static void queue_imsg(struct mproc *, struct imsg *); +static void queue_timeout(int, short, void *); +static void queue_bounce(struct envelope *, struct delivery_bounce *); +static void queue_shutdown(void); +static void queue_log(const struct envelope *, const char *, const char *); +static void queue_msgid_walk(int, short, void *); + + +static void +queue_imsg(struct mproc *p, struct imsg *imsg) +{ + struct delivery_bounce bounce; + struct msg_walkinfo *wi; + struct timeval tv; + struct bounce_req_msg *req_bounce; + struct envelope evp; + struct msg m; + const char *reason; + uint64_t reqid, evpid, holdq; + uint32_t msgid; + time_t nexttry; + size_t n_evp; + int fd, mta_ext, ret, v, flags, code; + + if (imsg == NULL) + queue_shutdown(); + + memset(&bounce, 0, sizeof(struct delivery_bounce)); + + switch (imsg->hdr.type) { + case IMSG_SMTP_MESSAGE_CREATE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_end(&m); + + ret = queue_message_create(&msgid); + + m_create(p, IMSG_SMTP_MESSAGE_CREATE, 0, 0, -1); + m_add_id(p, reqid); + if (ret == 0) + m_add_int(p, 0); + else { + m_add_int(p, 1); + m_add_msgid(p, msgid); + } + m_close(p); + return; + + case IMSG_SMTP_MESSAGE_ROLLBACK: + m_msg(&m, imsg); + m_get_msgid(&m, &msgid); + m_end(&m); + + queue_message_delete(msgid); + + m_create(p_scheduler, IMSG_QUEUE_MESSAGE_ROLLBACK, + 0, 0, -1); + m_add_msgid(p_scheduler, msgid); + m_close(p_scheduler); + return; + + case IMSG_SMTP_MESSAGE_COMMIT: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_msgid(&m, &msgid); + m_end(&m); + + ret = queue_message_commit(msgid); + + m_create(p, IMSG_SMTP_MESSAGE_COMMIT, 0, 0, -1); + m_add_id(p, reqid); + m_add_int(p, (ret == 0) ? 0 : 1); + m_close(p); + + if (ret) { + m_create(p_scheduler, IMSG_QUEUE_MESSAGE_COMMIT, + 0, 0, -1); + m_add_msgid(p_scheduler, msgid); + m_close(p_scheduler); + } + return; + + case IMSG_SMTP_MESSAGE_OPEN: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_msgid(&m, &msgid); + m_end(&m); + + fd = queue_message_fd_rw(msgid); + + m_create(p, IMSG_SMTP_MESSAGE_OPEN, 0, 0, fd); + m_add_id(p, reqid); + m_add_int(p, (fd == -1) ? 0 : 1); + m_close(p); + return; + + case IMSG_QUEUE_SMTP_SESSION: + bounce_fd(imsg->fd); + return; + + case IMSG_LKA_ENVELOPE_SUBMIT: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_envelope(&m, &evp); + m_end(&m); + + if (evp.id == 0) + log_warnx("warn: imsg_queue_submit_envelope: evpid=0"); + if (evpid_to_msgid(evp.id) == 0) + log_warnx("warn: imsg_queue_submit_envelope: msgid=0, " + "evpid=%016"PRIx64, evp.id); + ret = queue_envelope_create(&evp); + m_create(p_pony, IMSG_QUEUE_ENVELOPE_SUBMIT, 0, 0, -1); + m_add_id(p_pony, reqid); + if (ret == 0) + m_add_int(p_pony, 0); + else { + m_add_int(p_pony, 1); + m_add_evpid(p_pony, evp.id); + } + m_close(p_pony); + if (ret) { + m_create(p_scheduler, + IMSG_QUEUE_ENVELOPE_SUBMIT, 0, 0, -1); + m_add_envelope(p_scheduler, &evp); + m_close(p_scheduler); + } + return; + + case IMSG_LKA_ENVELOPE_COMMIT: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_end(&m); + m_create(p_pony, IMSG_QUEUE_ENVELOPE_COMMIT, 0, 0, -1); + m_add_id(p_pony, reqid); + m_add_int(p_pony, 1); + m_close(p_pony); + return; + + case IMSG_SCHED_ENVELOPE_REMOVE: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_ACK, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_close(p_scheduler); + + /* already removed by scheduler */ + if (queue_envelope_load(evpid, &evp) == 0) + return; + + queue_log(&evp, "Remove", "Removed by administrator"); + queue_envelope_delete(evpid); + return; + + case IMSG_SCHED_ENVELOPE_EXPIRE: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_ACK, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_close(p_scheduler); + + /* already removed by scheduler*/ + if (queue_envelope_load(evpid, &evp) == 0) + return; + + bounce.type = B_FAILED; + envelope_set_errormsg(&evp, "Envelope expired"); + envelope_set_esc_class(&evp, ESC_STATUS_TEMPFAIL); + envelope_set_esc_code(&evp, ESC_DELIVERY_TIME_EXPIRED); + queue_bounce(&evp, &bounce); + queue_log(&evp, "Expire", "Envelope expired"); + queue_envelope_delete(evpid); + return; + + case IMSG_SCHED_ENVELOPE_BOUNCE: + CHECK_IMSG_DATA_SIZE(imsg, sizeof *req_bounce); + req_bounce = imsg->data; + evpid = req_bounce->evpid; + + if (queue_envelope_load(evpid, &evp) == 0) { + log_warnx("queue: bounce: failed to load envelope"); + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_REMOVE, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_add_u32(p_scheduler, 0); /* not in-flight */ + m_close(p_scheduler); + return; + } + queue_bounce(&evp, &req_bounce->bounce); + evp.lastbounce = req_bounce->timestamp; + if (!queue_envelope_update(&evp)) + log_warnx("warn: could not update envelope %016"PRIx64, evpid); + return; + + case IMSG_SCHED_ENVELOPE_DELIVER: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + if (queue_envelope_load(evpid, &evp) == 0) { + log_warnx("queue: deliver: failed to load envelope"); + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_REMOVE, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_add_u32(p_scheduler, 1); /* in-flight */ + m_close(p_scheduler); + return; + } + evp.lasttry = time(NULL); + m_create(p_pony, IMSG_QUEUE_DELIVER, 0, 0, -1); + m_add_envelope(p_pony, &evp); + m_close(p_pony); + return; + + case IMSG_SCHED_ENVELOPE_INJECT: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + bounce_add(evpid); + return; + + case IMSG_SCHED_ENVELOPE_TRANSFER: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + if (queue_envelope_load(evpid, &evp) == 0) { + log_warnx("queue: failed to load envelope"); + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_REMOVE, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_add_u32(p_scheduler, 1); /* in-flight */ + m_close(p_scheduler); + return; + } + evp.lasttry = time(NULL); + m_create(p_pony, IMSG_QUEUE_TRANSFER, 0, 0, -1); + m_add_envelope(p_pony, &evp); + m_close(p_pony); + return; + + case IMSG_CTL_LIST_ENVELOPES: + if (imsg->hdr.len == sizeof imsg->hdr) { + m_forward(p_control, imsg); + return; + } + + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_get_int(&m, &flags); + m_get_time(&m, &nexttry); + m_end(&m); + + if (queue_envelope_load(evpid, &evp) == 0) + return; /* Envelope is gone, drop it */ + + /* + * XXX consistency: The envelope might already be on + * its way back to the scheduler. We need to detect + * this properly and report that state. + */ + if (flags & EF_INFLIGHT) { + /* + * Not exactly correct but pretty close: The + * value is not recorded on the envelope unless + * a tempfail occurs. + */ + evp.lasttry = nexttry; + } + + m_create(p_control, IMSG_CTL_LIST_ENVELOPES, + imsg->hdr.peerid, 0, -1); + m_add_int(p_control, flags); + m_add_time(p_control, nexttry); + m_add_envelope(p_control, &evp); + m_close(p_control); + return; + + case IMSG_MDA_OPEN_MESSAGE: + case IMSG_MTA_OPEN_MESSAGE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_msgid(&m, &msgid); + m_end(&m); + fd = queue_message_fd_r(msgid); + m_create(p, imsg->hdr.type, 0, 0, fd); + m_add_id(p, reqid); + m_close(p); + return; + + case IMSG_MDA_DELIVERY_OK: + case IMSG_MTA_DELIVERY_OK: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + if (imsg->hdr.type == IMSG_MTA_DELIVERY_OK) + m_get_int(&m, &mta_ext); + m_end(&m); + if (queue_envelope_load(evpid, &evp) == 0) { + log_warn("queue: dsn: failed to load envelope"); + return; + } + if (evp.dsn_notify & DSN_SUCCESS) { + bounce.type = B_DELIVERED; + bounce.dsn_ret = evp.dsn_ret; + envelope_set_esc_class(&evp, ESC_STATUS_OK); + if (imsg->hdr.type == IMSG_MDA_DELIVERY_OK) + queue_bounce(&evp, &bounce); + else if (imsg->hdr.type == IMSG_MTA_DELIVERY_OK && + (mta_ext & MTA_EXT_DSN) == 0) { + bounce.mta_without_dsn = 1; + queue_bounce(&evp, &bounce); + } + } + queue_envelope_delete(evpid); + m_create(p_scheduler, IMSG_QUEUE_DELIVERY_OK, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_close(p_scheduler); + return; + + case IMSG_MDA_DELIVERY_TEMPFAIL: + case IMSG_MTA_DELIVERY_TEMPFAIL: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_get_string(&m, &reason); + m_get_int(&m, &code); + m_end(&m); + if (queue_envelope_load(evpid, &evp) == 0) { + log_warnx("queue: tempfail: failed to load envelope"); + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_REMOVE, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_add_u32(p_scheduler, 1); /* in-flight */ + m_close(p_scheduler); + return; + } + envelope_set_errormsg(&evp, "%s", reason); + envelope_set_esc_class(&evp, ESC_STATUS_TEMPFAIL); + envelope_set_esc_code(&evp, code); + evp.retry++; + if (!queue_envelope_update(&evp)) + log_warnx("warn: could not update envelope %016"PRIx64, evpid); + m_create(p_scheduler, IMSG_QUEUE_DELIVERY_TEMPFAIL, 0, 0, -1); + m_add_envelope(p_scheduler, &evp); + m_close(p_scheduler); + return; + + case IMSG_MDA_DELIVERY_PERMFAIL: + case IMSG_MTA_DELIVERY_PERMFAIL: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_get_string(&m, &reason); + m_get_int(&m, &code); + m_end(&m); + if (queue_envelope_load(evpid, &evp) == 0) { + log_warnx("queue: permfail: failed to load envelope"); + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_REMOVE, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_add_u32(p_scheduler, 1); /* in-flight */ + m_close(p_scheduler); + return; + } + bounce.type = B_FAILED; + envelope_set_errormsg(&evp, "%s", reason); + envelope_set_esc_class(&evp, ESC_STATUS_PERMFAIL); + envelope_set_esc_code(&evp, code); + queue_bounce(&evp, &bounce); + queue_envelope_delete(evpid); + m_create(p_scheduler, IMSG_QUEUE_DELIVERY_PERMFAIL, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_close(p_scheduler); + return; + + case IMSG_MDA_DELIVERY_LOOP: + case IMSG_MTA_DELIVERY_LOOP: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + if (queue_envelope_load(evpid, &evp) == 0) { + log_warnx("queue: loop: failed to load envelope"); + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_REMOVE, 0, 0, -1); + m_add_evpid(p_scheduler, evpid); + m_add_u32(p_scheduler, 1); /* in-flight */ + m_close(p_scheduler); + return; + } + envelope_set_errormsg(&evp, "%s", "Loop detected"); + envelope_set_esc_class(&evp, ESC_STATUS_TEMPFAIL); + envelope_set_esc_code(&evp, ESC_ROUTING_LOOP_DETECTED); + bounce.type = B_FAILED; + queue_bounce(&evp, &bounce); + queue_envelope_delete(evp.id); + m_create(p_scheduler, IMSG_QUEUE_DELIVERY_LOOP, 0, 0, -1); + m_add_evpid(p_scheduler, evp.id); + m_close(p_scheduler); + return; + + case IMSG_MTA_DELIVERY_HOLD: + case IMSG_MDA_DELIVERY_HOLD: + imsg->hdr.type = IMSG_QUEUE_HOLDQ_HOLD; + m_forward(p_scheduler, imsg); + return; + + case IMSG_MTA_SCHEDULE: + imsg->hdr.type = IMSG_QUEUE_ENVELOPE_SCHEDULE; + m_forward(p_scheduler, imsg); + return; + + case IMSG_MTA_HOLDQ_RELEASE: + case IMSG_MDA_HOLDQ_RELEASE: + m_msg(&m, imsg); + m_get_id(&m, &holdq); + m_get_int(&m, &v); + m_end(&m); + m_create(p_scheduler, IMSG_QUEUE_HOLDQ_RELEASE, 0, 0, -1); + if (imsg->hdr.type == IMSG_MTA_HOLDQ_RELEASE) + m_add_int(p_scheduler, D_MTA); + else + m_add_int(p_scheduler, D_MDA); + m_add_id(p_scheduler, holdq); + m_add_int(p_scheduler, v); + m_close(p_scheduler); + return; + + case IMSG_CTL_PAUSE_MDA: + case IMSG_CTL_PAUSE_MTA: + case IMSG_CTL_RESUME_MDA: + case IMSG_CTL_RESUME_MTA: + m_forward(p_scheduler, imsg); + return; + + case IMSG_CTL_VERBOSE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + log_trace_verbose(v); + return; + + case IMSG_CTL_PROFILE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + profiling = v; + return; + + case IMSG_CTL_DISCOVER_EVPID: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + if (queue_envelope_load(evpid, &evp) == 0) { + log_warnx("queue: discover: failed to load " + "envelope %016" PRIx64, evpid); + n_evp = 0; + m_compose(p_control, imsg->hdr.type, + imsg->hdr.peerid, 0, -1, + &n_evp, sizeof n_evp); + return; + } + + m_create(p_scheduler, IMSG_QUEUE_DISCOVER_EVPID, + 0, 0, -1); + m_add_envelope(p_scheduler, &evp); + m_close(p_scheduler); + + m_create(p_scheduler, IMSG_QUEUE_DISCOVER_MSGID, + 0, 0, -1); + m_add_msgid(p_scheduler, evpid_to_msgid(evpid)); + m_close(p_scheduler); + n_evp = 1; + m_compose(p_control, imsg->hdr.type, imsg->hdr.peerid, + 0, -1, &n_evp, sizeof n_evp); + return; + + case IMSG_CTL_DISCOVER_MSGID: + m_msg(&m, imsg); + m_get_msgid(&m, &msgid); + m_end(&m); + /* handle concurrent walk requests */ + wi = xcalloc(1, sizeof *wi); + wi->msgid = msgid; + wi->peerid = imsg->hdr.peerid; + evtimer_set(&wi->ev, queue_msgid_walk, wi); + tv.tv_sec = 0; + tv.tv_usec = 10; + evtimer_add(&wi->ev, &tv); + return; + } + + errx(1, "queue_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type)); +} + +static void +queue_msgid_walk(int fd, short event, void *arg) +{ + struct envelope evp; + struct timeval tv; + struct msg_walkinfo *wi = arg; + int r; + + r = queue_message_walk(&evp, wi->msgid, &wi->done, &wi->data); + if (r == -1) { + if (wi->n_evp) { + m_create(p_scheduler, IMSG_QUEUE_DISCOVER_MSGID, + 0, 0, -1); + m_add_msgid(p_scheduler, wi->msgid); + m_close(p_scheduler); + } + + m_compose(p_control, IMSG_CTL_DISCOVER_MSGID, wi->peerid, 0, -1, + &wi->n_evp, sizeof wi->n_evp); + evtimer_del(&wi->ev); + free(wi); + return; + } + + if (r) { + m_create(p_scheduler, IMSG_QUEUE_DISCOVER_EVPID, 0, 0, -1); + m_add_envelope(p_scheduler, &evp); + m_close(p_scheduler); + wi->n_evp += 1; + } + + tv.tv_sec = 0; + tv.tv_usec = 10; + evtimer_set(&wi->ev, queue_msgid_walk, wi); + evtimer_add(&wi->ev, &tv); +} + +static void +queue_bounce(struct envelope *e, struct delivery_bounce *d) +{ + struct envelope b; + + b = *e; + b.type = D_BOUNCE; + b.agent.bounce = *d; + b.retry = 0; + b.lasttry = 0; + b.creation = time(NULL); + b.ttl = 3600 * 24 * 7; + + if (e->dsn_notify & DSN_NEVER) + return; + + if (b.id == 0) + log_warnx("warn: queue_bounce: evpid=0"); + if (evpid_to_msgid(b.id) == 0) + log_warnx("warn: queue_bounce: msgid=0, evpid=%016"PRIx64, + b.id); + if (e->type == D_BOUNCE) { + log_warnx("warn: queue: double bounce!"); + } else if (e->sender.user[0] == '\0') { + log_warnx("warn: queue: no return path!"); + } else if (!queue_envelope_create(&b)) { + log_warnx("warn: queue: cannot bounce!"); + } else { + log_debug("debug: queue: bouncing evp:%016" PRIx64 + " as evp:%016" PRIx64, e->id, b.id); + + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_SUBMIT, 0, 0, -1); + m_add_envelope(p_scheduler, &b); + m_close(p_scheduler); + + m_create(p_scheduler, IMSG_QUEUE_MESSAGE_COMMIT, 0, 0, -1); + m_add_msgid(p_scheduler, evpid_to_msgid(b.id)); + m_close(p_scheduler); + + stat_increment("queue.bounce", 1); + } +} + +static void +queue_shutdown(void) +{ + log_debug("debug: queue agent exiting"); + queue_close(); + _exit(0); +} + +int +queue(void) +{ + struct passwd *pw; + struct timeval tv; + struct event ev_qload; + + purge_config(PURGE_EVERYTHING & ~PURGE_DISPATCHERS); + + if ((pw = getpwnam(SMTPD_QUEUE_USER)) == NULL) + if ((pw = getpwnam(SMTPD_USER)) == NULL) + fatalx("unknown user " SMTPD_USER); + + env->sc_queue_flags |= QUEUE_EVPCACHE; + env->sc_queue_evpcache_size = 1024; + + if (chroot(PATH_SPOOL) == -1) + fatal("queue: chroot"); + if (chdir("/") == -1) + fatal("queue: chdir(\"/\")"); + + config_process(PROC_QUEUE); + + if (env->sc_queue_flags & QUEUE_COMPRESSION) + log_info("queue: queue compression enabled"); + + if (env->sc_queue_key) { + if (!crypto_setup(env->sc_queue_key, strlen(env->sc_queue_key))) + fatalx("crypto_setup: invalid key for queue encryption"); + log_info("queue: queue encryption enabled"); + } + + 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"); + + imsg_callback = queue_imsg; + event_init(); + + signal(SIGINT, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peer(PROC_PARENT); + config_peer(PROC_CONTROL); + config_peer(PROC_LKA); + config_peer(PROC_SCHEDULER); + config_peer(PROC_PONY); + + /* setup queue loading task */ + evtimer_set(&ev_qload, queue_timeout, &ev_qload); + tv.tv_sec = 0; + tv.tv_usec = 10; + evtimer_add(&ev_qload, &tv); + +#if HAVE_PLEDGE + if (pledge("stdio rpath wpath cpath flock recvfd sendfd", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + fatalx("exited event loop"); + + return (0); +} + +static void +queue_timeout(int fd, short event, void *p) +{ + static uint32_t msgid = 0; + struct envelope evp; + struct event *ev = p; + struct timeval tv; + int r; + + r = queue_envelope_walk(&evp); + if (r == -1) { + if (msgid) { + m_create(p_scheduler, IMSG_QUEUE_MESSAGE_COMMIT, + 0, 0, -1); + m_add_msgid(p_scheduler, msgid); + m_close(p_scheduler); + } + log_debug("debug: queue: done loading queue into scheduler"); + return; + } + + if (r) { + if (msgid && evpid_to_msgid(evp.id) != msgid) { + m_create(p_scheduler, IMSG_QUEUE_MESSAGE_COMMIT, + 0, 0, -1); + m_add_msgid(p_scheduler, msgid); + m_close(p_scheduler); + } + msgid = evpid_to_msgid(evp.id); + m_create(p_scheduler, IMSG_QUEUE_ENVELOPE_SUBMIT, 0, 0, -1); + m_add_envelope(p_scheduler, &evp); + m_close(p_scheduler); + } + + tv.tv_sec = 0; + tv.tv_usec = 10; + evtimer_add(ev, &tv); +} + +static void +queue_log(const struct envelope *e, const char *prefix, const char *status) +{ + char rcpt[LINE_MAX]; + + (void)strlcpy(rcpt, "-", sizeof rcpt); + if (strcmp(e->rcpt.user, e->dest.user) || + strcmp(e->rcpt.domain, e->dest.domain)) + (void)snprintf(rcpt, sizeof rcpt, "%s@%s", + e->rcpt.user, e->rcpt.domain); + + log_info("%s: %s for %016" PRIx64 ": from=<%s@%s>, to=<%s@%s>, " + "rcpt=<%s>, delay=%s, stat=%s", + e->type == D_MDA ? "delivery" : "relay", + prefix, + e->id, e->sender.user, e->sender.domain, + e->dest.user, e->dest.domain, + rcpt, + duration_to_text(time(NULL) - e->creation), + status); +} diff --git a/smtpd/queue_backend.c b/smtpd/queue_backend.c new file mode 100644 index 00000000..fa945f47 --- /dev/null +++ b/smtpd/queue_backend.c @@ -0,0 +1,806 @@ +/* $OpenBSD: queue_backend.c,v 1.66 2020/04/22 11:35:34 eric Exp $ */ + +/* + * Copyright (c) 2011 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <grp.h> +#include <imsg.h> +#include <limits.h> +#include <inttypes.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +static const char* envelope_validate(struct envelope *); + +extern struct queue_backend queue_backend_fs; +extern struct queue_backend queue_backend_null; +extern struct queue_backend queue_backend_proc; +extern struct queue_backend queue_backend_ram; + +static void queue_envelope_cache_add(struct envelope *); +static void queue_envelope_cache_update(struct envelope *); +static void queue_envelope_cache_del(uint64_t evpid); + +TAILQ_HEAD(evplst, envelope); + +static struct tree evpcache_tree; +static struct evplst evpcache_list; +static struct queue_backend *backend; + +static int (*handler_close)(void); +static int (*handler_message_create)(uint32_t *); +static int (*handler_message_commit)(uint32_t, const char*); +static int (*handler_message_delete)(uint32_t); +static int (*handler_message_fd_r)(uint32_t); +static int (*handler_envelope_create)(uint32_t, const char *, size_t, uint64_t *); +static int (*handler_envelope_delete)(uint64_t); +static int (*handler_envelope_update)(uint64_t, const char *, size_t); +static int (*handler_envelope_load)(uint64_t, char *, size_t); +static int (*handler_envelope_walk)(uint64_t *, char *, size_t); +static int (*handler_message_walk)(uint64_t *, char *, size_t, + uint32_t, int *, void **); + +#ifdef QUEUE_PROFILING + +static struct { + struct timespec t0; + const char *name; +} profile; + +static inline void profile_enter(const char *name) +{ + if ((profiling & PROFILE_QUEUE) == 0) + return; + + profile.name = name; + clock_gettime(CLOCK_MONOTONIC, &profile.t0); +} + +static inline void profile_leave(void) +{ + struct timespec t1, dt; + + if ((profiling & PROFILE_QUEUE) == 0) + return; + + clock_gettime(CLOCK_MONOTONIC, &t1); + timespecsub(&t1, &profile.t0, &dt); + log_debug("profile-queue: %s %lld.%09ld", profile.name, + (long long)dt.tv_sec, dt.tv_nsec); +} +#else +#define profile_enter(x) do {} while (0) +#define profile_leave() do {} while (0) +#endif + +static int +queue_message_path(uint32_t msgid, char *buf, size_t len) +{ + return bsnprintf(buf, len, "%s/%08"PRIx32, PATH_TEMPORARY, msgid); +} + +int +queue_init(const char *name, int server) +{ + struct passwd *pwq; + struct group *gr; + int r; + + pwq = getpwnam(SMTPD_QUEUE_USER); + if (pwq == NULL) + errx(1, "unknown user %s", SMTPD_QUEUE_USER); + + gr = getgrnam(SMTPD_QUEUE_GROUP); + if (gr == NULL) + errx(1, "unknown group %s", SMTPD_QUEUE_GROUP); + + tree_init(&evpcache_tree); + TAILQ_INIT(&evpcache_list); + + if (!strcmp(name, "fs")) + backend = &queue_backend_fs; + else if (!strcmp(name, "null")) + backend = &queue_backend_null; + else if (!strcmp(name, "ram")) + backend = &queue_backend_ram; + else + backend = &queue_backend_proc; + + if (server) { + if (ckdir(PATH_SPOOL, 0711, 0, 0, 1) == 0) + errx(1, "error in spool directory setup"); + if (ckdir(PATH_SPOOL PATH_OFFLINE, 0770, 0, gr->gr_gid, 1) == 0) + errx(1, "error in offline directory setup"); + if (ckdir(PATH_SPOOL PATH_PURGE, 0700, pwq->pw_uid, 0, 1) == 0) + errx(1, "error in purge directory setup"); + + mvpurge(PATH_SPOOL PATH_TEMPORARY, PATH_SPOOL PATH_PURGE); + + if (ckdir(PATH_SPOOL PATH_TEMPORARY, 0700, pwq->pw_uid, 0, 1) == 0) + errx(1, "error in purge directory setup"); + } + + r = backend->init(pwq, server, name); + + log_trace(TRACE_QUEUE, "queue-backend: queue_init(%d) -> %d", server, r); + + return (r); +} + +int +queue_close(void) +{ + if (handler_close) + return (handler_close()); + + return (1); +} + +int +queue_message_create(uint32_t *msgid) +{ + int r; + + profile_enter("queue_message_create"); + r = handler_message_create(msgid); + profile_leave(); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_message_create() -> %d (%08"PRIx32")", + r, *msgid); + + return (r); +} + +int +queue_message_delete(uint32_t msgid) +{ + char msgpath[PATH_MAX]; + uint64_t evpid; + void *iter; + int r; + + profile_enter("queue_message_delete"); + r = handler_message_delete(msgid); + profile_leave(); + + /* in case the message is incoming */ + queue_message_path(msgid, msgpath, sizeof(msgpath)); + unlink(msgpath); + + /* remove remaining envelopes from the cache if any (on rollback) */ + evpid = msgid_to_evpid(msgid); + for (;;) { + iter = NULL; + if (!tree_iterfrom(&evpcache_tree, &iter, evpid, &evpid, NULL)) + break; + if (evpid_to_msgid(evpid) != msgid) + break; + queue_envelope_cache_del(evpid); + } + + log_trace(TRACE_QUEUE, + "queue-backend: queue_message_delete(%08"PRIx32") -> %d", msgid, r); + + return (r); +} + +int +queue_message_commit(uint32_t msgid) +{ + int r; + char msgpath[PATH_MAX]; + char tmppath[PATH_MAX]; + FILE *ifp = NULL; + FILE *ofp = NULL; + + profile_enter("queue_message_commit"); + + queue_message_path(msgid, msgpath, sizeof(msgpath)); + + if (env->sc_queue_flags & QUEUE_COMPRESSION) { + bsnprintf(tmppath, sizeof tmppath, "%s.comp", msgpath); + ifp = fopen(msgpath, "r"); + ofp = fopen(tmppath, "w+"); + if (ifp == NULL || ofp == NULL) + goto err; + if (!compress_file(ifp, ofp)) + goto err; + fclose(ifp); + fclose(ofp); + ifp = NULL; + ofp = NULL; + + if (rename(tmppath, msgpath) == -1) { + if (errno == ENOSPC) + return (0); + unlink(tmppath); + log_warn("rename"); + return (0); + } + } + + if (env->sc_queue_flags & QUEUE_ENCRYPTION) { + bsnprintf(tmppath, sizeof tmppath, "%s.enc", msgpath); + ifp = fopen(msgpath, "r"); + ofp = fopen(tmppath, "w+"); + if (ifp == NULL || ofp == NULL) + goto err; + if (!crypto_encrypt_file(ifp, ofp)) + goto err; + fclose(ifp); + fclose(ofp); + ifp = NULL; + ofp = NULL; + + if (rename(tmppath, msgpath) == -1) { + if (errno == ENOSPC) + return (0); + unlink(tmppath); + log_warn("rename"); + return (0); + } + } + + r = handler_message_commit(msgid, msgpath); + profile_leave(); + + /* in case it's not done by the backend */ + unlink(msgpath); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_message_commit(%08"PRIx32") -> %d", + msgid, r); + + return (r); + +err: + if (ifp) + fclose(ifp); + if (ofp) + fclose(ofp); + return 0; +} + +int +queue_message_fd_r(uint32_t msgid) +{ + int fdin = -1, fdout = -1, fd = -1; + FILE *ifp = NULL; + FILE *ofp = NULL; + + profile_enter("queue_message_fd_r"); + fdin = handler_message_fd_r(msgid); + profile_leave(); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_message_fd_r(%08"PRIx32") -> %d", msgid, fdin); + + if (fdin == -1) + return (-1); + + if (env->sc_queue_flags & QUEUE_ENCRYPTION) { + if ((fdout = mktmpfile()) == -1) + goto err; + if ((fd = dup(fdout)) == -1) + goto err; + if ((ifp = fdopen(fdin, "r")) == NULL) + goto err; + fdin = fd; + fd = -1; + if ((ofp = fdopen(fdout, "w+")) == NULL) + goto err; + + if (!crypto_decrypt_file(ifp, ofp)) + goto err; + + fclose(ifp); + ifp = NULL; + fclose(ofp); + ofp = NULL; + lseek(fdin, SEEK_SET, 0); + } + + if (env->sc_queue_flags & QUEUE_COMPRESSION) { + if ((fdout = mktmpfile()) == -1) + goto err; + if ((fd = dup(fdout)) == -1) + goto err; + if ((ifp = fdopen(fdin, "r")) == NULL) + goto err; + fdin = fd; + fd = -1; + if ((ofp = fdopen(fdout, "w+")) == NULL) + goto err; + + if (!uncompress_file(ifp, ofp)) + goto err; + + fclose(ifp); + ifp = NULL; + fclose(ofp); + ofp = NULL; + lseek(fdin, SEEK_SET, 0); + } + + return (fdin); + +err: + if (fd != -1) + close(fd); + if (fdin != -1) + close(fdin); + if (fdout != -1) + close(fdout); + if (ifp) + fclose(ifp); + if (ofp) + fclose(ofp); + return -1; +} + +int +queue_message_fd_rw(uint32_t msgid) +{ + char buf[PATH_MAX]; + + queue_message_path(msgid, buf, sizeof(buf)); + + return open(buf, O_RDWR | O_CREAT | O_EXCL, 0600); +} + +static int +queue_envelope_dump_buffer(struct envelope *ep, char *evpbuf, size_t evpbufsize) +{ + char *evp; + size_t evplen; + size_t complen; + char compbuf[sizeof(struct envelope)]; + size_t enclen; + char encbuf[sizeof(struct envelope)]; + + evp = evpbuf; + evplen = envelope_dump_buffer(ep, evpbuf, evpbufsize); + if (evplen == 0) + return (0); + + if (env->sc_queue_flags & QUEUE_COMPRESSION) { + complen = compress_chunk(evp, evplen, compbuf, sizeof compbuf); + if (complen == 0) + return (0); + evp = compbuf; + evplen = complen; + } + + if (env->sc_queue_flags & QUEUE_ENCRYPTION) { + enclen = crypto_encrypt_buffer(evp, evplen, encbuf, sizeof encbuf); + if (enclen == 0) + return (0); + evp = encbuf; + evplen = enclen; + } + + memmove(evpbuf, evp, evplen); + + return (evplen); +} + +static int +queue_envelope_load_buffer(struct envelope *ep, char *evpbuf, size_t evpbufsize) +{ + char *evp; + size_t evplen; + char compbuf[sizeof(struct envelope)]; + size_t complen; + char encbuf[sizeof(struct envelope)]; + size_t enclen; + + evp = evpbuf; + evplen = evpbufsize; + + if (env->sc_queue_flags & QUEUE_ENCRYPTION) { + enclen = crypto_decrypt_buffer(evp, evplen, encbuf, sizeof encbuf); + if (enclen == 0) + return (0); + evp = encbuf; + evplen = enclen; + } + + if (env->sc_queue_flags & QUEUE_COMPRESSION) { + complen = uncompress_chunk(evp, evplen, compbuf, sizeof compbuf); + if (complen == 0) + return (0); + evp = compbuf; + evplen = complen; + } + + return (envelope_load_buffer(ep, evp, evplen)); +} + +static void +queue_envelope_cache_add(struct envelope *e) +{ + struct envelope *cached; + + while (tree_count(&evpcache_tree) >= env->sc_queue_evpcache_size) + queue_envelope_cache_del(TAILQ_LAST(&evpcache_list, evplst)->id); + + cached = xcalloc(1, sizeof *cached); + *cached = *e; + TAILQ_INSERT_HEAD(&evpcache_list, cached, entry); + tree_xset(&evpcache_tree, e->id, cached); + stat_increment("queue.evpcache.size", 1); +} + +static void +queue_envelope_cache_update(struct envelope *e) +{ + struct envelope *cached; + + if ((cached = tree_get(&evpcache_tree, e->id)) == NULL) { + queue_envelope_cache_add(e); + stat_increment("queue.evpcache.update.missed", 1); + } else { + TAILQ_REMOVE(&evpcache_list, cached, entry); + *cached = *e; + TAILQ_INSERT_HEAD(&evpcache_list, cached, entry); + stat_increment("queue.evpcache.update.hit", 1); + } +} + +static void +queue_envelope_cache_del(uint64_t evpid) +{ + struct envelope *cached; + + if ((cached = tree_pop(&evpcache_tree, evpid)) == NULL) + return; + + TAILQ_REMOVE(&evpcache_list, cached, entry); + free(cached); + stat_decrement("queue.evpcache.size", 1); +} + +int +queue_envelope_create(struct envelope *ep) +{ + int r; + char evpbuf[sizeof(struct envelope)]; + size_t evplen; + uint64_t evpid; + uint32_t msgid; + + ep->creation = time(NULL); + evplen = queue_envelope_dump_buffer(ep, evpbuf, sizeof evpbuf); + if (evplen == 0) + return (0); + + evpid = ep->id; + msgid = evpid_to_msgid(evpid); + + profile_enter("queue_envelope_create"); + r = handler_envelope_create(msgid, evpbuf, evplen, &ep->id); + profile_leave(); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_envelope_create(%016"PRIx64", %zu) -> %d (%016"PRIx64")", + evpid, evplen, r, ep->id); + + if (!r) { + ep->creation = 0; + ep->id = 0; + } + + if (r && env->sc_queue_flags & QUEUE_EVPCACHE) + queue_envelope_cache_add(ep); + + return (r); +} + +int +queue_envelope_delete(uint64_t evpid) +{ + int r; + + if (env->sc_queue_flags & QUEUE_EVPCACHE) + queue_envelope_cache_del(evpid); + + profile_enter("queue_envelope_delete"); + r = handler_envelope_delete(evpid); + profile_leave(); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_envelope_delete(%016"PRIx64") -> %d", + evpid, r); + + return (r); +} + +int +queue_envelope_load(uint64_t evpid, struct envelope *ep) +{ + const char *e; + char evpbuf[sizeof(struct envelope)]; + size_t evplen; + struct envelope *cached; + + if ((env->sc_queue_flags & QUEUE_EVPCACHE) && + (cached = tree_get(&evpcache_tree, evpid))) { + *ep = *cached; + stat_increment("queue.evpcache.load.hit", 1); + return (1); + } + + ep->id = evpid; + profile_enter("queue_envelope_load"); + evplen = handler_envelope_load(ep->id, evpbuf, sizeof evpbuf); + profile_leave(); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_envelope_load(%016"PRIx64") -> %zu", + evpid, evplen); + + if (evplen == 0) + return (0); + + if (queue_envelope_load_buffer(ep, evpbuf, evplen)) { + if ((e = envelope_validate(ep)) == NULL) { + ep->id = evpid; + if (env->sc_queue_flags & QUEUE_EVPCACHE) { + queue_envelope_cache_add(ep); + stat_increment("queue.evpcache.load.missed", 1); + } + return (1); + } + log_warnx("warn: invalid envelope %016" PRIx64 ": %s", + evpid, e); + } + return (0); +} + +int +queue_envelope_update(struct envelope *ep) +{ + char evpbuf[sizeof(struct envelope)]; + size_t evplen; + int r; + + evplen = queue_envelope_dump_buffer(ep, evpbuf, sizeof evpbuf); + if (evplen == 0) + return (0); + + profile_enter("queue_envelope_update"); + r = handler_envelope_update(ep->id, evpbuf, evplen); + profile_leave(); + + if (r && env->sc_queue_flags & QUEUE_EVPCACHE) + queue_envelope_cache_update(ep); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_envelope_update(%016"PRIx64") -> %d", + ep->id, r); + + return (r); +} + +int +queue_message_walk(struct envelope *ep, uint32_t msgid, int *done, void **data) +{ + char evpbuf[sizeof(struct envelope)]; + uint64_t evpid; + int r; + const char *e; + + profile_enter("queue_message_walk"); + r = handler_message_walk(&evpid, evpbuf, sizeof evpbuf, + msgid, done, data); + profile_leave(); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_message_walk() -> %d (%016"PRIx64")", + r, evpid); + + if (r == -1) + return (r); + + if (r && queue_envelope_load_buffer(ep, evpbuf, (size_t)r)) { + if ((e = envelope_validate(ep)) == NULL) { + ep->id = evpid; + /* + * do not cache the envelope here, while discovering + * envelopes one could re-run discover on already + * scheduled envelopes which leads to triggering of + * strict checks in caching. Envelopes could anyway + * be loaded from backend if it isn't cached. + */ + return (1); + } + log_warnx("warn: invalid envelope %016" PRIx64 ": %s", + evpid, e); + } + return (0); +} + +int +queue_envelope_walk(struct envelope *ep) +{ + const char *e; + uint64_t evpid; + char evpbuf[sizeof(struct envelope)]; + int r; + + profile_enter("queue_envelope_walk"); + r = handler_envelope_walk(&evpid, evpbuf, sizeof evpbuf); + profile_leave(); + + log_trace(TRACE_QUEUE, + "queue-backend: queue_envelope_walk() -> %d (%016"PRIx64")", + r, evpid); + + if (r == -1) + return (r); + + if (r && queue_envelope_load_buffer(ep, evpbuf, (size_t)r)) { + if ((e = envelope_validate(ep)) == NULL) { + ep->id = evpid; + if (env->sc_queue_flags & QUEUE_EVPCACHE) + queue_envelope_cache_add(ep); + return (1); + } + log_warnx("warn: invalid envelope %016" PRIx64 ": %s", + evpid, e); + } + return (0); +} + +uint32_t +queue_generate_msgid(void) +{ + uint32_t msgid; + + while ((msgid = arc4random()) == 0) + ; + + return msgid; +} + +uint64_t +queue_generate_evpid(uint32_t msgid) +{ + uint32_t rnd; + uint64_t evpid; + + while ((rnd = arc4random()) == 0) + ; + + evpid = msgid; + evpid <<= 32; + evpid |= rnd; + + return evpid; +} + +static const char* +envelope_validate(struct envelope *ep) +{ + if (ep->version != SMTPD_ENVELOPE_VERSION) + return "version mismatch"; + + if (memchr(ep->helo, '\0', sizeof(ep->helo)) == NULL) + return "invalid helo"; + if (ep->helo[0] == '\0') + return "empty helo"; + + if (memchr(ep->hostname, '\0', sizeof(ep->hostname)) == NULL) + return "invalid hostname"; + if (ep->hostname[0] == '\0') + return "empty hostname"; + + if (memchr(ep->errorline, '\0', sizeof(ep->errorline)) == NULL) + return "invalid error line"; + + if (dict_get(env->sc_dispatchers, ep->dispatcher) == NULL) + return "unknown dispatcher"; + + return NULL; +} + +void +queue_api_on_close(int(*cb)(void)) +{ + handler_close = cb; +} + +void +queue_api_on_message_create(int(*cb)(uint32_t *)) +{ + handler_message_create = cb; +} + +void +queue_api_on_message_commit(int(*cb)(uint32_t, const char *)) +{ + handler_message_commit = cb; +} + +void +queue_api_on_message_delete(int(*cb)(uint32_t)) +{ + handler_message_delete = cb; +} + +void +queue_api_on_message_fd_r(int(*cb)(uint32_t)) +{ + handler_message_fd_r = cb; +} + +void +queue_api_on_envelope_create(int(*cb)(uint32_t, const char *, size_t, uint64_t *)) +{ + handler_envelope_create = cb; +} + +void +queue_api_on_envelope_delete(int(*cb)(uint64_t)) +{ + handler_envelope_delete = cb; +} + +void +queue_api_on_envelope_update(int(*cb)(uint64_t, const char *, size_t)) +{ + handler_envelope_update = cb; +} + +void +queue_api_on_envelope_load(int(*cb)(uint64_t, char *, size_t)) +{ + handler_envelope_load = cb; +} + +void +queue_api_on_envelope_walk(int(*cb)(uint64_t *, char *, size_t)) +{ + handler_envelope_walk = cb; +} + +void +queue_api_on_message_walk(int(*cb)(uint64_t *, char *, size_t, + uint32_t, int *, void **)) +{ + handler_message_walk = cb; +} diff --git a/smtpd/queue_fs.c b/smtpd/queue_fs.c new file mode 100644 index 00000000..097ba1e2 --- /dev/null +++ b/smtpd/queue_fs.c @@ -0,0 +1,695 @@ +/* $OpenBSD: queue_fs.c,v 1.20 2020/02/25 17:03:13 millert Exp $ */ + +/* + * Copyright (c) 2011 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#if HAVE_SYS_MOUNT_H +#include <sys/mount.h> +#endif +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> +#ifdef HAVE_SYS_STATFS_H +#include <sys/statfs.h> +#endif + +#include <ctype.h> +#include <dirent.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <fts.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +#define PATH_QUEUE "/queue" +#define PATH_INCOMING "/incoming" +#define PATH_EVPTMP PATH_INCOMING "/envelope.tmp" +#define PATH_MESSAGE "/message" + +/* percentage of remaining space / inodes required to accept new messages */ +#define MINSPACE 5 +#define MININODES 5 + +struct qwalk { + FTS *fts; + int depth; +}; + +static int fsqueue_check_space(void); +static void fsqueue_envelope_path(uint64_t, char *, size_t); +static void fsqueue_envelope_incoming_path(uint64_t, char *, size_t); +static int fsqueue_envelope_dump(char *, const char *, size_t, int, int); +static void fsqueue_message_path(uint32_t, char *, size_t); +static void fsqueue_message_incoming_path(uint32_t, char *, size_t); +static void *fsqueue_qwalk_new(void); +static int fsqueue_qwalk(void *, uint64_t *); +static void fsqueue_qwalk_close(void *); + +struct tree evpcount; +static struct timespec startup; + +#define REF (int*)0xf00 + +static int +queue_fs_message_create(uint32_t *msgid) +{ + char rootdir[PATH_MAX]; + struct stat sb; + + if (!fsqueue_check_space()) + return 0; + +again: + *msgid = queue_generate_msgid(); + + /* prevent possible collision later when moving to Q_QUEUE */ + fsqueue_message_path(*msgid, rootdir, sizeof(rootdir)); + if (stat(rootdir, &sb) != -1) + goto again; + + /* we hit an unexpected error, temporarily fail */ + if (errno != ENOENT) { + *msgid = 0; + return 0; + } + + fsqueue_message_incoming_path(*msgid, rootdir, sizeof(rootdir)); + if (mkdir(rootdir, 0700) == -1) { + if (errno == EEXIST) + goto again; + + if (errno == ENOSPC) { + *msgid = 0; + return 0; + } + + log_warn("warn: queue-fs: mkdir"); + *msgid = 0; + return 0; + } + + return (1); +} + +static int +queue_fs_message_commit(uint32_t msgid, const char *path) +{ + char incomingdir[PATH_MAX]; + char queuedir[PATH_MAX]; + char msgdir[PATH_MAX]; + char msgpath[PATH_MAX]; + + /* before-first, move the message content in the incoming directory */ + fsqueue_message_incoming_path(msgid, msgpath, sizeof(msgpath)); + if (strlcat(msgpath, PATH_MESSAGE, sizeof(msgpath)) + >= sizeof(msgpath)) + return (0); + if (rename(path, msgpath) == -1) + return (0); + + fsqueue_message_incoming_path(msgid, incomingdir, sizeof(incomingdir)); + fsqueue_message_path(msgid, msgdir, sizeof(msgdir)); + if (strlcpy(queuedir, msgdir, sizeof(queuedir)) + >= sizeof(queuedir)) + return (0); + + /* first attempt to rename */ + if (rename(incomingdir, msgdir) == 0) + return 1; + if (errno == ENOSPC) + return 0; + if (errno != ENOENT) { + log_warn("warn: queue-fs: rename"); + return 0; + } + + /* create the bucket */ + *strrchr(queuedir, '/') = '\0'; + if (mkdir(queuedir, 0700) == -1) { + if (errno == ENOSPC) + return 0; + if (errno != EEXIST) { + log_warn("warn: queue-fs: mkdir"); + return 0; + } + } + + /* rename */ + if (rename(incomingdir, msgdir) == -1) { + if (errno == ENOSPC) + return 0; + log_warn("warn: queue-fs: rename"); + return 0; + } + + return 1; +} + +static int +queue_fs_message_fd_r(uint32_t msgid) +{ + int fd; + char path[PATH_MAX]; + + fsqueue_message_path(msgid, path, sizeof(path)); + if (strlcat(path, PATH_MESSAGE, sizeof(path)) + >= sizeof(path)) + return -1; + + if ((fd = open(path, O_RDONLY)) == -1) { + log_warn("warn: queue-fs: open"); + return -1; + } + + return fd; +} + +static int +queue_fs_message_delete(uint32_t msgid) +{ + char path[PATH_MAX]; + struct stat sb; + + fsqueue_message_incoming_path(msgid, path, sizeof(path)); + if (stat(path, &sb) == -1) + fsqueue_message_path(msgid, path, sizeof(path)); + + if (rmtree(path, 0) == -1) + log_warn("warn: queue-fs: rmtree"); + + tree_pop(&evpcount, msgid); + + return 1; +} + +static int +queue_fs_envelope_create(uint32_t msgid, const char *buf, size_t len, + uint64_t *evpid) +{ + char path[PATH_MAX]; + int queued = 0, i, r = 0, *n; + struct stat sb; + + if (msgid == 0) { + log_warnx("warn: queue-fs: msgid=0, evpid=%016"PRIx64, *evpid); + goto done; + } + + fsqueue_message_incoming_path(msgid, path, sizeof(path)); + if (stat(path, &sb) == -1) + queued = 1; + + for (i = 0; i < 20; i ++) { + *evpid = queue_generate_evpid(msgid); + if (queued) + fsqueue_envelope_path(*evpid, path, sizeof(path)); + else + fsqueue_envelope_incoming_path(*evpid, path, + sizeof(path)); + + if ((r = fsqueue_envelope_dump(path, buf, len, 0, 0)) != 0) + goto done; + } + r = 0; + log_warnx("warn: queue-fs: could not allocate evpid"); + +done: + if (r) { + n = tree_pop(&evpcount, msgid); + if (n == NULL) + n = REF; + n += 1; + tree_xset(&evpcount, msgid, n); + } + return (r); +} + +static int +queue_fs_envelope_load(uint64_t evpid, char *buf, size_t len) +{ + char pathname[PATH_MAX]; + FILE *fp; + size_t r; + + fsqueue_envelope_path(evpid, pathname, sizeof(pathname)); + + fp = fopen(pathname, "r"); + if (fp == NULL) { + if (errno != ENOENT && errno != ENFILE) + log_warn("warn: queue-fs: fopen"); + return 0; + } + + r = fread(buf, 1, len, fp); + if (r) { + if (r == len) { + log_warn("warn: queue-fs: too large"); + r = 0; + } + else + buf[r] = '\0'; + } + fclose(fp); + + return (r); +} + +static int +queue_fs_envelope_update(uint64_t evpid, const char *buf, size_t len) +{ + char dest[PATH_MAX]; + + fsqueue_envelope_path(evpid, dest, sizeof(dest)); + + return (fsqueue_envelope_dump(dest, buf, len, 1, 1)); +} + +static int +queue_fs_envelope_delete(uint64_t evpid) +{ + char pathname[PATH_MAX]; + uint32_t msgid; + int *n; + + fsqueue_envelope_path(evpid, pathname, sizeof(pathname)); + if (unlink(pathname) == -1) + if (errno != ENOENT) + return 0; + + msgid = evpid_to_msgid(evpid); + n = tree_pop(&evpcount, msgid); + n -= 1; + + if (n - REF == 0) + queue_fs_message_delete(msgid); + else + tree_xset(&evpcount, msgid, n); + + return (1); +} + +static int +queue_fs_message_walk(uint64_t *evpid, char *buf, size_t len, + uint32_t msgid, int *done, void **data) +{ + struct dirent *dp; + DIR *dir = *data; + char path[PATH_MAX]; + char msgid_str[9]; + char *tmp; + int r, *n; + + if (*done) + return (-1); + + if (!bsnprintf(path, sizeof path, "%s/%02x/%08x", + PATH_QUEUE, (msgid & 0xff000000) >> 24, msgid)) + fatalx("queue_fs_message_walk: path does not fit buffer"); + + if (dir == NULL) { + if ((dir = opendir(path)) == NULL) { + log_warn("warn: queue_fs: opendir: %s", path); + *done = 1; + return (-1); + } + + *data = dir; + } + + (void)snprintf(msgid_str, sizeof msgid_str, "%08" PRIx32, msgid); + while ((dp = readdir(dir)) != NULL) { +#if defined(HAVE_STRUCT_DIR_D_TYPE) + if (dp->d_type != DT_REG) + continue; +#endif + + /* ignore files other than envelopes */ + if (strlen(dp->d_name) != 16 || + strncmp(dp->d_name, msgid_str, 8)) + continue; + + tmp = NULL; + *evpid = strtoull(dp->d_name, &tmp, 16); + if (tmp && *tmp != '\0') { + log_debug("debug: fsqueue: bogus file %s", dp->d_name); + continue; + } + + memset(buf, 0, len); + r = queue_fs_envelope_load(*evpid, buf, len); + if (r) { + n = tree_pop(&evpcount, msgid); + if (n == NULL) + n = REF; + + n += 1; + tree_xset(&evpcount, msgid, n); + } + + return (r); + } + + (void)closedir(dir); + *done = 1; + return (-1); +} + +static int +queue_fs_envelope_walk(uint64_t *evpid, char *buf, size_t len) +{ + static int done = 0; + static void *hdl = NULL; + int r, *n; + uint32_t msgid; + + if (done) + return (-1); + + if (hdl == NULL) + hdl = fsqueue_qwalk_new(); + + if (fsqueue_qwalk(hdl, evpid)) { + memset(buf, 0, len); + r = queue_fs_envelope_load(*evpid, buf, len); + if (r) { + msgid = evpid_to_msgid(*evpid); + n = tree_pop(&evpcount, msgid); + if (n == NULL) + n = REF; + n += 1; + tree_xset(&evpcount, msgid, n); + } + return (r); + } + + fsqueue_qwalk_close(hdl); + done = 1; + return (-1); +} + +static int +fsqueue_check_space(void) +{ +#ifdef __OpenBSD__ + struct statfs buf; + uint64_t used; + uint64_t total; + + if (statfs(PATH_QUEUE, &buf) == -1) { + log_warn("warn: queue-fs: statfs"); + return 0; + } + + /* + * f_bfree and f_ffree is not set on all filesystems. + * They could be signed or unsigned integers. + * Some systems will set them to 0, others will set them to -1. + */ + if (buf.f_bfree == 0 || buf.f_ffree == 0 || + (int64_t)buf.f_bfree == -1 || (int64_t)buf.f_ffree == -1) + return 1; + + used = buf.f_blocks - buf.f_bfree; + total = buf.f_bavail + used; + if (total != 0) + used = (float)used / (float)total * 100; + else + used = 100; + if (100 - used < MINSPACE) { + log_warnx("warn: not enough disk space: %llu%% left", + (unsigned long long) 100 - used); + log_warnx("warn: temporarily rejecting messages"); + return 0; + } + + used = buf.f_files - buf.f_ffree; + total = buf.f_favail + used; + if (total != 0) + used = (float)used / (float)total * 100; + else + used = 100; + if (100 - used < MININODES) { + log_warnx("warn: not enough inodes: %llu%% left", + (unsigned long long) 100 - used); + log_warnx("warn: temporarily rejecting messages"); + return 0; + } +#endif + return 1; +} + +static void +fsqueue_envelope_path(uint64_t evpid, char *buf, size_t len) +{ + if (!bsnprintf(buf, len, "%s/%02x/%08x/%016" PRIx64, + PATH_QUEUE, + (evpid_to_msgid(evpid) & 0xff000000) >> 24, + evpid_to_msgid(evpid), + evpid)) + fatalx("fsqueue_envelope_path: path does not fit buffer"); +} + +static void +fsqueue_envelope_incoming_path(uint64_t evpid, char *buf, size_t len) +{ + if (!bsnprintf(buf, len, "%s/%08x/%016" PRIx64, + PATH_INCOMING, + evpid_to_msgid(evpid), + evpid)) + fatalx("fsqueue_envelope_incoming_path: path does not fit buffer"); +} + +static int +fsqueue_envelope_dump(char *dest, const char *evpbuf, size_t evplen, + int do_atomic, int do_sync) +{ + const char *path = do_atomic ? PATH_EVPTMP : dest; + FILE *fp = NULL; + int fd; + size_t w; + + if ((fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0600)) == -1) { + log_warn("warn: queue-fs: open"); + goto tempfail; + } + + if ((fp = fdopen(fd, "w")) == NULL) { + log_warn("warn: queue-fs: fdopen"); + goto tempfail; + } + + w = fwrite(evpbuf, 1, evplen, fp); + if (w < evplen) { + log_warn("warn: queue-fs: short write"); + goto tempfail; + } + if (fflush(fp)) { + log_warn("warn: queue-fs: fflush"); + goto tempfail; + } + if (do_sync && fsync(fileno(fp))) { + log_warn("warn: queue-fs: fsync"); + goto tempfail; + } + if (fclose(fp) != 0) { + log_warn("warn: queue-fs: fclose"); + fp = NULL; + goto tempfail; + } + fp = NULL; + fd = -1; + + if (do_atomic && rename(path, dest) == -1) { + log_warn("warn: queue-fs: rename"); + goto tempfail; + } + return (1); + +tempfail: + if (fp) + fclose(fp); + else if (fd != -1) + close(fd); + if (unlink(path) == -1) + log_warn("warn: queue-fs: unlink"); + return (0); +} + +static void +fsqueue_message_path(uint32_t msgid, char *buf, size_t len) +{ + if (!bsnprintf(buf, len, "%s/%02x/%08x", + PATH_QUEUE, + (msgid & 0xff000000) >> 24, + msgid)) + fatalx("fsqueue_message_path: path does not fit buffer"); +} + +static void +fsqueue_message_incoming_path(uint32_t msgid, char *buf, size_t len) +{ + if (!bsnprintf(buf, len, "%s/%08x", + PATH_INCOMING, + msgid)) + fatalx("fsqueue_message_incoming_path: path does not fit buffer"); +} + +static void * +fsqueue_qwalk_new(void) +{ + char path[PATH_MAX]; + char * const path_argv[] = { path, NULL }; + struct qwalk *q; + + q = xcalloc(1, sizeof(*q)); + (void)strlcpy(path, PATH_QUEUE, sizeof(path)); + q->fts = fts_open(path_argv, + FTS_PHYSICAL | FTS_NOCHDIR, NULL); + + if (q->fts == NULL) + err(1, "fsqueue_qwalk_new: fts_open: %s", path); + + return (q); +} + +static void +fsqueue_qwalk_close(void *hdl) +{ + struct qwalk *q = hdl; + + fts_close(q->fts); + + free(q); +} + +static int +fsqueue_qwalk(void *hdl, uint64_t *evpid) +{ + struct qwalk *q = hdl; + FTSENT *e; + char *tmp; + + while ((e = fts_read(q->fts)) != NULL) { + switch (e->fts_info) { + case FTS_D: + q->depth += 1; + if (q->depth == 2 && e->fts_namelen != 2) { + log_debug("debug: fsqueue: bogus directory %s", + e->fts_path); + fts_set(q->fts, e, FTS_SKIP); + break; + } + if (q->depth == 3 && e->fts_namelen != 8) { + log_debug("debug: fsqueue: bogus directory %s", + e->fts_path); + fts_set(q->fts, e, FTS_SKIP); + break; + } + break; + + case FTS_DP: + case FTS_DNR: + q->depth -= 1; + break; + + case FTS_F: + if (q->depth != 3) + break; + if (e->fts_namelen != 16) + break; +#if HAVE_STRUCT_STAT_ST_MTIM + if (timespeccmp(&e->fts_statp->st_mtim, &startup, >)) +#endif +#if HAVE_STRUCT_STAT_ST_MTIMSPEC + if (timespeccmp(&e->fts_statp->st_mtimspec, &startup, >)) +#endif + break; + tmp = NULL; + *evpid = strtoull(e->fts_name, &tmp, 16); + if (tmp && *tmp != '\0') { + log_debug("debug: fsqueue: bogus file %s", + e->fts_path); + break; + } + return (1); + default: + break; + } + } + + return (0); +} + +static int +queue_fs_init(struct passwd *pw, int server, const char *conf) +{ + unsigned int n; + char *paths[] = { PATH_QUEUE, PATH_INCOMING }; + char path[PATH_MAX]; + int ret; + + /* remove incoming/ if it exists */ + if (server) + mvpurge(PATH_SPOOL PATH_INCOMING, PATH_SPOOL PATH_PURGE); + + fsqueue_envelope_path(0, path, sizeof(path)); + + ret = 1; + for (n = 0; n < nitems(paths); n++) { + (void)strlcpy(path, PATH_SPOOL, sizeof(path)); + if (strlcat(path, paths[n], sizeof(path)) >= sizeof(path)) + errx(1, "path too long %s%s", PATH_SPOOL, paths[n]); + if (ckdir(path, 0700, pw->pw_uid, 0, server) == 0) + ret = 0; + } + + if (clock_gettime(CLOCK_REALTIME, &startup)) + err(1, "clock_gettime"); + + tree_init(&evpcount); + + queue_api_on_message_create(queue_fs_message_create); + queue_api_on_message_commit(queue_fs_message_commit); + queue_api_on_message_delete(queue_fs_message_delete); + queue_api_on_message_fd_r(queue_fs_message_fd_r); + queue_api_on_envelope_create(queue_fs_envelope_create); + queue_api_on_envelope_delete(queue_fs_envelope_delete); + queue_api_on_envelope_update(queue_fs_envelope_update); + queue_api_on_envelope_load(queue_fs_envelope_load); + queue_api_on_envelope_walk(queue_fs_envelope_walk); + queue_api_on_message_walk(queue_fs_message_walk); + + return (ret); +} + +struct queue_backend queue_backend_fs = { + queue_fs_init, +}; diff --git a/smtpd/queue_null.c b/smtpd/queue_null.c new file mode 100644 index 00000000..1e608be8 --- /dev/null +++ b/smtpd/queue_null.c @@ -0,0 +1,120 @@ +/* $OpenBSD: queue_null.c,v 1.8 2018/12/30 23:09:58 guenther Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +static int +queue_null_message_create(uint32_t *msgid) +{ + *msgid = queue_generate_msgid(); + return (1); +} + +static int +queue_null_message_commit(uint32_t msgid, const char *path) +{ + return (1); +} + +static int +queue_null_message_delete(uint32_t msgid) +{ + return (1); +} + +static int +queue_null_message_fd_r(uint32_t msgid) +{ + return (-1); +} + +static int +queue_null_envelope_create(uint32_t msgid, const char *buf, size_t len, + uint64_t *evpid) +{ + *evpid = queue_generate_evpid(msgid); + return (1); +} + +static int +queue_null_envelope_delete(uint64_t evpid) +{ + return (1); +} + +static int +queue_null_envelope_update(uint64_t evpid, const char *buf, size_t len) +{ + return (1); +} + +static int +queue_null_envelope_load(uint64_t evpid, char *buf, size_t len) +{ + return (0); +} + +static int +queue_null_envelope_walk(uint64_t *evpid, char *buf, size_t len) +{ + return (-1); +} + +static int +queue_null_init(struct passwd *pw, int server, const char *conf) +{ + queue_api_on_message_create(queue_null_message_create); + queue_api_on_message_commit(queue_null_message_commit); + queue_api_on_message_delete(queue_null_message_delete); + queue_api_on_message_fd_r(queue_null_message_fd_r); + queue_api_on_envelope_create(queue_null_envelope_create); + queue_api_on_envelope_delete(queue_null_envelope_delete); + queue_api_on_envelope_update(queue_null_envelope_update); + queue_api_on_envelope_load(queue_null_envelope_load); + queue_api_on_envelope_walk(queue_null_envelope_walk); + + return (1); +} + +struct queue_backend queue_backend_null = { + queue_null_init, +}; diff --git a/smtpd/queue_proc.c b/smtpd/queue_proc.c new file mode 100644 index 00000000..d6e0f409 --- /dev/null +++ b/smtpd/queue_proc.c @@ -0,0 +1,337 @@ +/* $OpenBSD: queue_proc.c,v 1.8 2018/12/30 23:09:58 guenther Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +static struct imsgbuf ibuf; +static struct imsg imsg; +static size_t rlen; +static char *rdata; + +static void +queue_proc_call(void) +{ + ssize_t n; + + if (imsg_flush(&ibuf) == -1) { + log_warn("warn: queue-proc: imsg_flush"); + fatalx("queue-proc: exiting"); + } + + while (1) { + if ((n = imsg_get(&ibuf, &imsg)) == -1) { + log_warn("warn: queue-proc: imsg_get"); + break; + } + if (n) { + rlen = imsg.hdr.len - IMSG_HEADER_SIZE; + rdata = imsg.data; + + if (imsg.hdr.type != PROC_QUEUE_OK) { + log_warnx("warn: queue-proc: bad response"); + break; + } + return; + } + + if ((n = imsg_read(&ibuf)) == -1 && errno != EAGAIN) { + log_warn("warn: queue-proc: imsg_read"); + break; + } + + if (n == 0) { + log_warnx("warn: queue-proc: pipe closed"); + break; + } + } + + fatalx("queue-proc: exiting"); +} + +static void +queue_proc_read(void *dst, size_t len) +{ + if (len > rlen) { + log_warnx("warn: queue-proc: bad msg len"); + fatalx("queue-proc: exiting"); + } + + memmove(dst, rdata, len); + rlen -= len; + rdata += len; +} + +static void +queue_proc_end(void) +{ + if (rlen) { + log_warnx("warn: queue-proc: bogus data"); + fatalx("queue-proc: exiting"); + } + imsg_free(&imsg); +} + +/* + * API + */ + +static int +queue_proc_close(void) +{ + int r; + + imsg_compose(&ibuf, PROC_QUEUE_CLOSE, 0, 0, -1, NULL, 0); + + queue_proc_call(); + queue_proc_read(&r, sizeof(r)); + queue_proc_end(); + + return (r); +} + +static int +queue_proc_message_create(uint32_t *msgid) +{ + int r; + + imsg_compose(&ibuf, PROC_QUEUE_MESSAGE_CREATE, 0, 0, -1, NULL, 0); + + queue_proc_call(); + queue_proc_read(&r, sizeof(r)); + if (r == 1) + queue_proc_read(msgid, sizeof(*msgid)); + queue_proc_end(); + + return (r); +} + +static int +queue_proc_message_commit(uint32_t msgid, const char *path) +{ + int r, fd; + + fd = open(path, O_RDONLY); + if (fd == -1) { + log_warn("queue-proc: open: %s", path); + return (0); + } + + imsg_compose(&ibuf, PROC_QUEUE_MESSAGE_COMMIT, 0, 0, fd, &msgid, + sizeof(msgid)); + + queue_proc_call(); + queue_proc_read(&r, sizeof(r)); + queue_proc_end(); + + return (r); +} + +static int +queue_proc_message_delete(uint32_t msgid) +{ + int r; + + imsg_compose(&ibuf, PROC_QUEUE_MESSAGE_DELETE, 0, 0, -1, &msgid, + sizeof(msgid)); + + queue_proc_call(); + queue_proc_read(&r, sizeof(r)); + queue_proc_end(); + + return (r); +} + +static int +queue_proc_message_fd_r(uint32_t msgid) +{ + imsg_compose(&ibuf, PROC_QUEUE_MESSAGE_FD_R, 0, 0, -1, &msgid, + sizeof(msgid)); + + queue_proc_call(); + queue_proc_end(); + + return (imsg.fd); +} + +static int +queue_proc_envelope_create(uint32_t msgid, const char *buf, size_t len, + uint64_t *evpid) +{ + struct ibuf *b; + int r; + + msgid = evpid_to_msgid(*evpid); + b = imsg_create(&ibuf, PROC_QUEUE_ENVELOPE_CREATE, 0, 0, + sizeof(msgid) + len); + if (imsg_add(b, &msgid, sizeof(msgid)) == -1 || + imsg_add(b, buf, len) == -1) + return (0); + imsg_close(&ibuf, b); + + queue_proc_call(); + queue_proc_read(&r, sizeof(r)); + if (r == 1) + queue_proc_read(evpid, sizeof(*evpid)); + queue_proc_end(); + + return (r); +} + +static int +queue_proc_envelope_delete(uint64_t evpid) +{ + int r; + + imsg_compose(&ibuf, PROC_QUEUE_ENVELOPE_DELETE, 0, 0, -1, &evpid, + sizeof(evpid)); + + queue_proc_call(); + queue_proc_read(&r, sizeof(r)); + queue_proc_end(); + + return (r); +} + +static int +queue_proc_envelope_update(uint64_t evpid, const char *buf, size_t len) +{ + struct ibuf *b; + int r; + + b = imsg_create(&ibuf, PROC_QUEUE_ENVELOPE_UPDATE, 0, 0, + len + sizeof(evpid)); + if (imsg_add(b, &evpid, sizeof(evpid)) == -1 || + imsg_add(b, buf, len) == -1) + return (0); + imsg_close(&ibuf, b); + + queue_proc_call(); + queue_proc_read(&r, sizeof(r)); + queue_proc_end(); + + return (r); +} + +static int +queue_proc_envelope_load(uint64_t evpid, char *buf, size_t len) +{ + int r; + + imsg_compose(&ibuf, PROC_QUEUE_ENVELOPE_LOAD, 0, 0, -1, &evpid, + sizeof(evpid)); + + queue_proc_call(); + + if (rlen > len) { + log_warnx("warn: queue-proc: buf too small"); + fatalx("queue-proc: exiting"); + } + + r = rlen; + queue_proc_read(buf, rlen); + queue_proc_end(); + + return (r); +} + +static int +queue_proc_envelope_walk(uint64_t *evpid, char *buf, size_t len) +{ + int r; + + imsg_compose(&ibuf, PROC_QUEUE_ENVELOPE_WALK, 0, 0, -1, NULL, 0); + + queue_proc_call(); + queue_proc_read(&r, sizeof(r)); + + if (r > 0) { + queue_proc_read(evpid, sizeof(*evpid)); + if (rlen > len) { + log_warnx("warn: queue-proc: buf too small"); + fatalx("queue-proc: exiting"); + } + if (r != (int)rlen) { + log_warnx("warn: queue-proc: len mismatch"); + fatalx("queue-proc: exiting"); + } + queue_proc_read(buf, rlen); + } + queue_proc_end(); + + return (r); +} + +static int +queue_proc_init(struct passwd *pw, int server, const char *conf) +{ + uint32_t version; + int fd; + + fd = fork_proc_backend("queue", conf, "queue-proc"); + if (fd == -1) + fatalx("queue-proc: exiting"); + + imsg_init(&ibuf, fd); + + version = PROC_QUEUE_API_VERSION; + imsg_compose(&ibuf, PROC_QUEUE_INIT, 0, 0, -1, + &version, sizeof(version)); + + queue_api_on_close(queue_proc_close); + queue_api_on_message_create(queue_proc_message_create); + queue_api_on_message_commit(queue_proc_message_commit); + queue_api_on_message_delete(queue_proc_message_delete); + queue_api_on_message_fd_r(queue_proc_message_fd_r); + queue_api_on_envelope_create(queue_proc_envelope_create); + queue_api_on_envelope_delete(queue_proc_envelope_delete); + queue_api_on_envelope_update(queue_proc_envelope_update); + queue_api_on_envelope_load(queue_proc_envelope_load); + queue_api_on_envelope_walk(queue_proc_envelope_walk); + + queue_proc_call(); + queue_proc_end(); + + return (1); +} + +struct queue_backend queue_backend_proc = { + queue_proc_init, +}; diff --git a/smtpd/queue_ram.c b/smtpd/queue_ram.c new file mode 100644 index 00000000..50ce17e1 --- /dev/null +++ b/smtpd/queue_ram.c @@ -0,0 +1,336 @@ +/* $OpenBSD: queue_ram.c,v 1.9 2018/12/30 23:09:58 guenther Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +struct qr_envelope { + char *buf; + size_t len; +}; + +struct qr_message { + char *buf; + size_t len; + struct tree envelopes; +}; + +static struct tree messages; + +static struct qr_message * +get_message(uint32_t msgid) +{ + struct qr_message *msg; + + msg = tree_get(&messages, msgid); + if (msg == NULL) + log_warn("warn: queue-ram: message not found"); + + return (msg); +} + +static int +queue_ram_message_create(uint32_t *msgid) +{ + struct qr_message *msg; + + msg = calloc(1, sizeof(*msg)); + if (msg == NULL) { + log_warn("warn: queue-ram: calloc"); + return (0); + } + tree_init(&msg->envelopes); + + do { + *msgid = queue_generate_msgid(); + } while (tree_check(&messages, *msgid)); + + tree_xset(&messages, *msgid, msg); + + return (1); +} + +static int +queue_ram_message_commit(uint32_t msgid, const char *path) +{ + struct qr_message *msg; + struct stat sb; + size_t n; + FILE *f; + int ret; + + if ((msg = tree_get(&messages, msgid)) == NULL) { + log_warnx("warn: queue-ram: msgid not found"); + return (0); + } + + f = fopen(path, "rb"); + if (f == NULL) { + log_warn("warn: queue-ram: fopen: %s", path); + return (0); + } + if (fstat(fileno(f), &sb) == -1) { + log_warn("warn: queue-ram: fstat"); + fclose(f); + return (0); + } + + msg->len = sb.st_size; + msg->buf = malloc(msg->len); + if (msg->buf == NULL) { + log_warn("warn: queue-ram: malloc"); + fclose(f); + return (0); + } + + ret = 0; + n = fread(msg->buf, 1, msg->len, f); + if (ferror(f)) + log_warn("warn: queue-ram: fread"); + else if ((off_t)n != sb.st_size) + log_warnx("warn: queue-ram: bad read"); + else { + ret = 1; + stat_increment("queue.ram.message.size", msg->len); + } + fclose(f); + + return (ret); +} + +static int +queue_ram_message_delete(uint32_t msgid) +{ + struct qr_message *msg; + struct qr_envelope *evp; + uint64_t evpid; + + if ((msg = tree_pop(&messages, msgid)) == NULL) { + log_warnx("warn: queue-ram: not found"); + return (0); + } + while (tree_poproot(&messages, &evpid, (void**)&evp)) { + stat_decrement("queue.ram.envelope.size", evp->len); + free(evp->buf); + free(evp); + } + stat_decrement("queue.ram.message.size", msg->len); + free(msg->buf); + free(msg); + return (0); +} + +static int +queue_ram_message_fd_r(uint32_t msgid) +{ + struct qr_message *msg; + size_t n; + FILE *f; + int fd, fd2; + + if ((msg = tree_get(&messages, msgid)) == NULL) { + log_warnx("warn: queue-ram: not found"); + return (-1); + } + + fd = mktmpfile(); + if (fd == -1) { + log_warn("warn: queue-ram: mktmpfile"); + return (-1); + } + + fd2 = dup(fd); + if (fd2 == -1) { + log_warn("warn: queue-ram: dup"); + close(fd); + return (-1); + } + f = fdopen(fd2, "w"); + if (f == NULL) { + log_warn("warn: queue-ram: fdopen"); + close(fd); + close(fd2); + return (-1); + } + n = fwrite(msg->buf, 1, msg->len, f); + if (n != msg->len) { + log_warn("warn: queue-ram: write"); + close(fd); + fclose(f); + return (-1); + } + fclose(f); + lseek(fd, 0, SEEK_SET); + return (fd); +} + +static int +queue_ram_envelope_create(uint32_t msgid, const char *buf, size_t len, + uint64_t *evpid) +{ + struct qr_envelope *evp; + struct qr_message *msg; + + if ((msg = get_message(msgid)) == NULL) + return (0); + + do { + *evpid = queue_generate_evpid(msgid); + } while (tree_check(&msg->envelopes, *evpid)); + evp = calloc(1, sizeof *evp); + if (evp == NULL) { + log_warn("warn: queue-ram: calloc"); + return (0); + } + evp->len = len; + evp->buf = malloc(len); + if (evp->buf == NULL) { + log_warn("warn: queue-ram: malloc"); + free(evp); + return (0); + } + memmove(evp->buf, buf, len); + tree_xset(&msg->envelopes, *evpid, evp); + stat_increment("queue.ram.envelope.size", len); + return (1); +} + +static int +queue_ram_envelope_delete(uint64_t evpid) +{ + struct qr_envelope *evp; + struct qr_message *msg; + + if ((msg = get_message(evpid_to_msgid(evpid))) == NULL) + return (0); + + if ((evp = tree_pop(&msg->envelopes, evpid)) == NULL) { + log_warnx("warn: queue-ram: not found"); + return (0); + } + stat_decrement("queue.ram.envelope.size", evp->len); + free(evp->buf); + free(evp); + if (tree_empty(&msg->envelopes)) { + tree_xpop(&messages, evpid_to_msgid(evpid)); + stat_decrement("queue.ram.message.size", msg->len); + free(msg->buf); + free(msg); + } + return (1); +} + +static int +queue_ram_envelope_update(uint64_t evpid, const char *buf, size_t len) +{ + struct qr_envelope *evp; + struct qr_message *msg; + void *tmp; + + if ((msg = get_message(evpid_to_msgid(evpid))) == NULL) + return (0); + + if ((evp = tree_get(&msg->envelopes, evpid)) == NULL) { + log_warn("warn: queue-ram: not found"); + return (0); + } + tmp = malloc(len); + if (tmp == NULL) { + log_warn("warn: queue-ram: malloc"); + return (0); + } + memmove(tmp, buf, len); + free(evp->buf); + evp->len = len; + evp->buf = tmp; + stat_decrement("queue.ram.envelope.size", evp->len); + stat_increment("queue.ram.envelope.size", len); + return (1); +} + +static int +queue_ram_envelope_load(uint64_t evpid, char *buf, size_t len) +{ + struct qr_envelope *evp; + struct qr_message *msg; + + if ((msg = get_message(evpid_to_msgid(evpid))) == NULL) + return (0); + + if ((evp = tree_get(&msg->envelopes, evpid)) == NULL) { + log_warn("warn: queue-ram: not found"); + return (0); + } + if (len < evp->len) { + log_warnx("warn: queue-ram: buffer too small"); + return (0); + } + memmove(buf, evp->buf, evp->len); + return (evp->len); +} + +static int +queue_ram_envelope_walk(uint64_t *evpid, char *buf, size_t len) +{ + return (-1); +} + +static int +queue_ram_init(struct passwd *pw, int server, const char * conf) +{ + tree_init(&messages); + + queue_api_on_message_create(queue_ram_message_create); + queue_api_on_message_commit(queue_ram_message_commit); + queue_api_on_message_delete(queue_ram_message_delete); + queue_api_on_message_fd_r(queue_ram_message_fd_r); + queue_api_on_envelope_create(queue_ram_envelope_create); + queue_api_on_envelope_delete(queue_ram_envelope_delete); + queue_api_on_envelope_update(queue_ram_envelope_update); + queue_api_on_envelope_load(queue_ram_envelope_load); + queue_api_on_envelope_walk(queue_ram_envelope_walk); + + return (1); +} + +struct queue_backend queue_backend_ram = { + queue_ram_init, +}; diff --git a/smtpd/report_smtp.c b/smtpd/report_smtp.c new file mode 100644 index 00000000..7802eaae --- /dev/null +++ b/smtpd/report_smtp.c @@ -0,0 +1,335 @@ +/* $OpenBSD: report_smtp.c,v 1.11 2020/01/07 23:03:37 gilles Exp $ */ + +/* + * Copyright (c) 2018 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <netinet/in.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <limits.h> +#include <inttypes.h> +#include <openssl/ssl.h> +#include <resolv.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#if defined(HAVE_VIS_H) && !defined(BROKEN_STRNVIS) +#include <vis.h> +#else +#include "bsd-vis.h" +#endif + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" +#include "rfc5322.h" + +void +report_smtp_link_connect(const char *direction, uint64_t qid, const char *rdns, int fcrdns, + const struct sockaddr_storage *ss_src, + const struct sockaddr_storage *ss_dest) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_LINK_CONNECT, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_string(p_lka, rdns); + m_add_int(p_lka, fcrdns); + m_add_sockaddr(p_lka, (const struct sockaddr *)ss_src); + m_add_sockaddr(p_lka, (const struct sockaddr *)ss_dest); + m_close(p_lka); +} + +void +report_smtp_link_greeting(const char *direction, uint64_t qid, + const char *domain) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_LINK_GREETING, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_string(p_lka, domain); + m_close(p_lka); +} + +void +report_smtp_link_identify(const char *direction, uint64_t qid, const char *method, const char *identity) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_LINK_IDENTIFY, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_string(p_lka, method); + m_add_string(p_lka, identity); + m_close(p_lka); +} + +void +report_smtp_link_tls(const char *direction, uint64_t qid, const char *ssl) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_LINK_TLS, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_string(p_lka, ssl); + m_close(p_lka); +} + +void +report_smtp_link_disconnect(const char *direction, uint64_t qid) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_LINK_DISCONNECT, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_close(p_lka); +} + +void +report_smtp_link_auth(const char *direction, uint64_t qid, const char *user, const char *result) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_LINK_AUTH, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_string(p_lka, user); + m_add_string(p_lka, result); + m_close(p_lka); +} + +void +report_smtp_tx_reset(const char *direction, uint64_t qid, uint32_t msgid) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TX_RESET, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_u32(p_lka, msgid); + m_close(p_lka); +} + +void +report_smtp_tx_begin(const char *direction, uint64_t qid, uint32_t msgid) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TX_BEGIN, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_u32(p_lka, msgid); + m_close(p_lka); +} + +void +report_smtp_tx_mail(const char *direction, uint64_t qid, uint32_t msgid, const char *address, int ok) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TX_MAIL, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_u32(p_lka, msgid); + m_add_string(p_lka, address); + m_add_int(p_lka, ok); + m_close(p_lka); +} + +void +report_smtp_tx_rcpt(const char *direction, uint64_t qid, uint32_t msgid, const char *address, int ok) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TX_RCPT, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_u32(p_lka, msgid); + m_add_string(p_lka, address); + m_add_int(p_lka, ok); + m_close(p_lka); +} + +void +report_smtp_tx_envelope(const char *direction, uint64_t qid, uint32_t msgid, uint64_t evpid) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TX_ENVELOPE, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_u32(p_lka, msgid); + m_add_id(p_lka, evpid); + m_close(p_lka); +} + +void +report_smtp_tx_data(const char *direction, uint64_t qid, uint32_t msgid, int ok) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TX_DATA, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_u32(p_lka, msgid); + m_add_int(p_lka, ok); + m_close(p_lka); +} + +void +report_smtp_tx_commit(const char *direction, uint64_t qid, uint32_t msgid, size_t msgsz) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TX_COMMIT, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_u32(p_lka, msgid); + m_add_size(p_lka, msgsz); + m_close(p_lka); +} + +void +report_smtp_tx_rollback(const char *direction, uint64_t qid, uint32_t msgid) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TX_ROLLBACK, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_u32(p_lka, msgid); + m_close(p_lka); +} + +void +report_smtp_protocol_client(const char *direction, uint64_t qid, const char *command) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_PROTOCOL_CLIENT, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_string(p_lka, command); + m_close(p_lka); +} + +void +report_smtp_protocol_server(const char *direction, uint64_t qid, const char *response) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_PROTOCOL_SERVER, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_string(p_lka, response); + m_close(p_lka); +} + +void +report_smtp_filter_response(const char *direction, uint64_t qid, int phase, int response, const char *param) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_FILTER_RESPONSE, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_add_int(p_lka, phase); + m_add_int(p_lka, response); + m_add_string(p_lka, param); + m_close(p_lka); +} + +void +report_smtp_timeout(const char *direction, uint64_t qid) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + + m_create(p_lka, IMSG_REPORT_SMTP_TIMEOUT, 0, 0, -1); + m_add_string(p_lka, direction); + m_add_timeval(p_lka, &tv); + m_add_id(p_lka, qid); + m_close(p_lka); +} diff --git a/smtpd/resolver.c b/smtpd/resolver.c new file mode 100644 index 00000000..f0f0f8ea --- /dev/null +++ b/smtpd/resolver.c @@ -0,0 +1,462 @@ +/* $OpenBSD: resolver.c,v 1.5 2019/06/13 11:45:35 eric Exp $ */ + +/* + * Copyright (c) 2017-2018 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/tree.h> +#include <sys/queue.h> +#include <netinet/in.h> + +#include <asr.h> +#include <ctype.h> +#include <errno.h> +#include <imsg.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +#define p_resolver p_lka + +struct request { + SPLAY_ENTRY(request) entry; + uint32_t id; + void (*cb_ai)(void *, int, struct addrinfo *); + void (*cb_ni)(void *, int, const char *, const char *); + void (*cb_res)(void *, int, int, int, const void *, int); + void *arg; + struct addrinfo *ai; +}; + +struct session { + uint32_t reqid; + struct mproc *proc; + char *host; + char *serv; +}; + +SPLAY_HEAD(reqtree, request); + +static void resolver_init(void); +static void resolver_getaddrinfo_cb(struct asr_result *, void *); +static void resolver_getnameinfo_cb(struct asr_result *, void *); +static void resolver_res_query_cb(struct asr_result *, void *); + +static int request_cmp(struct request *, struct request *); +SPLAY_PROTOTYPE(reqtree, request, entry, request_cmp); + +/* musl work-around */ +void portable_freeaddrinfo(struct addrinfo *); + +static struct reqtree reqs; + +void +resolver_getaddrinfo(const char *hostname, const char *servname, + const struct addrinfo *hints, void (*cb)(void *, int, struct addrinfo *), + void *arg) +{ + struct request *req; + + resolver_init(); + + req = calloc(1, sizeof(*req)); + if (req == NULL) { + cb(arg, EAI_MEMORY, NULL); + return; + } + + while (req->id == 0 || SPLAY_FIND(reqtree, &reqs, req)) + req->id = arc4random(); + req->cb_ai = cb; + req->arg = arg; + + SPLAY_INSERT(reqtree, &reqs, req); + + m_create(p_resolver, IMSG_GETADDRINFO, req->id, 0, -1); + m_add_int(p_resolver, hints ? hints->ai_flags : 0); + m_add_int(p_resolver, hints ? hints->ai_family : 0); + m_add_int(p_resolver, hints ? hints->ai_socktype : 0); + m_add_int(p_resolver, hints ? hints->ai_protocol : 0); + m_add_string(p_resolver, hostname); + m_add_string(p_resolver, servname); + m_close(p_resolver); +} + +void +resolver_getnameinfo(const struct sockaddr *sa, int flags, + void(*cb)(void *, int, const char *, const char *), void *arg) +{ + struct request *req; + + resolver_init(); + + req = calloc(1, sizeof(*req)); + if (req == NULL) { + cb(arg, EAI_MEMORY, NULL, NULL); + return; + } + + while (req->id == 0 || SPLAY_FIND(reqtree, &reqs, req)) + req->id = arc4random(); + req->cb_ni = cb; + req->arg = arg; + + SPLAY_INSERT(reqtree, &reqs, req); + + m_create(p_resolver, IMSG_GETNAMEINFO, req->id, 0, -1); + m_add_sockaddr(p_resolver, sa); + m_add_int(p_resolver, flags); + m_close(p_resolver); +} + +void +resolver_res_query(const char *dname, int class, int type, + void (*cb)(void *, int, int, int, const void *, int), void *arg) +{ + struct request *req; + + resolver_init(); + + req = calloc(1, sizeof(*req)); + if (req == NULL) { + cb(arg, NETDB_INTERNAL, 0, 0, NULL, 0); + return; + } + + while (req->id == 0 || SPLAY_FIND(reqtree, &reqs, req)) + req->id = arc4random(); + req->cb_res = cb; + req->arg = arg; + + SPLAY_INSERT(reqtree, &reqs, req); + + m_create(p_resolver, IMSG_RES_QUERY, req->id, 0, -1); + m_add_string(p_resolver, dname); + m_add_int(p_resolver, class); + m_add_int(p_resolver, type); + m_close(p_resolver); +} + +void +resolver_dispatch_request(struct mproc *proc, struct imsg *imsg) +{ + const char *hostname, *servname, *dname; + struct session *s; + struct asr_query *q; + struct addrinfo hints; + struct sockaddr_storage ss; + struct sockaddr *sa; + struct msg m; + uint32_t reqid; + int class, type, flags, save_errno; + + reqid = imsg->hdr.peerid; + m_msg(&m, imsg); + + switch (imsg->hdr.type) { + + case IMSG_GETADDRINFO: + servname = NULL; + memset(&hints, 0 , sizeof(hints)); + m_get_int(&m, &hints.ai_flags); + m_get_int(&m, &hints.ai_family); + m_get_int(&m, &hints.ai_socktype); + m_get_int(&m, &hints.ai_protocol); + m_get_string(&m, &hostname); + m_get_string(&m, &servname); + m_end(&m); + + s = NULL; + q = NULL; + if ((s = calloc(1, sizeof(*s))) && + (q = getaddrinfo_async(hostname, servname, &hints, NULL)) && + (event_asr_run(q, resolver_getaddrinfo_cb, s))) { + s->reqid = reqid; + s->proc = proc; + break; + } + save_errno = errno; + + if (q) + asr_abort(q); + if (s) + free(s); + + m_create(proc, IMSG_GETADDRINFO_END, reqid, 0, -1); + m_add_int(proc, EAI_SYSTEM); + m_add_int(proc, save_errno); + m_close(proc); + break; + + case IMSG_GETNAMEINFO: + sa = (struct sockaddr*)&ss; + m_get_sockaddr(&m, sa); + m_get_int(&m, &flags); + m_end(&m); + + s = NULL; + q = NULL; + if ((s = calloc(1, sizeof(*s))) && + (s->host = malloc(NI_MAXHOST)) && + (s->serv = malloc(NI_MAXSERV)) && + (q = getnameinfo_async(sa, SA_LEN(sa), s->host, NI_MAXHOST, + s->serv, NI_MAXSERV, flags, NULL)) && + (event_asr_run(q, resolver_getnameinfo_cb, s))) { + s->reqid = reqid; + s->proc = proc; + break; + } + save_errno = errno; + + if (q) + asr_abort(q); + if (s) { + free(s->host); + free(s->serv); + free(s); + } + + m_create(proc, IMSG_GETNAMEINFO, reqid, 0, -1); + m_add_int(proc, EAI_SYSTEM); + m_add_int(proc, save_errno); + m_add_string(proc, NULL); + m_add_string(proc, NULL); + m_close(proc); + break; + + case IMSG_RES_QUERY: + m_get_string(&m, &dname); + m_get_int(&m, &class); + m_get_int(&m, &type); + m_end(&m); + + s = NULL; + q = NULL; + if ((s = calloc(1, sizeof(*s))) && + (q = res_query_async(dname, class, type, NULL)) && + (event_asr_run(q, resolver_res_query_cb, s))) { + s->reqid = reqid; + s->proc = proc; + break; + } + save_errno = errno; + + if (q) + asr_abort(q); + if (s) + free(s); + + m_create(proc, IMSG_RES_QUERY, reqid, 0, -1); + m_add_int(proc, NETDB_INTERNAL); + m_add_int(proc, save_errno); + m_add_int(proc, 0); + m_add_int(proc, 0); + m_add_data(proc, NULL, 0); + m_close(proc); + break; + + default: + fatalx("%s: %s", __func__, imsg_to_str(imsg->hdr.type)); + } +} + +void +resolver_dispatch_result(struct mproc *proc, struct imsg *imsg) +{ + struct request key, *req; + struct sockaddr_storage ss; + struct addrinfo *ai; + struct msg m; + const char *cname, *host, *serv; + const void *data; + size_t datalen; + int gai_errno, herrno, rcode, count; + + key.id = imsg->hdr.peerid; + req = SPLAY_FIND(reqtree, &reqs, &key); + if (req == NULL) + fatalx("%s: unknown request %08x", __func__, imsg->hdr.peerid); + + m_msg(&m, imsg); + + switch (imsg->hdr.type) { + + case IMSG_GETADDRINFO: + ai = calloc(1, sizeof(*ai)); + if (ai == NULL) { + log_warn("%s: calloc", __func__); + break; + } + m_get_int(&m, &ai->ai_flags); + m_get_int(&m, &ai->ai_family); + m_get_int(&m, &ai->ai_socktype); + m_get_int(&m, &ai->ai_protocol); + m_get_sockaddr(&m, (struct sockaddr *)&ss); + m_get_string(&m, &cname); + m_end(&m); + + ai->ai_addr = malloc(SS_LEN(&ss)); + if (ai->ai_addr == NULL) { + log_warn("%s: malloc", __func__); + free(ai); + break; + } + + memmove(ai->ai_addr, &ss, SS_LEN(&ss)); + + if (cname) { + ai->ai_canonname = strdup(cname); + if (ai->ai_canonname == NULL) { + log_warn("%s: strdup", __func__); + free(ai->ai_addr); + free(ai); + break; + } + } + + ai->ai_next = req->ai; + req->ai = ai; + break; + + case IMSG_GETADDRINFO_END: + m_get_int(&m, &gai_errno); + m_get_int(&m, &errno); + m_end(&m); + + SPLAY_REMOVE(reqtree, &reqs, req); + req->cb_ai(req->arg, gai_errno, req->ai); + free(req); + break; + + case IMSG_GETNAMEINFO: + m_get_int(&m, &gai_errno); + m_get_int(&m, &errno); + m_get_string(&m, &host); + m_get_string(&m, &serv); + m_end(&m); + + SPLAY_REMOVE(reqtree, &reqs, req); + req->cb_ni(req->arg, gai_errno, host, serv); + free(req); + break; + + case IMSG_RES_QUERY: + m_get_int(&m, &herrno); + m_get_int(&m, &errno); + m_get_int(&m, &rcode); + m_get_int(&m, &count); + m_get_data(&m, &data, &datalen); + m_end(&m); + + SPLAY_REMOVE(reqtree, &reqs, req); + req->cb_res(req->arg, herrno, rcode, count, data, datalen); + free(req); + break; + } +} + +static void +resolver_init(void) +{ + static int init = 0; + + if (init == 0) { + SPLAY_INIT(&reqs); + init = 1; + } +} + +static void +resolver_getaddrinfo_cb(struct asr_result *ar, void *arg) +{ + struct session *s = arg; + struct addrinfo *ai; + + for (ai = ar->ar_addrinfo; ai; ai = ai->ai_next) { + m_create(s->proc, IMSG_GETADDRINFO, s->reqid, 0, -1); + m_add_int(s->proc, ai->ai_flags); + m_add_int(s->proc, ai->ai_family); + m_add_int(s->proc, ai->ai_socktype); + m_add_int(s->proc, ai->ai_protocol); + m_add_sockaddr(s->proc, ai->ai_addr); + m_add_string(s->proc, ai->ai_canonname); + m_close(s->proc); + } + + m_create(s->proc, IMSG_GETADDRINFO_END, s->reqid, 0, -1); + m_add_int(s->proc, ar->ar_gai_errno); + m_add_int(s->proc, ar->ar_errno); + m_close(s->proc); + + if (ar->ar_addrinfo) + portable_freeaddrinfo(ar->ar_addrinfo); + free(s); +} + +static void +resolver_getnameinfo_cb(struct asr_result *ar, void *arg) +{ + struct session *s = arg; + + m_create(s->proc, IMSG_GETNAMEINFO, s->reqid, 0, -1); + m_add_int(s->proc, ar->ar_gai_errno); + m_add_int(s->proc, ar->ar_errno); + m_add_string(s->proc, ar->ar_gai_errno ? NULL : s->host); + m_add_string(s->proc, ar->ar_gai_errno ? NULL : s->serv); + m_close(s->proc); + + free(s->host); + free(s->serv); + free(s); +} + +static void +resolver_res_query_cb(struct asr_result *ar, void *arg) +{ + struct session *s = arg; + + m_create(s->proc, IMSG_RES_QUERY, s->reqid, 0, -1); + m_add_int(s->proc, ar->ar_h_errno); + m_add_int(s->proc, ar->ar_errno); + m_add_int(s->proc, ar->ar_rcode); + m_add_int(s->proc, ar->ar_count); + m_add_data(s->proc, ar->ar_data, ar->ar_datalen); + m_close(s->proc); + + free(ar->ar_data); + free(s); +} + +static int +request_cmp(struct request *a, struct request *b) +{ + if (a->id < b->id) + return -1; + if (a->id > b->id) + return 1; + return 0; +} + +SPLAY_GENERATE(reqtree, request, entry, request_cmp); diff --git a/smtpd/rfc5322.c b/smtpd/rfc5322.c new file mode 100644 index 00000000..0af66772 --- /dev/null +++ b/smtpd/rfc5322.c @@ -0,0 +1,266 @@ +/* $OpenBSD: rfc5322.c,v 1.2 2018/10/24 18:59:29 gilles Exp $ */ + +/* + * Copyright (c) 2018 Eric Faurot <eric@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 "includes.h" + +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <stdlib.h> +#include <string.h> + +#include "rfc5322.h" + +struct buf { + char *buf; + size_t bufsz; + size_t buflen; + size_t bufmax; +}; + +static int buf_alloc(struct buf *, size_t); +static int buf_grow(struct buf *, size_t); +static int buf_cat(struct buf *, const char *); + +struct rfc5322_parser { + const char *line; + int state; /* last parser state */ + int next; /* parser needs data */ + int unfold; + const char *currhdr; + struct buf hdr; + struct buf val; +}; + +struct rfc5322_parser * +rfc5322_parser_new(void) +{ + struct rfc5322_parser *parser; + + parser = calloc(1, sizeof(*parser)); + if (parser == NULL) + return NULL; + + rfc5322_clear(parser); + parser->hdr.bufmax = 1024; + parser->val.bufmax = 65536; + + return parser; +} + +void +rfc5322_free(struct rfc5322_parser *parser) +{ + free(parser->hdr.buf); + free(parser->val.buf); + free(parser); +} + +void +rfc5322_clear(struct rfc5322_parser *parser) +{ + parser->line = NULL; + parser->state = RFC5322_NONE; + parser->next = 0; + parser->hdr.buflen = 0; + parser->val.buflen = 0; +} + +int +rfc5322_push(struct rfc5322_parser *parser, const char *line) +{ + if (parser->line) { + errno = EALREADY; + return -1; + } + + parser->line = line; + parser->next = 0; + + return 0; +} + +int +rfc5322_unfold_header(struct rfc5322_parser *parser) +{ + if (parser->unfold) { + errno = EALREADY; + return -1; + } + + if (parser->currhdr == NULL) { + errno = EOPNOTSUPP; + return -1; + } + + if (buf_cat(&parser->val, parser->currhdr) == -1) + return -1; + + parser->currhdr = NULL; + parser->unfold = 1; + + return 0; +} + +static int +_rfc5322_next(struct rfc5322_parser *parser, struct rfc5322_result *res) +{ + size_t len; + const char *pos, *line; + + line = parser->line; + + switch(parser->state) { + + case RFC5322_HEADER_START: + case RFC5322_HEADER_CONT: + res->hdr = parser->hdr.buf; + + if (line && (line[0] == ' ' || line[0] == '\t')) { + parser->line = NULL; + parser->next = 1; + if (parser->unfold) { + if (buf_cat(&parser->val, "\n") == -1 || + buf_cat(&parser->val, line) == -1) + return -1; + } + res->value = line; + return RFC5322_HEADER_CONT; + } + + if (parser->unfold) { + parser->val.buflen = 0; + parser->unfold = 0; + res->value = parser->val.buf; + } + return RFC5322_HEADER_END; + + case RFC5322_NONE: + case RFC5322_HEADER_END: + if (line && (pos = strchr(line, ':'))) { + len = pos - line; + if (buf_grow(&parser->hdr, len + 1) == -1) + return -1; + (void)memcpy(parser->hdr.buf, line, len); + parser->hdr.buf[len] = '\0'; + parser->hdr.buflen = len + 1; + parser->line = NULL; + parser->next = 1; + parser->currhdr = pos + 1; + res->hdr = parser->hdr.buf; + res->value = pos + 1; + return RFC5322_HEADER_START; + } + + return RFC5322_END_OF_HEADERS; + + case RFC5322_END_OF_HEADERS: + if (line == NULL) + return RFC5322_END_OF_MESSAGE; + + if (line[0] == '\0') { + parser->line = NULL; + parser->next = 1; + res->value = line; + return RFC5322_BODY_START; + } + + errno = EINVAL; + return -1; + + case RFC5322_BODY_START: + case RFC5322_BODY: + if (line == NULL) + return RFC5322_END_OF_MESSAGE; + + parser->line = NULL; + parser->next = 1; + res->value = line; + return RFC5322_BODY; + + case RFC5322_END_OF_MESSAGE: + errno = ENOMSG; + return -1; + + default: + errno = EINVAL; + return -1; + } +} + +int +rfc5322_next(struct rfc5322_parser *parser, struct rfc5322_result *res) +{ + memset(res, 0, sizeof(*res)); + + if (parser->next) + return RFC5322_NONE; + + return (parser->state = _rfc5322_next(parser, res)); +} + +static int +buf_alloc(struct buf *b, size_t need) +{ + char *buf; + size_t alloc; + + if (b->buf && b->bufsz >= need) + return 0; + + if (need >= b->bufmax) { + errno = ERANGE; + return -1; + } + +#define N 256 + alloc = N * (need / N) + ((need % N) ? N : 0); +#undef N + buf = reallocarray(b->buf, alloc, 1); + if (buf == NULL) + return -1; + + b->buf = buf; + b->bufsz = alloc; + + return 0; +} + +static int +buf_grow(struct buf *b, size_t sz) +{ + if (SIZE_T_MAX - b->buflen <= sz) { + errno = ERANGE; + return -1; + } + + return buf_alloc(b, b->buflen + sz); +} + +static int +buf_cat(struct buf *b, const char *s) +{ + size_t len = strlen(s); + + if (buf_grow(b, len + 1) == -1) + return -1; + + (void)memmove(b->buf + b->buflen, s, len + 1); + b->buflen += len; + return 0; +} diff --git a/smtpd/rfc5322.h b/smtpd/rfc5322.h new file mode 100644 index 00000000..0979bd4c --- /dev/null +++ b/smtpd/rfc5322.h @@ -0,0 +1,41 @@ +/* $OpenBSD: rfc5322.h,v 1.1 2018/08/23 10:07:06 eric Exp $ */ + +/* + * Copyright (c) 2018 Eric Faurot <eric@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. + */ + +struct rfc5322_result { + const char *hdr; + const char *value; +}; + +#define RFC5322_ERR -1 +#define RFC5322_NONE 0 +#define RFC5322_HEADER_START 1 +#define RFC5322_HEADER_CONT 2 +#define RFC5322_HEADER_END 3 +#define RFC5322_END_OF_HEADERS 4 +#define RFC5322_BODY_START 5 +#define RFC5322_BODY 6 +#define RFC5322_END_OF_MESSAGE 7 + +struct rfc5322_parser; + +struct rfc5322_parser *rfc5322_parser_new(void); +void rfc5322_free(struct rfc5322_parser *); +void rfc5322_clear(struct rfc5322_parser *); +int rfc5322_push(struct rfc5322_parser *, const char *); +int rfc5322_next(struct rfc5322_parser *, struct rfc5322_result *); +int rfc5322_unfold_header(struct rfc5322_parser *); diff --git a/smtpd/ruleset.c b/smtpd/ruleset.c new file mode 100644 index 00000000..719a2913 --- /dev/null +++ b/smtpd/ruleset.c @@ -0,0 +1,265 @@ +/* $OpenBSD: ruleset.c,v 1.47 2019/11/25 14:18:33 gilles Exp $ */ + +/* + * Copyright (c) 2009 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <netinet/in.h> + +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <string.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +#define MATCH_RESULT(r, neg) ((r) == -1 ? -1 : ((neg) < 0 ? !(r) : (r))) + +static int +ruleset_match_tag(struct rule *r, const struct envelope *evp) +{ + int ret; + struct table *table; + enum table_service service = K_STRING; + + if (!r->flag_tag) + return 1; + + if (r->flag_tag_regex) + service = K_REGEX; + + table = table_find(env, r->table_tag); + ret = table_match(table, service, evp->tag); + + return MATCH_RESULT(ret, r->flag_tag); +} + +static int +ruleset_match_from(struct rule *r, const struct envelope *evp) +{ + int ret; + int has_rdns; + const char *key; + struct table *table; + enum table_service service = K_NETADDR; + + if (!r->flag_from) + return 1; + + if (evp->flags & EF_INTERNAL) { + /* if expanded from an empty table_from, skip rule + * if no table + */ + if (r->table_from == NULL) + return 0; + key = "local"; + } + else if (r->flag_from_rdns) { + has_rdns = strcmp(evp->hostname, "<unknown>") != 0; + if (r->table_from == NULL) + return MATCH_RESULT(has_rdns, r->flag_from); + if (!has_rdns) + return 0; + key = evp->hostname; + } + else { + key = ss_to_text(&evp->ss); + if (r->flag_from_socket) { + if (strcmp(key, "local") == 0) + return MATCH_RESULT(1, r->flag_from); + else + return r->flag_from < 0 ? 1 : 0; + } + } + if (r->flag_from_regex) + service = K_REGEX; + + table = table_find(env, r->table_from); + ret = table_match(table, service, key); + + return MATCH_RESULT(ret, r->flag_from); +} + +static int +ruleset_match_to(struct rule *r, const struct envelope *evp) +{ + int ret; + struct table *table; + enum table_service service = K_DOMAIN; + + if (!r->flag_for) + return 1; + + if (r->flag_for_regex) + service = K_REGEX; + + table = table_find(env, r->table_for); + ret = table_match(table, service, evp->dest.domain); + + return MATCH_RESULT(ret, r->flag_for); +} + +static int +ruleset_match_smtp_helo(struct rule *r, const struct envelope *evp) +{ + int ret; + struct table *table; + enum table_service service = K_DOMAIN; + + if (!r->flag_smtp_helo) + return 1; + + if (r->flag_smtp_helo_regex) + service = K_REGEX; + + table = table_find(env, r->table_smtp_helo); + ret = table_match(table, service, evp->helo); + + return MATCH_RESULT(ret, r->flag_smtp_helo); +} + +static int +ruleset_match_smtp_starttls(struct rule *r, const struct envelope *evp) +{ + if (!r->flag_smtp_starttls) + return 1; + + /* XXX - not until TLS flag is added to envelope */ + return -1; +} + +static int +ruleset_match_smtp_auth(struct rule *r, const struct envelope *evp) +{ + int ret; + struct table *table; + enum table_service service; + + if (!r->flag_smtp_auth) + return 1; + + if (!(evp->flags & EF_AUTHENTICATED)) + ret = 0; + else if (r->table_smtp_auth) { + + if (r->flag_smtp_auth_regex) + service = K_REGEX; + else + service = strchr(evp->username, '@') ? + K_MAILADDR : K_STRING; + table = table_find(env, r->table_smtp_auth); + ret = table_match(table, service, evp->username); + } + else + ret = 1; + + return MATCH_RESULT(ret, r->flag_smtp_auth); +} + +static int +ruleset_match_smtp_mail_from(struct rule *r, const struct envelope *evp) +{ + int ret; + const char *key; + struct table *table; + enum table_service service = K_MAILADDR; + + if (!r->flag_smtp_mail_from) + return 1; + + if (r->flag_smtp_mail_from_regex) + service = K_REGEX; + + if ((key = mailaddr_to_text(&evp->sender)) == NULL) + return -1; + + table = table_find(env, r->table_smtp_mail_from); + ret = table_match(table, service, key); + + return MATCH_RESULT(ret, r->flag_smtp_mail_from); +} + +static int +ruleset_match_smtp_rcpt_to(struct rule *r, const struct envelope *evp) +{ + int ret; + const char *key; + struct table *table; + enum table_service service = K_MAILADDR; + + if (!r->flag_smtp_rcpt_to) + return 1; + + if (r->flag_smtp_rcpt_to_regex) + service = K_REGEX; + + if ((key = mailaddr_to_text(&evp->dest)) == NULL) + return -1; + + table = table_find(env, r->table_smtp_rcpt_to); + ret = table_match(table, service, key); + + return MATCH_RESULT(ret, r->flag_smtp_rcpt_to); +} + +struct rule * +ruleset_match(const struct envelope *evp) +{ + struct rule *r; + int i = 0; + +#define MATCH_EVAL(x) \ + switch ((x)) { \ + case -1: goto tempfail; \ + case 0: continue; \ + default: break; \ + } + TAILQ_FOREACH(r, env->sc_rules, r_entry) { + ++i; + MATCH_EVAL(ruleset_match_tag(r, evp)); + MATCH_EVAL(ruleset_match_from(r, evp)); + MATCH_EVAL(ruleset_match_to(r, evp)); + MATCH_EVAL(ruleset_match_smtp_helo(r, evp)); + MATCH_EVAL(ruleset_match_smtp_auth(r, evp)); + MATCH_EVAL(ruleset_match_smtp_starttls(r, evp)); + MATCH_EVAL(ruleset_match_smtp_mail_from(r, evp)); + MATCH_EVAL(ruleset_match_smtp_rcpt_to(r, evp)); + goto matched; + } +#undef MATCH_EVAL + + errno = 0; + log_trace(TRACE_RULES, "no rule matched"); + return (NULL); + +tempfail: + errno = EAGAIN; + log_trace(TRACE_RULES, "temporary failure in processing of a rule"); + return (NULL); + +matched: + log_trace(TRACE_RULES, "rule #%d matched: %s", i, rule_to_text(r)); + return r; +} diff --git a/smtpd/runq.c b/smtpd/runq.c new file mode 100644 index 00000000..786d36fb --- /dev/null +++ b/smtpd/runq.c @@ -0,0 +1,183 @@ +/* $OpenBSD: runq.c,v 1.3 2019/06/14 19:55:25 eric Exp $ */ + +/* + * Copyright (c) 2013,2019 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/uio.h> + +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <limits.h> +#include <time.h> + +#include "smtpd.h" + +struct job { + TAILQ_ENTRY(job) entry; + time_t when; + void *arg; +}; + +struct runq { + TAILQ_HEAD(, job) jobs; + void (*cb)(struct runq *, void *); + struct event ev; +}; + +static void runq_timeout(int, short, void *); + +static struct runq *active; + +static void +runq_reset(struct runq *runq) +{ + struct timeval tv; + struct job *job; + time_t now; + + job = TAILQ_FIRST(&runq->jobs); + if (job == NULL) + return; + + now = time(NULL); + if (job->when <= now) + tv.tv_sec = 0; + else + tv.tv_sec = job->when - now; + tv.tv_usec = 0; + evtimer_add(&runq->ev, &tv); +} + +static void +runq_timeout(int fd, short ev, void *arg) +{ + struct runq *runq = arg; + struct job *job; + time_t now; + + active = runq; + now = time(NULL); + + while((job = TAILQ_FIRST(&runq->jobs))) { + if (job->when > now) + break; + TAILQ_REMOVE(&runq->jobs, job, entry); + runq->cb(runq, job->arg); + free(job); + } + + active = NULL; + runq_reset(runq); +} + +int +runq_init(struct runq **runqp, void (*cb)(struct runq *, void *)) +{ + struct runq *runq; + + runq = malloc(sizeof(*runq)); + if (runq == NULL) + return (0); + + runq->cb = cb; + TAILQ_INIT(&runq->jobs); + evtimer_set(&runq->ev, runq_timeout, runq); + + *runqp = runq; + + return (1); +} + +int +runq_schedule(struct runq *runq, time_t delay, void *arg) +{ + time_t t; + + time(&t); + return runq_schedule_at(runq, t + delay, arg); +} + +int +runq_schedule_at(struct runq *runq, time_t when, void *arg) +{ + struct job *job, *tmpjob; + + job = malloc(sizeof(*job)); + if (job == NULL) + return (0); + + job->arg = arg; + job->when = when; + + TAILQ_FOREACH(tmpjob, &runq->jobs, entry) { + if (tmpjob->when > job->when) { + TAILQ_INSERT_BEFORE(tmpjob, job, entry); + goto done; + } + } + TAILQ_INSERT_TAIL(&runq->jobs, job, entry); + + done: + if (runq != active && job == TAILQ_FIRST(&runq->jobs)) { + evtimer_del(&runq->ev); + runq_reset(runq); + } + return (1); +} + +int +runq_cancel(struct runq *runq, void *arg) +{ + struct job *job, *first; + + first = TAILQ_FIRST(&runq->jobs); + TAILQ_FOREACH(job, &runq->jobs, entry) { + if (job->arg == arg) { + TAILQ_REMOVE(&runq->jobs, job, entry); + free(job); + if (runq != active && job == first) { + evtimer_del(&runq->ev); + runq_reset(runq); + } + return (1); + } + } + + return (0); +} + +int +runq_pending(struct runq *runq, void *arg, time_t *when) +{ + struct job *job; + + TAILQ_FOREACH(job, &runq->jobs, entry) { + if (job->arg == arg) { + if (when) + *when = job->when; + return (1); + } + } + + return (0); +} diff --git a/smtpd/scheduler.c b/smtpd/scheduler.c new file mode 100644 index 00000000..ea70a83d --- /dev/null +++ b/smtpd/scheduler.c @@ -0,0 +1,618 @@ +/* $OpenBSD: scheduler.c,v 1.60 2018/12/30 23:09:58 guenther Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008-2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <dirent.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <grp.h> /* needed for setgroups */ +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +static void scheduler_imsg(struct mproc *, struct imsg *); +static void scheduler_shutdown(void); +static void scheduler_reset_events(void); +static void scheduler_timeout(int, short, void *); + +static struct scheduler_backend *backend = NULL; +static struct event ev; +static size_t ninflight = 0; +static int *types; +static uint64_t *evpids; +static uint32_t *msgids; +static struct evpstate *state; + +extern const char *backend_scheduler; + +void +scheduler_imsg(struct mproc *p, struct imsg *imsg) +{ + struct bounce_req_msg req; + struct envelope evp; + struct scheduler_info si; + struct msg m; + uint64_t evpid, id, holdq; + uint32_t msgid; + uint32_t inflight; + size_t n, i; + time_t timestamp; + int v, r, type; + + if (imsg == NULL) + scheduler_shutdown(); + + switch (imsg->hdr.type) { + + case IMSG_QUEUE_ENVELOPE_SUBMIT: + m_msg(&m, imsg); + m_get_envelope(&m, &evp); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: inserting evp:%016" PRIx64, evp.id); + scheduler_info(&si, &evp); + stat_increment("scheduler.envelope.incoming", 1); + backend->insert(&si); + return; + + case IMSG_QUEUE_MESSAGE_COMMIT: + m_msg(&m, imsg); + m_get_msgid(&m, &msgid); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: committing msg:%08" PRIx32, msgid); + n = backend->commit(msgid); + stat_decrement("scheduler.envelope.incoming", n); + stat_increment("scheduler.envelope", n); + scheduler_reset_events(); + return; + + case IMSG_QUEUE_DISCOVER_EVPID: + m_msg(&m, imsg); + m_get_envelope(&m, &evp); + m_end(&m); + r = backend->query(evp.id); + if (r) { + log_debug("debug: scheduler: evp:%016" PRIx64 + " already scheduled", evp.id); + return; + } + log_trace(TRACE_SCHEDULER, + "scheduler: discovering evp:%016" PRIx64, evp.id); + scheduler_info(&si, &evp); + stat_increment("scheduler.envelope.incoming", 1); + backend->insert(&si); + return; + + case IMSG_QUEUE_DISCOVER_MSGID: + m_msg(&m, imsg); + m_get_msgid(&m, &msgid); + m_end(&m); + r = backend->query(msgid); + if (r) { + log_debug("debug: scheduler: msgid:%08" PRIx32 + " already scheduled", msgid); + return; + } + log_trace(TRACE_SCHEDULER, + "scheduler: committing msg:%08" PRIx32, msgid); + n = backend->commit(msgid); + stat_decrement("scheduler.envelope.incoming", n); + stat_increment("scheduler.envelope", n); + scheduler_reset_events(); + return; + + case IMSG_QUEUE_MESSAGE_ROLLBACK: + m_msg(&m, imsg); + m_get_msgid(&m, &msgid); + m_end(&m); + log_trace(TRACE_SCHEDULER, "scheduler: aborting msg:%08" PRIx32, + msgid); + n = backend->rollback(msgid); + stat_decrement("scheduler.envelope.incoming", n); + scheduler_reset_events(); + return; + + case IMSG_QUEUE_ENVELOPE_REMOVE: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_get_u32(&m, &inflight); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: queue requested removal of evp:%016" PRIx64, + evpid); + stat_decrement("scheduler.envelope", 1); + if (!inflight) + backend->remove(evpid); + else { + backend->delete(evpid); + ninflight -= 1; + stat_decrement("scheduler.envelope.inflight", 1); + } + + scheduler_reset_events(); + return; + + case IMSG_QUEUE_ENVELOPE_ACK: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: queue ack removal of evp:%016" PRIx64, + evpid); + ninflight -= 1; + stat_decrement("scheduler.envelope.inflight", 1); + scheduler_reset_events(); + return; + + case IMSG_QUEUE_DELIVERY_OK: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: deleting evp:%016" PRIx64 " (ok)", evpid); + backend->delete(evpid); + ninflight -= 1; + stat_increment("scheduler.delivery.ok", 1); + stat_decrement("scheduler.envelope.inflight", 1); + stat_decrement("scheduler.envelope", 1); + scheduler_reset_events(); + return; + + case IMSG_QUEUE_DELIVERY_TEMPFAIL: + m_msg(&m, imsg); + m_get_envelope(&m, &evp); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: updating evp:%016" PRIx64, evp.id); + scheduler_info(&si, &evp); + backend->update(&si); + ninflight -= 1; + stat_increment("scheduler.delivery.tempfail", 1); + stat_decrement("scheduler.envelope.inflight", 1); + + for (i = 0; i < MAX_BOUNCE_WARN; i++) { + if (env->sc_bounce_warn[i] == 0) + break; + timestamp = si.creation + env->sc_bounce_warn[i]; + if (si.nexttry >= timestamp && + si.lastbounce < timestamp) { + req.evpid = evp.id; + req.timestamp = timestamp; + req.bounce.type = B_DELAYED; + req.bounce.delay = env->sc_bounce_warn[i]; + req.bounce.ttl = si.ttl; + m_compose(p, IMSG_SCHED_ENVELOPE_BOUNCE, 0, 0, -1, + &req, sizeof req); + break; + } + } + scheduler_reset_events(); + return; + + case IMSG_QUEUE_DELIVERY_PERMFAIL: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: deleting evp:%016" PRIx64 " (fail)", evpid); + backend->delete(evpid); + ninflight -= 1; + stat_increment("scheduler.delivery.permfail", 1); + stat_decrement("scheduler.envelope.inflight", 1); + stat_decrement("scheduler.envelope", 1); + scheduler_reset_events(); + return; + + case IMSG_QUEUE_DELIVERY_LOOP: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: deleting evp:%016" PRIx64 " (loop)", evpid); + backend->delete(evpid); + ninflight -= 1; + stat_increment("scheduler.delivery.loop", 1); + stat_decrement("scheduler.envelope.inflight", 1); + stat_decrement("scheduler.envelope", 1); + scheduler_reset_events(); + return; + + case IMSG_QUEUE_HOLDQ_HOLD: + m_msg(&m, imsg); + m_get_evpid(&m, &evpid); + m_get_id(&m, &holdq); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: holding evp:%016" PRIx64 " on %016" PRIx64, + evpid, holdq); + backend->hold(evpid, holdq); + ninflight -= 1; + stat_decrement("scheduler.envelope.inflight", 1); + scheduler_reset_events(); + return; + + case IMSG_QUEUE_HOLDQ_RELEASE: + m_msg(&m, imsg); + m_get_int(&m, &type); + m_get_id(&m, &holdq); + m_get_int(&m, &r); + m_end(&m); + log_trace(TRACE_SCHEDULER, + "scheduler: releasing %d on holdq (%d, %016" PRIx64 ")", + r, type, holdq); + backend->release(type, holdq, r); + scheduler_reset_events(); + return; + + case IMSG_CTL_PAUSE_MDA: + log_trace(TRACE_SCHEDULER, "scheduler: pausing mda"); + env->sc_flags |= SMTPD_MDA_PAUSED; + return; + + case IMSG_CTL_RESUME_MDA: + log_trace(TRACE_SCHEDULER, "scheduler: resuming mda"); + env->sc_flags &= ~SMTPD_MDA_PAUSED; + scheduler_reset_events(); + return; + + case IMSG_CTL_PAUSE_MTA: + log_trace(TRACE_SCHEDULER, "scheduler: pausing mta"); + env->sc_flags |= SMTPD_MTA_PAUSED; + return; + + case IMSG_CTL_RESUME_MTA: + log_trace(TRACE_SCHEDULER, "scheduler: resuming mta"); + env->sc_flags &= ~SMTPD_MTA_PAUSED; + scheduler_reset_events(); + return; + + case IMSG_CTL_VERBOSE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + log_setverbose(v); + return; + + case IMSG_CTL_PROFILE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + profiling = v; + return; + + case IMSG_CTL_LIST_MESSAGES: + msgid = *(uint32_t *)(imsg->data); + n = backend->messages(msgid, msgids, env->sc_scheduler_max_msg_batch_size); + m_compose(p, IMSG_CTL_LIST_MESSAGES, imsg->hdr.peerid, 0, -1, + msgids, n * sizeof (*msgids)); + return; + + case IMSG_CTL_LIST_ENVELOPES: + id = *(uint64_t *)(imsg->data); + n = backend->envelopes(id, state, env->sc_scheduler_max_evp_batch_size); + for (i = 0; i < n; i++) { + m_create(p_queue, IMSG_CTL_LIST_ENVELOPES, + imsg->hdr.peerid, 0, -1); + m_add_evpid(p_queue, state[i].evpid); + m_add_int(p_queue, state[i].flags); + m_add_time(p_queue, state[i].time); + m_close(p_queue); + } + m_compose(p_queue, IMSG_CTL_LIST_ENVELOPES, + imsg->hdr.peerid, 0, -1, NULL, 0); + return; + + case IMSG_CTL_SCHEDULE: + id = *(uint64_t *)(imsg->data); + if (id <= 0xffffffffL) + log_debug("debug: scheduler: " + "scheduling msg:%08" PRIx64, id); + else + log_debug("debug: scheduler: " + "scheduling evp:%016" PRIx64, id); + r = backend->schedule(id); + scheduler_reset_events(); + m_compose(p, r ? IMSG_CTL_OK : IMSG_CTL_FAIL, imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + + case IMSG_QUEUE_ENVELOPE_SCHEDULE: + id = *(uint64_t *)(imsg->data); + backend->schedule(id); + scheduler_reset_events(); + return; + + case IMSG_CTL_REMOVE: + id = *(uint64_t *)(imsg->data); + if (id <= 0xffffffffL) + log_debug("debug: scheduler: " + "removing msg:%08" PRIx64, id); + else + log_debug("debug: scheduler: " + "removing evp:%016" PRIx64, id); + r = backend->remove(id); + scheduler_reset_events(); + m_compose(p, r ? IMSG_CTL_OK : IMSG_CTL_FAIL, imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + + case IMSG_CTL_PAUSE_EVP: + id = *(uint64_t *)(imsg->data); + if (id <= 0xffffffffL) + log_debug("debug: scheduler: " + "suspending msg:%08" PRIx64, id); + else + log_debug("debug: scheduler: " + "suspending evp:%016" PRIx64, id); + r = backend->suspend(id); + scheduler_reset_events(); + m_compose(p, r ? IMSG_CTL_OK : IMSG_CTL_FAIL, imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + + case IMSG_CTL_RESUME_EVP: + id = *(uint64_t *)(imsg->data); + if (id <= 0xffffffffL) + log_debug("debug: scheduler: " + "resuming msg:%08" PRIx64, id); + else + log_debug("debug: scheduler: " + "resuming evp:%016" PRIx64, id); + r = backend->resume(id); + scheduler_reset_events(); + m_compose(p, r ? IMSG_CTL_OK : IMSG_CTL_FAIL, imsg->hdr.peerid, + 0, -1, NULL, 0); + return; + } + + errx(1, "scheduler_imsg: unexpected %s imsg", + imsg_to_str(imsg->hdr.type)); +} + +static void +scheduler_shutdown(void) +{ + log_debug("debug: scheduler agent exiting"); + _exit(0); +} + +static void +scheduler_reset_events(void) +{ + struct timeval tv; + + evtimer_del(&ev); + tv.tv_sec = 0; + tv.tv_usec = 0; + evtimer_add(&ev, &tv); +} + +int +scheduler(void) +{ + struct passwd *pw; + + backend = scheduler_backend_lookup(backend_scheduler); + if (backend == NULL) + errx(1, "cannot find scheduler backend \"%s\"", + backend_scheduler); + + purge_config(PURGE_EVERYTHING & ~PURGE_DISPATCHERS); + + if ((pw = getpwnam(SMTPD_USER)) == NULL) + fatalx("unknown user " SMTPD_USER); + + config_process(PROC_SCHEDULER); + + backend->init(backend_scheduler); + + if (chroot(PATH_CHROOT) == -1) + fatal("scheduler: chroot"); + if (chdir("/") == -1) + fatal("scheduler: chdir(\"/\")"); + + 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("scheduler: cannot drop privileges"); + + evpids = xcalloc(env->sc_scheduler_max_schedule, sizeof *evpids); + types = xcalloc(env->sc_scheduler_max_schedule, sizeof *types); + msgids = xcalloc(env->sc_scheduler_max_msg_batch_size, sizeof *msgids); + state = xcalloc(env->sc_scheduler_max_evp_batch_size, sizeof *state); + + imsg_callback = scheduler_imsg; + event_init(); + + signal(SIGINT, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peer(PROC_CONTROL); + config_peer(PROC_QUEUE); + + evtimer_set(&ev, scheduler_timeout, NULL); + scheduler_reset_events(); + +#if HAVE_PLEDGE + if (pledge("stdio", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + fatalx("exited event loop"); + + return (0); +} + +static void +scheduler_timeout(int fd, short event, void *p) +{ + struct timeval tv; + size_t i; + size_t d_inflight; + size_t d_envelope; + size_t d_removed; + size_t d_expired; + size_t d_updated; + size_t count; + int mask, r, delay; + + tv.tv_sec = 0; + tv.tv_usec = 0; + + mask = SCHED_UPDATE; + + if (ninflight < env->sc_scheduler_max_inflight) { + mask |= SCHED_EXPIRE | SCHED_REMOVE | SCHED_BOUNCE; + if (!(env->sc_flags & SMTPD_MDA_PAUSED)) + mask |= SCHED_MDA; + if (!(env->sc_flags & SMTPD_MTA_PAUSED)) + mask |= SCHED_MTA; + } + + count = env->sc_scheduler_max_schedule; + + log_trace(TRACE_SCHEDULER, "scheduler: getting batch: mask=0x%x, count=%zu", mask, count); + + r = backend->batch(mask, &delay, &count, evpids, types); + + log_trace(TRACE_SCHEDULER, "scheduler: got r=%i, delay=%i, count=%zu", r, delay, count); + + if (r < 0) + fatalx("scheduler: error in batch handler"); + + if (r == 0) { + + if (delay < -1) + fatalx("scheduler: invalid delay %d", delay); + + if (delay == -1) { + log_trace(TRACE_SCHEDULER, "scheduler: sleeping"); + return; + } + + tv.tv_sec = delay; + tv.tv_usec = 0; + log_trace(TRACE_SCHEDULER, + "scheduler: waiting for %s", duration_to_text(tv.tv_sec)); + evtimer_add(&ev, &tv); + return; + } + + d_inflight = 0; + d_envelope = 0; + d_removed = 0; + d_expired = 0; + d_updated = 0; + + for (i = 0; i < count; i++) { + switch(types[i]) { + case SCHED_REMOVE: + log_debug("debug: scheduler: evp:%016" PRIx64 + " removed", evpids[i]); + m_create(p_queue, IMSG_SCHED_ENVELOPE_REMOVE, 0, 0, -1); + m_add_evpid(p_queue, evpids[i]); + m_close(p_queue); + d_envelope += 1; + d_removed += 1; + d_inflight += 1; + break; + + case SCHED_EXPIRE: + log_debug("debug: scheduler: evp:%016" PRIx64 + " expired", evpids[i]); + m_create(p_queue, IMSG_SCHED_ENVELOPE_EXPIRE, 0, 0, -1); + m_add_evpid(p_queue, evpids[i]); + m_close(p_queue); + d_envelope += 1; + d_expired += 1; + d_inflight += 1; + break; + + case SCHED_UPDATE: + log_debug("debug: scheduler: evp:%016" PRIx64 + " scheduled (update)", evpids[i]); + d_updated += 1; + break; + + case SCHED_BOUNCE: + log_debug("debug: scheduler: evp:%016" PRIx64 + " scheduled (bounce)", evpids[i]); + m_create(p_queue, IMSG_SCHED_ENVELOPE_INJECT, 0, 0, -1); + m_add_evpid(p_queue, evpids[i]); + m_close(p_queue); + d_inflight += 1; + break; + + case SCHED_MDA: + log_debug("debug: scheduler: evp:%016" PRIx64 + " scheduled (mda)", evpids[i]); + m_create(p_queue, IMSG_SCHED_ENVELOPE_DELIVER, 0, 0, -1); + m_add_evpid(p_queue, evpids[i]); + m_close(p_queue); + d_inflight += 1; + break; + + case SCHED_MTA: + log_debug("debug: scheduler: evp:%016" PRIx64 + " scheduled (mta)", evpids[i]); + m_create(p_queue, IMSG_SCHED_ENVELOPE_TRANSFER, 0, 0, -1); + m_add_evpid(p_queue, evpids[i]); + m_close(p_queue); + d_inflight += 1; + break; + } + } + + stat_decrement("scheduler.envelope", d_envelope); + stat_increment("scheduler.envelope.inflight", d_inflight); + stat_increment("scheduler.envelope.expired", d_expired); + stat_increment("scheduler.envelope.removed", d_removed); + stat_increment("scheduler.envelope.updated", d_updated); + + ninflight += d_inflight; + + tv.tv_sec = 0; + tv.tv_usec = 0; + evtimer_add(&ev, &tv); +} diff --git a/smtpd/scheduler_backend.c b/smtpd/scheduler_backend.c new file mode 100644 index 00000000..ad2b4cab --- /dev/null +++ b/smtpd/scheduler_backend.c @@ -0,0 +1,82 @@ +/* $OpenBSD: scheduler_backend.c,v 1.16 2018/05/24 11:38:24 gilles Exp $ */ + +/* + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +extern struct scheduler_backend scheduler_backend_null; +extern struct scheduler_backend scheduler_backend_proc; +extern struct scheduler_backend scheduler_backend_ramqueue; + +struct scheduler_backend * +scheduler_backend_lookup(const char *name) +{ + if (!strcmp(name, "null")) + return &scheduler_backend_null; + if (!strcmp(name, "ramqueue")) + return &scheduler_backend_ramqueue; + + return &scheduler_backend_proc; +} + +void +scheduler_info(struct scheduler_info *sched, struct envelope *evp) +{ + struct dispatcher *disp; + + disp = evp->type == D_BOUNCE ? + env->sc_dispatcher_bounce : + dict_xget(env->sc_dispatchers, evp->dispatcher); + + switch (disp->type) { + case DISPATCHER_LOCAL: + sched->type = D_MDA; + break; + case DISPATCHER_REMOTE: + sched->type = D_MTA; + break; + case DISPATCHER_BOUNCE: + sched->type = D_BOUNCE; + break; + } + sched->ttl = disp->ttl ? disp->ttl : env->sc_ttl; + + sched->evpid = evp->id; + sched->creation = evp->creation; + sched->retry = evp->retry; + sched->lasttry = evp->lasttry; + sched->lastbounce = evp->lastbounce; + sched->nexttry = 0; +} diff --git a/smtpd/scheduler_null.c b/smtpd/scheduler_null.c new file mode 100644 index 00000000..40db6205 --- /dev/null +++ b/smtpd/scheduler_null.c @@ -0,0 +1,164 @@ +/* $OpenBSD: scheduler_null.c,v 1.9 2015/01/20 17:37:54 deraadt Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <stdio.h> +#include <limits.h> + +#include "smtpd.h" + +static int scheduler_null_init(const char *); +static int scheduler_null_insert(struct scheduler_info *); +static size_t scheduler_null_commit(uint32_t); +static size_t scheduler_null_rollback(uint32_t); +static int scheduler_null_update(struct scheduler_info *); +static int scheduler_null_delete(uint64_t); +static int scheduler_null_hold(uint64_t, uint64_t); +static int scheduler_null_release(int, uint64_t, int); +static int scheduler_null_batch(int, int*, size_t*, uint64_t*, int*); +static size_t scheduler_null_messages(uint32_t, uint32_t *, size_t); +static size_t scheduler_null_envelopes(uint64_t, struct evpstate *, size_t); +static int scheduler_null_schedule(uint64_t); +static int scheduler_null_remove(uint64_t); +static int scheduler_null_suspend(uint64_t); +static int scheduler_null_resume(uint64_t); + +struct scheduler_backend scheduler_backend_null = { + scheduler_null_init, + + scheduler_null_insert, + scheduler_null_commit, + scheduler_null_rollback, + + scheduler_null_update, + scheduler_null_delete, + scheduler_null_hold, + scheduler_null_release, + + scheduler_null_batch, + + scheduler_null_messages, + scheduler_null_envelopes, + scheduler_null_schedule, + scheduler_null_remove, + scheduler_null_suspend, + scheduler_null_resume, +}; + +static int +scheduler_null_init(const char *arg) +{ + return (1); +} + +static int +scheduler_null_insert(struct scheduler_info *si) +{ + return (0); +} + +static size_t +scheduler_null_commit(uint32_t msgid) +{ + return (0); +} + +static size_t +scheduler_null_rollback(uint32_t msgid) +{ + return (0); +} + +static int +scheduler_null_update(struct scheduler_info *si) +{ + return (0); +} + +static int +scheduler_null_delete(uint64_t evpid) +{ + return (0); +} + +static int +scheduler_null_hold(uint64_t evpid, uint64_t holdq) +{ + return (0); +} + +static int +scheduler_null_release(int type, uint64_t holdq, int n) +{ + return (0); +} + +static int +scheduler_null_batch(int typemask, int *delay, size_t *count, uint64_t *evpids, int *types) +{ + *delay = 0; + + return (0); +} + +static int +scheduler_null_schedule(uint64_t evpid) +{ + return (0); +} + +static int +scheduler_null_remove(uint64_t evpid) +{ + return (0); +} + +static int +scheduler_null_suspend(uint64_t evpid) +{ + return (0); +} + +static int +scheduler_null_resume(uint64_t evpid) +{ + return (0); +} + +static size_t +scheduler_null_messages(uint32_t from, uint32_t *dst, size_t size) +{ + return (0); +} + +static size_t +scheduler_null_envelopes(uint64_t from, struct evpstate *dst, size_t size) +{ + return (0); +} diff --git a/smtpd/scheduler_proc.c b/smtpd/scheduler_proc.c new file mode 100644 index 00000000..5f4e8b70 --- /dev/null +++ b/smtpd/scheduler_proc.c @@ -0,0 +1,446 @@ +/* $OpenBSD: scheduler_proc.c,v 1.8 2015/12/05 13:14:21 claudio Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + +static struct imsgbuf ibuf; +static struct imsg imsg; +static size_t rlen; +static char *rdata; + +static void +scheduler_proc_call(void) +{ + ssize_t n; + + if (imsg_flush(&ibuf) == -1) { + log_warn("warn: scheduler-proc: imsg_flush"); + fatalx("scheduler-proc: exiting"); + } + + while (1) { + if ((n = imsg_get(&ibuf, &imsg)) == -1) { + log_warn("warn: scheduler-proc: imsg_get"); + break; + } + if (n) { + rlen = imsg.hdr.len - IMSG_HEADER_SIZE; + rdata = imsg.data; + + if (imsg.hdr.type != PROC_SCHEDULER_OK) { + log_warnx("warn: scheduler-proc: bad response"); + break; + } + return; + } + + if ((n = imsg_read(&ibuf)) == -1 && errno != EAGAIN) { + log_warn("warn: scheduler-proc: imsg_read"); + break; + } + + if (n == 0) { + log_warnx("warn: scheduler-proc: pipe closed"); + break; + } + } + + fatalx("scheduler-proc: exiting"); +} + +static void +scheduler_proc_read(void *dst, size_t len) +{ + if (len > rlen) { + log_warnx("warn: scheduler-proc: bad msg len"); + fatalx("scheduler-proc: exiting"); + } + + memmove(dst, rdata, len); + rlen -= len; + rdata += len; +} + +static void +scheduler_proc_end(void) +{ + if (rlen) { + log_warnx("warn: scheduler-proc: bogus data"); + fatalx("scheduler-proc: exiting"); + } + imsg_free(&imsg); +} + +/* + * API + */ + +static int +scheduler_proc_init(const char *conf) +{ + int fd, r; + uint32_t version; + + fd = fork_proc_backend("scheduler", conf, "scheduler-proc"); + if (fd == -1) + fatalx("scheduler-proc: exiting"); + + imsg_init(&ibuf, fd); + + version = PROC_SCHEDULER_API_VERSION; + imsg_compose(&ibuf, PROC_SCHEDULER_INIT, 0, 0, -1, + &version, sizeof(version)); + scheduler_proc_call(); + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (1); +} + +static int +scheduler_proc_insert(struct scheduler_info *si) +{ + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_INSERT"); + + imsg_compose(&ibuf, PROC_SCHEDULER_INSERT, 0, 0, -1, si, sizeof(*si)); + + scheduler_proc_call(); + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (r); +} + +static size_t +scheduler_proc_commit(uint32_t msgid) +{ + size_t s; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_COMMIT"); + + imsg_compose(&ibuf, PROC_SCHEDULER_COMMIT, 0, 0, -1, + &msgid, sizeof(msgid)); + + scheduler_proc_call(); + scheduler_proc_read(&s, sizeof(s)); + scheduler_proc_end(); + + return (s); +} + +static size_t +scheduler_proc_rollback(uint32_t msgid) +{ + size_t s; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_ROLLBACK"); + + imsg_compose(&ibuf, PROC_SCHEDULER_ROLLBACK, 0, 0, -1, + &msgid, sizeof(msgid)); + + scheduler_proc_call(); + scheduler_proc_read(&s, sizeof(s)); + scheduler_proc_end(); + + return (s); +} + +static int +scheduler_proc_update(struct scheduler_info *si) +{ + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_UPDATE"); + + imsg_compose(&ibuf, PROC_SCHEDULER_UPDATE, 0, 0, -1, si, sizeof(*si)); + + scheduler_proc_call(); + scheduler_proc_read(&r, sizeof(r)); + if (r == 1) + scheduler_proc_read(si, sizeof(*si)); + scheduler_proc_end(); + + return (r); +} + +static int +scheduler_proc_delete(uint64_t evpid) +{ + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_DELETE"); + + imsg_compose(&ibuf, PROC_SCHEDULER_DELETE, 0, 0, -1, + &evpid, sizeof(evpid)); + + scheduler_proc_call(); + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (r); +} + +static int +scheduler_proc_hold(uint64_t evpid, uint64_t holdq) +{ + struct ibuf *buf; + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_HOLD"); + + buf = imsg_create(&ibuf, PROC_SCHEDULER_HOLD, 0, 0, + sizeof(evpid) + sizeof(holdq)); + if (buf == NULL) + return (-1); + if (imsg_add(buf, &evpid, sizeof(evpid)) == -1) + return (-1); + if (imsg_add(buf, &holdq, sizeof(holdq)) == -1) + return (-1); + imsg_close(&ibuf, buf); + + scheduler_proc_call(); + + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (r); +} + +static int +scheduler_proc_release(int type, uint64_t holdq, int n) +{ + struct ibuf *buf; + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_RELEASE"); + + buf = imsg_create(&ibuf, PROC_SCHEDULER_RELEASE, 0, 0, + sizeof(holdq) + sizeof(n)); + if (buf == NULL) + return (-1); + if (imsg_add(buf, &type, sizeof(type)) == -1) + return (-1); + if (imsg_add(buf, &holdq, sizeof(holdq)) == -1) + return (-1); + if (imsg_add(buf, &n, sizeof(n)) == -1) + return (-1); + imsg_close(&ibuf, buf); + + scheduler_proc_call(); + + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (r); +} + +static int +scheduler_proc_batch(int typemask, int *delay, size_t *count, uint64_t *evpids, int *types) +{ + struct ibuf *buf; + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_BATCH"); + + buf = imsg_create(&ibuf, PROC_SCHEDULER_BATCH, 0, 0, + sizeof(typemask) + sizeof(*count)); + if (buf == NULL) + return (-1); + if (imsg_add(buf, &typemask, sizeof(typemask)) == -1) + return (-1); + if (imsg_add(buf, count, sizeof(*count)) == -1) + return (-1); + imsg_close(&ibuf, buf); + + scheduler_proc_call(); + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_read(delay, sizeof(*delay)); + scheduler_proc_read(count, sizeof(*count)); + if (r > 0) { + scheduler_proc_read(evpids, sizeof(*evpids) * (*count)); + scheduler_proc_read(types, sizeof(*types) * (*count)); + } + scheduler_proc_end(); + + return (r); +} + +static size_t +scheduler_proc_messages(uint32_t from, uint32_t *dst, size_t size) +{ + struct ibuf *buf; + size_t s; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_MESSAGES"); + + buf = imsg_create(&ibuf, PROC_SCHEDULER_MESSAGES, 0, 0, + sizeof(from) + sizeof(size)); + if (buf == NULL) + return (-1); + if (imsg_add(buf, &from, sizeof(from)) == -1) + return (-1); + if (imsg_add(buf, &size, sizeof(size)) == -1) + return (-1); + imsg_close(&ibuf, buf); + + scheduler_proc_call(); + + s = rlen / sizeof(*dst); + scheduler_proc_read(dst, s * sizeof(*dst)); + scheduler_proc_end(); + + return (s); +} + +static size_t +scheduler_proc_envelopes(uint64_t from, struct evpstate *dst, size_t size) +{ + struct ibuf *buf; + size_t s; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_ENVELOPES"); + + buf = imsg_create(&ibuf, PROC_SCHEDULER_ENVELOPES, 0, 0, + sizeof(from) + sizeof(size)); + if (buf == NULL) + return (-1); + if (imsg_add(buf, &from, sizeof(from)) == -1) + return (-1); + if (imsg_add(buf, &size, sizeof(size)) == -1) + return (-1); + imsg_close(&ibuf, buf); + + scheduler_proc_call(); + + s = rlen / sizeof(*dst); + scheduler_proc_read(dst, s * sizeof(*dst)); + scheduler_proc_end(); + + return (s); +} + +static int +scheduler_proc_schedule(uint64_t evpid) +{ + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_SCHEDULE"); + + imsg_compose(&ibuf, PROC_SCHEDULER_SCHEDULE, 0, 0, -1, + &evpid, sizeof(evpid)); + + scheduler_proc_call(); + + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (r); +} + +static int +scheduler_proc_remove(uint64_t evpid) +{ + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_REMOVE"); + + imsg_compose(&ibuf, PROC_SCHEDULER_REMOVE, 0, 0, -1, + &evpid, sizeof(evpid)); + + scheduler_proc_call(); + + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (r); +} + +static int +scheduler_proc_suspend(uint64_t evpid) +{ + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_SUSPEND"); + + imsg_compose(&ibuf, PROC_SCHEDULER_SUSPEND, 0, 0, -1, + &evpid, sizeof(evpid)); + + scheduler_proc_call(); + + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (r); +} + +static int +scheduler_proc_resume(uint64_t evpid) +{ + int r; + + log_debug("debug: scheduler-proc: PROC_SCHEDULER_RESUME"); + + imsg_compose(&ibuf, PROC_SCHEDULER_RESUME, 0, 0, -1, + &evpid, sizeof(evpid)); + + scheduler_proc_call(); + + scheduler_proc_read(&r, sizeof(r)); + scheduler_proc_end(); + + return (r); +} + +struct scheduler_backend scheduler_backend_proc = { + scheduler_proc_init, + scheduler_proc_insert, + scheduler_proc_commit, + scheduler_proc_rollback, + scheduler_proc_update, + scheduler_proc_delete, + scheduler_proc_hold, + scheduler_proc_release, + scheduler_proc_batch, + scheduler_proc_messages, + scheduler_proc_envelopes, + scheduler_proc_schedule, + scheduler_proc_remove, + scheduler_proc_suspend, + scheduler_proc_resume, +}; diff --git a/smtpd/scheduler_ramqueue.c b/smtpd/scheduler_ramqueue.c new file mode 100644 index 00000000..0c04fc0b --- /dev/null +++ b/smtpd/scheduler_ramqueue.c @@ -0,0 +1,1204 @@ +/* $OpenBSD: scheduler_ramqueue.c,v 1.45 2018/05/31 21:06:12 gilles Exp $ */ + +/* + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <inttypes.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include <time.h> + +#include "smtpd.h" +#include "log.h" + +TAILQ_HEAD(evplist, rq_envelope); + +struct rq_message { + uint32_t msgid; + struct tree envelopes; +}; + +struct rq_envelope { + TAILQ_ENTRY(rq_envelope) entry; + SPLAY_ENTRY(rq_envelope) t_entry; + + uint64_t evpid; + uint64_t holdq; + enum delivery_type type; + +#define RQ_EVPSTATE_PENDING 0 +#define RQ_EVPSTATE_SCHEDULED 1 +#define RQ_EVPSTATE_INFLIGHT 2 +#define RQ_EVPSTATE_HELD 3 + uint8_t state; + +#define RQ_ENVELOPE_EXPIRED 0x01 +#define RQ_ENVELOPE_REMOVED 0x02 +#define RQ_ENVELOPE_SUSPEND 0x04 +#define RQ_ENVELOPE_UPDATE 0x08 +#define RQ_ENVELOPE_OVERFLOW 0x10 + uint8_t flags; + + time_t ctime; + time_t sched; + time_t expire; + + struct rq_message *message; + + time_t t_inflight; + time_t t_scheduled; +}; + +struct rq_holdq { + struct evplist q; + size_t count; +}; + +struct rq_queue { + size_t evpcount; + struct tree messages; + SPLAY_HEAD(prioqtree, rq_envelope) q_priotree; + + struct evplist q_pending; + struct evplist q_inflight; + + struct evplist q_mta; + struct evplist q_mda; + struct evplist q_bounce; + struct evplist q_update; + struct evplist q_expired; + struct evplist q_removed; +}; + +static int rq_envelope_cmp(struct rq_envelope *, struct rq_envelope *); + +SPLAY_PROTOTYPE(prioqtree, rq_envelope, t_entry, rq_envelope_cmp); +static int scheduler_ram_init(const char *); +static int scheduler_ram_insert(struct scheduler_info *); +static size_t scheduler_ram_commit(uint32_t); +static size_t scheduler_ram_rollback(uint32_t); +static int scheduler_ram_update(struct scheduler_info *); +static int scheduler_ram_delete(uint64_t); +static int scheduler_ram_hold(uint64_t, uint64_t); +static int scheduler_ram_release(int, uint64_t, int); +static int scheduler_ram_batch(int, int *, size_t *, uint64_t *, int *); +static size_t scheduler_ram_messages(uint32_t, uint32_t *, size_t); +static size_t scheduler_ram_envelopes(uint64_t, struct evpstate *, size_t); +static int scheduler_ram_schedule(uint64_t); +static int scheduler_ram_remove(uint64_t); +static int scheduler_ram_suspend(uint64_t); +static int scheduler_ram_resume(uint64_t); +static int scheduler_ram_query(uint64_t); + +static void sorted_insert(struct rq_queue *, struct rq_envelope *); + +static void rq_queue_init(struct rq_queue *); +static void rq_queue_merge(struct rq_queue *, struct rq_queue *); +static void rq_queue_dump(struct rq_queue *, const char *); +static void rq_queue_schedule(struct rq_queue *rq); +static struct evplist *rq_envelope_list(struct rq_queue *, struct rq_envelope *); +static void rq_envelope_schedule(struct rq_queue *, struct rq_envelope *); +static int rq_envelope_remove(struct rq_queue *, struct rq_envelope *); +static int rq_envelope_suspend(struct rq_queue *, struct rq_envelope *); +static int rq_envelope_resume(struct rq_queue *, struct rq_envelope *); +static void rq_envelope_delete(struct rq_queue *, struct rq_envelope *); +static const char *rq_envelope_to_text(struct rq_envelope *); + +struct scheduler_backend scheduler_backend_ramqueue = { + scheduler_ram_init, + + scheduler_ram_insert, + scheduler_ram_commit, + scheduler_ram_rollback, + + scheduler_ram_update, + scheduler_ram_delete, + scheduler_ram_hold, + scheduler_ram_release, + + scheduler_ram_batch, + + scheduler_ram_messages, + scheduler_ram_envelopes, + scheduler_ram_schedule, + scheduler_ram_remove, + scheduler_ram_suspend, + scheduler_ram_resume, + scheduler_ram_query, +}; + +static struct rq_queue ramqueue; +static struct tree updates; +static struct tree holdqs[3]; /* delivery type */ + +static time_t currtime; + +#define BACKOFF_TRANSFER 400 +#define BACKOFF_DELIVERY 10 +#define BACKOFF_OVERFLOW 3 + +static time_t +scheduler_backoff(time_t t0, time_t base, uint32_t step) +{ + return (t0 + base * step * step); +} + +static time_t +scheduler_next(time_t t0, time_t base, uint32_t step) +{ + time_t t; + + /* XXX be more efficient */ + while ((t = scheduler_backoff(t0, base, step)) <= currtime) + step++; + + return (t); +} + +static int +scheduler_ram_init(const char *arg) +{ + rq_queue_init(&ramqueue); + tree_init(&updates); + tree_init(&holdqs[D_MDA]); + tree_init(&holdqs[D_MTA]); + tree_init(&holdqs[D_BOUNCE]); + + return (1); +} + +static int +scheduler_ram_insert(struct scheduler_info *si) +{ + struct rq_queue *update; + struct rq_message *message; + struct rq_envelope *envelope; + uint32_t msgid; + + currtime = time(NULL); + + msgid = evpid_to_msgid(si->evpid); + + /* find/prepare a ramqueue update */ + if ((update = tree_get(&updates, msgid)) == NULL) { + update = xcalloc(1, sizeof *update); + stat_increment("scheduler.ramqueue.update", 1); + rq_queue_init(update); + tree_xset(&updates, msgid, update); + } + + /* find/prepare the msgtree message in ramqueue update */ + if ((message = tree_get(&update->messages, msgid)) == NULL) { + message = xcalloc(1, sizeof *message); + message->msgid = msgid; + tree_init(&message->envelopes); + tree_xset(&update->messages, msgid, message); + stat_increment("scheduler.ramqueue.message", 1); + } + + /* create envelope in ramqueue message */ + envelope = xcalloc(1, sizeof *envelope); + envelope->evpid = si->evpid; + envelope->type = si->type; + envelope->message = message; + envelope->ctime = si->creation; + envelope->expire = si->creation + si->ttl; + envelope->sched = scheduler_backoff(si->creation, + (si->type == D_MTA) ? BACKOFF_TRANSFER : BACKOFF_DELIVERY, si->retry); + tree_xset(&message->envelopes, envelope->evpid, envelope); + + update->evpcount++; + stat_increment("scheduler.ramqueue.envelope", 1); + + envelope->state = RQ_EVPSTATE_PENDING; + TAILQ_INSERT_TAIL(&update->q_pending, envelope, entry); + + si->nexttry = envelope->sched; + + return (1); +} + +static size_t +scheduler_ram_commit(uint32_t msgid) +{ + struct rq_queue *update; + size_t r; + + currtime = time(NULL); + + update = tree_xpop(&updates, msgid); + r = update->evpcount; + + if (tracing & TRACE_SCHEDULER) + rq_queue_dump(update, "update to commit"); + + rq_queue_merge(&ramqueue, update); + + if (tracing & TRACE_SCHEDULER) + rq_queue_dump(&ramqueue, "resulting queue"); + + rq_queue_schedule(&ramqueue); + + free(update); + stat_decrement("scheduler.ramqueue.update", 1); + + return (r); +} + +static size_t +scheduler_ram_rollback(uint32_t msgid) +{ + struct rq_queue *update; + struct rq_envelope *evp; + size_t r; + + currtime = time(NULL); + + if ((update = tree_pop(&updates, msgid)) == NULL) + return (0); + r = update->evpcount; + + while ((evp = TAILQ_FIRST(&update->q_pending))) { + TAILQ_REMOVE(&update->q_pending, evp, entry); + rq_envelope_delete(update, evp); + } + + free(update); + stat_decrement("scheduler.ramqueue.update", 1); + + return (r); +} + +static int +scheduler_ram_update(struct scheduler_info *si) +{ + struct rq_message *msg; + struct rq_envelope *evp; + uint32_t msgid; + + currtime = time(NULL); + + msgid = evpid_to_msgid(si->evpid); + msg = tree_xget(&ramqueue.messages, msgid); + evp = tree_xget(&msg->envelopes, si->evpid); + + /* it *must* be in-flight */ + if (evp->state != RQ_EVPSTATE_INFLIGHT) + errx(1, "evp:%016" PRIx64 " not in-flight", si->evpid); + + TAILQ_REMOVE(&ramqueue.q_inflight, evp, entry); + + /* + * If the envelope was removed while inflight, schedule it for + * removal immediately. + */ + if (evp->flags & RQ_ENVELOPE_REMOVED) { + TAILQ_INSERT_TAIL(&ramqueue.q_removed, evp, entry); + evp->state = RQ_EVPSTATE_SCHEDULED; + evp->t_scheduled = currtime; + return (1); + } + + evp->sched = scheduler_next(evp->ctime, + (si->type == D_MTA) ? BACKOFF_TRANSFER : BACKOFF_DELIVERY, si->retry); + + evp->state = RQ_EVPSTATE_PENDING; + if (!(evp->flags & RQ_ENVELOPE_SUSPEND)) + sorted_insert(&ramqueue, evp); + + si->nexttry = evp->sched; + + return (1); +} + +static int +scheduler_ram_delete(uint64_t evpid) +{ + struct rq_message *msg; + struct rq_envelope *evp; + uint32_t msgid; + + currtime = time(NULL); + + msgid = evpid_to_msgid(evpid); + msg = tree_xget(&ramqueue.messages, msgid); + evp = tree_xget(&msg->envelopes, evpid); + + /* it *must* be in-flight */ + if (evp->state != RQ_EVPSTATE_INFLIGHT) + errx(1, "evp:%016" PRIx64 " not in-flight", evpid); + + TAILQ_REMOVE(&ramqueue.q_inflight, evp, entry); + + rq_envelope_delete(&ramqueue, evp); + + return (1); +} + +#define HOLDQ_MAXSIZE 1000 + +static int +scheduler_ram_hold(uint64_t evpid, uint64_t holdq) +{ + struct rq_holdq *hq; + struct rq_message *msg; + struct rq_envelope *evp; + uint32_t msgid; + + currtime = time(NULL); + + msgid = evpid_to_msgid(evpid); + msg = tree_xget(&ramqueue.messages, msgid); + evp = tree_xget(&msg->envelopes, evpid); + + /* it *must* be in-flight */ + if (evp->state != RQ_EVPSTATE_INFLIGHT) + errx(1, "evp:%016" PRIx64 " not in-flight", evpid); + + TAILQ_REMOVE(&ramqueue.q_inflight, evp, entry); + + /* If the envelope is suspended, just mark it as pending */ + if (evp->flags & RQ_ENVELOPE_SUSPEND) { + evp->state = RQ_EVPSTATE_PENDING; + return (0); + } + + hq = tree_get(&holdqs[evp->type], holdq); + if (hq == NULL) { + hq = xcalloc(1, sizeof(*hq)); + TAILQ_INIT(&hq->q); + tree_xset(&holdqs[evp->type], holdq, hq); + stat_increment("scheduler.ramqueue.holdq", 1); + } + + /* If the holdq is full, just "tempfail" the envelope */ + if (hq->count >= HOLDQ_MAXSIZE) { + evp->state = RQ_EVPSTATE_PENDING; + evp->flags |= RQ_ENVELOPE_UPDATE; + evp->flags |= RQ_ENVELOPE_OVERFLOW; + sorted_insert(&ramqueue, evp); + stat_increment("scheduler.ramqueue.hold-overflow", 1); + return (0); + } + + evp->state = RQ_EVPSTATE_HELD; + evp->holdq = holdq; + /* This is an optimization: upon release, the envelopes will be + * inserted in the pending queue from the first element to the last. + * Since elements already in the queue were received first, they + * were scheduled first, so they will be reinserted before the + * current element. + */ + TAILQ_INSERT_HEAD(&hq->q, evp, entry); + hq->count += 1; + stat_increment("scheduler.ramqueue.hold", 1); + + return (1); +} + +static int +scheduler_ram_release(int type, uint64_t holdq, int n) +{ + struct rq_holdq *hq; + struct rq_envelope *evp; + int i, update; + + currtime = time(NULL); + + hq = tree_get(&holdqs[type], holdq); + if (hq == NULL) + return (0); + + if (n == -1) { + n = 0; + update = 1; + } + else + update = 0; + + for (i = 0; n == 0 || i < n; i++) { + evp = TAILQ_FIRST(&hq->q); + if (evp == NULL) + break; + + TAILQ_REMOVE(&hq->q, evp, entry); + hq->count -= 1; + evp->holdq = 0; + + /* When released, all envelopes are put in the pending queue + * and will be rescheduled immediately. As an optimization, + * we could just schedule them directly. + */ + evp->state = RQ_EVPSTATE_PENDING; + if (update) + evp->flags |= RQ_ENVELOPE_UPDATE; + sorted_insert(&ramqueue, evp); + } + + if (TAILQ_EMPTY(&hq->q)) { + tree_xpop(&holdqs[type], holdq); + free(hq); + stat_decrement("scheduler.ramqueue.holdq", 1); + } + stat_decrement("scheduler.ramqueue.hold", i); + + return (i); +} + +static int +scheduler_ram_batch(int mask, int *delay, size_t *count, uint64_t *evpids, int *types) +{ + struct rq_envelope *evp; + size_t i, n; + time_t t; + + currtime = time(NULL); + + rq_queue_schedule(&ramqueue); + if (tracing & TRACE_SCHEDULER) + rq_queue_dump(&ramqueue, "scheduler_ram_batch()"); + + i = 0; + n = 0; + + for (;;) { + + if (mask & SCHED_REMOVE && (evp = TAILQ_FIRST(&ramqueue.q_removed))) { + TAILQ_REMOVE(&ramqueue.q_removed, evp, entry); + types[i] = SCHED_REMOVE; + evpids[i] = evp->evpid; + rq_envelope_delete(&ramqueue, evp); + + if (++i == *count) + break; + } + + if (mask & SCHED_EXPIRE && (evp = TAILQ_FIRST(&ramqueue.q_expired))) { + TAILQ_REMOVE(&ramqueue.q_expired, evp, entry); + types[i] = SCHED_EXPIRE; + evpids[i] = evp->evpid; + rq_envelope_delete(&ramqueue, evp); + + if (++i == *count) + break; + } + + if (mask & SCHED_UPDATE && (evp = TAILQ_FIRST(&ramqueue.q_update))) { + TAILQ_REMOVE(&ramqueue.q_update, evp, entry); + types[i] = SCHED_UPDATE; + evpids[i] = evp->evpid; + + if (evp->flags & RQ_ENVELOPE_OVERFLOW) + t = BACKOFF_OVERFLOW; + else if (evp->type == D_MTA) + t = BACKOFF_TRANSFER; + else + t = BACKOFF_DELIVERY; + + evp->sched = scheduler_next(evp->ctime, t, 0); + evp->flags &= ~(RQ_ENVELOPE_UPDATE|RQ_ENVELOPE_OVERFLOW); + evp->state = RQ_EVPSTATE_PENDING; + if (!(evp->flags & RQ_ENVELOPE_SUSPEND)) + sorted_insert(&ramqueue, evp); + + if (++i == *count) + break; + } + + if (mask & SCHED_BOUNCE && (evp = TAILQ_FIRST(&ramqueue.q_bounce))) { + TAILQ_REMOVE(&ramqueue.q_bounce, evp, entry); + types[i] = SCHED_BOUNCE; + evpids[i] = evp->evpid; + + TAILQ_INSERT_TAIL(&ramqueue.q_inflight, evp, entry); + evp->state = RQ_EVPSTATE_INFLIGHT; + evp->t_inflight = currtime; + + if (++i == *count) + break; + } + + if (mask & SCHED_MDA && (evp = TAILQ_FIRST(&ramqueue.q_mda))) { + TAILQ_REMOVE(&ramqueue.q_mda, evp, entry); + types[i] = SCHED_MDA; + evpids[i] = evp->evpid; + + TAILQ_INSERT_TAIL(&ramqueue.q_inflight, evp, entry); + evp->state = RQ_EVPSTATE_INFLIGHT; + evp->t_inflight = currtime; + + if (++i == *count) + break; + } + + if (mask & SCHED_MTA && (evp = TAILQ_FIRST(&ramqueue.q_mta))) { + TAILQ_REMOVE(&ramqueue.q_mta, evp, entry); + types[i] = SCHED_MTA; + evpids[i] = evp->evpid; + + TAILQ_INSERT_TAIL(&ramqueue.q_inflight, evp, entry); + evp->state = RQ_EVPSTATE_INFLIGHT; + evp->t_inflight = currtime; + + if (++i == *count) + break; + } + + /* nothing seen this round */ + if (i == n) + break; + + n = i; + } + + if (i) { + *count = i; + return (1); + } + + if ((evp = TAILQ_FIRST(&ramqueue.q_pending))) { + if (evp->sched < evp->expire) + t = evp->sched; + else + t = evp->expire; + *delay = (t < currtime) ? 0 : (t - currtime); + } + else + *delay = -1; + + return (0); +} + +static size_t +scheduler_ram_messages(uint32_t from, uint32_t *dst, size_t size) +{ + uint64_t id; + size_t n; + void *i; + + for (n = 0, i = NULL; n < size; n++) { + if (tree_iterfrom(&ramqueue.messages, &i, from, &id, NULL) == 0) + break; + dst[n] = id; + } + + return (n); +} + +static size_t +scheduler_ram_envelopes(uint64_t from, struct evpstate *dst, size_t size) +{ + struct rq_message *msg; + struct rq_envelope *evp; + void *i; + size_t n; + + if ((msg = tree_get(&ramqueue.messages, evpid_to_msgid(from))) == NULL) + return (0); + + for (n = 0, i = NULL; n < size; ) { + + if (tree_iterfrom(&msg->envelopes, &i, from, NULL, + (void**)&evp) == 0) + break; + + if (evp->flags & (RQ_ENVELOPE_REMOVED | RQ_ENVELOPE_EXPIRED)) + continue; + + dst[n].evpid = evp->evpid; + dst[n].flags = 0; + dst[n].retry = 0; + dst[n].time = 0; + + if (evp->state == RQ_EVPSTATE_PENDING) { + dst[n].time = evp->sched; + dst[n].flags = EF_PENDING; + } + else if (evp->state == RQ_EVPSTATE_SCHEDULED) { + dst[n].time = evp->t_scheduled; + dst[n].flags = EF_PENDING; + } + else if (evp->state == RQ_EVPSTATE_INFLIGHT) { + dst[n].time = evp->t_inflight; + dst[n].flags = EF_INFLIGHT; + } + else if (evp->state == RQ_EVPSTATE_HELD) { + /* same as scheduled */ + dst[n].time = evp->t_scheduled; + dst[n].flags = EF_PENDING; + dst[n].flags |= EF_HOLD; + } + if (evp->flags & RQ_ENVELOPE_SUSPEND) + dst[n].flags |= EF_SUSPEND; + + n++; + } + + return (n); +} + +static int +scheduler_ram_schedule(uint64_t evpid) +{ + struct rq_message *msg; + struct rq_envelope *evp; + uint32_t msgid; + void *i; + int r; + + currtime = time(NULL); + + if (evpid > 0xffffffff) { + msgid = evpid_to_msgid(evpid); + if ((msg = tree_get(&ramqueue.messages, msgid)) == NULL) + return (0); + if ((evp = tree_get(&msg->envelopes, evpid)) == NULL) + return (0); + if (evp->state == RQ_EVPSTATE_INFLIGHT) + return (0); + rq_envelope_schedule(&ramqueue, evp); + return (1); + } + else { + msgid = evpid; + if ((msg = tree_get(&ramqueue.messages, msgid)) == NULL) + return (0); + i = NULL; + r = 0; + while (tree_iter(&msg->envelopes, &i, NULL, (void*)(&evp))) { + if (evp->state == RQ_EVPSTATE_INFLIGHT) + continue; + rq_envelope_schedule(&ramqueue, evp); + r++; + } + return (r); + } +} + +static int +scheduler_ram_remove(uint64_t evpid) +{ + struct rq_message *msg; + struct rq_envelope *evp; + uint32_t msgid; + void *i; + int r; + + currtime = time(NULL); + + if (evpid > 0xffffffff) { + msgid = evpid_to_msgid(evpid); + if ((msg = tree_get(&ramqueue.messages, msgid)) == NULL) + return (0); + if ((evp = tree_get(&msg->envelopes, evpid)) == NULL) + return (0); + if (rq_envelope_remove(&ramqueue, evp)) + return (1); + return (0); + } + else { + msgid = evpid; + if ((msg = tree_get(&ramqueue.messages, msgid)) == NULL) + return (0); + i = NULL; + r = 0; + while (tree_iter(&msg->envelopes, &i, NULL, (void*)(&evp))) + if (rq_envelope_remove(&ramqueue, evp)) + r++; + return (r); + } +} + +static int +scheduler_ram_suspend(uint64_t evpid) +{ + struct rq_message *msg; + struct rq_envelope *evp; + uint32_t msgid; + void *i; + int r; + + currtime = time(NULL); + + if (evpid > 0xffffffff) { + msgid = evpid_to_msgid(evpid); + if ((msg = tree_get(&ramqueue.messages, msgid)) == NULL) + return (0); + if ((evp = tree_get(&msg->envelopes, evpid)) == NULL) + return (0); + if (rq_envelope_suspend(&ramqueue, evp)) + return (1); + return (0); + } + else { + msgid = evpid; + if ((msg = tree_get(&ramqueue.messages, msgid)) == NULL) + return (0); + i = NULL; + r = 0; + while (tree_iter(&msg->envelopes, &i, NULL, (void*)(&evp))) + if (rq_envelope_suspend(&ramqueue, evp)) + r++; + return (r); + } +} + +static int +scheduler_ram_resume(uint64_t evpid) +{ + struct rq_message *msg; + struct rq_envelope *evp; + uint32_t msgid; + void *i; + int r; + + currtime = time(NULL); + + if (evpid > 0xffffffff) { + msgid = evpid_to_msgid(evpid); + if ((msg = tree_get(&ramqueue.messages, msgid)) == NULL) + return (0); + if ((evp = tree_get(&msg->envelopes, evpid)) == NULL) + return (0); + if (rq_envelope_resume(&ramqueue, evp)) + return (1); + return (0); + } + else { + msgid = evpid; + if ((msg = tree_get(&ramqueue.messages, msgid)) == NULL) + return (0); + i = NULL; + r = 0; + while (tree_iter(&msg->envelopes, &i, NULL, (void*)(&evp))) + if (rq_envelope_resume(&ramqueue, evp)) + r++; + return (r); + } +} + +static int +scheduler_ram_query(uint64_t evpid) +{ + uint32_t msgid; + + if (evpid > 0xffffffff) + msgid = evpid_to_msgid(evpid); + else + msgid = evpid; + + if (tree_get(&ramqueue.messages, msgid) == NULL) + return (0); + + return (1); +} + +static void +sorted_insert(struct rq_queue *rq, struct rq_envelope *evp) +{ + struct rq_envelope *evp2; + + SPLAY_INSERT(prioqtree, &rq->q_priotree, evp); + evp2 = SPLAY_NEXT(prioqtree, &rq->q_priotree, evp); + if (evp2) + TAILQ_INSERT_BEFORE(evp2, evp, entry); + else + TAILQ_INSERT_TAIL(&rq->q_pending, evp, entry); +} + +static void +rq_queue_init(struct rq_queue *rq) +{ + memset(rq, 0, sizeof *rq); + tree_init(&rq->messages); + TAILQ_INIT(&rq->q_pending); + TAILQ_INIT(&rq->q_inflight); + TAILQ_INIT(&rq->q_mta); + TAILQ_INIT(&rq->q_mda); + TAILQ_INIT(&rq->q_bounce); + TAILQ_INIT(&rq->q_update); + TAILQ_INIT(&rq->q_expired); + TAILQ_INIT(&rq->q_removed); + SPLAY_INIT(&rq->q_priotree); +} + +static void +rq_queue_merge(struct rq_queue *rq, struct rq_queue *update) +{ + struct rq_message *message, *tomessage; + struct rq_envelope *envelope; + uint64_t id; + void *i; + + while (tree_poproot(&update->messages, &id, (void*)&message)) { + if ((tomessage = tree_get(&rq->messages, id)) == NULL) { + /* message does not exist. re-use structure */ + tree_xset(&rq->messages, id, message); + continue; + } + /* need to re-link all envelopes before merging them */ + i = NULL; + while ((tree_iter(&message->envelopes, &i, &id, + (void*)&envelope))) + envelope->message = tomessage; + tree_merge(&tomessage->envelopes, &message->envelopes); + free(message); + stat_decrement("scheduler.ramqueue.message", 1); + } + + /* Sorted insert in the pending queue */ + while ((envelope = TAILQ_FIRST(&update->q_pending))) { + TAILQ_REMOVE(&update->q_pending, envelope, entry); + sorted_insert(rq, envelope); + } + + rq->evpcount += update->evpcount; +} + +#define SCHEDULEMAX 1024 + +static void +rq_queue_schedule(struct rq_queue *rq) +{ + struct rq_envelope *evp; + size_t n; + + n = 0; + while ((evp = TAILQ_FIRST(&rq->q_pending))) { + if (evp->sched > currtime && evp->expire > currtime) + break; + + if (n == SCHEDULEMAX) + break; + + if (evp->state != RQ_EVPSTATE_PENDING) + errx(1, "evp:%016" PRIx64 " flags=0x%x", evp->evpid, + evp->flags); + + if (evp->expire <= currtime) { + TAILQ_REMOVE(&rq->q_pending, evp, entry); + SPLAY_REMOVE(prioqtree, &rq->q_priotree, evp); + TAILQ_INSERT_TAIL(&rq->q_expired, evp, entry); + evp->state = RQ_EVPSTATE_SCHEDULED; + evp->flags |= RQ_ENVELOPE_EXPIRED; + evp->t_scheduled = currtime; + continue; + } + rq_envelope_schedule(rq, evp); + n += 1; + } +} + +static struct evplist * +rq_envelope_list(struct rq_queue *rq, struct rq_envelope *evp) +{ + switch (evp->state) { + case RQ_EVPSTATE_PENDING: + return &rq->q_pending; + + case RQ_EVPSTATE_SCHEDULED: + if (evp->flags & RQ_ENVELOPE_EXPIRED) + return &rq->q_expired; + if (evp->flags & RQ_ENVELOPE_REMOVED) + return &rq->q_removed; + if (evp->flags & RQ_ENVELOPE_UPDATE) + return &rq->q_update; + if (evp->type == D_MTA) + return &rq->q_mta; + if (evp->type == D_MDA) + return &rq->q_mda; + if (evp->type == D_BOUNCE) + return &rq->q_bounce; + errx(1, "%016" PRIx64 " bad evp type %d", evp->evpid, evp->type); + + case RQ_EVPSTATE_INFLIGHT: + return &rq->q_inflight; + + case RQ_EVPSTATE_HELD: + return (NULL); + } + + errx(1, "%016" PRIx64 " bad state %d", evp->evpid, evp->state); + return (NULL); +} + +static void +rq_envelope_schedule(struct rq_queue *rq, struct rq_envelope *evp) +{ + struct rq_holdq *hq; + struct evplist *q = NULL; + + switch (evp->type) { + case D_MTA: + q = &rq->q_mta; + break; + case D_MDA: + q = &rq->q_mda; + break; + case D_BOUNCE: + q = &rq->q_bounce; + break; + } + + if (evp->flags & RQ_ENVELOPE_UPDATE) + q = &rq->q_update; + + if (evp->state == RQ_EVPSTATE_HELD) { + hq = tree_xget(&holdqs[evp->type], evp->holdq); + TAILQ_REMOVE(&hq->q, evp, entry); + hq->count -= 1; + if (TAILQ_EMPTY(&hq->q)) { + tree_xpop(&holdqs[evp->type], evp->holdq); + free(hq); + } + evp->holdq = 0; + stat_decrement("scheduler.ramqueue.hold", 1); + } + else if (!(evp->flags & RQ_ENVELOPE_SUSPEND)) { + TAILQ_REMOVE(&rq->q_pending, evp, entry); + SPLAY_REMOVE(prioqtree, &rq->q_priotree, evp); + } + + TAILQ_INSERT_TAIL(q, evp, entry); + evp->state = RQ_EVPSTATE_SCHEDULED; + evp->t_scheduled = currtime; +} + +static int +rq_envelope_remove(struct rq_queue *rq, struct rq_envelope *evp) +{ + struct rq_holdq *hq; + struct evplist *evl; + + if (evp->flags & (RQ_ENVELOPE_REMOVED | RQ_ENVELOPE_EXPIRED)) + return (0); + /* + * If envelope is inflight, mark it envelope for removal. + */ + if (evp->state == RQ_EVPSTATE_INFLIGHT) { + evp->flags |= RQ_ENVELOPE_REMOVED; + return (1); + } + + if (evp->state == RQ_EVPSTATE_HELD) { + hq = tree_xget(&holdqs[evp->type], evp->holdq); + TAILQ_REMOVE(&hq->q, evp, entry); + hq->count -= 1; + if (TAILQ_EMPTY(&hq->q)) { + tree_xpop(&holdqs[evp->type], evp->holdq); + free(hq); + } + evp->holdq = 0; + stat_decrement("scheduler.ramqueue.hold", 1); + } + else if (!(evp->flags & RQ_ENVELOPE_SUSPEND)) { + evl = rq_envelope_list(rq, evp); + TAILQ_REMOVE(evl, evp, entry); + if (evl == &rq->q_pending) + SPLAY_REMOVE(prioqtree, &rq->q_priotree, evp); + } + + TAILQ_INSERT_TAIL(&rq->q_removed, evp, entry); + evp->state = RQ_EVPSTATE_SCHEDULED; + evp->flags |= RQ_ENVELOPE_REMOVED; + evp->t_scheduled = currtime; + + return (1); +} + +static int +rq_envelope_suspend(struct rq_queue *rq, struct rq_envelope *evp) +{ + struct rq_holdq *hq; + struct evplist *evl; + + if (evp->flags & RQ_ENVELOPE_SUSPEND) + return (0); + + if (evp->state == RQ_EVPSTATE_HELD) { + hq = tree_xget(&holdqs[evp->type], evp->holdq); + TAILQ_REMOVE(&hq->q, evp, entry); + hq->count -= 1; + if (TAILQ_EMPTY(&hq->q)) { + tree_xpop(&holdqs[evp->type], evp->holdq); + free(hq); + } + evp->holdq = 0; + evp->state = RQ_EVPSTATE_PENDING; + stat_decrement("scheduler.ramqueue.hold", 1); + } + else if (evp->state != RQ_EVPSTATE_INFLIGHT) { + evl = rq_envelope_list(rq, evp); + TAILQ_REMOVE(evl, evp, entry); + if (evl == &rq->q_pending) + SPLAY_REMOVE(prioqtree, &rq->q_priotree, evp); + } + + evp->flags |= RQ_ENVELOPE_SUSPEND; + + return (1); +} + +static int +rq_envelope_resume(struct rq_queue *rq, struct rq_envelope *evp) +{ + struct evplist *evl; + + if (!(evp->flags & RQ_ENVELOPE_SUSPEND)) + return (0); + + if (evp->state != RQ_EVPSTATE_INFLIGHT) { + evl = rq_envelope_list(rq, evp); + if (evl == &rq->q_pending) + sorted_insert(rq, evp); + else + TAILQ_INSERT_TAIL(evl, evp, entry); + } + + evp->flags &= ~RQ_ENVELOPE_SUSPEND; + + return (1); +} + +static void +rq_envelope_delete(struct rq_queue *rq, struct rq_envelope *evp) +{ + tree_xpop(&evp->message->envelopes, evp->evpid); + if (tree_empty(&evp->message->envelopes)) { + tree_xpop(&rq->messages, evp->message->msgid); + free(evp->message); + stat_decrement("scheduler.ramqueue.message", 1); + } + + free(evp); + rq->evpcount--; + stat_decrement("scheduler.ramqueue.envelope", 1); +} + +static const char * +rq_envelope_to_text(struct rq_envelope *e) +{ + static char buf[256]; + char t[64]; + + (void)snprintf(buf, sizeof buf, "evp:%016" PRIx64 " [", e->evpid); + + if (e->type == D_BOUNCE) + (void)strlcat(buf, "bounce", sizeof buf); + else if (e->type == D_MDA) + (void)strlcat(buf, "mda", sizeof buf); + else if (e->type == D_MTA) + (void)strlcat(buf, "mta", sizeof buf); + + (void)snprintf(t, sizeof t, ",expire=%s", + duration_to_text(e->expire - currtime)); + (void)strlcat(buf, t, sizeof buf); + + + switch (e->state) { + case RQ_EVPSTATE_PENDING: + (void)snprintf(t, sizeof t, ",pending=%s", + duration_to_text(e->sched - currtime)); + (void)strlcat(buf, t, sizeof buf); + break; + + case RQ_EVPSTATE_SCHEDULED: + (void)snprintf(t, sizeof t, ",scheduled=%s", + duration_to_text(currtime - e->t_scheduled)); + (void)strlcat(buf, t, sizeof buf); + break; + + case RQ_EVPSTATE_INFLIGHT: + (void)snprintf(t, sizeof t, ",inflight=%s", + duration_to_text(currtime - e->t_inflight)); + (void)strlcat(buf, t, sizeof buf); + break; + + case RQ_EVPSTATE_HELD: + (void)snprintf(t, sizeof t, ",held=%s", + duration_to_text(currtime - e->t_inflight)); + (void)strlcat(buf, t, sizeof buf); + break; + default: + errx(1, "%016" PRIx64 " bad state %d", e->evpid, e->state); + } + + if (e->flags & RQ_ENVELOPE_REMOVED) + (void)strlcat(buf, ",removed", sizeof buf); + if (e->flags & RQ_ENVELOPE_EXPIRED) + (void)strlcat(buf, ",expired", sizeof buf); + if (e->flags & RQ_ENVELOPE_SUSPEND) + (void)strlcat(buf, ",suspended", sizeof buf); + + (void)strlcat(buf, "]", sizeof buf); + + return (buf); +} + +static void +rq_queue_dump(struct rq_queue *rq, const char * name) +{ + struct rq_message *message; + struct rq_envelope *envelope; + void *i, *j; + uint64_t id; + + log_debug("debug: /--- ramqueue: %s", name); + + i = NULL; + while ((tree_iter(&rq->messages, &i, &id, (void*)&message))) { + log_debug("debug: | msg:%08" PRIx32, message->msgid); + j = NULL; + while ((tree_iter(&message->envelopes, &j, &id, + (void*)&envelope))) + log_debug("debug: | %s", + rq_envelope_to_text(envelope)); + } + log_debug("debug: \\---"); +} + +static int +rq_envelope_cmp(struct rq_envelope *e1, struct rq_envelope *e2) +{ + time_t ref1, ref2; + + ref1 = (e1->sched < e1->expire) ? e1->sched : e1->expire; + ref2 = (e2->sched < e2->expire) ? e2->sched : e2->expire; + if (ref1 != ref2) + return (ref1 < ref2) ? -1 : 1; + + if (e1->evpid != e2->evpid) + return (e1->evpid < e2->evpid) ? -1 : 1; + + return 0; +} + +SPLAY_GENERATE(prioqtree, rq_envelope, t_entry, rq_envelope_cmp); diff --git a/smtpd/sendmail.8 b/smtpd/sendmail.8 new file mode 100644 index 00000000..1696a861 --- /dev/null +++ b/smtpd/sendmail.8 @@ -0,0 +1,86 @@ +.\" $OpenBSD: sendmail.8,v 1.4 2015/10/23 15:48:16 jung Exp $ +.\" +.\" Copyright (C) 2013 Ryan Kavanagh <rak@debian.org> +.\" All rights reserved. +.\" +.\" Permission to use, copy, modify, and/or 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: October 23 2015 $ +.Dt SENDMAIL 8 +.Os +.Sh NAME +.Nm sendmail +.Nd a mail enqueuer for +.Xr smtpd 8 +.Sh SYNOPSIS +.Nm +.Op Fl tv +.Op Fl F Ar name +.Op Fl f Ar from +.Ar to ... +.Sh DESCRIPTION +The +.Nm +utility is a local enqueuer for the +.Xr smtpd 8 +daemon, +compatible with +.Xr mailwrapper 8 . +The message is read on standard input (stdin) until +.Nm +encounters an end-of-file. +The +.Nm +enqueuer is not intended to be used directly to send mail, +but rather via a frontend known as a mail user agent. +.Pp +Unless the optional +.Fl t +flag is specified, +one or more recipients must be specified on the command line. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl F Ar name +Set the sender's full name. +.It Fl f Ar from +Set the sender's address. +.It Fl t +Read the message's To:, Cc:, and Bcc: fields for recipients. +The Bcc: field will be deleted before sending. +.It Fl v +Enable verbose output. +.El +.Pp +To maintain compatibility with Sendmail, Inc.'s implementation of +.Nm , +various other flags are accepted, +but have no effect. +.Sh EXIT STATUS +.Ex -std +.Sh SEE ALSO +.Xr smtpctl 8 , +.Xr smtpd 8 +.Sh AUTHORS +.Sy OpenSMTPD +is primarily developed by Gilles Chehade, +Eric Faurot, +and Charles Longeau, +with contributions from various +.Ox +hackers. +It is distributed under the ISC license. +.Pp +This manpage was written by +.An Ryan Kavanagh +.Aq Mt rak@debian.org +for the Debian project and is distributed under the ISC license. diff --git a/smtpd/smtp.1 b/smtpd/smtp.1 new file mode 100644 index 00000000..3cc03844 --- /dev/null +++ b/smtpd/smtp.1 @@ -0,0 +1,96 @@ +.\" $OpenBSD: smtp.1,v 1.7 2018/07/04 08:23:43 jmc Exp $ +.\" +.\" Copyright (c) 2018, Eric Faurot <eric@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: July 4 2018 $ +.Dt SMTP 1 +.Os +.Sh NAME +.Nm smtp +.Nd Simple Mail Transfer Protocol client +.Sh SYNOPSIS +.Nm +.Op Fl Chnv +.Op Fl F Ar from +.Op Fl H Ar helo +.Op Fl s Ar server +.Op Ar recipient ... +.Sh DESCRIPTION +The +.Nm +utility is a Simple Mail Transfer Protocol +.Pq SMTP +client which can be used to run an SMTP transaction against an SMTP server. +.Pp +By default, +.Nm +reads the mail content from the standard input, establishes an SMTP session, +and runs an SMTP transaction for all the specified recipients. +The content is sent unaltered as mail data. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl C +Do not require server certificate to be valid. +.It Fl F Ar from +Set the return-path (MAIL FROM) for the SMTP transaction. +Default to the current username. +.It Fl H Ar helo +Define the hostname to advertise (HELO) when establishing the SMTP session. +.It Fl h +Display version and usage. +.It Fl n +Do not actually execute a transaction, +just try to establish an SMTP session and quit. +When this option is given, no message is read from the standard input. +.It Fl s Ar server +Specify the server to connect to and connection parameters. +The format is +.Sm off +.Op Ar proto No :// Op Ar user : pass No @ +.Ar host Op : Ar port . +.Sm on +The following protocols are available: +.Pp +.Bl -tag -width "smtp+notls" -compact +.It smtp +Normal SMTP session with opportunistic STARTTLS. +.It smtp+tls +Normal SMTP session with mandatory STARTTLS. +.It smtp+notls +Plain text SMTP session without TLS. +.It lmtp +LMTP session with opportunistic STARTTLS. +.It lmtp+tls +LMTP session with mandatory STARTTLS. +.It lmtp+notls +Plain text LMTP session without TLS. +.It smtps +SMTP session with forced TLS on connection. +.El +.Pp +Defaults to +.Dq smtp://localhost:25 . +.It Fl v +Be more verbose. +This option can be specified multiple times. +.El +.Sh SEE ALSO +.Xr smtpd 8 +.Sh HISTORY +The +.Nm +program first appeared in +.Ox 6.4 . diff --git a/smtpd/smtp.c b/smtpd/smtp.c new file mode 100644 index 00000000..602fd0d6 --- /dev/null +++ b/smtpd/smtp.c @@ -0,0 +1,387 @@ +/* $OpenBSD: smtp.c,v 1.166 2019/08/10 16:07:01 gilles Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <grp.h> /* needed for setgroups */ +#include <imsg.h> +#include <netdb.h> +#include <pwd.h> +#include <signal.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/ssl.h> + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" + +static void smtp_setup_events(void); +static void smtp_pause(void); +static void smtp_resume(void); +static void smtp_accept(int, short, void *); +static void smtp_dropped(struct listener *, int, const struct sockaddr_storage *); +static int smtp_enqueue(void); +static int smtp_can_accept(void); +static void smtp_setup_listeners(void); +static int smtp_sni_callback(SSL *, int *, void *); + +int +proxy_session(struct listener *listener, int sock, + const struct sockaddr_storage *ss, + void (*accepted)(struct listener *, int, + const struct sockaddr_storage *, struct io *), + void (*dropped)(struct listener *, int, + const struct sockaddr_storage *)); + +static void smtp_accepted(struct listener *, int, const struct sockaddr_storage *, struct io *); + + +#define SMTP_FD_RESERVE 5 +#define getdtablecount() 0 + +static size_t sessions; +static size_t maxsessions; + +void +smtp_imsg(struct mproc *p, struct imsg *imsg) +{ + switch (imsg->hdr.type) { + case IMSG_SMTP_CHECK_SENDER: + case IMSG_SMTP_EXPAND_RCPT: + case IMSG_SMTP_LOOKUP_HELO: + case IMSG_SMTP_AUTHENTICATE: + case IMSG_FILTER_SMTP_PROTOCOL: + case IMSG_FILTER_SMTP_DATA_BEGIN: + smtp_session_imsg(p, imsg); + return; + + case IMSG_SMTP_MESSAGE_COMMIT: + case IMSG_SMTP_MESSAGE_CREATE: + case IMSG_SMTP_MESSAGE_OPEN: + case IMSG_QUEUE_ENVELOPE_SUBMIT: + case IMSG_QUEUE_ENVELOPE_COMMIT: + smtp_session_imsg(p, imsg); + return; + + case IMSG_QUEUE_SMTP_SESSION: + m_compose(p, IMSG_QUEUE_SMTP_SESSION, 0, 0, smtp_enqueue(), + imsg->data, imsg->hdr.len - sizeof imsg->hdr); + return; + + case IMSG_CTL_SMTP_SESSION: + m_compose(p, IMSG_CTL_SMTP_SESSION, imsg->hdr.peerid, 0, + smtp_enqueue(), NULL, 0); + return; + + case IMSG_CTL_PAUSE_SMTP: + log_debug("debug: smtp: pausing listening sockets"); + smtp_pause(); + env->sc_flags |= SMTPD_SMTP_PAUSED; + return; + + case IMSG_CTL_RESUME_SMTP: + log_debug("debug: smtp: resuming listening sockets"); + env->sc_flags &= ~SMTPD_SMTP_PAUSED; + smtp_resume(); + return; + } + + errx(1, "smtp_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type)); +} + +void +smtp_postfork(void) +{ + smtp_setup_listeners(); +} + +void +smtp_postprivdrop(void) +{ +} + +void +smtp_configure(void) +{ + smtp_setup_events(); +} + +static void +smtp_setup_listeners(void) +{ + struct listener *l; + int opt; + + TAILQ_FOREACH(l, env->sc_listeners, entry) { + if ((l->fd = socket(l->ss.ss_family, SOCK_STREAM, 0)) == -1) { + if (errno == EAFNOSUPPORT) { + log_warn("smtpd: socket"); + continue; + } + fatal("smtpd: socket"); + } + opt = 1; +#ifdef SO_REUSEADDR + if (setsockopt(l->fd, SOL_SOCKET, SO_REUSEADDR, &opt, + sizeof(opt)) == -1) + fatal("smtpd: setsockopt"); +#else + if (setsockopt(l->fd, SOL_SOCKET, SO_REUSEPORT, &opt, + sizeof(opt)) < 0) + fatal("smtpd: setsockopt"); +#endif +#ifdef IPV6_V6ONLY + /* + * If using IPv6, bind only to IPv6 if possible. + * This avoids ambiguities with IPv4-mapped IPv6 addresses. + */ + if (l->ss.ss_family == AF_INET6) + if (setsockopt(l->fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, + sizeof(opt)) < 0) + fatal("smtpd: setsockopt"); +#endif + if (bind(l->fd, (struct sockaddr *)&l->ss, SS_LEN(&l->ss)) == -1) + fatal("smtpd: bind"); + } +} + +static void +smtp_setup_events(void) +{ + struct listener *l; + struct pki *pki; + SSL_CTX *ssl_ctx; + void *iter; + const char *k; + + TAILQ_FOREACH(l, env->sc_listeners, entry) { + log_debug("debug: smtp: listen on %s port %d flags 0x%01x" + " pki \"%s\"" + " ca \"%s\"", ss_to_text(&l->ss), ntohs(l->port), + l->flags, l->pki_name, l->ca_name); + + io_set_nonblocking(l->fd); + if (listen(l->fd, SMTPD_BACKLOG) == -1) + fatal("listen"); + event_set(&l->ev, l->fd, EV_READ|EV_PERSIST, smtp_accept, l); + + if (!(env->sc_flags & SMTPD_SMTP_PAUSED)) + event_add(&l->ev, NULL); + } + + iter = NULL; + while (dict_iter(env->sc_pki_dict, &iter, &k, (void **)&pki)) { + if (!ssl_setup((SSL_CTX **)&ssl_ctx, pki, smtp_sni_callback, + env->sc_tls_ciphers)) + fatal("smtp_setup_events: ssl_setup failure"); + dict_xset(env->sc_ssl_dict, k, ssl_ctx); + } + + purge_config(PURGE_PKI_KEYS); + + maxsessions = (getdtablesize() - getdtablecount()) / 2 - SMTP_FD_RESERVE; + log_debug("debug: smtp: will accept at most %zu clients", maxsessions); +} + +static void +smtp_pause(void) +{ + struct listener *l; + + if (env->sc_flags & (SMTPD_SMTP_DISABLED|SMTPD_SMTP_PAUSED)) + return; + + TAILQ_FOREACH(l, env->sc_listeners, entry) + event_del(&l->ev); +} + +static void +smtp_resume(void) +{ + struct listener *l; + + if (env->sc_flags & (SMTPD_SMTP_DISABLED|SMTPD_SMTP_PAUSED)) + return; + + TAILQ_FOREACH(l, env->sc_listeners, entry) + event_add(&l->ev, NULL); +} + +static int +smtp_enqueue(void) +{ + struct listener *listener = env->sc_sock_listener; + int fd[2]; + + /* + * Some enqueue requests buffered in IMSG may still arrive even after + * call to smtp_pause() because enqueue listener is not a real socket + * and thus cannot be paused properly. + */ + if (env->sc_flags & SMTPD_SMTP_PAUSED) + return (-1); + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, fd)) + return (-1); + + if ((smtp_session(listener, fd[0], &listener->ss, env->sc_hostname, NULL)) == -1) { + close(fd[0]); + close(fd[1]); + return (-1); + } + + sessions++; + stat_increment("smtp.session", 1); + stat_increment("smtp.session.local", 1); + + return (fd[1]); +} + +static void +smtp_accept(int fd, short event, void *p) +{ + struct listener *listener = p; + struct sockaddr_storage ss; + socklen_t len; + int sock; + + if (env->sc_flags & SMTPD_SMTP_PAUSED) + fatalx("smtp_session: unexpected client"); + + if (!smtp_can_accept()) { + log_warnx("warn: Disabling incoming SMTP connections: " + "Client limit reached"); + goto pause; + } + + len = sizeof(ss); + if ((sock = accept(fd, (struct sockaddr *)&ss, &len)) == -1) { + if (errno == ENFILE || errno == EMFILE) { + log_warn("warn: Disabling incoming SMTP connections"); + goto pause; + } + if (errno == EINTR || errno == EWOULDBLOCK || + errno == ECONNABORTED) + return; + fatal("smtp_accept"); + } + + if (listener->flags & F_PROXY) { + io_set_nonblocking(sock); + if (proxy_session(listener, sock, &ss, + smtp_accepted, smtp_dropped) == -1) { + close(sock); + return; + } + return; + } + + smtp_accepted(listener, sock, &ss, NULL); + return; + +pause: + smtp_pause(); + env->sc_flags |= SMTPD_SMTP_DISABLED; + return; +} + +static int +smtp_can_accept(void) +{ + if (sessions + 1 == maxsessions) + return 0; + return (getdtablesize() - getdtablecount() - SMTP_FD_RESERVE >= 2); +} + +void +smtp_collect(void) +{ + sessions--; + stat_decrement("smtp.session", 1); + + if (!smtp_can_accept()) + return; + + if (env->sc_flags & SMTPD_SMTP_DISABLED) { + log_warnx("warn: smtp: " + "fd exhaustion over, re-enabling incoming connections"); + env->sc_flags &= ~SMTPD_SMTP_DISABLED; + smtp_resume(); + } +} + +static int +smtp_sni_callback(SSL *ssl, int *ad, void *arg) +{ + const char *sn; + void *ssl_ctx; + + sn = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + if (sn == NULL) + return SSL_TLSEXT_ERR_NOACK; + ssl_ctx = dict_get(env->sc_ssl_dict, sn); + if (ssl_ctx == NULL) + return SSL_TLSEXT_ERR_NOACK; + SSL_set_SSL_CTX(ssl, ssl_ctx); + return SSL_TLSEXT_ERR_OK; +} + +static void +smtp_accepted(struct listener *listener, int sock, const struct sockaddr_storage *ss, struct io *io) +{ + int ret; + + ret = smtp_session(listener, sock, ss, NULL, io); + if (ret == -1) { + log_warn("warn: Failed to create SMTP session"); + close(sock); + return; + } + io_set_nonblocking(sock); + + sessions++; + stat_increment("smtp.session", 1); + if (listener->ss.ss_family == AF_LOCAL) + stat_increment("smtp.session.local", 1); + if (listener->ss.ss_family == AF_INET) + stat_increment("smtp.session.inet4", 1); + if (listener->ss.ss_family == AF_INET6) + stat_increment("smtp.session.inet6", 1); +} + +static void +smtp_dropped(struct listener *listener, int sock, const struct sockaddr_storage *ss) +{ + close(sock); + sessions--; +} diff --git a/smtpd/smtp.h b/smtpd/smtp.h new file mode 100644 index 00000000..dc91d878 --- /dev/null +++ b/smtpd/smtp.h @@ -0,0 +1,95 @@ +/* $OpenBSD: smtp.h,v 1.3 2019/09/02 20:05:21 eric Exp $ */ + +/* + * Copyright (c) 2018 Eric Faurot <eric@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 TLS_NO 0 +#define TLS_YES 1 +#define TLS_FORCE 2 +#define TLS_SMTPS 3 + +#define CERT_OK 0 +#define CERT_UNKNOWN 1 +#define CERT_INVALID 2 + +#define FAIL_INTERNAL 1 /* malloc, etc. (check errno) */ +#define FAIL_CONN 2 /* disconnect, timeout... (check errno) */ +#define FAIL_PROTO 3 /* protocol violation */ +#define FAIL_IMPL 4 /* server lacks a required feature */ +#define FAIL_RESP 5 /* rejected command */ + +struct smtp_params { + + /* Client options */ + size_t linemax; /* max input line size */ + size_t ibufmax; /* max input buffer size */ + size_t obufmax; /* max output buffer size */ + + /* Connection settings */ + const struct sockaddr *dst; /* address to connect to */ + const struct sockaddr *src; /* address to bind to */ + int timeout; /* timeout in seconds */ + + /* TLS options */ + int tls_req; /* requested TLS mode */ + int tls_verify; /* need valid server certificate */ + + /* SMTP options */ + int lmtp; /* use LMTP protocol */ + const char *helo; /* string to use with HELO */ + const char *auth_user; /* for AUTH */ + const char *auth_pass; /* for AUTH */ +}; + +struct smtp_rcpt { + const char *to; + const char *dsn_notify; + const char *dsn_orcpt; + int done; +}; + +struct smtp_mail { + const char *from; + const char *dsn_ret; + const char *dsn_envid; + struct smtp_rcpt *rcpt; + int rcptcount; + FILE *fp; +}; + +struct smtp_status { + struct smtp_rcpt *rcpt; + const char *cmd; + const char *status; +}; + +struct smtp_client; + +/* smtp_client.c */ +struct smtp_client *smtp_connect(const struct smtp_params *, void *); +void smtp_cert_verified(struct smtp_client *, int); +void smtp_set_tls(struct smtp_client *, void *); +void smtp_quit(struct smtp_client *); +void smtp_sendmail(struct smtp_client *, struct smtp_mail *); + +/* callbacks */ +void smtp_verify_server_cert(void *, struct smtp_client *, void *); +void smtp_require_tls(void *, struct smtp_client *); +void smtp_ready(void *, struct smtp_client *); +void smtp_failed(void *, struct smtp_client *, int, const char *); +void smtp_closed(void *, struct smtp_client *); +void smtp_status(void *, struct smtp_client *, struct smtp_status *); +void smtp_done(void *, struct smtp_client *, struct smtp_mail *); diff --git a/smtpd/smtp/Makefile b/smtpd/smtp/Makefile new file mode 100644 index 00000000..380e3ad6 --- /dev/null +++ b/smtpd/smtp/Makefile @@ -0,0 +1,24 @@ +# $OpenBSD: Makefile,v 1.1 2018/04/26 13:57:13 eric Exp $ + +.PATH: ${.CURDIR}/.. + +PROG= smtp + +BINDIR= /usr/bin +MAN= smtp.1 + +SRCS+= iobuf.c +SRCS+= ioev.c +SRCS+= log.c +SRCS+= smtp_client.c +SRCS+= smtpc.c +SRCS+= ssl.c +SRCS+= ssl_smtpd.c +SRCS+= ssl_verify.c + +CPPFLAGS+= -DIO_TLS + +LDADD+= -levent -lutil -lssl -lcrypto -lm -lz +DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBSSL} ${LIBCRYPTO} ${LIBM} ${LIBZ} + +.include <bsd.prog.mk> diff --git a/smtpd/smtp_client.c b/smtpd/smtp_client.c new file mode 100644 index 00000000..8e146e1b --- /dev/null +++ b/smtpd/smtp_client.c @@ -0,0 +1,923 @@ +/* $OpenBSD: smtp_client.c,v 1.14 2020/04/24 11:34:07 eric Exp $ */ + +/* + * Copyright (c) 2018 Eric Faurot <eric@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 <netinet/in.h> + +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <resolv.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "log.h" +#include "ioev.h" +#include "smtp.h" + +#define TRACE_SMTPCLT 2 +#define TRACE_IO 3 + +enum { + STATE_INIT = 0, + STATE_BANNER, + STATE_EHLO, + STATE_HELO, + STATE_LHLO, + STATE_STARTTLS, + STATE_AUTH, + STATE_AUTH_PLAIN, + STATE_AUTH_LOGIN, + STATE_AUTH_LOGIN_USER, + STATE_AUTH_LOGIN_PASS, + STATE_READY, + STATE_MAIL, + STATE_RCPT, + STATE_DATA, + STATE_BODY, + STATE_EOM, + STATE_RSET, + STATE_QUIT, + + STATE_LAST +}; + +#define base64_encode __b64_ntop +#define base64_decode __b64_pton + +#define FLAG_TLS 0x01 +#define FLAG_TLS_VERIFIED 0x02 + +#define SMTP_EXT_STARTTLS 0x01 +#define SMTP_EXT_PIPELINING 0x02 +#define SMTP_EXT_AUTH 0x04 +#define SMTP_EXT_AUTH_PLAIN 0x08 +#define SMTP_EXT_AUTH_LOGIN 0x10 +#define SMTP_EXT_DSN 0x20 +#define SMTP_EXT_SIZE 0x40 + +struct smtp_client { + void *tag; + struct smtp_params params; + + int state; + int flags; + int ext; + size_t ext_size; + + struct io *io; + char *reply; + size_t replysz; + + struct smtp_mail *mail; + int rcptidx; + int rcptok; +}; + +void log_trace_verbose(int); +void log_trace(int, const char *, ...) + __attribute__((format (printf, 2, 3))); + +static void smtp_client_io(struct io *, int, void *); +static void smtp_client_free(struct smtp_client *); +static void smtp_client_state(struct smtp_client *, int); +static void smtp_client_abort(struct smtp_client *, int, const char *); +static void smtp_client_cancel(struct smtp_client *, int, const char *); +static void smtp_client_sendcmd(struct smtp_client *, char *, ...); +static void smtp_client_sendbody(struct smtp_client *); +static int smtp_client_readline(struct smtp_client *); +static int smtp_client_replycat(struct smtp_client *, const char *); +static void smtp_client_response(struct smtp_client *, const char *); +static void smtp_client_mail_abort(struct smtp_client *); +static void smtp_client_mail_status(struct smtp_client *, const char *); +static void smtp_client_rcpt_status(struct smtp_client *, struct smtp_rcpt *, const char *); + +static const char *strstate[STATE_LAST] = { + "INIT", + "BANNER", + "EHLO", + "HELO", + "LHLO", + "STARTTLS", + "AUTH", + "AUTH_PLAIN", + "AUTH_LOGIN", + "AUTH_LOGIN_USER", + "AUTH_LOGIN_PASS", + "READY", + "MAIL", + "RCPT", + "DATA", + "BODY", + "EOM", + "RSET", + "QUIT", +}; + +struct smtp_client * +smtp_connect(const struct smtp_params *params, void *tag) +{ + struct smtp_client *proto; + + proto = calloc(1, sizeof *proto); + if (proto == NULL) + return NULL; + + memmove(&proto->params, params, sizeof(*params)); + proto->tag = tag; + proto->io = io_new(); + if (proto->io == NULL) { + free(proto); + return NULL; + } + io_set_callback(proto->io, smtp_client_io, proto); + io_set_timeout(proto->io, proto->params.timeout); + + if (io_connect(proto->io, proto->params.dst, proto->params.src) == -1) { + smtp_client_abort(proto, FAIL_CONN, io_error(proto->io)); + return NULL; + } + + return proto; +} + +void +smtp_cert_verified(struct smtp_client *proto, int verified) +{ + if (verified == CERT_OK) + proto->flags |= FLAG_TLS_VERIFIED; + + else if (proto->params.tls_verify) { + errno = EAUTH; + smtp_client_abort(proto, FAIL_CONN, + "Invalid server certificate"); + return; + } + + io_resume(proto->io, IO_IN); + + if (proto->state == STATE_INIT) + smtp_client_state(proto, STATE_BANNER); + else { + /* Clear extensions before re-issueing an EHLO command. */ + proto->ext = 0; + smtp_client_state(proto, STATE_EHLO); + } +} + +void +smtp_set_tls(struct smtp_client *proto, void *ctx) +{ + io_start_tls(proto->io, ctx); +} + +void +smtp_quit(struct smtp_client *proto) +{ + if (proto->state != STATE_READY) + fatalx("connection is not ready"); + + smtp_client_state(proto, STATE_QUIT); +} + +void +smtp_sendmail(struct smtp_client *proto, struct smtp_mail *mail) +{ + if (proto->state != STATE_READY) + fatalx("connection is not ready"); + + proto->mail = mail; + smtp_client_state(proto, STATE_MAIL); +} + +static void +smtp_client_free(struct smtp_client *proto) +{ + if (proto->mail) + fatalx("current task should have been deleted already"); + + smtp_closed(proto->tag, proto); + + if (proto->io) + io_free(proto->io); + + free(proto->reply); + free(proto); +} + +/* + * End the session immediatly. + */ +static void +smtp_client_abort(struct smtp_client *proto, int err, const char *reason) +{ + smtp_failed(proto->tag, proto, err, reason); + + if (proto->mail) + smtp_client_mail_abort(proto); + + smtp_client_free(proto); +} + +/* + * Properly close the session. + */ +static void +smtp_client_cancel(struct smtp_client *proto, int err, const char *reason) +{ + if (proto->mail) + fatal("not supposed to have a mail"); + + smtp_failed(proto->tag, proto, err, reason); + + smtp_client_state(proto, STATE_QUIT); +} + +static void +smtp_client_state(struct smtp_client *proto, int newstate) +{ + struct smtp_rcpt *rcpt; + char ibuf[LINE_MAX], obuf[LINE_MAX]; + size_t n; + int oldstate; + + if (proto->reply) + proto->reply[0] = '\0'; + + again: + oldstate = proto->state; + proto->state = newstate; + + log_trace(TRACE_SMTPCLT, "%p: %s -> %s", proto, + strstate[oldstate], + strstate[newstate]); + + /* don't try this at home! */ +#define smtp_client_state(_s, _st) do { newstate = _st; goto again; } while (0) + + switch (proto->state) { + case STATE_BANNER: + io_set_read(proto->io); + break; + + case STATE_EHLO: + smtp_client_sendcmd(proto, "EHLO %s", proto->params.helo); + break; + + case STATE_HELO: + smtp_client_sendcmd(proto, "HELO %s", proto->params.helo); + break; + + case STATE_LHLO: + smtp_client_sendcmd(proto, "LHLO %s", proto->params.helo); + break; + + case STATE_STARTTLS: + if (proto->params.tls_req == TLS_NO || proto->flags & FLAG_TLS) + smtp_client_state(proto, STATE_AUTH); + else if (proto->ext & SMTP_EXT_STARTTLS) + smtp_client_sendcmd(proto, "STARTTLS"); + else if (proto->params.tls_req == TLS_FORCE) + smtp_client_cancel(proto, FAIL_IMPL, + "TLS not supported by remote host"); + else + smtp_client_state(proto, STATE_AUTH); + break; + + case STATE_AUTH: + if (!proto->params.auth_user) + smtp_client_state(proto, STATE_READY); + else if ((proto->flags & FLAG_TLS) == 0) + smtp_client_cancel(proto, FAIL_IMPL, + "Authentication requires TLS"); + else if ((proto->ext & SMTP_EXT_AUTH) == 0) + smtp_client_cancel(proto, FAIL_IMPL, + "AUTH not supported by remote host"); + else if (proto->ext & SMTP_EXT_AUTH_PLAIN) + smtp_client_state(proto, STATE_AUTH_PLAIN); + else if (proto->ext & SMTP_EXT_AUTH_LOGIN) + smtp_client_state(proto, STATE_AUTH_LOGIN); + else + smtp_client_cancel(proto, FAIL_IMPL, + "No supported AUTH method"); + break; + + case STATE_AUTH_PLAIN: + (void)strlcpy(ibuf, "-", sizeof(ibuf)); + (void)strlcat(ibuf, proto->params.auth_user, sizeof(ibuf)); + if (strlcat(ibuf, ":", sizeof(ibuf)) >= sizeof(ibuf)) { + errno = EMSGSIZE; + smtp_client_cancel(proto, FAIL_INTERNAL, + "credentials too large"); + break; + } + n = strlcat(ibuf, proto->params.auth_pass, sizeof(ibuf)); + if (n >= sizeof(ibuf)) { + errno = EMSGSIZE; + smtp_client_cancel(proto, FAIL_INTERNAL, + "credentials too large"); + break; + } + *strchr(ibuf, ':') = '\0'; + ibuf[0] = '\0'; + if (base64_encode(ibuf, n, obuf, sizeof(obuf)) == -1) { + errno = EMSGSIZE; + smtp_client_cancel(proto, FAIL_INTERNAL, + "credentials too large"); + break; + } + smtp_client_sendcmd(proto, "AUTH PLAIN %s", obuf); + explicit_bzero(ibuf, sizeof ibuf); + explicit_bzero(obuf, sizeof obuf); + break; + + case STATE_AUTH_LOGIN: + smtp_client_sendcmd(proto, "AUTH LOGIN"); + break; + + case STATE_AUTH_LOGIN_USER: + if (base64_encode(proto->params.auth_user, + strlen(proto->params.auth_user), obuf, + sizeof(obuf)) == -1) { + errno = EMSGSIZE; + smtp_client_cancel(proto, FAIL_INTERNAL, + "credentials too large"); + break; + } + smtp_client_sendcmd(proto, "%s", obuf); + explicit_bzero(obuf, sizeof obuf); + break; + + case STATE_AUTH_LOGIN_PASS: + if (base64_encode(proto->params.auth_pass, + strlen(proto->params.auth_pass), obuf, + sizeof(obuf)) == -1) { + errno = EMSGSIZE; + smtp_client_cancel(proto, FAIL_INTERNAL, + "credentials too large"); + break; + } + smtp_client_sendcmd(proto, "%s", obuf); + explicit_bzero(obuf, sizeof obuf); + break; + + case STATE_READY: + smtp_ready(proto->tag, proto); + break; + + case STATE_MAIL: + if (proto->ext & SMTP_EXT_DSN) + smtp_client_sendcmd(proto, "MAIL FROM:<%s>%s%s%s%s", + proto->mail->from, + proto->mail->dsn_ret ? " RET=" : "", + proto->mail->dsn_ret ? proto->mail->dsn_ret : "", + proto->mail->dsn_envid ? " ENVID=" : "", + proto->mail->dsn_envid ? proto->mail->dsn_envid : ""); + else + smtp_client_sendcmd(proto, "MAIL FROM:<%s>", + proto->mail->from); + break; + + case STATE_RCPT: + if (proto->rcptidx == proto->mail->rcptcount) { + smtp_client_state(proto, STATE_DATA); + break; + } + rcpt = &proto->mail->rcpt[proto->rcptidx]; + if (proto->ext & SMTP_EXT_DSN) + smtp_client_sendcmd(proto, "RCPT TO:<%s>%s%s%s%s", + rcpt->to, + rcpt->dsn_notify ? " NOTIFY=" : "", + rcpt->dsn_notify ? rcpt->dsn_notify : "", + rcpt->dsn_orcpt ? " ORCPT=" : "", + rcpt->dsn_orcpt ? rcpt->dsn_orcpt : ""); + else + smtp_client_sendcmd(proto, "RCPT TO:<%s>", rcpt->to); + break; + + case STATE_DATA: + if (proto->rcptok == 0) { + smtp_client_mail_abort(proto); + smtp_client_state(proto, STATE_RSET); + } + else + smtp_client_sendcmd(proto, "DATA"); + break; + + case STATE_BODY: + fseek(proto->mail->fp, 0, SEEK_SET); + smtp_client_sendbody(proto); + break; + + case STATE_EOM: + smtp_client_sendcmd(proto, "."); + break; + + case STATE_RSET: + smtp_client_sendcmd(proto, "RSET"); + break; + + case STATE_QUIT: + smtp_client_sendcmd(proto, "QUIT"); + break; + + default: + fatalx("%s: bad state %d", __func__, proto->state); + } +#undef smtp_client_state +} + +/* + * Handle a response to an SMTP command + */ +static void +smtp_client_response(struct smtp_client *proto, const char *line) +{ + struct smtp_rcpt *rcpt; + int i, seen; + + switch (proto->state) { + case STATE_BANNER: + if (line[0] != '2') + smtp_client_abort(proto, FAIL_RESP, line); + else if (proto->params.lmtp) + smtp_client_state(proto, STATE_LHLO); + else + smtp_client_state(proto, STATE_EHLO); + break; + + case STATE_EHLO: + if (line[0] != '2') { + /* + * Either rejected or not implemented. If we want to + * use EHLO extensions, report an SMTP error. + * Otherwise, fallback to using HELO. + */ + if ((proto->params.tls_req == TLS_FORCE) || + (proto->params.auth_user)) + smtp_client_cancel(proto, FAIL_RESP, line); + else + smtp_client_state(proto, STATE_HELO); + break; + } + smtp_client_state(proto, STATE_STARTTLS); + break; + + case STATE_HELO: + if (line[0] != '2') + smtp_client_cancel(proto, FAIL_RESP, line); + else + smtp_client_state(proto, STATE_READY); + break; + + case STATE_LHLO: + if (line[0] != '2') + smtp_client_cancel(proto, FAIL_RESP, line); + else + smtp_client_state(proto, STATE_READY); + break; + + case STATE_STARTTLS: + if (line[0] != '2') { + if ((proto->params.tls_req == TLS_FORCE) || + (proto->params.auth_user)) { + smtp_client_cancel(proto, FAIL_RESP, line); + break; + } + smtp_client_state(proto, STATE_AUTH); + } + else + smtp_require_tls(proto->tag, proto); + break; + + case STATE_AUTH_PLAIN: + if (line[0] != '2') + smtp_client_cancel(proto, FAIL_RESP, line); + else + smtp_client_state(proto, STATE_READY); + break; + + case STATE_AUTH_LOGIN: + if (strncmp(line, "334 ", 4)) + smtp_client_cancel(proto, FAIL_RESP, line); + else + smtp_client_state(proto, STATE_AUTH_LOGIN_USER); + break; + + case STATE_AUTH_LOGIN_USER: + if (strncmp(line, "334 ", 4)) + smtp_client_cancel(proto, FAIL_RESP, line); + else + smtp_client_state(proto, STATE_AUTH_LOGIN_PASS); + break; + + case STATE_AUTH_LOGIN_PASS: + if (line[0] != '2') + smtp_client_cancel(proto, FAIL_RESP, line); + else + smtp_client_state(proto, STATE_READY); + break; + + case STATE_MAIL: + if (line[0] != '2') { + smtp_client_mail_status(proto, line); + smtp_client_state(proto, STATE_RSET); + } + else + smtp_client_state(proto, STATE_RCPT); + break; + + case STATE_RCPT: + rcpt = &proto->mail->rcpt[proto->rcptidx++]; + if (line[0] != '2') + smtp_client_rcpt_status(proto, rcpt, line); + else { + proto->rcptok++; + smtp_client_state(proto, STATE_RCPT); + } + break; + + case STATE_DATA: + if (line[0] != '2' && line[0] != '3') { + smtp_client_mail_status(proto, line); + smtp_client_state(proto, STATE_RSET); + } + else + smtp_client_state(proto, STATE_BODY); + break; + + case STATE_EOM: + if (proto->params.lmtp) { + /* + * LMTP reports a status of each accepted RCPT. + * Report status for the first pending RCPT and read + * more lines if another rcpt needs a status. + */ + for (i = 0, seen = 0; i < proto->mail->rcptcount; i++) { + rcpt = &proto->mail->rcpt[i]; + if (rcpt->done) + continue; + if (seen) { + io_set_read(proto->io); + return; + } + smtp_client_rcpt_status(proto, + &proto->mail->rcpt[i], line); + seen = 1; + } + } + smtp_client_mail_status(proto, line); + smtp_client_state(proto, STATE_READY); + break; + + case STATE_RSET: + if (line[0] != '2') + smtp_client_cancel(proto, FAIL_RESP, line); + else + smtp_client_state(proto, STATE_READY); + break; + + case STATE_QUIT: + smtp_client_free(proto); + break; + + default: + fatalx("%s: bad state %d", __func__, proto->state); + } +} + +static void +smtp_client_io(struct io *io, int evt, void *arg) +{ + struct smtp_client *proto = arg; + + log_trace(TRACE_IO, "%p: %s %s", proto, io_strevent(evt), io_strio(io)); + + switch (evt) { + case IO_CONNECTED: + if (proto->params.tls_req == TLS_SMTPS) { + io_set_write(io); + smtp_require_tls(proto->tag, proto); + } + else + smtp_client_state(proto, STATE_BANNER); + break; + + case IO_TLSREADY: + proto->flags |= FLAG_TLS; + io_pause(proto->io, IO_IN); + smtp_verify_server_cert(proto->tag, proto, io_tls(proto->io)); + break; + + case IO_DATAIN: + while (smtp_client_readline(proto)) + ; + break; + + case IO_LOWAT: + if (proto->state == STATE_BODY) + smtp_client_sendbody(proto); + else + io_set_read(io); + break; + + case IO_TIMEOUT: + errno = ETIMEDOUT; + smtp_client_abort(proto, FAIL_CONN, "Connection timeout"); + break; + + case IO_ERROR: + smtp_client_abort(proto, FAIL_CONN, io_error(io)); + break; + + case IO_TLSERROR: + smtp_client_abort(proto, FAIL_CONN, io_error(io)); + break; + + case IO_DISCONNECTED: + smtp_client_abort(proto, FAIL_CONN, io_error(io)); + break; + + default: + fatalx("%s: bad event %d", __func__, evt); + } +} + +/* + * return 1 if a new line is expected. + */ +static int +smtp_client_readline(struct smtp_client *proto) +{ + const char *e; + size_t len; + char *line, *msg, *p; + int cont; + + line = io_getline(proto->io, &len); + if (line == NULL) { + if (io_datalen(proto->io) >= proto->params.linemax) + smtp_client_abort(proto, FAIL_PROTO, "Line too long"); + return 0; + } + + /* Strip trailing '\r' */ + if (len && line[len - 1] == '\r') + line[--len] = '\0'; + + log_trace(TRACE_SMTPCLT, "%p: <<< %s", proto, line); + + /* Validate SMTP */ + if (len > 3) { + msg = line + 4; + cont = (line[3] == '-'); + } else if (len == 3) { + msg = line + 3; + cont = 0; + } else { + smtp_client_abort(proto, FAIL_PROTO, "Response too short"); + return 0; + } + + /* Validate reply code. */ + if (line[0] < '2' || line[0] > '5' || !isdigit((unsigned char)line[1]) || + !isdigit((unsigned char)line[2])) { + smtp_client_abort(proto, FAIL_PROTO, "Invalid reply code"); + return 0; + } + + /* Validate reply message. */ + for (p = msg; *p; p++) + if (!isprint((unsigned char)*p)) { + smtp_client_abort(proto, FAIL_PROTO, + "Non-printable characters in response"); + return 0; + } + + /* Read extensions. */ + if (proto->state == STATE_EHLO) { + if (strcmp(msg, "STARTTLS") == 0) + proto->ext |= SMTP_EXT_STARTTLS; + else if (strncmp(msg, "AUTH ", 5) == 0) { + proto->ext |= SMTP_EXT_AUTH; + if ((p = strstr(msg, " PLAIN")) && + (*(p+6) == '\0' || *(p+6) == ' ')) + proto->ext |= SMTP_EXT_AUTH_PLAIN; + if ((p = strstr(msg, " LOGIN")) && + (*(p+6) == '\0' || *(p+6) == ' ')) + proto->ext |= SMTP_EXT_AUTH_LOGIN; + } + else if (strcmp(msg, "PIPELINING") == 0) + proto->ext |= SMTP_EXT_PIPELINING; + else if (strcmp(msg, "DSN") == 0) + proto->ext |= SMTP_EXT_DSN; + else if (strncmp(msg, "SIZE ", 5) == 0) { + proto->ext_size = strtonum(msg + 5, 0, SIZE_T_MAX, &e); + if (e == NULL) + proto->ext |= SMTP_EXT_SIZE; + } + } + + if (smtp_client_replycat(proto, line) == -1) { + smtp_client_abort(proto, FAIL_INTERNAL, NULL); + return 0; + } + + if (cont) + return 1; + + if (io_datalen(proto->io)) { + /* + * There should be no pending data after a response is read, + * except for the multiple status lines after a LMTP message. + * It can also happen with pipelineing, but we don't do that + * for now. + */ + if (!(proto->params.lmtp && proto->state == STATE_EOM)) { + smtp_client_abort(proto, FAIL_PROTO, "Trailing data"); + return 0; + } + } + + io_set_write(proto->io); + smtp_client_response(proto, proto->reply); + return 0; +} + +/* + * Concatenate the given response line. + */ +static int +smtp_client_replycat(struct smtp_client *proto, const char *line) +{ + size_t len; + char *tmp; + int first; + + if (proto->reply && proto->reply[0]) { + /* + * If the line is the continuation of an multi-line response, + * skip the status and ESC parts. First, skip the status, then + * skip the separator amd ESC if found. + */ + first = 0; + line += 3; + if (line[0]) { + line += 1; + if (isdigit((unsigned char)line[0]) && line[1] == '.' && + isdigit((unsigned char)line[2]) && line[3] == '.' && + isdigit((unsigned char)line[4]) && + isspace((unsigned char)line[5])) + line += 5; + } + } else + first = 1; + + if (proto->reply) { + len = strlcat(proto->reply, line, proto->replysz); + if (len < proto->replysz) + return 0; + } + else + len = strlen(line); + + if (len > proto->params.ibufmax) { + errno = EMSGSIZE; + return -1; + } + + /* Allocate by multiples of 2^8 */ + len += (len % 256) ? (256 - (len % 256)) : 0; + + tmp = realloc(proto->reply, len); + if (tmp == NULL) + return -1; + if (proto->reply == NULL) + tmp[0] = '\0'; + + proto->reply = tmp; + proto->replysz = len; + (void)strlcat(proto->reply, line, proto->replysz); + + /* Replace the separator with a space for the first line. */ + if (first && proto->reply[3]) + proto->reply[3] = ' '; + + return 0; +} + +static void +smtp_client_sendbody(struct smtp_client *proto) +{ + ssize_t len; + size_t sz = 0, total, w; + char *ln = NULL; + int n; + + total = io_queued(proto->io); + w = 0; + + while (total < proto->params.obufmax) { + if ((len = getline(&ln, &sz, proto->mail->fp)) == -1) + break; + if (ln[len - 1] == '\n') + ln[len - 1] = '\0'; + n = io_printf(proto->io, "%s%s\r\n", *ln == '.'?".":"", ln); + if (n == -1) { + free(ln); + smtp_client_abort(proto, FAIL_INTERNAL, NULL); + return; + } + total += n; + w += n; + } + free(ln); + + if (ferror(proto->mail->fp)) { + smtp_client_abort(proto, FAIL_INTERNAL, "Cannot read message"); + return; + } + + log_trace(TRACE_SMTPCLT, "%p: >>> [...%zd bytes...]", proto, w); + + if (feof(proto->mail->fp)) + smtp_client_state(proto, STATE_EOM); +} + +static void +smtp_client_sendcmd(struct smtp_client *proto, char *fmt, ...) +{ + va_list ap; + char *p; + int len; + + va_start(ap, fmt); + len = vasprintf(&p, fmt, ap); + va_end(ap); + + if (len == -1) { + smtp_client_abort(proto, FAIL_INTERNAL, NULL); + return; + } + + log_trace(TRACE_SMTPCLT, "mta: %p: >>> %s", proto, p); + + len = io_printf(proto->io, "%s\r\n", p); + free(p); + + if (len == -1) + smtp_client_abort(proto, FAIL_INTERNAL, NULL); +} + +static void +smtp_client_mail_status(struct smtp_client *proto, const char *status) +{ + int i; + + for (i = 0; i < proto->mail->rcptcount; i++) + smtp_client_rcpt_status(proto, &proto->mail->rcpt[i], status); + + smtp_done(proto->tag, proto, proto->mail); + proto->mail = NULL; +} + +static void +smtp_client_mail_abort(struct smtp_client *proto) +{ + smtp_done(proto->tag, proto, proto->mail); + proto->mail = NULL; +} + +static void +smtp_client_rcpt_status(struct smtp_client *proto, struct smtp_rcpt *rcpt, const char *line) +{ + struct smtp_status status; + + if (rcpt->done) + return; + + rcpt->done = 1; + status.rcpt = rcpt; + status.cmd = strstate[proto->state]; + status.status = line; + smtp_status(proto->tag, proto, &status); +} diff --git a/smtpd/smtp_session.c b/smtpd/smtp_session.c new file mode 100644 index 00000000..aefce155 --- /dev/null +++ b/smtpd/smtp_session.c @@ -0,0 +1,3223 @@ +/* $OpenBSD: smtp_session.c,v 1.426 2020/04/24 11:34:07 eric Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008-2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <netinet/in.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <limits.h> +#include <inttypes.h> +#include <openssl/ssl.h> +#include <resolv.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#if defined(HAVE_VIS_H) && !defined(BROKEN_STRNVIS) +#include <vis.h> +#else +#include "bsd-vis.h" +#endif + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" +#include "rfc5322.h" + +#define SMTP_LINE_MAX 65535 +#define DATA_HIWAT 65535 +#define APPEND_DOMAIN_BUFFER_SIZE SMTP_LINE_MAX + +enum smtp_state { + STATE_NEW = 0, + STATE_CONNECTED, + STATE_TLS, + STATE_HELO, + STATE_AUTH_INIT, + STATE_AUTH_USERNAME, + STATE_AUTH_PASSWORD, + STATE_AUTH_FINALIZE, + STATE_BODY, + STATE_QUIT, +}; + +enum session_flags { + SF_EHLO = 0x0001, + SF_8BITMIME = 0x0002, + SF_SECURE = 0x0004, + SF_AUTHENTICATED = 0x0008, + SF_BOUNCE = 0x0010, + SF_VERIFIED = 0x0020, + SF_BADINPUT = 0x0080, +}; + +enum { + TX_OK = 0, + TX_ERROR_ENVELOPE, + TX_ERROR_SIZE, + TX_ERROR_IO, + TX_ERROR_LOOP, + TX_ERROR_MALFORMED, + TX_ERROR_RESOURCES, + TX_ERROR_INTERNAL, +}; + +enum smtp_command { + CMD_HELO = 0, + CMD_EHLO, + CMD_STARTTLS, + CMD_AUTH, + CMD_MAIL_FROM, + CMD_RCPT_TO, + CMD_DATA, + CMD_RSET, + CMD_QUIT, + CMD_HELP, + CMD_WIZ, + CMD_NOOP, + CMD_COMMIT, +}; + +struct smtp_rcpt { + TAILQ_ENTRY(smtp_rcpt) entry; + uint64_t evpid; + struct mailaddr maddr; + size_t destcount; +}; + +struct smtp_tx { + struct smtp_session *session; + uint32_t msgid; + + struct envelope evp; + size_t rcptcount; + size_t destcount; + TAILQ_HEAD(, smtp_rcpt) rcpts; + + time_t time; + int error; + size_t datain; + size_t odatalen; + FILE *ofile; + struct io *filter; + struct rfc5322_parser *parser; + int rcvcount; + int has_date; + int has_message_id; + + uint8_t junk; +}; + +struct smtp_session { + uint64_t id; + struct io *io; + struct listener *listener; + void *ssl_ctx; + struct sockaddr_storage ss; + char rdns[HOST_NAME_MAX+1]; + char smtpname[HOST_NAME_MAX+1]; + int fcrdns; + + int flags; + enum smtp_state state; + + uint8_t banner_sent; + char helo[LINE_MAX]; + char cmd[LINE_MAX]; + char username[SMTPD_MAXMAILADDRSIZE]; + + size_t mailcount; + struct event pause; + + struct smtp_tx *tx; + + enum smtp_command last_cmd; + enum filter_phase filter_phase; + const char *filter_param; + + uint8_t junk; +}; + +#define ADVERTISE_TLS(s) \ + ((s)->listener->flags & F_STARTTLS && !((s)->flags & SF_SECURE)) + +#define ADVERTISE_AUTH(s) \ + ((s)->listener->flags & F_AUTH && (s)->flags & SF_SECURE && \ + !((s)->flags & SF_AUTHENTICATED)) + +#define ADVERTISE_EXT_DSN(s) \ + ((s)->listener->flags & F_EXT_DSN) + +#define SESSION_FILTERED(s) \ + ((s)->listener->flags & F_FILTERED) + +#define SESSION_DATA_FILTERED(s) \ + ((s)->listener->flags & F_FILTERED) + + +static int smtp_mailaddr(struct mailaddr *, char *, int, char **, const char *); +static void smtp_session_init(void); +static void smtp_lookup_servername(struct smtp_session *); +static void smtp_getnameinfo_cb(void *, int, const char *, const char *); +static void smtp_getaddrinfo_cb(void *, int, struct addrinfo *); +static void smtp_connected(struct smtp_session *); +static void smtp_send_banner(struct smtp_session *); +static void smtp_tls_verified(struct smtp_session *); +static void smtp_io(struct io *, int, void *); +static void smtp_enter_state(struct smtp_session *, int); +static void smtp_reply(struct smtp_session *, char *, ...); +static void smtp_command(struct smtp_session *, char *); +static void smtp_rfc4954_auth_plain(struct smtp_session *, char *); +static void smtp_rfc4954_auth_login(struct smtp_session *, char *); +static void smtp_free(struct smtp_session *, const char *); +static const char *smtp_strstate(int); +static void smtp_cert_init(struct smtp_session *); +static void smtp_cert_init_cb(void *, int, const char *, const void *, size_t); +static void smtp_cert_verify(struct smtp_session *); +static void smtp_cert_verify_cb(void *, int); +static void smtp_auth_failure_pause(struct smtp_session *); +static void smtp_auth_failure_resume(int, short, void *); + +static int smtp_tx(struct smtp_session *); +static void smtp_tx_free(struct smtp_tx *); +static void smtp_tx_create_message(struct smtp_tx *); +static void smtp_tx_mail_from(struct smtp_tx *, const char *); +static void smtp_tx_rcpt_to(struct smtp_tx *, const char *); +static void smtp_tx_open_message(struct smtp_tx *); +static void smtp_tx_commit(struct smtp_tx *); +static void smtp_tx_rollback(struct smtp_tx *); +static int smtp_tx_dataline(struct smtp_tx *, const char *); +static int smtp_tx_filtered_dataline(struct smtp_tx *, const char *); +static void smtp_tx_eom(struct smtp_tx *); +static void smtp_filter_fd(struct smtp_tx *, int); +static int smtp_message_fd(struct smtp_tx *, int); +static void smtp_message_begin(struct smtp_tx *); +static void smtp_message_end(struct smtp_tx *); +static int smtp_filter_printf(struct smtp_tx *, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +static int smtp_message_printf(struct smtp_tx *, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); + +static int smtp_check_rset(struct smtp_session *, const char *); +static int smtp_check_helo(struct smtp_session *, const char *); +static int smtp_check_ehlo(struct smtp_session *, const char *); +static int smtp_check_auth(struct smtp_session *s, const char *); +static int smtp_check_starttls(struct smtp_session *, const char *); +static int smtp_check_mail_from(struct smtp_session *, const char *); +static int smtp_check_rcpt_to(struct smtp_session *, const char *); +static int smtp_check_data(struct smtp_session *, const char *); +static int smtp_check_noparam(struct smtp_session *, const char *); + +static void smtp_filter_phase(enum filter_phase, struct smtp_session *, const char *); + +static void smtp_proceed_connected(struct smtp_session *); +static void smtp_proceed_rset(struct smtp_session *, const char *); +static void smtp_proceed_helo(struct smtp_session *, const char *); +static void smtp_proceed_ehlo(struct smtp_session *, const char *); +static void smtp_proceed_auth(struct smtp_session *, const char *); +static void smtp_proceed_starttls(struct smtp_session *, const char *); +static void smtp_proceed_mail_from(struct smtp_session *, const char *); +static void smtp_proceed_rcpt_to(struct smtp_session *, const char *); +static void smtp_proceed_data(struct smtp_session *, const char *); +static void smtp_proceed_noop(struct smtp_session *, const char *); +static void smtp_proceed_help(struct smtp_session *, const char *); +static void smtp_proceed_wiz(struct smtp_session *, const char *); +static void smtp_proceed_quit(struct smtp_session *, const char *); +static void smtp_proceed_commit(struct smtp_session *, const char *); +static void smtp_proceed_rollback(struct smtp_session *, const char *); + +static void smtp_filter_begin(struct smtp_session *); +static void smtp_filter_end(struct smtp_session *); +static void smtp_filter_data_begin(struct smtp_session *); +static void smtp_filter_data_end(struct smtp_session *); + +static void smtp_report_link_connect(struct smtp_session *, const char *, int, + const struct sockaddr_storage *, + const struct sockaddr_storage *); +static void smtp_report_link_greeting(struct smtp_session *, const char *); +static void smtp_report_link_identify(struct smtp_session *, const char *, const char *); +static void smtp_report_link_tls(struct smtp_session *, const char *); +static void smtp_report_link_disconnect(struct smtp_session *); +static void smtp_report_link_auth(struct smtp_session *, const char *, const char *); +static void smtp_report_tx_reset(struct smtp_session *, uint32_t); +static void smtp_report_tx_begin(struct smtp_session *, uint32_t); +static void smtp_report_tx_mail(struct smtp_session *, uint32_t, const char *, int); +static void smtp_report_tx_rcpt(struct smtp_session *, uint32_t, const char *, int); +static void smtp_report_tx_envelope(struct smtp_session *, uint32_t, uint64_t); +static void smtp_report_tx_data(struct smtp_session *, uint32_t, int); +static void smtp_report_tx_commit(struct smtp_session *, uint32_t, size_t); +static void smtp_report_tx_rollback(struct smtp_session *, uint32_t); +static void smtp_report_protocol_client(struct smtp_session *, const char *); +static void smtp_report_protocol_server(struct smtp_session *, const char *); +static void smtp_report_filter_response(struct smtp_session *, int, int, const char *); +static void smtp_report_timeout(struct smtp_session *); + + +/* musl work-around */ +void portable_freeaddrinfo(struct addrinfo *); + + +static struct { + int code; + enum filter_phase filter_phase; + const char *cmd; + + int (*check)(struct smtp_session *, const char *); + void (*proceed)(struct smtp_session *, const char *); +} commands[] = { + { CMD_HELO, FILTER_HELO, "HELO", smtp_check_helo, smtp_proceed_helo }, + { CMD_EHLO, FILTER_EHLO, "EHLO", smtp_check_ehlo, smtp_proceed_ehlo }, + { CMD_STARTTLS, FILTER_STARTTLS, "STARTTLS", smtp_check_starttls, smtp_proceed_starttls }, + { CMD_AUTH, FILTER_AUTH, "AUTH", smtp_check_auth, smtp_proceed_auth }, + { CMD_MAIL_FROM, FILTER_MAIL_FROM, "MAIL FROM", smtp_check_mail_from, smtp_proceed_mail_from }, + { CMD_RCPT_TO, FILTER_RCPT_TO, "RCPT TO", smtp_check_rcpt_to, smtp_proceed_rcpt_to }, + { CMD_DATA, FILTER_DATA, "DATA", smtp_check_data, smtp_proceed_data }, + { CMD_RSET, FILTER_RSET, "RSET", smtp_check_rset, smtp_proceed_rset }, + { CMD_QUIT, FILTER_QUIT, "QUIT", smtp_check_noparam, smtp_proceed_quit }, + { CMD_NOOP, FILTER_NOOP, "NOOP", smtp_check_noparam, smtp_proceed_noop }, + { CMD_HELP, FILTER_HELP, "HELP", smtp_check_noparam, smtp_proceed_help }, + { CMD_WIZ, FILTER_WIZ, "WIZ", smtp_check_noparam, smtp_proceed_wiz }, + { CMD_COMMIT, FILTER_COMMIT, ".", smtp_check_noparam, smtp_proceed_commit }, + { -1, 0, NULL, NULL }, +}; + +static struct tree wait_lka_helo; +static struct tree wait_lka_mail; +static struct tree wait_lka_rcpt; +static struct tree wait_parent_auth; +static struct tree wait_queue_msg; +static struct tree wait_queue_fd; +static struct tree wait_queue_commit; +static struct tree wait_ssl_init; +static struct tree wait_ssl_verify; +static struct tree wait_filters; +static struct tree wait_filter_fd; + +static void +header_append_domain_buffer(char *buffer, char *domain, size_t len) +{ + size_t i; + int escape, quote, comment, bracket; + int has_domain, has_bracket, has_group; + int pos_bracket, pos_component, pos_insert; + char copy[APPEND_DOMAIN_BUFFER_SIZE]; + + escape = quote = comment = bracket = 0; + has_domain = has_bracket = has_group = 0; + pos_bracket = pos_insert = pos_component = 0; + for (i = 0; buffer[i]; ++i) { + if (buffer[i] == '(' && !escape && !quote) + comment++; + if (buffer[i] == '"' && !escape && !comment) + quote = !quote; + if (buffer[i] == ')' && !escape && !quote && comment) + comment--; + if (buffer[i] == '\\' && !escape && !comment && !quote) + escape = 1; + else + escape = 0; + if (buffer[i] == '<' && !escape && !comment && !quote && !bracket) { + bracket++; + has_bracket = 1; + } + if (buffer[i] == '>' && !escape && !comment && !quote && bracket) { + bracket--; + pos_bracket = i; + } + if (buffer[i] == '@' && !escape && !comment && !quote) + has_domain = 1; + if (buffer[i] == ':' && !escape && !comment && !quote) + has_group = 1; + + /* update insert point if not in comment and not on a whitespace */ + if (!comment && buffer[i] != ')' && !isspace((unsigned char)buffer[i])) + pos_component = i; + } + + /* parse error, do not attempt to modify */ + if (escape || quote || comment || bracket) + return; + + /* domain already present, no need to modify */ + if (has_domain) + return; + + /* address is group, skip */ + if (has_group) + return; + + /* there's an address between brackets, just append domain */ + if (has_bracket) { + pos_bracket--; + while (isspace((unsigned char)buffer[pos_bracket])) + pos_bracket--; + if (buffer[pos_bracket] == '<') + return; + pos_insert = pos_bracket + 1; + } + else { + /* otherwise append address to last component */ + pos_insert = pos_component + 1; + + /* empty address */ + if (buffer[pos_component] == '\0' || + isspace((unsigned char)buffer[pos_component])) + return; + } + + if (snprintf(copy, sizeof copy, "%.*s@%s%s", + (int)pos_insert, buffer, + domain, + buffer+pos_insert) >= (int)sizeof copy) + return; + + memcpy(buffer, copy, len); +} + +static void +header_address_rewrite_buffer(char *buffer, const char *address, size_t len) +{ + size_t i; + int address_len; + int escape, quote, comment, bracket; + int has_bracket, has_group; + int pos_bracket_beg, pos_bracket_end, pos_component_beg, pos_component_end; + int insert_beg, insert_end; + char copy[APPEND_DOMAIN_BUFFER_SIZE]; + + escape = quote = comment = bracket = 0; + has_bracket = has_group = 0; + pos_bracket_beg = pos_bracket_end = pos_component_beg = pos_component_end = 0; + for (i = 0; buffer[i]; ++i) { + if (buffer[i] == '(' && !escape && !quote) + comment++; + if (buffer[i] == '"' && !escape && !comment) + quote = !quote; + if (buffer[i] == ')' && !escape && !quote && comment) + comment--; + if (buffer[i] == '\\' && !escape && !comment && !quote) + escape = 1; + else + escape = 0; + if (buffer[i] == '<' && !escape && !comment && !quote && !bracket) { + bracket++; + has_bracket = 1; + pos_bracket_beg = i+1; + } + if (buffer[i] == '>' && !escape && !comment && !quote && bracket) { + bracket--; + pos_bracket_end = i; + } + if (buffer[i] == ':' && !escape && !comment && !quote) + has_group = 1; + + /* update insert point if not in comment and not on a whitespace */ + if (!comment && buffer[i] != ')' && !isspace((unsigned char)buffer[i])) + pos_component_end = i; + } + + /* parse error, do not attempt to modify */ + if (escape || quote || comment || bracket) + return; + + /* address is group, skip */ + if (has_group) + return; + + /* there's an address between brackets, just replace everything brackets */ + if (has_bracket) { + insert_beg = pos_bracket_beg; + insert_end = pos_bracket_end; + } + else { + if (pos_component_end == 0) + pos_component_beg = 0; + else { + for (pos_component_beg = pos_component_end; pos_component_beg >= 0; --pos_component_beg) + if (buffer[pos_component_beg] == ')' || isspace((unsigned char)buffer[pos_component_beg])) + break; + pos_component_beg += 1; + pos_component_end += 1; + } + insert_beg = pos_component_beg; + insert_end = pos_component_end; + } + + /* check that masquerade won' t overflow */ + address_len = strlen(address); + if (strlen(buffer) - (insert_end - insert_beg) + address_len >= len) + return; + + (void)strlcpy(copy, buffer, sizeof copy); + (void)strlcpy(copy+insert_beg, address, sizeof (copy) - insert_beg); + (void)strlcat(copy, buffer+insert_end, sizeof (copy)); + memcpy(buffer, copy, len); +} + +static void +header_domain_append_callback(struct smtp_tx *tx, const char *hdr, + const char *val) +{ + size_t i, j, linelen; + int escape, quote, comment, skip; + char buffer[APPEND_DOMAIN_BUFFER_SIZE]; + const char *line, *end; + + if (smtp_message_printf(tx, "%s:", hdr) == -1) + return; + + j = 0; + escape = quote = comment = skip = 0; + memset(buffer, 0, sizeof buffer); + + for (line = val; line; line = end) { + end = strchr(line, '\n'); + if (end) { + linelen = end - line; + end++; + } + else + linelen = strlen(line); + + for (i = 0; i < linelen; ++i) { + if (line[i] == '(' && !escape && !quote) + comment++; + if (line[i] == '"' && !escape && !comment) + quote = !quote; + if (line[i] == ')' && !escape && !quote && comment) + comment--; + if (line[i] == '\\' && !escape && !comment && !quote) + escape = 1; + else + escape = 0; + + /* found a separator, buffer contains a full address */ + if (line[i] == ',' && !escape && !quote && !comment) { + if (!skip && j + strlen(tx->session->listener->hostname) + 1 < sizeof buffer) { + header_append_domain_buffer(buffer, tx->session->listener->hostname, sizeof buffer); + if (tx->session->flags & SF_AUTHENTICATED && + tx->session->listener->sendertable[0] && + tx->session->listener->flags & F_MASQUERADE && + !(strcasecmp(hdr, "From"))) + header_address_rewrite_buffer(buffer, mailaddr_to_text(&tx->evp.sender), + sizeof buffer); + } + if (smtp_message_printf(tx, "%s,", buffer) == -1) + return; + j = 0; + skip = 0; + memset(buffer, 0, sizeof buffer); + } + else { + if (skip) { + if (smtp_message_printf(tx, "%c", line[i]) == -1) + return; + } + else { + buffer[j++] = line[i]; + if (j == sizeof (buffer) - 1) { + if (smtp_message_printf(tx, "%s", buffer) == -1) + return; + skip = 1; + j = 0; + memset(buffer, 0, sizeof buffer); + } + } + } + } + if (skip) { + if (smtp_message_printf(tx, "\n") == -1) + return; + } + else { + buffer[j++] = '\n'; + if (j == sizeof (buffer) - 1) { + if (smtp_message_printf(tx, "%s", buffer) == -1) + return; + skip = 1; + j = 0; + memset(buffer, 0, sizeof buffer); + } + } + } + + /* end of header, if buffer is not empty we'll process it */ + if (buffer[0]) { + if (j + strlen(tx->session->listener->hostname) + 1 < sizeof buffer) { + header_append_domain_buffer(buffer, tx->session->listener->hostname, sizeof buffer); + if (tx->session->flags & SF_AUTHENTICATED && + tx->session->listener->sendertable[0] && + tx->session->listener->flags & F_MASQUERADE && + !(strcasecmp(hdr, "From"))) + header_address_rewrite_buffer(buffer, mailaddr_to_text(&tx->evp.sender), + sizeof buffer); + } + smtp_message_printf(tx, "%s", buffer); + } +} + +static void +smtp_session_init(void) +{ + static int init = 0; + + if (!init) { + tree_init(&wait_lka_helo); + tree_init(&wait_lka_mail); + tree_init(&wait_lka_rcpt); + tree_init(&wait_parent_auth); + tree_init(&wait_queue_msg); + tree_init(&wait_queue_fd); + tree_init(&wait_queue_commit); + tree_init(&wait_ssl_init); + tree_init(&wait_ssl_verify); + tree_init(&wait_filters); + tree_init(&wait_filter_fd); + init = 1; + } +} + +int +smtp_session(struct listener *listener, int sock, + const struct sockaddr_storage *ss, const char *hostname, struct io *io) +{ + struct smtp_session *s; + + smtp_session_init(); + + if ((s = calloc(1, sizeof(*s))) == NULL) + return (-1); + + s->id = generate_uid(); + s->listener = listener; + memmove(&s->ss, ss, sizeof(*ss)); + + if (io != NULL) + s->io = io; + else + s->io = io_new(); + + io_set_callback(s->io, smtp_io, s); + io_set_fd(s->io, sock); + io_set_timeout(s->io, SMTPD_SESSION_TIMEOUT * 1000); + io_set_write(s->io); + s->state = STATE_NEW; + + (void)strlcpy(s->smtpname, listener->hostname, sizeof(s->smtpname)); + + log_trace(TRACE_SMTP, "smtp: %p: connected to listener %p " + "[hostname=%s, port=%d, tag=%s]", s, listener, + listener->hostname, ntohs(listener->port), listener->tag); + + /* For local enqueueing, the hostname is already set */ + if (hostname) { + s->flags |= SF_AUTHENTICATED; + /* A bit of a hack */ + if (!strcmp(hostname, "localhost")) + s->flags |= SF_BOUNCE; + (void)strlcpy(s->rdns, hostname, sizeof(s->rdns)); + s->fcrdns = 1; + smtp_lookup_servername(s); + } else { + resolver_getnameinfo((struct sockaddr *)&s->ss, NI_NAMEREQD, + smtp_getnameinfo_cb, s); + } + + /* session may have been freed by now */ + + return (0); +} + +static void +smtp_getnameinfo_cb(void *arg, int gaierrno, const char *host, const char *serv) +{ + struct smtp_session *s = arg; + struct addrinfo hints; + + if (gaierrno) { + (void)strlcpy(s->rdns, "<unknown>", sizeof(s->rdns)); + + if (gaierrno == EAI_NODATA || gaierrno == EAI_NONAME) + s->fcrdns = 0; + else { + log_warnx("getnameinfo: %s: %s", ss_to_text(&s->ss), + gai_strerror(gaierrno)); + s->fcrdns = -1; + } + + smtp_lookup_servername(s); + return; + } + + (void)strlcpy(s->rdns, host, sizeof(s->rdns)); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = s->ss.ss_family; + hints.ai_socktype = SOCK_STREAM; + resolver_getaddrinfo(s->rdns, NULL, &hints, smtp_getaddrinfo_cb, s); +} + +static void +smtp_getaddrinfo_cb(void *arg, int gaierrno, struct addrinfo *ai0) +{ + struct smtp_session *s = arg; + struct addrinfo *ai; + char fwd[64], rev[64]; + + if (gaierrno) { + if (gaierrno == EAI_NODATA || gaierrno == EAI_NONAME) + s->fcrdns = 0; + else { + log_warnx("getaddrinfo: %s: %s", s->rdns, + gai_strerror(gaierrno)); + s->fcrdns = -1; + } + } + else { + strlcpy(rev, ss_to_text(&s->ss), sizeof(rev)); + for (ai = ai0; ai; ai = ai->ai_next) { + strlcpy(fwd, sa_to_text(ai->ai_addr), sizeof(fwd)); + if (!strcmp(fwd, rev)) { + s->fcrdns = 1; + break; + } + } + portable_freeaddrinfo(ai0); + } + + smtp_lookup_servername(s); +} + +void +smtp_session_imsg(struct mproc *p, struct imsg *imsg) +{ + struct smtp_session *s; + struct smtp_rcpt *rcpt; + char user[SMTPD_MAXMAILADDRSIZE]; + char tmp[SMTP_LINE_MAX]; + struct msg m; + const char *line, *helo; + uint64_t reqid, evpid; + uint32_t msgid; + int status, success; + int filter_response; + const char *filter_param; + uint8_t i; + + switch (imsg->hdr.type) { + + case IMSG_SMTP_CHECK_SENDER: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &status); + m_end(&m); + s = tree_xpop(&wait_lka_mail, reqid); + switch (status) { + case LKA_OK: + smtp_tx_create_message(s->tx); + break; + + case LKA_PERMFAIL: + smtp_tx_free(s->tx); + smtp_reply(s, "%d %s", 530, "Sender rejected"); + break; + case LKA_TEMPFAIL: + smtp_tx_free(s->tx); + smtp_reply(s, "421 %s Temporary Error", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + break; + } + return; + + case IMSG_SMTP_EXPAND_RCPT: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &status); + m_get_string(&m, &line); + m_end(&m); + s = tree_xpop(&wait_lka_rcpt, reqid); + + tmp[0] = '\0'; + if (s->tx->evp.rcpt.user[0]) { + (void)strlcpy(tmp, s->tx->evp.rcpt.user, sizeof tmp); + if (s->tx->evp.rcpt.domain[0]) { + (void)strlcat(tmp, "@", sizeof tmp); + (void)strlcat(tmp, s->tx->evp.rcpt.domain, + sizeof tmp); + } + } + + switch (status) { + case LKA_OK: + fatalx("unexpected ok"); + case LKA_PERMFAIL: + smtp_reply(s, "%s: <%s>", line, tmp); + break; + case LKA_TEMPFAIL: + smtp_reply(s, "%s: <%s>", line, tmp); + break; + } + return; + + case IMSG_SMTP_LOOKUP_HELO: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + s = tree_xpop(&wait_lka_helo, reqid); + m_get_int(&m, &status); + if (status == LKA_OK) { + m_get_string(&m, &helo); + (void)strlcpy(s->smtpname, helo, sizeof(s->smtpname)); + } + m_end(&m); + smtp_connected(s); + return; + + case IMSG_SMTP_MESSAGE_CREATE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &success); + s = tree_xpop(&wait_queue_msg, reqid); + if (success) { + m_get_msgid(&m, &msgid); + s->tx->msgid = msgid; + s->tx->evp.id = msgid_to_evpid(msgid); + s->tx->rcptcount = 0; + smtp_reply(s, "250 %s Ok", + esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS)); + } else { + smtp_reply(s, "421 %s Temporary Error", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + smtp_tx_free(s->tx); + smtp_enter_state(s, STATE_QUIT); + } + m_end(&m); + return; + + case IMSG_SMTP_MESSAGE_OPEN: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &success); + m_end(&m); + + s = tree_xpop(&wait_queue_fd, reqid); + if (!success || imsg->fd == -1) { + if (imsg->fd != -1) + close(imsg->fd); + smtp_reply(s, "421 %s Temporary Error", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + smtp_enter_state(s, STATE_QUIT); + return; + } + + log_debug("smtp: %p: fd %d from queue", s, imsg->fd); + + if (smtp_message_fd(s->tx, imsg->fd)) { + if (!SESSION_DATA_FILTERED(s)) + smtp_message_begin(s->tx); + else + smtp_filter_data_begin(s); + } + return; + + case IMSG_FILTER_SMTP_DATA_BEGIN: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &success); + m_end(&m); + + s = tree_xpop(&wait_filter_fd, reqid); + if (!success || imsg->fd == -1) { + if (imsg->fd != -1) + close(imsg->fd); + smtp_reply(s, "421 %s Temporary Error", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + smtp_enter_state(s, STATE_QUIT); + return; + } + + log_debug("smtp: %p: fd %d from lka", s, imsg->fd); + + smtp_filter_fd(s->tx, imsg->fd); + smtp_message_begin(s->tx); + return; + + case IMSG_QUEUE_ENVELOPE_SUBMIT: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &success); + s = tree_xget(&wait_lka_rcpt, reqid); + if (success) { + m_get_evpid(&m, &evpid); + s->tx->evp.id = evpid; + s->tx->destcount++; + smtp_report_tx_envelope(s, s->tx->msgid, evpid); + } + else + s->tx->error = TX_ERROR_ENVELOPE; + m_end(&m); + return; + + case IMSG_QUEUE_ENVELOPE_COMMIT: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &success); + m_end(&m); + if (!success) + fatalx("commit evp failed: not supposed to happen"); + s = tree_xpop(&wait_lka_rcpt, reqid); + if (s->tx->error) { + /* + * If an envelope failed, we can't cancel the last + * RCPT only so we must cancel the whole transaction + * and close the connection. + */ + smtp_reply(s, "421 %s Temporary failure", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + smtp_enter_state(s, STATE_QUIT); + } + else { + rcpt = xcalloc(1, sizeof(*rcpt)); + rcpt->evpid = s->tx->evp.id; + rcpt->destcount = s->tx->destcount; + rcpt->maddr = s->tx->evp.rcpt; + TAILQ_INSERT_TAIL(&s->tx->rcpts, rcpt, entry); + + s->tx->destcount = 0; + s->tx->rcptcount++; + smtp_reply(s, "250 %s %s: Recipient ok", + esc_code(ESC_STATUS_OK, ESC_DESTINATION_ADDRESS_VALID), + esc_description(ESC_DESTINATION_ADDRESS_VALID)); + } + return; + + case IMSG_SMTP_MESSAGE_COMMIT: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &success); + m_end(&m); + s = tree_xpop(&wait_queue_commit, reqid); + if (!success) { + smtp_reply(s, "421 %s Temporary failure", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + smtp_tx_free(s->tx); + smtp_enter_state(s, STATE_QUIT); + return; + } + + smtp_reply(s, "250 %s %08x Message accepted for delivery", + esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS), + s->tx->msgid); + smtp_report_tx_commit(s, s->tx->msgid, s->tx->odatalen); + smtp_report_tx_reset(s, s->tx->msgid); + + log_info("%016"PRIx64" smtp message " + "msgid=%08x size=%zu nrcpt=%zu proto=%s", + s->id, + s->tx->msgid, + s->tx->odatalen, + s->tx->rcptcount, + s->flags & SF_EHLO ? "ESMTP" : "SMTP"); + TAILQ_FOREACH(rcpt, &s->tx->rcpts, entry) { + log_info("%016"PRIx64" smtp envelope " + "evpid=%016"PRIx64" from=<%s%s%s> to=<%s%s%s>", + s->id, + rcpt->evpid, + s->tx->evp.sender.user, + s->tx->evp.sender.user[0] == '\0' ? "" : "@", + s->tx->evp.sender.domain, + rcpt->maddr.user, + rcpt->maddr.user[0] == '\0' ? "" : "@", + rcpt->maddr.domain); + } + smtp_tx_free(s->tx); + s->mailcount++; + smtp_enter_state(s, STATE_HELO); + return; + + case IMSG_SMTP_AUTHENTICATE: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &success); + m_end(&m); + + s = tree_xpop(&wait_parent_auth, reqid); + strnvis(user, s->username, sizeof user, VIS_WHITE | VIS_SAFE); + if (success == LKA_OK) { + log_info("%016"PRIx64" smtp " + "authentication user=%s " + "result=ok", + s->id, user); + s->flags |= SF_AUTHENTICATED; + smtp_report_link_auth(s, user, "pass"); + smtp_reply(s, "235 %s Authentication succeeded", + esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS)); + } + else if (success == LKA_PERMFAIL) { + log_info("%016"PRIx64" smtp " + "authentication user=%s " + "result=permfail", + s->id, user); + smtp_report_link_auth(s, user, "fail"); + smtp_auth_failure_pause(s); + return; + } + else if (success == LKA_TEMPFAIL) { + log_info("%016"PRIx64" smtp " + "authentication user=%s " + "result=tempfail", + s->id, user); + smtp_report_link_auth(s, user, "error"); + smtp_reply(s, "421 %s Temporary failure", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + } + else + fatalx("bad lka response"); + + smtp_enter_state(s, STATE_HELO); + return; + + case IMSG_FILTER_SMTP_PROTOCOL: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_int(&m, &filter_response); + if (filter_response != FILTER_PROCEED && + filter_response != FILTER_JUNK) + m_get_string(&m, &filter_param); + else + filter_param = NULL; + m_end(&m); + + s = tree_xpop(&wait_filters, reqid); + + switch (filter_response) { + case FILTER_REJECT: + case FILTER_DISCONNECT: + if (!valid_smtp_response(filter_param) || + (filter_param[0] != '4' && filter_param[0] != '5')) + filter_param = "421 Internal server error"; + if (!strncmp(filter_param, "421", 3)) + filter_response = FILTER_DISCONNECT; + + smtp_report_filter_response(s, s->filter_phase, + filter_response, filter_param); + + smtp_reply(s, "%s", filter_param); + + if (filter_response == FILTER_DISCONNECT) + smtp_enter_state(s, STATE_QUIT); + else if (s->filter_phase == FILTER_COMMIT) + smtp_proceed_rollback(s, NULL); + break; + + + case FILTER_JUNK: + if (s->tx) + s->tx->junk = 1; + else + s->junk = 1; + /* fallthrough */ + + case FILTER_PROCEED: + filter_param = s->filter_param; + /* fallthrough */ + + case FILTER_REWRITE: + smtp_report_filter_response(s, s->filter_phase, + filter_response, + filter_param == s->filter_param ? NULL : filter_param); + if (s->filter_phase == FILTER_CONNECT) { + smtp_proceed_connected(s); + return; + } + for (i = 0; i < nitems(commands); ++i) + if (commands[i].filter_phase == s->filter_phase) { + if (filter_response == FILTER_REWRITE) + if (!commands[i].check(s, filter_param)) + break; + commands[i].proceed(s, filter_param); + break; + } + break; + } + return; + } + + log_warnx("smtp_session_imsg: unexpected %s imsg", + imsg_to_str(imsg->hdr.type)); + fatalx(NULL); +} + +static void +smtp_tls_verified(struct smtp_session *s) +{ + X509 *x; + + x = SSL_get_peer_certificate(io_tls(s->io)); + if (x) { + log_info("%016"PRIx64" smtp " + "client-cert-check result=\"%s\"", + s->id, + (s->flags & SF_VERIFIED) ? "success" : "failure"); + X509_free(x); + } + + if (s->listener->flags & F_SMTPS) { + stat_increment("smtp.smtps", 1); + io_set_write(s->io); + smtp_send_banner(s); + } + else { + stat_increment("smtp.tls", 1); + smtp_enter_state(s, STATE_HELO); + } +} + +static void +smtp_io(struct io *io, int evt, void *arg) +{ + struct smtp_session *s = arg; + char *line; + size_t len; + int eom; + + log_trace(TRACE_IO, "smtp: %p: %s %s", s, io_strevent(evt), + io_strio(io)); + + switch (evt) { + + case IO_TLSREADY: + log_info("%016"PRIx64" smtp tls ciphers=%s", + s->id, ssl_to_text(io_tls(s->io))); + + smtp_report_link_tls(s, ssl_to_text(io_tls(s->io))); + + s->flags |= SF_SECURE; + s->helo[0] = '\0'; + + smtp_cert_verify(s); + break; + + case IO_DATAIN: + nextline: + line = io_getline(s->io, &len); + if ((line == NULL && io_datalen(s->io) >= SMTP_LINE_MAX) || + (line && len >= SMTP_LINE_MAX)) { + s->flags |= SF_BADINPUT; + smtp_reply(s, "500 %s Line too long", + esc_code(ESC_STATUS_PERMFAIL, ESC_OTHER_STATUS)); + smtp_enter_state(s, STATE_QUIT); + io_set_write(io); + return; + } + + /* No complete line received */ + if (line == NULL) + return; + + /* Strip trailing '\r' */ + if (len && line[len - 1] == '\r') + line[--len] = '\0'; + + /* Message body */ + eom = 0; + if (s->state == STATE_BODY) { + if (strcmp(line, ".")) { + s->tx->datain += strlen(line) + 1; + if (s->tx->datain > env->sc_maxsize) + s->tx->error = TX_ERROR_SIZE; + } + eom = (s->tx->filter == NULL) ? + smtp_tx_dataline(s->tx, line) : + smtp_tx_filtered_dataline(s->tx, line); + if (eom == 0) + goto nextline; + } + + /* Pipelining not supported */ + if (io_datalen(s->io)) { + s->flags |= SF_BADINPUT; + smtp_reply(s, "500 %s %s: Pipelining not supported", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + smtp_enter_state(s, STATE_QUIT); + io_set_write(io); + return; + } + + if (eom) { + io_set_write(io); + if (s->tx->filter == NULL) + smtp_tx_eom(s->tx); + return; + } + + /* Must be a command */ + if (strlcpy(s->cmd, line, sizeof(s->cmd)) >= sizeof(s->cmd)) { + s->flags |= SF_BADINPUT; + smtp_reply(s, "500 %s Command line too long", + esc_code(ESC_STATUS_PERMFAIL, ESC_OTHER_STATUS)); + smtp_enter_state(s, STATE_QUIT); + io_set_write(io); + return; + } + io_set_write(io); + smtp_command(s, line); + break; + + case IO_LOWAT: + if (s->state == STATE_QUIT) { + log_info("%016"PRIx64" smtp disconnected " + "reason=quit", + s->id); + smtp_free(s, "done"); + break; + } + + /* Wait for the client to start tls */ + if (s->state == STATE_TLS) { + smtp_cert_init(s); + break; + } + + io_set_read(io); + break; + + case IO_TIMEOUT: + log_info("%016"PRIx64" smtp disconnected " + "reason=timeout", + s->id); + smtp_report_timeout(s); + smtp_free(s, "timeout"); + break; + + case IO_DISCONNECTED: + log_info("%016"PRIx64" smtp disconnected " + "reason=disconnect", + s->id); + smtp_free(s, "disconnected"); + break; + + case IO_ERROR: + log_info("%016"PRIx64" smtp disconnected " + "reason=\"io-error: %s\"", + s->id, io_error(io)); + smtp_free(s, "IO error"); + break; + + default: + fatalx("smtp_io()"); + } +} + +static void +smtp_command(struct smtp_session *s, char *line) +{ + char *args; + int cmd, i; + + log_trace(TRACE_SMTP, "smtp: %p: <<< %s", s, line); + + /* + * These states are special. + */ + if (s->state == STATE_AUTH_INIT) { + smtp_report_protocol_client(s, "********"); + smtp_rfc4954_auth_plain(s, line); + return; + } + if (s->state == STATE_AUTH_USERNAME || s->state == STATE_AUTH_PASSWORD) { + smtp_report_protocol_client(s, "********"); + smtp_rfc4954_auth_login(s, line); + return; + } + + if (s->state == STATE_HELO && strncasecmp(line, "AUTH PLAIN ", 11) == 0) + smtp_report_protocol_client(s, "AUTH PLAIN ********"); + else + smtp_report_protocol_client(s, line); + + + /* + * Unlike other commands, "mail from" and "rcpt to" contain a + * space in the command name. + */ + if (strncasecmp("mail from:", line, 10) == 0 || + strncasecmp("rcpt to:", line, 8) == 0) + args = strchr(line, ':'); + else + args = strchr(line, ' '); + + if (args) { + *args++ = '\0'; + while (isspace((unsigned char)*args)) + args++; + } + + cmd = -1; + for (i = 0; commands[i].code != -1; i++) + if (!strcasecmp(line, commands[i].cmd)) { + cmd = commands[i].code; + break; + } + + s->last_cmd = cmd; + switch (cmd) { + /* + * INIT + */ + case CMD_HELO: + if (!smtp_check_helo(s, args)) + break; + smtp_filter_phase(FILTER_HELO, s, args); + break; + + case CMD_EHLO: + if (!smtp_check_ehlo(s, args)) + break; + smtp_filter_phase(FILTER_EHLO, s, args); + break; + + /* + * SETUP + */ + case CMD_STARTTLS: + if (!smtp_check_starttls(s, args)) + break; + + smtp_filter_phase(FILTER_STARTTLS, s, NULL); + break; + + case CMD_AUTH: + if (!smtp_check_auth(s, args)) + break; + smtp_filter_phase(FILTER_AUTH, s, args); + break; + + case CMD_MAIL_FROM: + if (!smtp_check_mail_from(s, args)) + break; + smtp_filter_phase(FILTER_MAIL_FROM, s, args); + break; + + /* + * TRANSACTION + */ + case CMD_RCPT_TO: + if (!smtp_check_rcpt_to(s, args)) + break; + smtp_filter_phase(FILTER_RCPT_TO, s, args); + break; + + case CMD_RSET: + if (!smtp_check_rset(s, args)) + break; + smtp_filter_phase(FILTER_RSET, s, NULL); + break; + + case CMD_DATA: + if (!smtp_check_data(s, args)) + break; + smtp_filter_phase(FILTER_DATA, s, NULL); + break; + + /* + * ANY + */ + case CMD_QUIT: + if (!smtp_check_noparam(s, args)) + break; + smtp_filter_phase(FILTER_QUIT, s, NULL); + break; + + case CMD_NOOP: + if (!smtp_check_noparam(s, args)) + break; + smtp_filter_phase(FILTER_NOOP, s, NULL); + break; + + case CMD_HELP: + if (!smtp_check_noparam(s, args)) + break; + smtp_proceed_help(s, NULL); + break; + + case CMD_WIZ: + if (!smtp_check_noparam(s, args)) + break; + smtp_proceed_wiz(s, NULL); + break; + + default: + smtp_reply(s, "500 %s %s: Command unrecognized", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + break; + } +} + +static int +smtp_check_rset(struct smtp_session *s, const char *args) +{ + if (!smtp_check_noparam(s, args)) + return 0; + + if (s->helo[0] == '\0') { + smtp_reply(s, "503 %s %s: Command not allowed at this point.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + return 1; +} + +static int +smtp_check_helo(struct smtp_session *s, const char *args) +{ + if (!s->banner_sent) { + smtp_reply(s, "503 %s %s: Command not allowed at this point.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->helo[0]) { + smtp_reply(s, "503 %s %s: Already identified", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (args == NULL) { + smtp_reply(s, "501 %s %s: HELO requires domain name", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (!valid_domainpart(args)) { + smtp_reply(s, "501 %s %s: Invalid domain name", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS), + esc_description(ESC_INVALID_COMMAND_ARGUMENTS)); + return 0; + } + + return 1; +} + +static int +smtp_check_ehlo(struct smtp_session *s, const char *args) +{ + if (!s->banner_sent) { + smtp_reply(s, "503 %s %s: Command not allowed at this point.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->helo[0]) { + smtp_reply(s, "503 %s %s: Already identified", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (args == NULL) { + smtp_reply(s, "501 %s %s: EHLO requires domain name", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (!valid_domainpart(args)) { + smtp_reply(s, "501 %s %s: Invalid domain name", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS), + esc_description(ESC_INVALID_COMMAND_ARGUMENTS)); + return 0; + } + + return 1; +} + +static int +smtp_check_auth(struct smtp_session *s, const char *args) +{ + if (s->helo[0] == '\0' || s->tx) { + smtp_reply(s, "503 %s %s: Command not allowed at this point.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->flags & SF_AUTHENTICATED) { + smtp_reply(s, "503 %s %s: Already authenticated", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (!ADVERTISE_AUTH(s)) { + smtp_reply(s, "503 %s %s: Command not supported", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (args == NULL) { + smtp_reply(s, "501 %s %s: No parameters given", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS), + esc_description(ESC_INVALID_COMMAND_ARGUMENTS)); + return 0; + } + + return 1; +} + +static int +smtp_check_starttls(struct smtp_session *s, const char *args) +{ + if (s->helo[0] == '\0' || s->tx) { + smtp_reply(s, "503 %s %s: Command not allowed at this point.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (!(s->listener->flags & F_STARTTLS)) { + smtp_reply(s, "503 %s %s: Command not supported", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->flags & SF_SECURE) { + smtp_reply(s, "503 %s %s: Channel already secured", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (args != NULL) { + smtp_reply(s, "501 %s %s: No parameters allowed", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS), + esc_description(ESC_INVALID_COMMAND_ARGUMENTS)); + return 0; + } + + return 1; +} + +static int +smtp_check_mail_from(struct smtp_session *s, const char *args) +{ + char *copy; + char tmp[SMTP_LINE_MAX]; + struct mailaddr sender; + + (void)strlcpy(tmp, args, sizeof tmp); + copy = tmp; + + if (s->helo[0] == '\0' || s->tx) { + smtp_reply(s, "503 %s %s: Command not allowed at this point.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->listener->flags & F_STARTTLS_REQUIRE && + !(s->flags & SF_SECURE)) { + smtp_reply(s, + "530 %s %s: Must issue a STARTTLS command first", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->listener->flags & F_AUTH_REQUIRE && + !(s->flags & SF_AUTHENTICATED)) { + smtp_reply(s, + "530 %s %s: Must issue an AUTH command first", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->mailcount >= env->sc_session_max_mails) { + /* we can pretend we had too many recipients */ + smtp_reply(s, "452 %s %s: Too many messages sent", + esc_code(ESC_STATUS_TEMPFAIL, ESC_TOO_MANY_RECIPIENTS), + esc_description(ESC_TOO_MANY_RECIPIENTS)); + return 0; + } + + if (smtp_mailaddr(&sender, copy, 1, ©, + s->smtpname) == 0) { + smtp_reply(s, "553 %s Sender address syntax error", + esc_code(ESC_STATUS_PERMFAIL, ESC_OTHER_ADDRESS_STATUS)); + return 0; + } + + return 1; +} + +static int +smtp_check_rcpt_to(struct smtp_session *s, const char *args) +{ + char *copy; + char tmp[SMTP_LINE_MAX]; + + (void)strlcpy(tmp, args, sizeof tmp); + copy = tmp; + + if (s->tx == NULL) { + smtp_reply(s, "503 %s %s: Command not allowed at this point.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->tx->rcptcount >= env->sc_session_max_rcpt) { + smtp_reply(s->tx->session, "451 %s %s: Too many recipients", + esc_code(ESC_STATUS_TEMPFAIL, ESC_TOO_MANY_RECIPIENTS), + esc_description(ESC_TOO_MANY_RECIPIENTS)); + return 0; + } + + if (smtp_mailaddr(&s->tx->evp.rcpt, copy, 0, ©, + s->tx->session->smtpname) == 0) { + smtp_reply(s->tx->session, + "501 %s Recipient address syntax error", + esc_code(ESC_STATUS_PERMFAIL, + ESC_BAD_DESTINATION_MAILBOX_ADDRESS_SYNTAX)); + return 0; + } + + return 1; +} + +static int +smtp_check_data(struct smtp_session *s, const char *args) +{ + if (!smtp_check_noparam(s, args)) + return 0; + + if (s->tx == NULL) { + smtp_reply(s, "503 %s %s: Command not allowed at this point.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); + return 0; + } + + if (s->tx->rcptcount == 0) { + smtp_reply(s, "503 %s %s: No recipient specified", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS), + esc_description(ESC_INVALID_COMMAND_ARGUMENTS)); + return 0; + } + + return 1; +} + +static int +smtp_check_noparam(struct smtp_session *s, const char *args) +{ + if (args != NULL) { + smtp_reply(s, "500 %s %s: command does not accept arguments.", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS), + esc_description(ESC_INVALID_COMMAND_ARGUMENTS)); + return 0; + } + return 1; +} + +static void +smtp_query_filters(enum filter_phase phase, struct smtp_session *s, const char *args) +{ + m_create(p_lka, IMSG_FILTER_SMTP_PROTOCOL, 0, 0, -1); + m_add_id(p_lka, s->id); + m_add_int(p_lka, phase); + m_add_string(p_lka, args); + m_close(p_lka); + tree_xset(&wait_filters, s->id, s); +} + +static void +smtp_filter_begin(struct smtp_session *s) +{ + if (!SESSION_FILTERED(s)) + return; + + m_create(p_lka, IMSG_FILTER_SMTP_BEGIN, 0, 0, -1); + m_add_id(p_lka, s->id); + m_add_string(p_lka, s->listener->filter_name); + m_close(p_lka); +} + +static void +smtp_filter_end(struct smtp_session *s) +{ + if (!SESSION_FILTERED(s)) + return; + + m_create(p_lka, IMSG_FILTER_SMTP_END, 0, 0, -1); + m_add_id(p_lka, s->id); + m_close(p_lka); +} + +static void +smtp_filter_data_begin(struct smtp_session *s) +{ + if (!SESSION_FILTERED(s)) + return; + + m_create(p_lka, IMSG_FILTER_SMTP_DATA_BEGIN, 0, 0, -1); + m_add_id(p_lka, s->id); + m_close(p_lka); + tree_xset(&wait_filter_fd, s->id, s); +} + +static void +smtp_filter_data_end(struct smtp_session *s) +{ + if (!SESSION_FILTERED(s)) + return; + + if (s->tx->filter == NULL) + return; + + io_free(s->tx->filter); + s->tx->filter = NULL; + + m_create(p_lka, IMSG_FILTER_SMTP_DATA_END, 0, 0, -1); + m_add_id(p_lka, s->id); + m_close(p_lka); +} + +static void +smtp_filter_phase(enum filter_phase phase, struct smtp_session *s, const char *param) +{ + uint8_t i; + + s->filter_phase = phase; + s->filter_param = param; + + if (SESSION_FILTERED(s)) { + smtp_query_filters(phase, s, param ? param : ""); + return; + } + + if (s->filter_phase == FILTER_CONNECT) { + smtp_proceed_connected(s); + return; + } + + for (i = 0; i < nitems(commands); ++i) + if (commands[i].filter_phase == s->filter_phase) { + commands[i].proceed(s, param); + break; + } +} + +static void +smtp_proceed_rset(struct smtp_session *s, const char *args) +{ + smtp_reply(s, "250 %s Reset state", + esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS)); + + if (s->tx) { + if (s->tx->msgid) + smtp_tx_rollback(s->tx); + smtp_tx_free(s->tx); + } +} + +static void +smtp_proceed_helo(struct smtp_session *s, const char *args) +{ + (void)strlcpy(s->helo, args, sizeof(s->helo)); + s->flags &= SF_SECURE | SF_AUTHENTICATED | SF_VERIFIED; + + smtp_report_link_identify(s, "HELO", s->helo); + + smtp_enter_state(s, STATE_HELO); + + smtp_reply(s, "250 %s Hello %s %s%s%s, pleased to meet you", + s->smtpname, + s->helo, + s->ss.ss_family == AF_INET6 ? "" : "[", + ss_to_text(&s->ss), + s->ss.ss_family == AF_INET6 ? "" : "]"); +} + +static void +smtp_proceed_ehlo(struct smtp_session *s, const char *args) +{ + (void)strlcpy(s->helo, args, sizeof(s->helo)); + s->flags &= SF_SECURE | SF_AUTHENTICATED | SF_VERIFIED; + s->flags |= SF_EHLO; + s->flags |= SF_8BITMIME; + + smtp_report_link_identify(s, "EHLO", s->helo); + + smtp_enter_state(s, STATE_HELO); + smtp_reply(s, "250-%s Hello %s %s%s%s, pleased to meet you", + s->smtpname, + s->helo, + s->ss.ss_family == AF_INET6 ? "" : "[", + ss_to_text(&s->ss), + s->ss.ss_family == AF_INET6 ? "" : "]"); + + smtp_reply(s, "250-8BITMIME"); + smtp_reply(s, "250-ENHANCEDSTATUSCODES"); + smtp_reply(s, "250-SIZE %zu", env->sc_maxsize); + if (ADVERTISE_EXT_DSN(s)) + smtp_reply(s, "250-DSN"); + if (ADVERTISE_TLS(s)) + smtp_reply(s, "250-STARTTLS"); + if (ADVERTISE_AUTH(s)) + smtp_reply(s, "250-AUTH PLAIN LOGIN"); + smtp_reply(s, "250 HELP"); +} + +static void +smtp_proceed_auth(struct smtp_session *s, const char *args) +{ + char tmp[SMTP_LINE_MAX]; + char *eom, *method; + + (void)strlcpy(tmp, args, sizeof tmp); + + method = tmp; + eom = strchr(tmp, ' '); + if (eom == NULL) + eom = strchr(tmp, '\t'); + if (eom != NULL) + *eom++ = '\0'; + if (strcasecmp(method, "PLAIN") == 0) + smtp_rfc4954_auth_plain(s, eom); + else if (strcasecmp(method, "LOGIN") == 0) + smtp_rfc4954_auth_login(s, eom); + else + smtp_reply(s, "504 %s %s: AUTH method \"%s\" not supported", + esc_code(ESC_STATUS_PERMFAIL, ESC_SECURITY_FEATURES_NOT_SUPPORTED), + esc_description(ESC_SECURITY_FEATURES_NOT_SUPPORTED), + method); +} + +static void +smtp_proceed_starttls(struct smtp_session *s, const char *args) +{ + smtp_reply(s, "220 %s Ready to start TLS", + esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS)); + smtp_enter_state(s, STATE_TLS); +} + +static void +smtp_proceed_mail_from(struct smtp_session *s, const char *args) +{ + char *copy; + char tmp[SMTP_LINE_MAX]; + + (void)strlcpy(tmp, args, sizeof tmp); + copy = tmp; + + if (!smtp_tx(s)) { + smtp_reply(s, "421 %s Temporary Error", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + smtp_enter_state(s, STATE_QUIT); + return; + } + + if (smtp_mailaddr(&s->tx->evp.sender, copy, 1, ©, + s->smtpname) == 0) { + smtp_reply(s, "553 %s Sender address syntax error", + esc_code(ESC_STATUS_PERMFAIL, ESC_OTHER_ADDRESS_STATUS)); + smtp_tx_free(s->tx); + return; + } + + smtp_tx_mail_from(s->tx, args); +} + +static void +smtp_proceed_rcpt_to(struct smtp_session *s, const char *args) +{ + smtp_tx_rcpt_to(s->tx, args); +} + +static void +smtp_proceed_data(struct smtp_session *s, const char *args) +{ + smtp_tx_open_message(s->tx); +} + +static void +smtp_proceed_quit(struct smtp_session *s, const char *args) +{ + smtp_reply(s, "221 %s Bye", + esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS)); + smtp_enter_state(s, STATE_QUIT); +} + +static void +smtp_proceed_noop(struct smtp_session *s, const char *args) +{ + smtp_reply(s, "250 %s Ok", + esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS)); +} + +static void +smtp_proceed_help(struct smtp_session *s, const char *args) +{ + const char *code = esc_code(ESC_STATUS_OK, ESC_OTHER_STATUS); + + smtp_reply(s, "214-%s This is " SMTPD_NAME, code); + smtp_reply(s, "214-%s To report bugs in the implementation, " + "please contact bugs@openbsd.org", code); + smtp_reply(s, "214-%s with full details", code); + smtp_reply(s, "214 %s End of HELP info", code); +} + +static void +smtp_proceed_wiz(struct smtp_session *s, const char *args) +{ + smtp_reply(s, "500 %s %s: this feature is not supported yet ;-)", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND), + esc_description(ESC_INVALID_COMMAND)); +} + +static void +smtp_proceed_commit(struct smtp_session *s, const char *args) +{ + smtp_message_end(s->tx); +} + +static void +smtp_proceed_rollback(struct smtp_session *s, const char *args) +{ + struct smtp_tx *tx; + + tx = s->tx; + + fclose(tx->ofile); + tx->ofile = NULL; + + smtp_tx_rollback(tx); + smtp_tx_free(tx); + smtp_enter_state(s, STATE_HELO); +} + +static void +smtp_rfc4954_auth_plain(struct smtp_session *s, char *arg) +{ + char buf[1024], *user, *pass; + int len; + + switch (s->state) { + case STATE_HELO: + if (arg == NULL) { + smtp_enter_state(s, STATE_AUTH_INIT); + smtp_reply(s, "334 "); + return; + } + smtp_enter_state(s, STATE_AUTH_INIT); + /* FALLTHROUGH */ + + case STATE_AUTH_INIT: + /* String is not NUL terminated, leave room. */ + if ((len = base64_decode(arg, (unsigned char *)buf, + sizeof(buf) - 1)) == -1) + goto abort; + /* buf is a byte string, NUL terminate. */ + buf[len] = '\0'; + + /* + * Skip "foo" in "foo\0user\0pass", if present. + */ + user = memchr(buf, '\0', len); + if (user == NULL || user >= buf + len - 2) + goto abort; + user++; /* skip NUL */ + if (strlcpy(s->username, user, sizeof(s->username)) + >= sizeof(s->username)) + goto abort; + + pass = memchr(user, '\0', len - (user - buf)); + if (pass == NULL || pass >= buf + len - 2) + goto abort; + pass++; /* skip NUL */ + + m_create(p_lka, IMSG_SMTP_AUTHENTICATE, 0, 0, -1); + m_add_id(p_lka, s->id); + m_add_string(p_lka, s->listener->authtable); + m_add_string(p_lka, user); + m_add_string(p_lka, pass); + m_close(p_lka); + tree_xset(&wait_parent_auth, s->id, s); + return; + + default: + fatal("smtp_rfc4954_auth_plain: unknown state"); + } + +abort: + smtp_reply(s, "501 %s %s: Syntax error", + esc_code(ESC_STATUS_PERMFAIL, ESC_SYNTAX_ERROR), + esc_description(ESC_SYNTAX_ERROR)); + smtp_enter_state(s, STATE_HELO); +} + +static void +smtp_rfc4954_auth_login(struct smtp_session *s, char *arg) +{ + char buf[LINE_MAX]; + + switch (s->state) { + case STATE_HELO: + smtp_enter_state(s, STATE_AUTH_USERNAME); + if (arg != NULL && *arg != '\0') { + smtp_rfc4954_auth_login(s, arg); + return; + } + smtp_reply(s, "334 VXNlcm5hbWU6"); + return; + + case STATE_AUTH_USERNAME: + memset(s->username, 0, sizeof(s->username)); + if (base64_decode(arg, (unsigned char *)s->username, + sizeof(s->username) - 1) == -1) + goto abort; + + smtp_enter_state(s, STATE_AUTH_PASSWORD); + smtp_reply(s, "334 UGFzc3dvcmQ6"); + return; + + case STATE_AUTH_PASSWORD: + memset(buf, 0, sizeof(buf)); + if (base64_decode(arg, (unsigned char *)buf, + sizeof(buf)-1) == -1) + goto abort; + + m_create(p_lka, IMSG_SMTP_AUTHENTICATE, 0, 0, -1); + m_add_id(p_lka, s->id); + m_add_string(p_lka, s->listener->authtable); + m_add_string(p_lka, s->username); + m_add_string(p_lka, buf); + m_close(p_lka); + tree_xset(&wait_parent_auth, s->id, s); + return; + + default: + fatal("smtp_rfc4954_auth_login: unknown state"); + } + +abort: + smtp_reply(s, "501 %s %s: Syntax error", + esc_code(ESC_STATUS_PERMFAIL, ESC_SYNTAX_ERROR), + esc_description(ESC_SYNTAX_ERROR)); + smtp_enter_state(s, STATE_HELO); +} + +static void +smtp_lookup_servername(struct smtp_session *s) +{ + if (s->listener->hostnametable[0]) { + m_create(p_lka, IMSG_SMTP_LOOKUP_HELO, 0, 0, -1); + m_add_id(p_lka, s->id); + m_add_string(p_lka, s->listener->hostnametable); + m_add_sockaddr(p_lka, (struct sockaddr*)&s->listener->ss); + m_close(p_lka); + tree_xset(&wait_lka_helo, s->id, s); + return; + } + + smtp_connected(s); +} + +static void +smtp_connected(struct smtp_session *s) +{ + smtp_enter_state(s, STATE_CONNECTED); + + log_info("%016"PRIx64" smtp connected address=%s host=%s", + s->id, ss_to_text(&s->ss), s->rdns); + + smtp_filter_begin(s); + + smtp_report_link_connect(s, s->rdns, s->fcrdns, &s->ss, + &s->listener->ss); + + smtp_filter_phase(FILTER_CONNECT, s, ss_to_text(&s->ss)); +} + +static void +smtp_proceed_connected(struct smtp_session *s) +{ + if (s->listener->flags & F_SMTPS) + smtp_cert_init(s); + else + smtp_send_banner(s); +} + +static void +smtp_send_banner(struct smtp_session *s) +{ + smtp_reply(s, "220 %s ESMTP %s", s->smtpname, SMTPD_NAME); + s->banner_sent = 1; + smtp_report_link_greeting(s, s->smtpname); +} + +void +smtp_enter_state(struct smtp_session *s, int newstate) +{ + log_trace(TRACE_SMTP, "smtp: %p: %s -> %s", s, + smtp_strstate(s->state), + smtp_strstate(newstate)); + + s->state = newstate; +} + +static void +smtp_reply(struct smtp_session *s, char *fmt, ...) +{ + va_list ap; + int n; + char buf[LINE_MAX*2], tmp[LINE_MAX*2]; + + va_start(ap, fmt); + n = vsnprintf(buf, sizeof buf, fmt, ap); + va_end(ap); + if (n < 0) + fatalx("smtp_reply: response format error"); + if (n < 4) + fatalx("smtp_reply: response too short"); + if (n >= (int)sizeof buf) { + /* only first three bytes are used by SMTP logic, + * so if _our_ reply does not fit entirely in the + * buffer, it's ok to truncate. + */ + } + + log_trace(TRACE_SMTP, "smtp: %p: >>> %s", s, buf); + smtp_report_protocol_server(s, buf); + + switch (buf[0]) { + case '2': + if (s->tx) { + if (s->last_cmd == CMD_MAIL_FROM) { + smtp_report_tx_begin(s, s->tx->msgid); + smtp_report_tx_mail(s, s->tx->msgid, s->cmd + 10, 1); + } + else if (s->last_cmd == CMD_RCPT_TO) + smtp_report_tx_rcpt(s, s->tx->msgid, s->cmd + 8, 1); + } + break; + case '3': + if (s->tx) { + if (s->last_cmd == CMD_DATA) + smtp_report_tx_data(s, s->tx->msgid, 1); + } + break; + case '5': + case '4': + /* do not report smtp_tx_mail/smtp_tx_rcpt errors + * if they happened outside of a transaction. + */ + if (s->tx) { + if (s->last_cmd == CMD_MAIL_FROM) + smtp_report_tx_mail(s, s->tx->msgid, + s->cmd + 10, buf[0] == '4' ? -1 : 0); + else if (s->last_cmd == CMD_RCPT_TO) + smtp_report_tx_rcpt(s, + s->tx->msgid, s->cmd + 8, buf[0] == '4' ? -1 : 0); + else if (s->last_cmd == CMD_DATA && s->tx->rcptcount) + smtp_report_tx_data(s, s->tx->msgid, + buf[0] == '4' ? -1 : 0); + } + + if (s->flags & SF_BADINPUT) { + log_info("%016"PRIx64" smtp " + "bad-input result=\"%.*s\"", + s->id, n, buf); + } + else if (s->state == STATE_AUTH_INIT) { + log_info("%016"PRIx64" smtp " + "failed-command " + "command=\"AUTH PLAIN (...)\" result=\"%.*s\"", + s->id, n, buf); + } + else if (s->state == STATE_AUTH_USERNAME) { + log_info("%016"PRIx64" smtp " + "failed-command " + "command=\"AUTH LOGIN (username)\" result=\"%.*s\"", + s->id, n, buf); + } + else if (s->state == STATE_AUTH_PASSWORD) { + log_info("%016"PRIx64" smtp " + "failed-command " + "command=\"AUTH LOGIN (password)\" result=\"%.*s\"", + s->id, n, buf); + } + else { + strnvis(tmp, s->cmd, sizeof tmp, VIS_SAFE | VIS_CSTYLE); + log_info("%016"PRIx64" smtp " + "failed-command command=\"%s\" " + "result=\"%.*s\"", + s->id, tmp, n, buf); + } + break; + } + + io_xprintf(s->io, "%s\r\n", buf); +} + +static void +smtp_free(struct smtp_session *s, const char * reason) +{ + if (s->tx) { + if (s->tx->msgid) + smtp_tx_rollback(s->tx); + smtp_tx_free(s->tx); + } + + smtp_report_link_disconnect(s); + smtp_filter_end(s); + + if (s->flags & SF_SECURE && s->listener->flags & F_SMTPS) + stat_decrement("smtp.smtps", 1); + if (s->flags & SF_SECURE && s->listener->flags & F_STARTTLS) + stat_decrement("smtp.tls", 1); + + io_free(s->io); + free(s); + + smtp_collect(); +} + +static int +smtp_mailaddr(struct mailaddr *maddr, char *line, int mailfrom, char **args, + const char *domain) +{ + char *p, *e; + + if (line == NULL) + return (0); + + if (*line != '<') + return (0); + + e = strchr(line, '>'); + if (e == NULL) + return (0); + *e++ = '\0'; + while (*e == ' ') + e++; + *args = e; + + if (!text_to_mailaddr(maddr, line + 1)) + return (0); + + p = strchr(maddr->user, ':'); + if (p != NULL) { + p++; + memmove(maddr->user, p, strlen(p) + 1); + } + + /* accept empty return-path in MAIL FROM, required for bounces */ + if (mailfrom && maddr->user[0] == '\0' && maddr->domain[0] == '\0') + return (1); + + /* no or invalid user-part, reject */ + if (maddr->user[0] == '\0' || !valid_localpart(maddr->user)) + return (0); + + /* no domain part, local user */ + if (maddr->domain[0] == '\0') { + (void)strlcpy(maddr->domain, domain, + sizeof(maddr->domain)); + } + + if (!valid_domainpart(maddr->domain)) + return (0); + + return (1); +} + +static void +smtp_cert_init(struct smtp_session *s) +{ + const char *name; + int fallback; + + if (s->listener->pki_name[0]) { + name = s->listener->pki_name; + fallback = 0; + } + else { + name = s->smtpname; + fallback = 1; + } + + if (cert_init(name, fallback, smtp_cert_init_cb, s)) + tree_xset(&wait_ssl_init, s->id, s); +} + +static void +smtp_cert_init_cb(void *arg, int status, const char *name, const void *cert, + size_t cert_len) +{ + struct smtp_session *s = arg; + void *ssl, *ssl_ctx; + + tree_pop(&wait_ssl_init, s->id); + + if (status == CA_FAIL) { + log_info("%016"PRIx64" smtp disconnected " + "reason=ca-failure", + s->id); + smtp_free(s, "CA failure"); + return; + } + + ssl_ctx = dict_get(env->sc_ssl_dict, name); + ssl = ssl_smtp_init(ssl_ctx, s->listener->flags & F_TLS_VERIFY); + io_set_read(s->io); + io_start_tls(s->io, ssl); +} + +static void +smtp_cert_verify(struct smtp_session *s) +{ + const char *name; + int fallback; + + if (s->listener->ca_name[0]) { + name = s->listener->ca_name; + fallback = 0; + } + else { + name = s->smtpname; + fallback = 1; + } + + if (cert_verify(io_tls(s->io), name, fallback, smtp_cert_verify_cb, s)) { + tree_xset(&wait_ssl_verify, s->id, s); + io_pause(s->io, IO_IN); + } +} + +static void +smtp_cert_verify_cb(void *arg, int status) +{ + struct smtp_session *s = arg; + const char *reason = NULL; + int resume; + + resume = tree_pop(&wait_ssl_verify, s->id) != NULL; + + switch (status) { + case CERT_OK: + reason = "cert-ok"; + s->flags |= SF_VERIFIED; + break; + case CERT_NOCA: + reason = "no-ca"; + break; + case CERT_NOCERT: + reason = "no-client-cert"; + break; + case CERT_INVALID: + reason = "cert-invalid"; + break; + default: + reason = "cert-check-failed"; + break; + } + + log_debug("smtp: %p: smtp_cert_verify_cb: %s", s, reason); + + if (!(s->flags & SF_VERIFIED) && (s->listener->flags & F_TLS_VERIFY)) { + log_info("%016"PRIx64" smtp disconnected " + " reason=%s", s->id, + reason); + smtp_free(s, "SSL certificate check failed"); + return; + } + + smtp_tls_verified(s); + if (resume) + io_resume(s->io, IO_IN); +} + +static void +smtp_auth_failure_resume(int fd, short event, void *p) +{ + struct smtp_session *s = p; + + smtp_reply(s, "535 Authentication failed"); + smtp_enter_state(s, STATE_HELO); +} + +static void +smtp_auth_failure_pause(struct smtp_session *s) +{ + struct timeval tv; + + tv.tv_sec = 0; + tv.tv_usec = arc4random_uniform(1000000); + log_trace(TRACE_SMTP, "smtp: timing-attack protection triggered, " + "will defer answer for %lu microseconds", tv.tv_usec); + evtimer_set(&s->pause, smtp_auth_failure_resume, s); + evtimer_add(&s->pause, &tv); +} + +static int +smtp_tx(struct smtp_session *s) +{ + struct smtp_tx *tx; + + tx = calloc(1, sizeof(*tx)); + if (tx == NULL) + return 0; + + TAILQ_INIT(&tx->rcpts); + + s->tx = tx; + tx->session = s; + + /* setup the envelope */ + tx->evp.ss = s->ss; + (void)strlcpy(tx->evp.tag, s->listener->tag, sizeof(tx->evp.tag)); + (void)strlcpy(tx->evp.smtpname, s->smtpname, sizeof(tx->evp.smtpname)); + (void)strlcpy(tx->evp.hostname, s->rdns, sizeof tx->evp.hostname); + (void)strlcpy(tx->evp.helo, s->helo, sizeof(tx->evp.helo)); + (void)strlcpy(tx->evp.username, s->username, sizeof(tx->evp.username)); + + if (s->flags & SF_BOUNCE) + tx->evp.flags |= EF_BOUNCE; + if (s->flags & SF_AUTHENTICATED) + tx->evp.flags |= EF_AUTHENTICATED; + + if ((tx->parser = rfc5322_parser_new()) == NULL) { + free(tx); + return 0; + } + + return 1; +} + +static void +smtp_tx_free(struct smtp_tx *tx) +{ + struct smtp_rcpt *rcpt; + + rfc5322_free(tx->parser); + + while ((rcpt = TAILQ_FIRST(&tx->rcpts))) { + TAILQ_REMOVE(&tx->rcpts, rcpt, entry); + free(rcpt); + } + + if (tx->ofile) + fclose(tx->ofile); + + tx->session->tx = NULL; + + free(tx); +} + +static void +smtp_tx_mail_from(struct smtp_tx *tx, const char *line) +{ + char *opt; + char *copy; + char tmp[SMTP_LINE_MAX]; + + (void)strlcpy(tmp, line, sizeof tmp); + copy = tmp; + + if (smtp_mailaddr(&tx->evp.sender, copy, 1, ©, + tx->session->smtpname) == 0) { + smtp_reply(tx->session, "553 %s Sender address syntax error", + esc_code(ESC_STATUS_PERMFAIL, ESC_OTHER_ADDRESS_STATUS)); + smtp_tx_free(tx); + return; + } + + while ((opt = strsep(©, " "))) { + if (*opt == '\0') + continue; + + if (strncasecmp(opt, "AUTH=", 5) == 0) + log_debug("debug: smtp: AUTH in MAIL FROM command"); + else if (strncasecmp(opt, "SIZE=", 5) == 0) + log_debug("debug: smtp: SIZE in MAIL FROM command"); + else if (strcasecmp(opt, "BODY=7BIT") == 0) + /* XXX only for this transaction */ + tx->session->flags &= ~SF_8BITMIME; + else if (strcasecmp(opt, "BODY=8BITMIME") == 0) + ; + else if (ADVERTISE_EXT_DSN(tx->session) && strncasecmp(opt, "RET=", 4) == 0) { + opt += 4; + if (strcasecmp(opt, "HDRS") == 0) + tx->evp.dsn_ret = DSN_RETHDRS; + else if (strcasecmp(opt, "FULL") == 0) + tx->evp.dsn_ret = DSN_RETFULL; + } else if (ADVERTISE_EXT_DSN(tx->session) && strncasecmp(opt, "ENVID=", 6) == 0) { + opt += 6; + if (strlcpy(tx->evp.dsn_envid, opt, sizeof(tx->evp.dsn_envid)) + >= sizeof(tx->evp.dsn_envid)) { + smtp_reply(tx->session, + "503 %s %s: option too large, truncated: %s", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS), + esc_description(ESC_INVALID_COMMAND_ARGUMENTS), opt); + smtp_tx_free(tx); + return; + } + } else { + smtp_reply(tx->session, "503 %s %s: Unsupported option %s", + esc_code(ESC_STATUS_PERMFAIL, ESC_INVALID_COMMAND_ARGUMENTS), + esc_description(ESC_INVALID_COMMAND_ARGUMENTS), opt); + smtp_tx_free(tx); + return; + } + } + + /* only check sendertable if defined and user has authenticated */ + if (tx->session->flags & SF_AUTHENTICATED && + tx->session->listener->sendertable[0]) { + m_create(p_lka, IMSG_SMTP_CHECK_SENDER, 0, 0, -1); + m_add_id(p_lka, tx->session->id); + m_add_string(p_lka, tx->session->listener->sendertable); + m_add_string(p_lka, tx->session->username); + m_add_mailaddr(p_lka, &tx->evp.sender); + m_close(p_lka); + tree_xset(&wait_lka_mail, tx->session->id, tx->session); + } + else + smtp_tx_create_message(tx); +} + +static void +smtp_tx_create_message(struct smtp_tx *tx) +{ + m_create(p_queue, IMSG_SMTP_MESSAGE_CREATE, 0, 0, -1); + m_add_id(p_queue, tx->session->id); + m_close(p_queue); + tree_xset(&wait_queue_msg, tx->session->id, tx->session); +} + +static void +smtp_tx_rcpt_to(struct smtp_tx *tx, const char *line) +{ + char *opt, *p; + char *copy; + char tmp[SMTP_LINE_MAX]; + + (void)strlcpy(tmp, line, sizeof tmp); + copy = tmp; + + if (tx->rcptcount >= env->sc_session_max_rcpt) { + smtp_reply(tx->session, "451 %s %s: Too many recipients", + esc_code(ESC_STATUS_TEMPFAIL, ESC_TOO_MANY_RECIPIENTS), + esc_description(ESC_TOO_MANY_RECIPIENTS)); + return; + } + + if (smtp_mailaddr(&tx->evp.rcpt, copy, 0, ©, + tx->session->smtpname) == 0) { + smtp_reply(tx->session, + "501 %s Recipient address syntax error", + esc_code(ESC_STATUS_PERMFAIL, + ESC_BAD_DESTINATION_MAILBOX_ADDRESS_SYNTAX)); + return; + } + + while ((opt = strsep(©, " "))) { + if (*opt == '\0') + continue; + + if (ADVERTISE_EXT_DSN(tx->session) && strncasecmp(opt, "NOTIFY=", 7) == 0) { + opt += 7; + while ((p = strsep(&opt, ","))) { + if (strcasecmp(p, "SUCCESS") == 0) + tx->evp.dsn_notify |= DSN_SUCCESS; + else if (strcasecmp(p, "FAILURE") == 0) + tx->evp.dsn_notify |= DSN_FAILURE; + else if (strcasecmp(p, "DELAY") == 0) + tx->evp.dsn_notify |= DSN_DELAY; + else if (strcasecmp(p, "NEVER") == 0) + tx->evp.dsn_notify |= DSN_NEVER; + } + + if (tx->evp.dsn_notify & DSN_NEVER && + tx->evp.dsn_notify & (DSN_SUCCESS | DSN_FAILURE | + DSN_DELAY)) { + smtp_reply(tx->session, + "553 NOTIFY option NEVER cannot be" + " combined with other options"); + return; + } + } else if (ADVERTISE_EXT_DSN(tx->session) && strncasecmp(opt, "ORCPT=", 6) == 0) { + opt += 6; + + if (strncasecmp(opt, "rfc822;", 7) == 0) + opt += 7; + + if (!text_to_mailaddr(&tx->evp.dsn_orcpt, opt) || + !valid_localpart(tx->evp.dsn_orcpt.user) || + !valid_domainpart(tx->evp.dsn_orcpt.domain)) { + smtp_reply(tx->session, + "553 ORCPT address syntax error"); + return; + } + } else { + smtp_reply(tx->session, "503 Unsupported option %s", opt); + return; + } + } + + m_create(p_lka, IMSG_SMTP_EXPAND_RCPT, 0, 0, -1); + m_add_id(p_lka, tx->session->id); + m_add_envelope(p_lka, &tx->evp); + m_close(p_lka); + tree_xset(&wait_lka_rcpt, tx->session->id, tx->session); +} + +static void +smtp_tx_open_message(struct smtp_tx *tx) +{ + m_create(p_queue, IMSG_SMTP_MESSAGE_OPEN, 0, 0, -1); + m_add_id(p_queue, tx->session->id); + m_add_msgid(p_queue, tx->msgid); + m_close(p_queue); + tree_xset(&wait_queue_fd, tx->session->id, tx->session); +} + +static void +smtp_tx_commit(struct smtp_tx *tx) +{ + m_create(p_queue, IMSG_SMTP_MESSAGE_COMMIT, 0, 0, -1); + m_add_id(p_queue, tx->session->id); + m_add_msgid(p_queue, tx->msgid); + m_close(p_queue); + tree_xset(&wait_queue_commit, tx->session->id, tx->session); + smtp_filter_data_end(tx->session); +} + +static void +smtp_tx_rollback(struct smtp_tx *tx) +{ + m_create(p_queue, IMSG_SMTP_MESSAGE_ROLLBACK, 0, 0, -1); + m_add_msgid(p_queue, tx->msgid); + m_close(p_queue); + smtp_report_tx_rollback(tx->session, tx->msgid); + smtp_report_tx_reset(tx->session, tx->msgid); + smtp_filter_data_end(tx->session); +} + +static int +smtp_tx_dataline(struct smtp_tx *tx, const char *line) +{ + struct rfc5322_result res; + int r; + + log_trace(TRACE_SMTP, "<<< [MSG] %s", line); + + if (!strcmp(line, ".")) { + smtp_report_protocol_client(tx->session, "."); + log_trace(TRACE_SMTP, "<<< [EOM]"); + if (tx->error) + return 1; + line = NULL; + } + else { + /* ignore data line if an error is set */ + if (tx->error) + return 0; + + /* escape lines starting with a '.' */ + if (line[0] == '.') + line += 1; + } + + if (rfc5322_push(tx->parser, line) == -1) { + log_warnx("failed to push dataline"); + tx->error = TX_ERROR_INTERNAL; + return 0; + } + + for(;;) { + r = rfc5322_next(tx->parser, &res); + switch (r) { + case -1: + if (errno == ENOMEM) + tx->error = TX_ERROR_INTERNAL; + else + tx->error = TX_ERROR_MALFORMED; + return 0; + + case RFC5322_NONE: + /* Need more data */ + return 0; + + case RFC5322_HEADER_START: + /* ignore bcc */ + if (!strcasecmp("Bcc", res.hdr)) + continue; + + if (!strcasecmp("To", res.hdr) || + !strcasecmp("Cc", res.hdr) || + !strcasecmp("From", res.hdr)) { + rfc5322_unfold_header(tx->parser); + continue; + } + + if (!strcasecmp("Received", res.hdr)) { + if (++tx->rcvcount >= MAX_HOPS_COUNT) { + log_warnx("warn: loop detected"); + tx->error = TX_ERROR_LOOP; + return 0; + } + } + else if (!tx->has_date && !strcasecmp("Date", res.hdr)) + tx->has_date = 1; + else if (!tx->has_message_id && + !strcasecmp("Message-Id", res.hdr)) + tx->has_message_id = 1; + + smtp_message_printf(tx, "%s:%s\n", res.hdr, res.value); + break; + + case RFC5322_HEADER_CONT: + + if (!strcasecmp("Bcc", res.hdr) || + !strcasecmp("To", res.hdr) || + !strcasecmp("Cc", res.hdr) || + !strcasecmp("From", res.hdr)) + continue; + + smtp_message_printf(tx, "%s\n", res.value); + break; + + case RFC5322_HEADER_END: + if (!strcasecmp("To", res.hdr) || + !strcasecmp("Cc", res.hdr) || + !strcasecmp("From", res.hdr)) + header_domain_append_callback(tx, res.hdr, + res.value); + break; + + case RFC5322_END_OF_HEADERS: + if (tx->session->listener->local || + tx->session->listener->port == 587) { + + if (!tx->has_date) { + log_debug("debug: %p: adding Date", tx); + smtp_message_printf(tx, "Date: %s\n", + time_to_text(tx->time)); + } + + if (!tx->has_message_id) { + log_debug("debug: %p: adding Message-ID", tx); + smtp_message_printf(tx, + "Message-ID: <%016"PRIx64"@%s>\n", + generate_uid(), + tx->session->listener->hostname); + } + } + break; + + case RFC5322_BODY_START: + case RFC5322_BODY: + smtp_message_printf(tx, "%s\n", res.value); + break; + + case RFC5322_END_OF_MESSAGE: + return 1; + + default: + fatalx("%s", __func__); + } + } +} + +static int +smtp_tx_filtered_dataline(struct smtp_tx *tx, const char *line) +{ + if (!strcmp(line, ".")) + line = NULL; + else { + /* ignore data line if an error is set */ + if (tx->error) + return 0; + } + io_printf(tx->filter, "%s\n", line ? line : "."); + return line ? 0 : 1; +} + +static void +smtp_tx_eom(struct smtp_tx *tx) +{ + smtp_filter_phase(FILTER_COMMIT, tx->session, NULL); +} + +static int +smtp_message_fd(struct smtp_tx *tx, int fd) +{ + struct smtp_session *s; + + s = tx->session; + + log_debug("smtp: %p: message fd %d", s, fd); + + if ((tx->ofile = fdopen(fd, "w")) == NULL) { + close(fd); + smtp_reply(s, "421 %s Temporary Error", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + smtp_enter_state(s, STATE_QUIT); + return 0; + } + return 1; +} + +static void +filter_session_io(struct io *io, int evt, void *arg) +{ + struct smtp_tx*tx = arg; + char*line = NULL; + ssize_t len; + + log_trace(TRACE_IO, "filter session io (smtp): %p: %s %s", tx, io_strevent(evt), + io_strio(io)); + + switch (evt) { + case IO_DATAIN: + nextline: + line = io_getline(tx->filter, &len); + /* No complete line received */ + if (line == NULL) + return; + + if (smtp_tx_dataline(tx, line)) { + smtp_tx_eom(tx); + return; + } + + goto nextline; + } +} + +static void +smtp_filter_fd(struct smtp_tx *tx, int fd) +{ + struct smtp_session *s; + + s = tx->session; + + log_debug("smtp: %p: filter fd %d", s, fd); + + tx->filter = io_new(); + io_set_fd(tx->filter, fd); + io_set_callback(tx->filter, filter_session_io, tx); +} + +static void +smtp_message_begin(struct smtp_tx *tx) +{ + struct smtp_session *s; + X509 *x; + int (*m_printf)(struct smtp_tx *, const char *, ...); + + m_printf = smtp_message_printf; + if (tx->filter) + m_printf = smtp_filter_printf; + + s = tx->session; + + log_debug("smtp: %p: message begin", s); + + smtp_reply(s, "354 Enter mail, end with \".\"" + " on a line by itself"); + + if (s->junk || (s->tx && s->tx->junk)) + m_printf(tx, "X-Spam: Yes\n"); + + m_printf(tx, "Received: "); + if (!(s->listener->flags & F_MASK_SOURCE)) { + m_printf(tx, "from %s (%s %s%s%s)", + s->helo, + s->rdns, + s->ss.ss_family == AF_INET6 ? "" : "[", + ss_to_text(&s->ss), + s->ss.ss_family == AF_INET6 ? "" : "]"); + } + m_printf(tx, "\n\tby %s (%s) with %sSMTP%s%s id %08x", + s->smtpname, + SMTPD_NAME, + s->flags & SF_EHLO ? "E" : "", + s->flags & SF_SECURE ? "S" : "", + s->flags & SF_AUTHENTICATED ? "A" : "", + tx->msgid); + + if (s->flags & SF_SECURE) { + x = SSL_get_peer_certificate(io_tls(s->io)); + m_printf(tx, " (%s:%s:%d:%s)", + SSL_get_version(io_tls(s->io)), + SSL_get_cipher_name(io_tls(s->io)), + SSL_get_cipher_bits(io_tls(s->io), NULL), + (s->flags & SF_VERIFIED) ? "YES" : (x ? "FAIL" : "NO")); + X509_free(x); + + if (s->listener->flags & F_RECEIVEDAUTH) { + m_printf(tx, " auth=%s", + s->username[0] ? "yes" : "no"); + if (s->username[0]) + m_printf(tx, " user=%s", s->username); + } + } + + if (tx->rcptcount == 1) { + m_printf(tx, "\n\tfor <%s@%s>", + tx->evp.rcpt.user, + tx->evp.rcpt.domain); + } + + m_printf(tx, ";\n\t%s\n", time_to_text(time(&tx->time))); + + smtp_enter_state(s, STATE_BODY); +} + +static void +smtp_message_end(struct smtp_tx *tx) +{ + struct smtp_session *s; + + s = tx->session; + + log_debug("debug: %p: end of message, error=%d", s, tx->error); + + fclose(tx->ofile); + tx->ofile = NULL; + + switch(tx->error) { + case TX_OK: + smtp_tx_commit(tx); + return; + + case TX_ERROR_SIZE: + smtp_reply(s, "554 %s %s: Transaction failed, message too big", + esc_code(ESC_STATUS_PERMFAIL, ESC_MESSAGE_TOO_BIG_FOR_SYSTEM), + esc_description(ESC_MESSAGE_TOO_BIG_FOR_SYSTEM)); + break; + + case TX_ERROR_LOOP: + smtp_reply(s, "500 %s %s: Loop detected", + esc_code(ESC_STATUS_PERMFAIL, ESC_ROUTING_LOOP_DETECTED), + esc_description(ESC_ROUTING_LOOP_DETECTED)); + break; + + case TX_ERROR_MALFORMED: + smtp_reply(s, "550 %s %s: Message is not RFC 2822 compliant", + esc_code(ESC_STATUS_PERMFAIL, ESC_DELIVERY_NOT_AUTHORIZED_MESSAGE_REFUSED), + esc_description(ESC_DELIVERY_NOT_AUTHORIZED_MESSAGE_REFUSED)); + break; + + case TX_ERROR_IO: + case TX_ERROR_RESOURCES: + smtp_reply(s, "421 %s Temporary Error", + esc_code(ESC_STATUS_TEMPFAIL, ESC_OTHER_MAIL_SYSTEM_STATUS)); + break; + + default: + /* fatal? */ + smtp_reply(s, "421 Internal server error"); + } + + smtp_tx_rollback(tx); + smtp_tx_free(tx); + smtp_enter_state(s, STATE_HELO); +} + +static int +smtp_filter_printf(struct smtp_tx *tx, const char *fmt, ...) +{ + va_list ap; + int len; + + if (tx->error) + return -1; + + va_start(ap, fmt); + len = io_vprintf(tx->filter, fmt, ap); + va_end(ap); + + if (len < 0) { + log_warn("smtp-in: session %016"PRIx64": vfprintf", tx->session->id); + tx->error = TX_ERROR_IO; + } + else + tx->odatalen += len; + + return len; +} + +static int +smtp_message_printf(struct smtp_tx *tx, const char *fmt, ...) +{ + va_list ap; + int len; + + if (tx->error) + return -1; + + va_start(ap, fmt); + len = vfprintf(tx->ofile, fmt, ap); + va_end(ap); + + if (len == -1) { + log_warn("smtp-in: session %016"PRIx64": vfprintf", tx->session->id); + tx->error = TX_ERROR_IO; + } + else + tx->odatalen += len; + + return len; +} + +#define CASE(x) case x : return #x + +const char * +smtp_strstate(int state) +{ + static char buf[32]; + + switch (state) { + CASE(STATE_NEW); + CASE(STATE_CONNECTED); + CASE(STATE_TLS); + CASE(STATE_HELO); + CASE(STATE_AUTH_INIT); + CASE(STATE_AUTH_USERNAME); + CASE(STATE_AUTH_PASSWORD); + CASE(STATE_AUTH_FINALIZE); + CASE(STATE_BODY); + CASE(STATE_QUIT); + default: + (void)snprintf(buf, sizeof(buf), "STATE_??? (%d)", state); + return (buf); + } +} + + +static void +smtp_report_link_connect(struct smtp_session *s, const char *rdns, int fcrdns, + const struct sockaddr_storage *ss_src, + const struct sockaddr_storage *ss_dest) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_connect("smtp-in", s->id, rdns, fcrdns, ss_src, ss_dest); +} + +static void +smtp_report_link_greeting(struct smtp_session *s, + const char *domain) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_greeting("smtp-in", s->id, domain); +} + +static void +smtp_report_link_identify(struct smtp_session *s, const char *method, const char *identity) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_identify("smtp-in", s->id, method, identity); +} + +static void +smtp_report_link_tls(struct smtp_session *s, const char *ssl) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_tls("smtp-in", s->id, ssl); +} + +static void +smtp_report_link_disconnect(struct smtp_session *s) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_disconnect("smtp-in", s->id); +} + +static void +smtp_report_link_auth(struct smtp_session *s, const char *user, const char *result) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_link_auth("smtp-in", s->id, user, result); +} + +static void +smtp_report_tx_reset(struct smtp_session *s, uint32_t msgid) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_reset("smtp-in", s->id, msgid); +} + +static void +smtp_report_tx_begin(struct smtp_session *s, uint32_t msgid) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_begin("smtp-in", s->id, msgid); +} + +static void +smtp_report_tx_mail(struct smtp_session *s, uint32_t msgid, const char *address, int ok) +{ + char mailaddr[SMTPD_MAXMAILADDRSIZE]; + char *p; + + if (! SESSION_FILTERED(s)) + return; + + if ((p = strchr(address, '<')) == NULL) + return; + (void)strlcpy(mailaddr, p + 1, sizeof mailaddr); + if ((p = strchr(mailaddr, '>')) == NULL) + return; + *p = '\0'; + + report_smtp_tx_mail("smtp-in", s->id, msgid, mailaddr, ok); +} + +static void +smtp_report_tx_rcpt(struct smtp_session *s, uint32_t msgid, const char *address, int ok) +{ + char mailaddr[SMTPD_MAXMAILADDRSIZE]; + char *p; + + if (! SESSION_FILTERED(s)) + return; + + if ((p = strchr(address, '<')) == NULL) + return; + (void)strlcpy(mailaddr, p + 1, sizeof mailaddr); + if ((p = strchr(mailaddr, '>')) == NULL) + return; + *p = '\0'; + + report_smtp_tx_rcpt("smtp-in", s->id, msgid, mailaddr, ok); +} + +static void +smtp_report_tx_envelope(struct smtp_session *s, uint32_t msgid, uint64_t evpid) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_envelope("smtp-in", s->id, msgid, evpid); +} + +static void +smtp_report_tx_data(struct smtp_session *s, uint32_t msgid, int ok) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_data("smtp-in", s->id, msgid, ok); +} + +static void +smtp_report_tx_commit(struct smtp_session *s, uint32_t msgid, size_t msgsz) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_commit("smtp-in", s->id, msgid, msgsz); +} + +static void +smtp_report_tx_rollback(struct smtp_session *s, uint32_t msgid) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_tx_rollback("smtp-in", s->id, msgid); +} + +static void +smtp_report_protocol_client(struct smtp_session *s, const char *command) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_protocol_client("smtp-in", s->id, command); +} + +static void +smtp_report_protocol_server(struct smtp_session *s, const char *response) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_protocol_server("smtp-in", s->id, response); +} + +static void +smtp_report_filter_response(struct smtp_session *s, int phase, int response, const char *param) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_filter_response("smtp-in", s->id, phase, response, param); +} + +static void +smtp_report_timeout(struct smtp_session *s) +{ + if (! SESSION_FILTERED(s)) + return; + + report_smtp_timeout("smtp-in", s->id); +} diff --git a/smtpd/smtpc.c b/smtpd/smtpc.c new file mode 100644 index 00000000..59479703 --- /dev/null +++ b/smtpd/smtpc.c @@ -0,0 +1,465 @@ +/* $OpenBSD: smtpc.c,v 1.10 2019/09/21 09:04:08 semarie Exp $ */ + +/* + * Copyright (c) 2018 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> + +#include <event.h> +#include <limits.h> +#include <netdb.h> +#include <pwd.h> +#include <resolv.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <unistd.h> + +#include <openssl/ssl.h> + +#include "smtp.h" +#include "ssl.h" +#include "log.h" + +static void parse_server(char *); +static void parse_message(FILE *); +static void resume(void); + +static int verbose = 1; +static int done = 0; +static int noaction = 0; +static struct addrinfo *res0, *ai; +static struct smtp_params params; +static struct smtp_mail mail; +static const char *servname = NULL; + +static SSL_CTX *ssl_ctx; + +static void +usage(void) +{ + extern char *__progname; + + fprintf(stderr, + "usage: %s [-Chnv] [-F from] [-H helo] [-s server] [-S name] rcpt ...\n", + __progname); + exit(1); +} + +int +main(int argc, char **argv) +{ + char hostname[256]; + int ch, i; + char *server = "localhost"; + struct passwd *pw; + + log_init(1, 0); + + if (gethostname(hostname, sizeof(hostname)) == -1) + fatal("gethostname"); + + if ((pw = getpwuid(getuid())) == NULL) + fatal("getpwuid"); + + memset(¶ms, 0, sizeof(params)); + + params.linemax = 16392; + params.ibufmax = 65536; + params.obufmax = 65536; + params.timeout = 100000; + params.helo = hostname; + + params.tls_verify = 1; + + memset(&mail, 0, sizeof(mail)); + mail.from = pw->pw_name; + + while ((ch = getopt(argc, argv, "CF:H:S:hns:v")) != -1) { + switch (ch) { + case 'C': + params.tls_verify = 0; + break; + case 'F': + mail.from = optarg; + break; + case 'H': + params.helo = optarg; + break; + case 'S': + servname = optarg; + break; + case 'h': + usage(); + break; + case 'n': + noaction = 1; + break; + case 's': + server = optarg; + break; + case 'v': + verbose++; + break; + default: + usage(); + } + } + + log_setverbose(verbose); + + argc -= optind; + argv += optind; + + if (argc) { + mail.rcpt = calloc(argc, sizeof(*mail.rcpt)); + if (mail.rcpt == NULL) + fatal("calloc"); + for (i = 0; i < argc; i++) + mail.rcpt[i].to = argv[i]; + mail.rcptcount = argc; + } + + ssl_init(); + event_init(); + + ssl_ctx = ssl_ctx_create(NULL, NULL, 0, NULL); + if (!SSL_CTX_load_verify_locations(ssl_ctx, + X509_get_default_cert_file(), NULL)) + fatal("SSL_CTX_load_verify_locations"); + if (!SSL_CTX_set_ssl_version(ssl_ctx, SSLv23_client_method())) + fatal("SSL_CTX_set_ssl_version"); + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_NONE , NULL); + +#if HAVE_PLEDGE + if (pledge("stdio inet dns tmppath", NULL) == -1) + fatal("pledge"); +#endif + + if (!noaction) + parse_message(stdin); + +#if HAVE_PLEDGE + if (pledge("stdio inet dns", NULL) == -1) + fatal("pledge"); +#endif + + parse_server(server); + +#if HAVE_PLEDGE + if (pledge("stdio inet", NULL) == -1) + fatal("pledge"); +#endif + + resume(); + + log_debug("done..."); + + return 0; +} + +static void +parse_server(char *server) +{ + struct addrinfo hints; + char *scheme, *creds, *host, *port, *p, *c; + int error; + + creds = NULL; + host = NULL; + port = NULL; + scheme = server; + + p = strstr(server, "://"); + if (p) { + *p = '\0'; + p += 3; + /* check for credentials */ + c = strchr(p, '@'); + if (c) { + creds = p; + *c = '\0'; + host = c + 1; + } else + host = p; + } else { + /* Assume a simple server name */ + scheme = "smtp"; + host = server; + } + + if (host[0] == '[') { + /* IPV6 address? */ + p = strchr(host, ']'); + if (p) { + if (p[1] == ':' || p[1] == '\0') { + *p++ = '\0'; /* remove ']' */ + host++; /* skip '[' */ + if (*p == ':') + port = p + 1; + } + } + } + else { + port = strchr(host, ':'); + if (port) + *port++ = '\0'; + } + + if (port && port[0] == '\0') + port = NULL; + + if (creds) { + p = strchr(creds, ':'); + if (p == NULL) + fatalx("invalid credentials"); + *p = '\0'; + + params.auth_user = creds; + params.auth_pass = p + 1; + } + params.tls_req = TLS_YES; + + if (!strcmp(scheme, "lmtp")) { + params.lmtp = 1; + } + else if (!strcmp(scheme, "lmtp+tls")) { + params.lmtp = 1; + params.tls_req = TLS_FORCE; + } + else if (!strcmp(scheme, "lmtp+notls")) { + params.lmtp = 1; + params.tls_req = TLS_NO; + } + else if (!strcmp(scheme, "smtps")) { + params.tls_req = TLS_SMTPS; + if (port == NULL) + port = "smtps"; + } + else if (!strcmp(scheme, "smtp")) { + } + else if (!strcmp(scheme, "smtp+tls")) { + params.tls_req = TLS_FORCE; + } + else if (!strcmp(scheme, "smtp+notls")) { + params.tls_req = TLS_NO; + } + else + fatalx("invalid url scheme %s", scheme); + + if (port == NULL) + port = "smtp"; + + if (servname == NULL) + servname = host; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + error = getaddrinfo(host, port, &hints, &res0); + if (error) + fatalx("%s: %s", host, gai_strerror(error)); + ai = res0; +} + +void +parse_message(FILE *ifp) +{ + char *line = NULL; + size_t linesz = 0; + ssize_t len; + + if ((mail.fp = tmpfile()) == NULL) + fatal("tmpfile"); + + for (;;) { + if ((len = getline(&line, &linesz, ifp)) == -1) { + if (feof(ifp)) + break; + fatal("getline"); + } + + if (len >= 2 && line[len - 2] == '\r' && line[len - 1] == '\n') + line[--len - 1] = '\n'; + + if (fwrite(line, 1, len, mail.fp) != (size_t)len) + fatal("fwrite"); + + if (line[len - 1] != '\n' && fputc('\n', mail.fp) == EOF) + fatal("fputc"); + } + + fclose(ifp); + rewind(mail.fp); +} + +void +resume(void) +{ + static int started = 0; + char host[256]; + char serv[16]; + + if (done) { + event_loopexit(NULL); + return; + } + + if (ai == NULL) + fatalx("no more host"); + + getnameinfo(ai->ai_addr, SA_LEN(ai->ai_addr), + host, sizeof(host), serv, sizeof(serv), + NI_NUMERICHOST | NI_NUMERICSERV); + log_debug("trying host %s port %s...", host, serv); + + params.dst = ai->ai_addr; + if (smtp_connect(¶ms, NULL) == NULL) + fatal("smtp_connect"); + + if (started == 0) { + started = 1; + event_loop(0); + } +} + +void +log_trace(int lvl, const char *emsg, ...) +{ + va_list ap; + + if (verbose > lvl) { + va_start(ap, emsg); + vlog(LOG_DEBUG, emsg, ap); + va_end(ap); + } +} + +void +smtp_verify_server_cert(void *tag, struct smtp_client *proto, void *ctx) +{ + SSL *ssl = ctx; + X509 *cert; + long res; + int match; + + if ((cert = SSL_get_peer_certificate(ssl))) { + (void)ssl_check_name(cert, servname, &match); + X509_free(cert); + res = SSL_get_verify_result(ssl); + if (res == X509_V_OK) { + if (match) { + log_debug("valid certificate"); + smtp_cert_verified(proto, CERT_OK); + } + else { + log_debug("certificate does not match hostname"); + smtp_cert_verified(proto, CERT_INVALID); + } + return; + } + log_debug("certificate validation error %ld", res); + } + else + log_debug("no certificate provided"); + + smtp_cert_verified(proto, CERT_INVALID); +} + +void +smtp_require_tls(void *tag, struct smtp_client *proto) +{ + SSL *ssl = NULL; + + if ((ssl = SSL_new(ssl_ctx)) == NULL) + fatal("SSL_new"); + smtp_set_tls(proto, ssl); +} + +void +smtp_ready(void *tag, struct smtp_client *proto) +{ + log_debug("connection ready..."); + + if (done || noaction) + smtp_quit(proto); + else + smtp_sendmail(proto, &mail); +} + +void +smtp_failed(void *tag, struct smtp_client *proto, int failure, const char *detail) +{ + switch (failure) { + case FAIL_INTERNAL: + log_warnx("internal error: %s", detail); + break; + case FAIL_CONN: + log_warnx("connection error: %s", detail); + break; + case FAIL_PROTO: + log_warnx("protocol error: %s", detail); + break; + case FAIL_IMPL: + log_warnx("missing feature: %s", detail); + break; + case FAIL_RESP: + log_warnx("rejected by server: %s", detail); + break; + default: + fatalx("unknown failure %d: %s", failure, detail); + } +} + +void +smtp_status(void *tag, struct smtp_client *proto, struct smtp_status *status) +{ + log_info("%s: %s: %s", status->rcpt->to, status->cmd, status->status); +} + +void +smtp_done(void *tag, struct smtp_client *proto, struct smtp_mail *mail) +{ + int i; + + log_debug("mail done..."); + + if (noaction) + return; + + for (i = 0; i < mail->rcptcount; i++) + if (!mail->rcpt[i].done) + return; + + done = 1; +} + +void +smtp_closed(void *tag, struct smtp_client *proto) +{ + log_debug("connection closed..."); + + ai = ai->ai_next; + if (noaction && ai == NULL) + done = 1; + + resume(); +} diff --git a/smtpd/smtpctl.8 b/smtpd/smtpctl.8 new file mode 100644 index 00000000..1efcff63 --- /dev/null +++ b/smtpd/smtpctl.8 @@ -0,0 +1,336 @@ +.\" $OpenBSD: smtpctl.8,v 1.64 2018/09/18 06:21:45 miko Exp $ +.\" +.\" Copyright (c) 2006 Pierre-Yves Ritschard <pyr@openbsd.org> +.\" Copyright (c) 2012 Gilles Chehade <gilles@poolp.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: September 18 2018 $ +.Dt SMTPCTL 8 +.Os +.Sh NAME +.Nm smtpctl , +.Nm mailq +.Nd control the Simple Mail Transfer Protocol daemon +.Sh SYNOPSIS +.Nm +.Ar command +.Op Ar argument ... +.Nm mailq +.Sh DESCRIPTION +The +.Nm +program controls +.Xr smtpd 8 . +Commands may be abbreviated to the minimum unambiguous prefix; for example, +.Cm sh ro +for +.Cm show routes . +.Pp +The +.Nm mailq +command is provided for compatibility with other MTAs +and is simply a shortcut for +.Cm show queue . +.Pp +The following commands are available: +.Bl -tag -width Ds +.It Cm discover Ar envelope-id | message-id +Schedule a single envelope, or all envelopes with the same message ID +that were manually moved to the queue. +.It Cm encrypt Op Ar string +Encrypt the password +.Ar string +to a representation suitable for user credentials and print it to the +standard output. +If +.Ar string +is not provided, cleartext passwords are read from standard input. +.Pp +It is advised to avoid providing the password as a parameter as it will be +visible from +.Xr top 1 +and +.Xr ps 1 +output. +.It Cm log brief +Disable verbose debug logging. +.It Cm log verbose +Enable verbose debug logging. +.It Cm monitor +Display updates of some +.Xr smtpd 8 +internal counters in one second intervals. +Each line reports the increment of all counters since the last update, +except for some counters which are always absolute values. +The first line reports the current value of each counter. +The fields are: +.Pp +.Bl -bullet -compact +.It +Current number of active SMTP clients (absolute value). +.It +New SMTP clients. +.It +Disconnected clients. +.It +Current number of envelopes in the queue (absolute value). +.It +Newly enqueued envelopes. +.It +Dequeued envelopes. +.It +Successful deliveries. +.It +Temporary failures. +.It +Permanent failures. +.It +Message loops. +.It +Expired envelopes. +.It +Envelopes removed by the administrator. +.It +Generated bounces. +.El +.It Cm pause envelope Ar envelope-id | message-id | Cm all +Temporarily suspend scheduling for the envelope with the given ID, +envelopes with the given message ID, +or all envelopes. +.It Cm pause mda +Temporarily stop deliveries to local users. +.It Cm pause mta +Temporarily stop relaying and deliveries to +remote users. +.It Cm pause smtp +Temporarily stop accepting incoming sessions. +.It Cm profile Ar subsystem +Enables real-time profiling of +.Ar subsystem . +Supported subsystems are: +.Pp +.Bl -bullet -compact +.It +queue, to profile cost of queue IO +.It +imsg, to profile cost of event handlers +.El +.It Cm remove Ar envelope-id | message-id | Cm all +Remove a single envelope, +envelopes with the given message ID, +or all envelopes. +.It Cm resume envelope Ar envelope-id | message-id | Cm all +Resume scheduling for the envelope with the given ID, +envelopes with the given message ID, +or all envelopes. +.It Cm resume mda +Resume deliveries to local users. +.It Cm resume mta +Resume relaying and deliveries to remote users. +.It Cm resume route Ar route-id +Resume routing on disabled route +.Ar route-id . +.It Cm resume smtp +Resume accepting incoming sessions. +.It Cm schedule Ar envelope-id | message-id | Cm all +Mark as ready for immediate delivery +a single envelope, +envelopes with the given message ID, +or all envelopes. +.It Cm show envelope Ar envelope-id +Display envelope content for the given ID. +.It Cm show hosts +Display the list of known remote MX hosts. +For each of them, it shows the IP address, the canonical hostname, +a reference count, the number of active connections to this host, +and the elapsed time since the last connection. +.It Cm show hoststats +Display status of last delivery for domains that have been active in the +last 4 hours. +It consists of the following fields, separated by a "|": +.Pp +.Bl -bullet -compact +.It +Domain. +.It +.Ux +timestamp of last delivery. +.It +Status of last delivery. +.El +.It Cm show message Ar envelope-id +Display message content for the given ID. +.It Cm show queue +Display information concerning envelopes that are currently in the queue. +Each line of output describes a single envelope. +It consists of the following fields, separated by a "|": +.Pp +.Bl -bullet -compact +.It +Envelope ID. +.It +Address family of the client which enqueued the mail. +.It +Type of delivery: one of "mta", "mda" or "bounce". +.It +Various flags on the envelope. +.It +Sender address (return path). +.It +The original recipient address. +.It +The destination address. +.It +Time of creation. +.It +Time of expiration. +.It +Time of last delivery or relaying attempt. +.It +Number of delivery or relaying attempts. +.It +Current runstate: either "pending" or "inflight" if +.Xr smtpd 8 +is running, or "offline" otherwise. +.It +Delay in seconds before the next attempt if pending, or time elapsed +if currently running. +This field is blank if +.Xr smtpd 8 +is not running. +.It +Error string for the last failed delivery or relay attempt. +.El +.It Cm show relays +Display the list of currently active relays and associated connectors. +For each relay, it shows a number of counters and information on its +internal state on a single line. +Then comes the list of connectors +(source addresses to connect from for this relay). +.It Cm show routes +Display status of routes currently known by +.Xr smtpd 8 . +Each line consists of a route number, a source address, a destination +address, a set of flags, the number of connections on this +route, the current penalty level which determines the amount of time +the route is disabled if an error occurs, and the delay before it +gets reactivated. +The following flags are defined: +.Pp +.Bl -tag -width xx -compact +.It D +The route is currently disabled. +.It N +The route is new. +No SMTP session has been established yet. +.It Q +The route has a timeout registered to lower its penalty level and possibly +reactivate or discard it. +.El +.It Cm show stats +Displays runtime statistics concerning +.Xr smtpd 8 . +.It Cm show status +Shows if MTA, MDA and SMTP systems are currently running or paused. +.It Cm spf walk +Recursively look up SPF records for the domains read from stdin. +For example: +.Bd -literal -offset indent +# smtpctl spf walk < domains.txt +.Ed +.It Cm trace Ar subsystem +Enables real-time tracing of +.Ar subsystem . +Supported subsystems are: +.Pp +.Bl -bullet -compact +.It +imsg +.It +io +.It +smtp (incoming sessions) +.It +filters +.It +mta (outgoing sessions) +.It +bounce +.It +scheduler +.It +expand (aliases/virtual/forward expansion) +.It +lookup (user/credentials lookups) +.It +stat +.It +rules (matched by incoming sessions) +.It +mproc +.It +all +.El +.It Cm unprofile Ar subsystem +Disables real-time profiling of +.Ar subsystem . +.It Cm untrace Ar subsystem +Disables real-time tracing of +.Ar subsystem . +.It Cm update table Ar name +Updates the contents of table +.Ar name , +for tables using the +.Dq file +backend. +.El +.Pp +When +.Nm smtpd +receives a message, it generates a +.Ar message-id +for the message, and one +.Ar envelope-id +per recipient. +The +.Ar message-id +is a 32-bit random identifier that is guaranteed to be +unique on the host system. +The +.Ar envelope-id +is a 64-bit unique identifier that encodes the +.Ar message-id +in the 32 upper bits and a random envelope identifier +in the 32 lower bits. +.Pp +A command which specifies a +.Ar message-id +applies to all recipients of a message; +a command which specifies an +.Ar envelope-id +applies to a specific recipient of a message. +.Sh FILES +.Bl -tag -width "/var/run/smtpd.sockXXX" -compact +.It Pa /var/run/smtpd.sock +.Ux Ns -domain +socket used for communication with +.Xr smtpd 8 . +.El +.Sh SEE ALSO +.Xr smtpd 8 +.Sh HISTORY +The +.Nm +program first appeared in +.Ox 4.6 . diff --git a/smtpd/smtpctl.c b/smtpd/smtpctl.c new file mode 100644 index 00000000..7dba4224 --- /dev/null +++ b/smtpd/smtpctl.c @@ -0,0 +1,1469 @@ +/* $OpenBSD: smtpctl.c,v 1.167 2020/02/24 16:16:07 millert Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2006 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2006 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2005 Claudio Jeker <claudio@openbsd.org> + * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org> + * Copyright (c) 2003 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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/un.h> +#include <sys/wait.h> +#include <sys/stat.h> + +#include <net/if.h> +/* #include <net/if_media.h> */ +/* #include <net/if_types.h> */ +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fts.h> +#include <grp.h> +#include <imsg.h> +#include <inttypes.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <time.h> +#include <unistd.h> +#if defined(HAVE_VIS_H) && !defined(BROKEN_STRNVIS) +#include <vis.h> +#else +#include "bsd-vis.h" +#endif +#include <limits.h> + +#include "smtpd.h" +#include "parser.h" +#include "log.h" + +#ifndef PATH_GZCAT +#define PATH_GZCAT "/usr/bin/gzcat" +#endif +#define PATH_CAT "/bin/cat" +#define PATH_QUEUE "/queue" +#ifndef PATH_ENCRYPT +#define PATH_ENCRYPT "/usr/bin/encrypt" +#endif + +#ifndef HAVE_DB_API +#define makemap(x, y, z) 1 +#endif + + +int srv_connect(void); +int srv_connected(void); + +void usage(void); +static void show_queue_envelope(struct envelope *, int); +static void getflag(uint *, int, char *, char *, size_t); +static void display(const char *); +static int str_to_trace(const char *); +static int str_to_profile(const char *); +static void show_offline_envelope(uint64_t); +static int is_gzip_fp(FILE *); +static int is_encrypted_fp(FILE *); +static int is_encrypted_buffer(const char *); +static int is_gzip_buffer(const char *); +static FILE *offline_file(void); +static void sendmail_compat(int, char **); + +extern int spfwalk(int, struct parameter *); + +extern char *__progname; +int sendmail; +struct smtpd *env; +struct imsgbuf *ibuf; +struct imsg imsg; +char *rdata; +size_t rlen; +time_t now; + +struct queue_backend queue_backend_null; +struct queue_backend queue_backend_proc; +struct queue_backend queue_backend_ram; + +__dead void +usage(void) +{ + if (sendmail) + fprintf(stderr, "usage: %s [-tv] [-f from] [-F name] to ...\n", + __progname); + else + fprintf(stderr, "usage: %s command [argument ...]\n", + __progname); + exit(1); +} + +void stat_increment(const char *k, size_t v) +{ +} + +void stat_decrement(const char *k, size_t v) +{ +} + +int +srv_connect(void) +{ + struct sockaddr_un s_un; + int ctl_sock, saved_errno; + + /* connect to smtpd control socket */ + if ((ctl_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + err(1, "socket"); + + memset(&s_un, 0, sizeof(s_un)); + s_un.sun_family = AF_UNIX; + (void)strlcpy(s_un.sun_path, SMTPD_SOCKET, sizeof(s_un.sun_path)); + if (connect(ctl_sock, (struct sockaddr *)&s_un, sizeof(s_un)) == -1) { + saved_errno = errno; + close(ctl_sock); + errno = saved_errno; + return (0); + } + + ibuf = xcalloc(1, sizeof(struct imsgbuf)); + imsg_init(ibuf, ctl_sock); + + return (1); +} + +int +srv_connected(void) +{ + return ibuf != NULL ? 1 : 0; +} + +FILE * +offline_file(void) +{ + char path[PATH_MAX]; + int fd; + FILE *fp; + + if (!bsnprintf(path, sizeof(path), "%s%s/%lld.XXXXXXXXXX", PATH_SPOOL, + PATH_OFFLINE, (long long int) time(NULL))) + err(EX_UNAVAILABLE, "snprintf"); + + if ((fd = mkstemp(path)) == -1 || (fp = fdopen(fd, "w+")) == NULL) { + if (fd != -1) + unlink(path); + err(EX_UNAVAILABLE, "cannot create temporary file %s", path); + } + + if (fchmod(fd, 0600) == -1) { + unlink(path); + err(EX_SOFTWARE, "fchmod"); + } + + return fp; +} + + +static void +srv_flush(void) +{ + if (imsg_flush(ibuf) == -1) + err(1, "write error"); +} + +static void +srv_send(int msg, const void *data, size_t len) +{ + if (ibuf == NULL && !srv_connect()) + errx(1, "smtpd doesn't seem to be running"); + imsg_compose(ibuf, msg, IMSG_VERSION, 0, -1, data, len); +} + +static void +srv_recv(int type) +{ + ssize_t n; + + srv_flush(); + + while (1) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + errx(1, "imsg_get error"); + if (n) { + if (imsg.hdr.type == IMSG_CTL_FAIL && + imsg.hdr.peerid != 0 && + imsg.hdr.peerid != IMSG_VERSION) + errx(1, "incompatible smtpctl and smtpd"); + if (type != -1 && type != (int)imsg.hdr.type) + errx(1, "bad message type"); + rdata = imsg.data; + rlen = imsg.hdr.len - sizeof(imsg.hdr); + break; + } + + if ((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) + errx(1, "imsg_read error"); + if (n == 0) + errx(1, "pipe closed"); + } +} + +static void +srv_read(void *dst, size_t sz) +{ + if (sz == 0) + return; + if (rlen < sz) + errx(1, "message too short"); + if (dst) + memmove(dst, rdata, sz); + rlen -= sz; + rdata += sz; +} + +static void +srv_get_int(int *i) +{ + srv_read(i, sizeof(*i)); +} + +static void +srv_get_time(time_t *t) +{ + srv_read(t, sizeof(*t)); +} + +static void +srv_get_evpid(uint64_t *evpid) +{ + srv_read(evpid, sizeof(*evpid)); +} + +static void +srv_get_string(const char **s) +{ + const char *end; + size_t len; + + if (rlen == 0) + errx(1, "message too short"); + + rlen -= 1; + if (*rdata++ == '\0') { + *s = NULL; + return; + } + + if (rlen == 0) + errx(1, "bogus string"); + + end = memchr(rdata, 0, rlen); + if (end == NULL) + errx(1, "unterminated string"); + + len = end + 1 - rdata; + + *s = rdata; + rlen -= len; + rdata += len; +} + +static void +srv_get_envelope(struct envelope *evp) +{ + uint64_t evpid; + const char *str; + + srv_get_evpid(&evpid); + srv_get_string(&str); + + envelope_load_buffer(evp, str, strlen(str)); + evp->id = evpid; +} + +static void +srv_end(void) +{ + if (rlen) + errx(1, "bogus data"); + imsg_free(&imsg); +} + +static int +srv_check_result(int verbose_) +{ + srv_recv(-1); + srv_end(); + + switch (imsg.hdr.type) { + case IMSG_CTL_OK: + if (verbose_) + printf("command succeeded\n"); + return (0); + case IMSG_CTL_FAIL: + if (verbose_) { + if (rlen) + printf("command failed: %s\n", rdata); + else + printf("command failed\n"); + } + return (1); + default: + errx(1, "wrong message in response: %u", imsg.hdr.type); + } + return (0); +} + +static int +srv_iter_messages(uint32_t *res) +{ + static uint32_t *msgids = NULL, from = 0; + static size_t n, curr; + static int done = 0; + + if (done) + return (0); + + if (msgids == NULL) { + srv_send(IMSG_CTL_LIST_MESSAGES, &from, sizeof(from)); + srv_recv(IMSG_CTL_LIST_MESSAGES); + if (rlen == 0) { + srv_end(); + done = 1; + return (0); + } + msgids = malloc(rlen); + n = rlen / sizeof(*msgids); + srv_read(msgids, rlen); + srv_end(); + + curr = 0; + from = msgids[n - 1] + 1; + if (from == 0) + done = 1; + } + + *res = msgids[curr++]; + if (curr == n) { + free(msgids); + msgids = NULL; + } + + return (1); +} + +static int +srv_iter_envelopes(uint32_t msgid, struct envelope *evp) +{ + static uint32_t currmsgid = 0; + static uint64_t from = 0; + static int done = 0, need_send = 1, found; + int flags; + time_t nexttry; + + if (currmsgid != msgid) { + if (currmsgid != 0 && !done) + errx(1, "must finish current iteration first"); + currmsgid = msgid; + from = msgid_to_evpid(msgid); + done = 0; + found = 0; + need_send = 1; + } + + if (done) + return (0); + + again: + if (need_send) { + found = 0; + srv_send(IMSG_CTL_LIST_ENVELOPES, &from, sizeof(from)); + } + need_send = 0; + + srv_recv(IMSG_CTL_LIST_ENVELOPES); + if (rlen == 0) { + srv_end(); + if (!found || evpid_to_msgid(from) != msgid) { + done = 1; + return (0); + } + need_send = 1; + goto again; + } + + srv_get_int(&flags); + srv_get_time(&nexttry); + srv_get_envelope(evp); + srv_end(); + + evp->flags |= flags; + evp->nexttry = nexttry; + + from = evp->id + 1; + found++; + return (1); +} + +static int +srv_iter_evpids(uint32_t msgid, uint64_t *evpid, int *offset) +{ + static uint64_t *evpids = NULL, *tmp; + static int n, tmpalloc, alloc = 0; + struct envelope evp; + + if (*offset == 0) { + n = 0; + while (srv_iter_envelopes(msgid, &evp)) { + if (n == alloc) { + tmpalloc = alloc ? (alloc * 2) : 128; + tmp = recallocarray(evpids, alloc, tmpalloc, + sizeof(*evpids)); + if (tmp == NULL) + err(1, "recallocarray"); + evpids = tmp; + alloc = tmpalloc; + } + evpids[n++] = evp.id; + } + } + + if (*offset >= n) + return (0); + *evpid = evpids[*offset]; + *offset += 1; + return (1); +} + +static void +srv_foreach_envelope(struct parameter *argv, int ctl, size_t *total, size_t *ok) +{ + uint32_t msgid; + uint64_t evpid; + int i; + + *total = 0; + *ok = 0; + + if (argv == NULL) { + while (srv_iter_messages(&msgid)) { + i = 0; + while (srv_iter_evpids(msgid, &evpid, &i)) { + *total += 1; + srv_send(ctl, &evpid, sizeof(evpid)); + if (srv_check_result(0) == 0) + *ok += 1; + } + } + } else if (argv->type == P_MSGID) { + i = 0; + while (srv_iter_evpids(argv->u.u_msgid, &evpid, &i)) { + srv_send(ctl, &evpid, sizeof(evpid)); + if (srv_check_result(0) == 0) + *ok += 1; + } + } else { + *total += 1; + srv_send(ctl, &argv->u.u_evpid, sizeof(evpid)); + if (srv_check_result(0) == 0) + *ok += 1; + } +} + +static void +srv_show_cmd(int cmd, const void *data, size_t len) +{ + int done = 0; + + srv_send(cmd, data, len); + + do { + srv_recv(cmd); + if (rlen) { + printf("%s\n", rdata); + srv_read(NULL, rlen); + } + else + done = 1; + srv_end(); + } while (!done); +} + +static void +droppriv(void) +{ + struct passwd *pw; + + if (geteuid()) + return; + + if ((pw = getpwnam(SMTPD_USER)) == NULL) + errx(1, "unknown user " SMTPD_USER); + + if ((setgroups(1, &pw->pw_gid) || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))) + err(1, "cannot drop privileges"); +} + +static int +do_permission_denied(int argc, struct parameter *argv) +{ + errx(1, "need root privileges"); +} + +static int +do_log_brief(int argc, struct parameter *argv) +{ + int v = 0; + + srv_send(IMSG_CTL_VERBOSE, &v, sizeof(v)); + return srv_check_result(1); +} + +static int +do_log_verbose(int argc, struct parameter *argv) +{ + int v = TRACE_DEBUG; + + srv_send(IMSG_CTL_VERBOSE, &v, sizeof(v)); + return srv_check_result(1); +} + +static int +do_monitor(int argc, struct parameter *argv) +{ + struct stat_digest last, digest; + size_t count; + + memset(&last, 0, sizeof(last)); + count = 0; + + while (1) { + srv_send(IMSG_CTL_GET_DIGEST, NULL, 0); + srv_recv(IMSG_CTL_GET_DIGEST); + srv_read(&digest, sizeof(digest)); + srv_end(); + + if (count % 25 == 0) { + if (count != 0) + printf("\n"); + printf("--- client --- " + "-- envelope -- " + "---- relay/delivery --- " + "------- misc -------\n" + "curr conn disc " + "curr enq deq " + "ok tmpfail prmfail loop " + "expire remove bounce\n"); + } + printf("%4zu %4zu %4zu " + "%4zu %4zu %4zu " + "%4zu %4zu %4zu %4zu " + "%4zu %4zu %4zu\n", + digest.clt_connect - digest.clt_disconnect, + digest.clt_connect - last.clt_connect, + digest.clt_disconnect - last.clt_disconnect, + + digest.evp_enqueued - digest.evp_dequeued, + digest.evp_enqueued - last.evp_enqueued, + digest.evp_dequeued - last.evp_dequeued, + + digest.dlv_ok - last.dlv_ok, + digest.dlv_tempfail - last.dlv_tempfail, + digest.dlv_permfail - last.dlv_permfail, + digest.dlv_loop - last.dlv_loop, + + digest.evp_expired - last.evp_expired, + digest.evp_removed - last.evp_removed, + digest.evp_bounce - last.evp_bounce); + + last = digest; + count++; + sleep(1); + } + + return (0); +} + +static int +do_pause_envelope(int argc, struct parameter *argv) +{ + size_t total, ok; + + srv_foreach_envelope(argv, IMSG_CTL_PAUSE_EVP, &total, &ok); + printf("%zu envelope%s paused\n", ok, (ok > 1) ? "s" : ""); + + return (0); +} + +static int +do_pause_mda(int argc, struct parameter *argv) +{ + srv_send(IMSG_CTL_PAUSE_MDA, NULL, 0); + return srv_check_result(1); +} + +static int +do_pause_mta(int argc, struct parameter *argv) +{ + srv_send(IMSG_CTL_PAUSE_MTA, NULL, 0); + return srv_check_result(1); +} + +static int +do_pause_smtp(int argc, struct parameter *argv) +{ + srv_send(IMSG_CTL_PAUSE_SMTP, NULL, 0); + return srv_check_result(1); +} + +static int +do_profile(int argc, struct parameter *argv) +{ + int v; + + v = str_to_profile(argv[0].u.u_str); + + srv_send(IMSG_CTL_PROFILE_ENABLE, &v, sizeof(v)); + return srv_check_result(1); +} + +static int +do_remove(int argc, struct parameter *argv) +{ + size_t total, ok; + + srv_foreach_envelope(argv, IMSG_CTL_REMOVE, &total, &ok); + printf("%zu envelope%s removed\n", ok, (ok > 1) ? "s" : ""); + + return (0); +} + +static int +do_resume_envelope(int argc, struct parameter *argv) +{ + size_t total, ok; + + srv_foreach_envelope(argv, IMSG_CTL_RESUME_EVP, &total, &ok); + printf("%zu envelope%s resumed\n", ok, (ok > 1) ? "s" : ""); + + return (0); +} + +static int +do_resume_mda(int argc, struct parameter *argv) +{ + srv_send(IMSG_CTL_RESUME_MDA, NULL, 0); + return srv_check_result(1); +} + +static int +do_resume_mta(int argc, struct parameter *argv) +{ + srv_send(IMSG_CTL_RESUME_MTA, NULL, 0); + return srv_check_result(1); +} + +static int +do_resume_route(int argc, struct parameter *argv) +{ + uint64_t v; + + if (argc == 0) + v = 0; + else + v = argv[0].u.u_routeid; + + srv_send(IMSG_CTL_RESUME_ROUTE, &v, sizeof(v)); + return srv_check_result(1); +} + +static int +do_resume_smtp(int argc, struct parameter *argv) +{ + srv_send(IMSG_CTL_RESUME_SMTP, NULL, 0); + return srv_check_result(1); +} + +static int +do_schedule(int argc, struct parameter *argv) +{ + size_t total, ok; + + srv_foreach_envelope(argv, IMSG_CTL_SCHEDULE, &total, &ok); + printf("%zu envelope%s scheduled\n", ok, (ok > 1) ? "s" : ""); + + return (0); +} + +static int +do_show_envelope(int argc, struct parameter *argv) +{ + char buf[PATH_MAX]; + + if (!bsnprintf(buf, sizeof(buf), "%s%s/%02x/%08x/%016" PRIx64, + PATH_SPOOL, + PATH_QUEUE, + (evpid_to_msgid(argv[0].u.u_evpid) & 0xff000000) >> 24, + evpid_to_msgid(argv[0].u.u_evpid), + argv[0].u.u_evpid)) + errx(1, "unable to retrieve envelope"); + + display(buf); + + return (0); +} + +static int +do_show_hoststats(int argc, struct parameter *argv) +{ + srv_show_cmd(IMSG_CTL_MTA_SHOW_HOSTSTATS, NULL, 0); + + return (0); +} + +static int +do_show_message(int argc, struct parameter *argv) +{ + char buf[PATH_MAX]; + uint32_t msgid; + + if (argv[0].type == P_EVPID) + msgid = evpid_to_msgid(argv[0].u.u_evpid); + else + msgid = argv[0].u.u_msgid; + + if (!bsnprintf(buf, sizeof(buf), "%s%s/%02x/%08x/message", + PATH_SPOOL, + PATH_QUEUE, + (msgid & 0xff000000) >> 24, + msgid)) + errx(1, "unable to retrieve message"); + + display(buf); + + return (0); +} + +static int +do_show_queue(int argc, struct parameter *argv) +{ + struct envelope evp; + uint32_t msgid; + FTS *fts; + FTSENT *ftse; + char *qpath[] = {"/queue", NULL}; + char *tmp; + uint64_t evpid; + + now = time(NULL); + + if (!srv_connect()) { + log_init(1, LOG_MAIL); + queue_init("fs", 0); + if (chroot(PATH_SPOOL) == -1 || chdir("/") == -1) + err(1, "%s", PATH_SPOOL); + fts = fts_open(qpath, FTS_PHYSICAL|FTS_NOCHDIR, NULL); + if (fts == NULL) + err(1, "%s/queue", PATH_SPOOL); + + while ((ftse = fts_read(fts)) != NULL) { + switch (ftse->fts_info) { + case FTS_DP: + case FTS_DNR: + break; + case FTS_F: + tmp = NULL; + evpid = strtoull(ftse->fts_name, &tmp, 16); + if (tmp && *tmp != '\0') + break; + show_offline_envelope(evpid); + } + } + + fts_close(fts); + return (0); + } + + if (argc == 0) { + msgid = 0; + while (srv_iter_messages(&msgid)) + while (srv_iter_envelopes(msgid, &evp)) + show_queue_envelope(&evp, 1); + } else if (argv[0].type == P_MSGID) { + while (srv_iter_envelopes(argv[0].u.u_msgid, &evp)) + show_queue_envelope(&evp, 1); + } + + return (0); +} + +static int +do_show_hosts(int argc, struct parameter *argv) +{ + srv_show_cmd(IMSG_CTL_MTA_SHOW_HOSTS, NULL, 0); + + return (0); +} + +static int +do_show_relays(int argc, struct parameter *argv) +{ + srv_show_cmd(IMSG_CTL_MTA_SHOW_RELAYS, NULL, 0); + + return (0); +} + +static int +do_show_routes(int argc, struct parameter *argv) +{ + srv_show_cmd(IMSG_CTL_MTA_SHOW_ROUTES, NULL, 0); + + return (0); +} + +static int +do_show_stats(int argc, struct parameter *argv) +{ + struct stat_kv kv; + time_t duration; + + memset(&kv, 0, sizeof kv); + + while (1) { + srv_send(IMSG_CTL_GET_STATS, &kv, sizeof kv); + srv_recv(IMSG_CTL_GET_STATS); + srv_read(&kv, sizeof(kv)); + srv_end(); + + if (kv.iter == NULL) + break; + + if (strcmp(kv.key, "uptime") == 0) { + duration = time(NULL) - kv.val.u.counter; + printf("uptime=%lld\n", (long long)duration); + printf("uptime.human=%s\n", + duration_to_text(duration)); + } + else { + switch (kv.val.type) { + case STAT_COUNTER: + printf("%s=%zd\n", + kv.key, kv.val.u.counter); + break; + case STAT_TIMESTAMP: + printf("%s=%" PRId64 "\n", + kv.key, (int64_t)kv.val.u.timestamp); + break; + case STAT_TIMEVAL: + printf("%s=%lld.%lld\n", + kv.key, (long long)kv.val.u.tv.tv_sec, + (long long)kv.val.u.tv.tv_usec); + break; + case STAT_TIMESPEC: + printf("%s=%lld.%06ld\n", + kv.key, + (long long)kv.val.u.ts.tv_sec * 1000000 + + kv.val.u.ts.tv_nsec / 1000000, + kv.val.u.ts.tv_nsec % 1000000); + break; + } + } + } + + return (0); +} + +static int +do_show_status(int argc, struct parameter *argv) +{ + uint32_t sc_flags; + + srv_send(IMSG_CTL_SHOW_STATUS, NULL, 0); + srv_recv(IMSG_CTL_SHOW_STATUS); + srv_read(&sc_flags, sizeof(sc_flags)); + srv_end(); + printf("MDA %s\n", + (sc_flags & SMTPD_MDA_PAUSED) ? "paused" : "running"); + printf("MTA %s\n", + (sc_flags & SMTPD_MTA_PAUSED) ? "paused" : "running"); + printf("SMTP %s\n", + (sc_flags & SMTPD_SMTP_PAUSED) ? "paused" : "running"); + return (0); +} + +static int +do_trace(int argc, struct parameter *argv) +{ + int v; + + v = str_to_trace(argv[0].u.u_str); + + srv_send(IMSG_CTL_TRACE_ENABLE, &v, sizeof(v)); + return srv_check_result(1); +} + +static int +do_unprofile(int argc, struct parameter *argv) +{ + int v; + + v = str_to_profile(argv[0].u.u_str); + + srv_send(IMSG_CTL_PROFILE_DISABLE, &v, sizeof(v)); + return srv_check_result(1); +} + +static int +do_untrace(int argc, struct parameter *argv) +{ + int v; + + v = str_to_trace(argv[0].u.u_str); + + srv_send(IMSG_CTL_TRACE_DISABLE, &v, sizeof(v)); + return srv_check_result(1); +} + +static int +do_update_table(int argc, struct parameter *argv) +{ + const char *name = argv[0].u.u_str; + + srv_send(IMSG_CTL_UPDATE_TABLE, name, strlen(name) + 1); + return srv_check_result(1); +} + +static int +do_encrypt(int argc, struct parameter *argv) +{ + const char *p = NULL; + + droppriv(); + + if (argv) + p = argv[0].u.u_str; + execl(PATH_ENCRYPT, "encrypt", "--", p, (char *)NULL); + errx(1, "execl"); +} + +static int +do_block_mta(int argc, struct parameter *argv) +{ + struct ibuf *m; + + if (ibuf == NULL && !srv_connect()) + errx(1, "smtpd doesn't seem to be running"); + m = imsg_create(ibuf, IMSG_CTL_MTA_BLOCK, IMSG_VERSION, 0, + sizeof(argv[0].u.u_ss) + strlen(argv[1].u.u_str) + 1); + if (imsg_add(m, &argv[0].u.u_ss, sizeof(argv[0].u.u_ss)) == -1) + errx(1, "imsg_add"); + if (imsg_add(m, argv[1].u.u_str, strlen(argv[1].u.u_str) + 1) == -1) + errx(1, "imsg_add"); + imsg_close(ibuf, m); + + return srv_check_result(1); +} + +static int +do_unblock_mta(int argc, struct parameter *argv) +{ + struct ibuf *m; + + if (ibuf == NULL && !srv_connect()) + errx(1, "smtpd doesn't seem to be running"); + + m = imsg_create(ibuf, IMSG_CTL_MTA_UNBLOCK, IMSG_VERSION, 0, + sizeof(argv[0].u.u_ss) + strlen(argv[1].u.u_str) + 1); + if (imsg_add(m, &argv[0].u.u_ss, sizeof(argv[0].u.u_ss)) == -1) + errx(1, "imsg_add"); + if (imsg_add(m, argv[1].u.u_str, strlen(argv[1].u.u_str) + 1) == -1) + errx(1, "imsg_add"); + imsg_close(ibuf, m); + + return srv_check_result(1); +} + +static int +do_show_mta_block(int argc, struct parameter *argv) +{ + srv_show_cmd(IMSG_CTL_MTA_SHOW_BLOCK, NULL, 0); + + return (0); +} + +static int +do_discover(int argc, struct parameter *argv) +{ + uint64_t evpid; + uint32_t msgid; + size_t n_evp; + + if (ibuf == NULL && !srv_connect()) + errx(1, "smtpd doesn't seem to be running"); + + if (argv[0].type == P_EVPID) { + evpid = argv[0].u.u_evpid; + srv_send(IMSG_CTL_DISCOVER_EVPID, &evpid, sizeof evpid); + srv_recv(IMSG_CTL_DISCOVER_EVPID); + } else { + msgid = argv[0].u.u_msgid; + srv_send(IMSG_CTL_DISCOVER_MSGID, &msgid, sizeof msgid); + srv_recv(IMSG_CTL_DISCOVER_MSGID); + } + + if (rlen == 0) { + srv_end(); + return (0); + } else { + srv_read(&n_evp, sizeof n_evp); + srv_end(); + } + + printf("%zu envelope%s discovered\n", n_evp, (n_evp != 1) ? "s" : ""); + return (0); +} + +static int +do_spf_walk(int argc, struct parameter *argv) +{ + droppriv(); + + return spfwalk(argc, argv); +} + +#define cmd_install_priv(s, f) \ + cmd_install((s), privileged ? (f) : do_permission_denied) + +int +main(int argc, char **argv) +{ + gid_t gid; + struct group *gr; + int privileged; + char *argv_mailq[] = { "show", "queue", NULL }; + +#ifndef HAVE___PROGNAME + __progname = ssh_get_progname(argv[0]); +#endif + + /* check that smtpctl was installed setgid */ + if ((gr = getgrnam(SMTPD_QUEUE_GROUP)) == NULL) + errx(1, "unknown group %s", SMTPD_QUEUE_GROUP); + else if (gr->gr_gid != getegid()) + errx(1, "this program must be setgid %s", SMTPD_QUEUE_GROUP); + + sendmail_compat(argc, argv); + privileged = geteuid() == 0; + + gid = getgid(); + if (setresgid(gid, gid, gid) == -1) + err(1, "setresgid"); + + /* Privileged commands */ + cmd_install_priv("discover <evpid>", do_discover); + cmd_install_priv("discover <msgid>", do_discover); + cmd_install_priv("pause mta from <addr> for <str>", do_block_mta); + cmd_install_priv("resume mta from <addr> for <str>", do_unblock_mta); + cmd_install_priv("show mta paused", do_show_mta_block); + cmd_install_priv("log brief", do_log_brief); + cmd_install_priv("log verbose", do_log_verbose); + cmd_install_priv("monitor", do_monitor); + cmd_install_priv("pause envelope <evpid>", do_pause_envelope); + cmd_install_priv("pause envelope <msgid>", do_pause_envelope); + cmd_install_priv("pause envelope all", do_pause_envelope); + cmd_install_priv("pause mda", do_pause_mda); + cmd_install_priv("pause mta", do_pause_mta); + cmd_install_priv("pause smtp", do_pause_smtp); + cmd_install_priv("profile <str>", do_profile); + cmd_install_priv("remove <evpid>", do_remove); + cmd_install_priv("remove <msgid>", do_remove); + cmd_install_priv("remove all", do_remove); + cmd_install_priv("resume envelope <evpid>", do_resume_envelope); + cmd_install_priv("resume envelope <msgid>", do_resume_envelope); + cmd_install_priv("resume envelope all", do_resume_envelope); + cmd_install_priv("resume mda", do_resume_mda); + cmd_install_priv("resume mta", do_resume_mta); + cmd_install_priv("resume route <routeid>", do_resume_route); + cmd_install_priv("resume smtp", do_resume_smtp); + cmd_install_priv("schedule <msgid>", do_schedule); + cmd_install_priv("schedule <evpid>", do_schedule); + cmd_install_priv("schedule all", do_schedule); + cmd_install_priv("show envelope <evpid>", do_show_envelope); + cmd_install_priv("show hoststats", do_show_hoststats); + cmd_install_priv("show message <msgid>", do_show_message); + cmd_install_priv("show message <evpid>", do_show_message); + cmd_install_priv("show queue", do_show_queue); + cmd_install_priv("show queue <msgid>", do_show_queue); + cmd_install_priv("show hosts", do_show_hosts); + cmd_install_priv("show relays", do_show_relays); + cmd_install_priv("show routes", do_show_routes); + cmd_install_priv("show stats", do_show_stats); + cmd_install_priv("show status", do_show_status); + cmd_install_priv("trace <str>", do_trace); + cmd_install_priv("unprofile <str>", do_unprofile); + cmd_install_priv("untrace <str>", do_untrace); + cmd_install_priv("update table <str>", do_update_table); + + /* Unprivileged commands */ + cmd_install("encrypt", do_encrypt); + cmd_install("encrypt <str>", do_encrypt); + cmd_install("spf walk", do_spf_walk); + + if (strcmp(__progname, "mailq") == 0) + return cmd_run(2, argv_mailq); + if (strcmp(__progname, "smtpctl") == 0) + return cmd_run(argc - 1, argv + 1); + + errx(1, "unsupported mode"); + return (0); +} + +void +sendmail_compat(int argc, char **argv) +{ + FILE *offlinefp = NULL; + gid_t gid; + int i, r; + + if (strcmp(__progname, "sendmail") == 0 || + strcmp(__progname, "send-mail") == 0) { + /* + * determine whether we are called with flags + * that should invoke makemap/newaliases. + */ + for (i = 1; i < argc; i++) + if (strncmp(argv[i], "-bi", 3) == 0) + exit(makemap(P_SENDMAIL, argc, argv)); + + if (!srv_connect()) + offlinefp = offline_file(); + + gid = getgid(); + if (setresgid(gid, gid, gid) == -1) + err(1, "setresgid"); + +#if HAVE_PLEDGE + /* we'll reduce further down the road */ + if (pledge("stdio rpath wpath cpath tmppath flock " + "dns getpw recvfd", NULL) == -1) + err(1, "pledge"); +#endif + + sendmail = 1; + exit(enqueue(argc, argv, offlinefp)); + } else if (strcmp(__progname, "makemap") == 0) + exit(makemap(P_MAKEMAP, argc, argv)); + else if (strcmp(__progname, "newaliases") == 0) { + r = makemap(P_NEWALIASES, argc, argv); + /* + * if server is available, notify of table update. + * only makes sense for static tables AND if server is up. + */ + if (srv_connect()) { + srv_send(IMSG_CTL_UPDATE_TABLE, "aliases", strlen("aliases") + 1); + srv_check_result(0); + } + exit(r); + } +} + +static void +show_queue_envelope(struct envelope *e, int online) +{ + const char *src = "?", *agent = "?"; + char status[128], runstate[128], errline[LINE_MAX]; + + status[0] = '\0'; + + getflag(&e->flags, EF_BOUNCE, "bounce", status, sizeof(status)); + getflag(&e->flags, EF_AUTHENTICATED, "auth", status, sizeof(status)); + getflag(&e->flags, EF_INTERNAL, "internal", status, sizeof(status)); + getflag(&e->flags, EF_SUSPEND, "suspend", status, sizeof(status)); + getflag(&e->flags, EF_HOLD, "hold", status, sizeof(status)); + + if (online) { + if (e->flags & EF_PENDING) + (void)snprintf(runstate, sizeof runstate, "pending|%zd", + (ssize_t)(e->nexttry - now)); + else if (e->flags & EF_INFLIGHT) + (void)snprintf(runstate, sizeof runstate, + "inflight|%zd", (ssize_t)(now - e->lasttry)); + else + (void)snprintf(runstate, sizeof runstate, "invalid|"); + e->flags &= ~(EF_PENDING|EF_INFLIGHT); + } + else + (void)strlcpy(runstate, "offline|", sizeof runstate); + + if (e->flags) + errx(1, "%016" PRIx64 ": unexpected flags 0x%04x", e->id, + e->flags); + + if (status[0]) + status[strlen(status) - 1] = '\0'; + + if (e->type == D_MDA) + agent = "mda"; + else if (e->type == D_MTA) + agent = "mta"; + else if (e->type == D_BOUNCE) + agent = "bounce"; + + if (e->ss.ss_family == AF_LOCAL) + src = "local"; + else if (e->ss.ss_family == AF_INET) + src = "inet4"; + else if (e->ss.ss_family == AF_INET6) + src = "inet6"; + + strnvis(errline, e->errorline, sizeof(errline), 0); + + printf("%016"PRIx64 + "|%s|%s|%s|%s@%s|%s@%s|%s@%s" + "|%zu|%zu|%zu|%zu|%s|%s\n", + + e->id, + + src, + agent, + status, + e->sender.user, e->sender.domain, + e->rcpt.user, e->rcpt.domain, + e->dest.user, e->dest.domain, + + (size_t) e->creation, + (size_t) (e->creation + e->ttl), + (size_t) e->lasttry, + (size_t) e->retry, + runstate, + errline); +} + +static void +getflag(uint *bitmap, int bit, char *bitstr, char *buf, size_t len) +{ + if (*bitmap & bit) { + *bitmap &= ~bit; + (void)strlcat(buf, bitstr, len); + (void)strlcat(buf, ",", len); + } +} + +static void +show_offline_envelope(uint64_t evpid) +{ + FILE *fp = NULL; + char pathname[PATH_MAX]; + size_t plen; + char *p; + size_t buflen; + char buffer[sizeof(struct envelope)]; + + struct envelope evp; + + if (!bsnprintf(pathname, sizeof pathname, + "/queue/%02x/%08x/%016"PRIx64, + (evpid_to_msgid(evpid) & 0xff000000) >> 24, + evpid_to_msgid(evpid), evpid)) + goto end; + fp = fopen(pathname, "r"); + if (fp == NULL) + goto end; + + buflen = fread(buffer, 1, sizeof (buffer) - 1, fp); + p = buffer; + plen = buflen; + buffer[buflen] = '\0'; + + if (is_encrypted_buffer(p)) { + warnx("offline encrypted queue is not supported yet"); + goto end; + } + + if (is_gzip_buffer(p)) { + warnx("offline compressed queue is not supported yet"); + goto end; + } + + if (!envelope_load_buffer(&evp, p, plen)) + goto end; + evp.id = evpid; + show_queue_envelope(&evp, 0); + +end: + if (fp) + fclose(fp); +} + +static void +display(const char *s) +{ + FILE *fp; + char *key; + int gzipped; + char *gzcat_argv0 = strrchr(PATH_GZCAT, '/') + 1; + + if ((fp = fopen(s, "r")) == NULL) + err(1, "fopen"); + + if (is_encrypted_fp(fp)) { + int i; + FILE *ofp = NULL; + + if ((ofp = tmpfile()) == NULL) + err(1, "tmpfile"); + + for (i = 0; i < 3; i++) { + key = getpass("key> "); + if (crypto_setup(key, strlen(key))) + break; + } + if (i == 3) + errx(1, "crypto-setup: invalid key"); + + if (!crypto_decrypt_file(fp, ofp)) { + printf("object is encrypted: %s\n", key); + exit(1); + } + + fclose(fp); + fp = ofp; + fseek(fp, 0, SEEK_SET); + } + gzipped = is_gzip_fp(fp); + + lseek(fileno(fp), 0, SEEK_SET); + (void)dup2(fileno(fp), STDIN_FILENO); + if (gzipped) + execl(PATH_GZCAT, gzcat_argv0, (char *)NULL); + else + execl(PATH_CAT, "cat", (char *)NULL); + err(1, "execl"); +} + +static int +str_to_trace(const char *str) +{ + if (!strcmp(str, "imsg")) + return TRACE_IMSG; + if (!strcmp(str, "io")) + return TRACE_IO; + if (!strcmp(str, "smtp")) + return TRACE_SMTP; + if (!strcmp(str, "filters")) + return TRACE_FILTERS; + if (!strcmp(str, "mta")) + return TRACE_MTA; + if (!strcmp(str, "bounce")) + return TRACE_BOUNCE; + if (!strcmp(str, "scheduler")) + return TRACE_SCHEDULER; + if (!strcmp(str, "lookup")) + return TRACE_LOOKUP; + if (!strcmp(str, "stat")) + return TRACE_STAT; + if (!strcmp(str, "rules")) + return TRACE_RULES; + if (!strcmp(str, "mproc")) + return TRACE_MPROC; + if (!strcmp(str, "expand")) + return TRACE_EXPAND; + if (!strcmp(str, "all")) + return ~TRACE_DEBUG; + errx(1, "invalid trace keyword: %s", str); + return (0); +} + +static int +str_to_profile(const char *str) +{ + if (!strcmp(str, "imsg")) + return PROFILE_IMSG; + if (!strcmp(str, "queue")) + return PROFILE_QUEUE; + errx(1, "invalid profile keyword: %s", str); + return (0); +} + +static int +is_gzip_buffer(const char *buffer) +{ + uint16_t magic; + + memcpy(&magic, buffer, sizeof magic); +#define GZIP_MAGIC 0x8b1f + return (magic == GZIP_MAGIC); +} + +static int +is_gzip_fp(FILE *fp) +{ + uint8_t magic[2]; + int ret = 0; + + if (fread(&magic, 1, sizeof magic, fp) != sizeof magic) + goto end; + + ret = is_gzip_buffer((const char *)&magic); +end: + fseek(fp, 0, SEEK_SET); + return ret; +} + + +/* XXX */ +/* + * queue supports transparent encryption. + * encrypted chunks are prefixed with an API version byte + * which we ensure is unambiguous with gzipped / plain + * objects. + */ + +static int +is_encrypted_buffer(const char *buffer) +{ + uint8_t magic; + + magic = *buffer; +#define ENCRYPTION_MAGIC 0x1 + return (magic == ENCRYPTION_MAGIC); +} + +static int +is_encrypted_fp(FILE *fp) +{ + uint8_t magic; + int ret = 0; + + if (fread(&magic, 1, sizeof magic, fp) != sizeof magic) + goto end; + + ret = is_encrypted_buffer((const char *)&magic); +end: + fseek(fp, 0, SEEK_SET); + return ret; +} diff --git a/smtpd/smtpctl/Makefile b/smtpd/smtpctl/Makefile new file mode 100644 index 00000000..ef8148be --- /dev/null +++ b/smtpd/smtpctl/Makefile @@ -0,0 +1,56 @@ +# $OpenBSD: Makefile,v 1.47 2018/07/03 01:34:43 mortimer Exp $ + +.PATH: ${.CURDIR}/.. + +PROG= smtpctl +BINOWN= root +BINGRP= _smtpq + +BINMODE?=2555 + +BINDIR= /usr/sbin +MAN= smtpctl.8 aliases.5 forward.5 makemap.8 newaliases.8 + +CFLAGS+= -fstack-protector-all +CFLAGS+= -I${.CURDIR}/.. +CFLAGS+= -Wall -Wstrict-prototypes -Wmissing-prototypes +CFLAGS+= -Wmissing-declarations +CFLAGS+= -Wshadow -Wpointer-arith -Wcast-qual +CFLAGS+= -Wsign-compare +CFLAGS+= -Werror-implicit-function-declaration +CFLAGS+= -DNO_IO +CFLAGS+= -DCONFIG_MINIMUM +YFLAGS= + +SRCS= enqueue.c +SRCS+= parser.c +SRCS+= log.c +SRCS+= envelope.c +SRCS+= crypto.c +SRCS+= queue_backend.c +SRCS+= queue_fs.c +SRCS+= smtpctl.c +SRCS+= util.c +SRCS+= compress_backend.c +SRCS+= compress_gzip.c +SRCS+= to.c +SRCS+= expand.c +SRCS+= tree.c +SRCS+= config.c +SRCS+= dict.c +SRCS+= aliases.c +SRCS+= limit.c +SRCS+= makemap.c +SRCS+= parse.y +SRCS+= mailaddr.c +SRCS+= table.c +SRCS+= table_static.c +SRCS+= table_db.c +SRCS+= table_getpwnam.c +SRCS+= table_proc.c +SRCS+= unpack_dns.c +SRCS+= spfwalk.c + +LDADD+= -levent -lutil -lz -lcrypto +DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBZ} ${LIBCRYPTO} +.include <bsd.prog.mk> diff --git a/smtpd/smtpd-api.h b/smtpd/smtpd-api.h new file mode 100644 index 00000000..f83edd05 --- /dev/null +++ b/smtpd/smtpd-api.h @@ -0,0 +1,290 @@ +/* $OpenBSD: smtpd-api.h,v 1.36 2018/12/23 16:06:24 gilles Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2011 Gilles Chehade <gilles@poolp.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. + */ + +#ifndef _SMTPD_API_H_ +#define _SMTPD_API_H_ + +#include "dict.h" +#include "tree.h" + +struct mailaddr { + char user[SMTPD_MAXLOCALPARTSIZE]; + char domain[SMTPD_MAXDOMAINPARTSIZE]; +}; + +#define PROC_QUEUE_API_VERSION 2 + +enum { + PROC_QUEUE_OK, + PROC_QUEUE_FAIL, + PROC_QUEUE_INIT, + PROC_QUEUE_CLOSE, + PROC_QUEUE_MESSAGE_CREATE, + PROC_QUEUE_MESSAGE_DELETE, + PROC_QUEUE_MESSAGE_COMMIT, + PROC_QUEUE_MESSAGE_FD_R, + PROC_QUEUE_ENVELOPE_CREATE, + PROC_QUEUE_ENVELOPE_DELETE, + PROC_QUEUE_ENVELOPE_LOAD, + PROC_QUEUE_ENVELOPE_UPDATE, + PROC_QUEUE_ENVELOPE_WALK, +}; + +#define PROC_SCHEDULER_API_VERSION 2 + +struct scheduler_info; + +enum { + PROC_SCHEDULER_OK, + PROC_SCHEDULER_FAIL, + PROC_SCHEDULER_INIT, + PROC_SCHEDULER_INSERT, + PROC_SCHEDULER_COMMIT, + PROC_SCHEDULER_ROLLBACK, + PROC_SCHEDULER_UPDATE, + PROC_SCHEDULER_DELETE, + PROC_SCHEDULER_HOLD, + PROC_SCHEDULER_RELEASE, + PROC_SCHEDULER_BATCH, + PROC_SCHEDULER_MESSAGES, + PROC_SCHEDULER_ENVELOPES, + PROC_SCHEDULER_SCHEDULE, + PROC_SCHEDULER_REMOVE, + PROC_SCHEDULER_SUSPEND, + PROC_SCHEDULER_RESUME, +}; + +enum envelope_flags { + EF_AUTHENTICATED = 0x01, + EF_BOUNCE = 0x02, + EF_INTERNAL = 0x04, /* Internal expansion forward */ + + /* runstate, not saved on disk */ + + EF_PENDING = 0x10, + EF_INFLIGHT = 0x20, + EF_SUSPEND = 0x40, + EF_HOLD = 0x80, +}; + +struct evpstate { + uint64_t evpid; + uint16_t flags; + uint16_t retry; + time_t time; +}; + +enum delivery_type { + D_MDA, + D_MTA, + D_BOUNCE, +}; + +struct scheduler_info { + uint64_t evpid; + enum delivery_type type; + uint16_t retry; + time_t creation; + time_t ttl; + time_t lasttry; + time_t lastbounce; + time_t nexttry; +}; + +#define SCHED_REMOVE 0x01 +#define SCHED_EXPIRE 0x02 +#define SCHED_UPDATE 0x04 +#define SCHED_BOUNCE 0x08 +#define SCHED_MDA 0x10 +#define SCHED_MTA 0x20 + +#define PROC_TABLE_API_VERSION 2 + +struct table_open_params { + uint32_t version; + char name[LINE_MAX]; +}; + +enum table_service { + K_NONE = 0x000, + K_ALIAS = 0x001, /* returns struct expand */ + K_DOMAIN = 0x002, /* returns struct destination */ + K_CREDENTIALS = 0x004, /* returns struct credentials */ + K_NETADDR = 0x008, /* returns struct netaddr */ + K_USERINFO = 0x010, /* returns struct userinfo */ + K_SOURCE = 0x020, /* returns struct source */ + K_MAILADDR = 0x040, /* returns struct mailaddr */ + K_ADDRNAME = 0x080, /* returns struct addrname */ + K_MAILADDRMAP = 0x100, /* returns struct maddrmap */ + K_RELAYHOST = 0x200, /* returns struct relayhost */ + K_STRING = 0x400, + K_REGEX = 0x800, +}; +#define K_ANY 0xfff + +enum { + PROC_TABLE_OK, + PROC_TABLE_FAIL, + PROC_TABLE_OPEN, + PROC_TABLE_CLOSE, + PROC_TABLE_UPDATE, + PROC_TABLE_CHECK, + PROC_TABLE_LOOKUP, + PROC_TABLE_FETCH, +}; + +enum enhanced_status_code { + /* 0.0 */ + ESC_OTHER_STATUS = 00, + + /* 1.x */ + ESC_OTHER_ADDRESS_STATUS = 10, + ESC_BAD_DESTINATION_MAILBOX_ADDRESS = 11, + ESC_BAD_DESTINATION_SYSTEM_ADDRESS = 12, + ESC_BAD_DESTINATION_MAILBOX_ADDRESS_SYNTAX = 13, + ESC_DESTINATION_MAILBOX_ADDRESS_AMBIGUOUS = 14, + ESC_DESTINATION_ADDRESS_VALID = 15, + ESC_DESTINATION_MAILBOX_HAS_MOVED = 16, + ESC_BAD_SENDER_MAILBOX_ADDRESS_SYNTAX = 17, + ESC_BAD_SENDER_SYSTEM_ADDRESS = 18, + + /* 2.x */ + ESC_OTHER_MAILBOX_STATUS = 20, + ESC_MAILBOX_DISABLED = 21, + ESC_MAILBOX_FULL = 22, + ESC_MESSAGE_LENGTH_TOO_LARGE = 23, + ESC_MAILING_LIST_EXPANSION_PROBLEM = 24, + + /* 3.x */ + ESC_OTHER_MAIL_SYSTEM_STATUS = 30, + ESC_MAIL_SYSTEM_FULL = 31, + ESC_SYSTEM_NOT_ACCEPTING_MESSAGES = 32, + ESC_SYSTEM_NOT_CAPABLE_OF_SELECTED_FEATURES = 33, + ESC_MESSAGE_TOO_BIG_FOR_SYSTEM = 34, + ESC_SYSTEM_INCORRECTLY_CONFIGURED = 35, + + /* 4.x */ + ESC_OTHER_NETWORK_ROUTING_STATUS = 40, + ESC_NO_ANSWER_FROM_HOST = 41, + ESC_BAD_CONNECTION = 42, + ESC_DIRECTORY_SERVER_FAILURE = 43, + ESC_UNABLE_TO_ROUTE = 44, + ESC_MAIL_SYSTEM_CONGESTION = 45, + ESC_ROUTING_LOOP_DETECTED = 46, + ESC_DELIVERY_TIME_EXPIRED = 47, + + /* 5.x */ + ESC_INVALID_RECIPIENT = 50, + ESC_INVALID_COMMAND = 51, + ESC_SYNTAX_ERROR = 52, + ESC_TOO_MANY_RECIPIENTS = 53, + ESC_INVALID_COMMAND_ARGUMENTS = 54, + ESC_WRONG_PROTOCOL_VERSION = 55, + + /* 6.x */ + ESC_OTHER_MEDIA_ERROR = 60, + ESC_MEDIA_NOT_SUPPORTED = 61, + ESC_CONVERSION_REQUIRED_AND_PROHIBITED = 62, + ESC_CONVERSION_REQUIRED_BUT_NOT_SUPPORTED = 63, + ESC_CONVERSION_WITH_LOSS_PERFORMED = 64, + ESC_CONVERSION_FAILED = 65, + + /* 7.x */ + ESC_OTHER_SECURITY_STATUS = 70, + ESC_DELIVERY_NOT_AUTHORIZED_MESSAGE_REFUSED = 71, + ESC_MAILING_LIST_EXPANSION_PROHIBITED = 72, + ESC_SECURITY_CONVERSION_REQUIRED_NOT_POSSIBLE = 73, + ESC_SECURITY_FEATURES_NOT_SUPPORTED = 74, + ESC_CRYPTOGRAPHIC_FAILURE = 75, + ESC_CRYPTOGRAPHIC_ALGORITHM_NOT_SUPPORTED = 76, + ESC_MESSAGE_INTEGRITY_FAILURE = 77, +}; + +enum enhanced_status_class { + ESC_STATUS_OK = 2, + ESC_STATUS_TEMPFAIL = 4, + ESC_STATUS_PERMFAIL = 5, +}; + +static inline uint32_t +evpid_to_msgid(uint64_t evpid) +{ + return (evpid >> 32); +} + +static inline uint64_t +msgid_to_evpid(uint32_t msgid) +{ + return ((uint64_t)msgid << 32); +} + + +/* esc.c */ +const char *esc_code(enum enhanced_status_class, enum enhanced_status_code); +const char *esc_description(enum enhanced_status_code); + + +/* queue */ +void queue_api_on_close(int(*)(void)); +void queue_api_on_message_create(int(*)(uint32_t *)); +void queue_api_on_message_commit(int(*)(uint32_t, const char*)); +void queue_api_on_message_delete(int(*)(uint32_t)); +void queue_api_on_message_fd_r(int(*)(uint32_t)); +void queue_api_on_envelope_create(int(*)(uint32_t, const char *, size_t, uint64_t *)); +void queue_api_on_envelope_delete(int(*)(uint64_t)); +void queue_api_on_envelope_update(int(*)(uint64_t, const char *, size_t)); +void queue_api_on_envelope_load(int(*)(uint64_t, char *, size_t)); +void queue_api_on_envelope_walk(int(*)(uint64_t *, char *, size_t)); +void queue_api_on_message_walk(int(*)(uint64_t *, char *, size_t, + uint32_t, int *, void **)); +void queue_api_no_chroot(void); +void queue_api_set_chroot(const char *); +void queue_api_set_user(const char *); +int queue_api_dispatch(void); + +/* scheduler */ +void scheduler_api_on_init(int(*)(void)); +void scheduler_api_on_insert(int(*)(struct scheduler_info *)); +void scheduler_api_on_commit(size_t(*)(uint32_t)); +void scheduler_api_on_rollback(size_t(*)(uint32_t)); +void scheduler_api_on_update(int(*)(struct scheduler_info *)); +void scheduler_api_on_delete(int(*)(uint64_t)); +void scheduler_api_on_hold(int(*)(uint64_t, uint64_t)); +void scheduler_api_on_release(int(*)(int, uint64_t, int)); +void scheduler_api_on_batch(int(*)(int, int *, size_t *, uint64_t *, int *)); +void scheduler_api_on_messages(size_t(*)(uint32_t, uint32_t *, size_t)); +void scheduler_api_on_envelopes(size_t(*)(uint64_t, struct evpstate *, size_t)); +void scheduler_api_on_schedule(int(*)(uint64_t)); +void scheduler_api_on_remove(int(*)(uint64_t)); +void scheduler_api_on_suspend(int(*)(uint64_t)); +void scheduler_api_on_resume(int(*)(uint64_t)); +void scheduler_api_no_chroot(void); +void scheduler_api_set_chroot(const char *); +void scheduler_api_set_user(const char *); +int scheduler_api_dispatch(void); + +/* table */ +void table_api_on_update(int(*)(void)); +void table_api_on_check(int(*)(int, struct dict *, const char *)); +void table_api_on_lookup(int(*)(int, struct dict *, const char *, char *, size_t)); +void table_api_on_fetch(int(*)(int, struct dict *, char *, size_t)); +int table_api_dispatch(void); +const char *table_api_get_name(void); + +#endif diff --git a/smtpd/smtpd-defines.h b/smtpd/smtpd-defines.h new file mode 100644 index 00000000..f22a546f --- /dev/null +++ b/smtpd/smtpd-defines.h @@ -0,0 +1,68 @@ +/* $OpenBSD: smtpd-defines.h,v 1.12 2020/02/24 16:16:08 millert Exp $ */ + +/* + * Copyright (c) 2013 Gilles Chehade <gilles@poolp.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. + */ + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +#define SMTPD_TABLENAME_SIZE (64 + 1) +#define SMTPD_TAG_SIZE (32 + 1) + +/* buffer sizes for email address components */ +#define SMTPD_MAXLOCALPARTSIZE (255 + 1) +#define SMTPD_MAXDOMAINPARTSIZE (255 + 1) +#define SMTPD_MAXMAILADDRSIZE (255 + 1) + +/* buffer size for virtual username (can be email addresses) */ +#define SMTPD_VUSERNAME_SIZE (255 + 1) +#define SMTPD_SUBADDRESS_SIZE (255 + 1) + +#ifndef SMTPD_USER +#define SMTPD_USER "_smtpd" +#endif +#ifndef PATH_CHROOT +#define PATH_CHROOT "/var/empty" +#endif +#ifndef SMTPD_QUEUE_USER +#define SMTPD_QUEUE_USER "_smtpq" +#endif +#ifndef SMTPD_QUEUE_GROUP +#define SMTPD_QUEUE_GROUP "_smtpq" +#endif +#ifndef PATH_SPOOL +#define PATH_SPOOL "/var/spool/smtpd" +#endif + +#ifndef PATH_MAILLOCAL +#define PATH_MAILLOCAL PATH_LIBEXEC "/mail.local" +#endif + +#ifndef PATH_MAKEMAP +#define PATH_MAKEMAP "/usr/sbin/makemap" +#endif + +#define SUBADDRESSING_DELIMITER "+" + + +/* sendmail compat */ + +#define EX_OK 0 +#define EX_NOHOST 68 +#define EX_UNAVAILABLE 69 +#define EX_SOFTWARE 70 +#define EX_TEMPFAIL 75 diff --git a/smtpd/smtpd-filters.7 b/smtpd/smtpd-filters.7 new file mode 100644 index 00000000..5af7008e --- /dev/null +++ b/smtpd/smtpd-filters.7 @@ -0,0 +1,653 @@ +.\" $OpenBSD: smtpd-filters.7,v 1.6 2020/04/25 09:44:02 eric Exp $ +.\" +.\" Copyright (c) 2008 Janne Johansson <jj@openbsd.org> +.\" Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> +.\" Copyright (c) 2012 Gilles Chehade <gilles@poolp.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: April 25 2020 $ +.Dt FILTERS 7 +.Os +.Sh NAME +.Nm filters +.Nd filtering API for the +.Xr smtpd 8 +daemon +.Sh DESCRIPTION +The +.Xr smtpd 8 +daemon provides a Simple Mail Transfer Protocol (SMTP) implementation +that allows an ordinary machine to become Mail eXchangers (MX). +Many features that are commonly used by MX, +such as delivery reporting or Spam filtering, +are outside the scope of SMTP and too complex to fit in +.Xr smtpd 8 . +.Pp +Because an MX needs to provide these features, +.Xr smtpd 8 +provides an API to extend its behavior through pluggable +.Nm . +.Pp +At runtime, +.Xr smtpd 8 +can report events to +.Nm +and query what it should answer to these events. +This allows the decision logic to rely on third-party programs. +.Sh DESIGN +The +.Nm +are programs that run as unique standalone processes, +they do not share +.Xr smtpd 8 +memory space. +They are executed by +.Xr smtpd 8 +at startup and expected to run in an infinite loop, +reading events and filtering requests from +.Xr stdin 4 , +writing responses to +.Xr stdout 4 +and logging to +.Xr stderr 4 . +They are not allowed to terminate. +.Pp +Because +.Nm +are standalone programs that communicate with +.Xr smtpd 8 +through +.Xr fd 4 , +they may run as different users than +.Xr smtpd 8 +and may be written in any language. +The +.Nm +must not use blocking I/O, +they must support answering asynchronously to +.Xr smtpd 8 . +.Sh REPORT AND FILTER +The API relies on two streams, +report and filter. +.Pp +The report stream is a one-way stream which allows +.Xr smtpd 8 +to inform +.Nm +in real-time about events that are occurring in the daemon. +The report events do not expect an answer from +.Nm , +it is just meant to provide them with informations. +A filter should be able to replicate the +.Xr smtpd 8 +state for a session by gathering informations coming from report events. +No decision is ever taken by the report stream. +.Pp +The filter stream is a two-way stream which allows +.Xr smtpd 8 +to query +.Nm +about what it should do with a session at a given phase. +The filter requests expects an answer from +.Nm , +.Xr smtpd 8 +will not let the session move forward until then. +A decision must always be taken by the filter stream. +.Pp +It is sometimes possible to rely on filter requests to gather information, +but because a reponse is expected by +.Xr smtpd 8 , +this is more costly than using report events. +The correct pattern for writing filters is to use the report events to +create a local state for a session, +then use filter requests to take decisions based on this state. +The only case when using filter request instead of report events is correct, +is when a decision is required for the filter request and there is no need for +more information than that of the event. +.Sh PROTOCOL +The protocol is straightforward, +it consists of a human-readable line exchanges between +.Nm +and +.Xr smtpd 8 +through +.Xr fd 4 . +.Pp +The protocol begins with a handshake. +First, +.Xr smtpd 8 +provides +.Nm +with general configuration information in the form of key-value lines: +.Bd -literal -offset indent +config|smtpd-version|6.6.1 +config|smtp-session-timeout|300 +config|subsystem|smtp-in +config|ready +.Ed +.Pp +Then, +.Nm +register the stream, +subsystem and event they want to handle: +.Bd -literal -offset indent +register|report|smtp-in|link-connect +register|ready +.Ed +.Pp +Finally, +.Xr smtpd 8 +will emit report events and filter requests, +expecting +.Nm +to react accordingly either by responding or not depending on the stream: +.Bd -literal -offset indent +report|0.5|1576146008.006099|smtp-in|link-connect|7641df9771b4ed00|mail.openbsd.org|pass|199.185.178.25:33174|45.77.67.80:25 +report|0.5|1576147242.200225|smtp-in|link-connect|7641dfb3798eb5bf|mail.openbsd.org|pass|199.185.178.25:31205|45.77.67.80:25 +report|0.5|1576148447.982572|smtp-in|link-connect|7641dfc063102cbd|mail.openbsd.org|pass|199.185.178.25:24786|45.77.67.80:25 +.Ed +.Pp +The char +.Dq | +may only appear in the last field of a payload, +in which case it should be considered a regular char and not a separator. +Other fields have strict formatting excluding the possibility of having a +.Dq | . +.Pp +The list of subsystems and events, +as well as the format of requests and reponses, +will be documented in the sections below. +.Sh CONFIGURATION +During the initial handshake, +.Xr smtpd 8 +will emit a serie of configuration keys and values. +The list is meant to be ignored by +.Nm +that do not require it and consumed gracefully by filters that do. +.Pp +There are currently three keys: +.Bd -literal -offset indent +config|smtpd-version|6.6.1 +config|smtp-session-timeout|300 +config|subsystem|smtp-in +.Ed +.Pp +When +.Xr smtpd 8 +has sent all configuration keys it emits the following line: +.Bd -literal -offset indent +config|ready +.Ed +.Sh REPORT EVENTS +There is currently only one subsystem supported in the API: +smtp-in. +.Pp +Each report event is generated by +.Xr smtpd 8 +as a single line similar to the one below: +.Bd -literal -offset indent +report|0.5|1576146008.006099|smtp-in|link-connect|7641df9771b4ed00|mail.openbsd.org|pass|199.185.178.25:33174|45.77.67.80:25 +.Ed +.Pp +The format consists of a protocol prefix containing the stream, +the protocol version, +the timestamp, +the subsystem, +the event and the unique session identifier separated by +.Dq | : +.Bd -literal -offset indent +report|0.5|1576146008.006099|smtp-in|link-connect|7641df9771b4ed00 +.Ed +.Pp +It is followed by a suffix containing the event-specific parameters, +also separated by +.Dq | : +.Bd -literal -offset indent +mail.openbsd.org|pass|199.185.178.25:33174|45.77.67.80:25 +.Ed +.Pp +The list of events and event-specific parameters are provided here for smtp-in: +.Bl -tag -width Ds +.It Ic link-connect : Ar rdns fcrdns src dest +This event is generated upon connection. +.Pp +.Ar rdns +contains the reverse DNS hostname for the remote end or an empty string if none. +.Pp +.Ar fcrdns +contains the string +.Dq pass +or +.Dq fail +depending on if the remote end validates FCrDNS. +.Pp +.Ar src +holds either the IP address and port from source address, +in the format +.Dq address:port +or the path to a UNIX socket in the format +.Dq unix:/path . +.Pp +.Ar dest +holds either the IP address and port from destination address, +in the format +.Dq address:port +or the path to a UNIX socket in the format +.Dq unix:/path . +.It Ic link-greeting : Ar hostname +This event is generated upon display of the server banner. +.Pp +.Ar hostname +contains the hostname displayed in the banner. +.It Ic link-identify : Ar method identity +This event is generated upon +.Dq HELO +or +.Dq EHLO +command from the client. +.Pp +.Ar method +contains the string +.Dq HELO +or +.Dq EHLO +indicating the method used by the client. +.Pp +.Ar identity +contains the identity provided by the client. +.It Ic link-tls : Ar tls-string +This event is generated upon successful negotiation of TLS. +.Pp +.Ar tls-string +contains a colon-separated list of TLS properties including the TLS version, +the cipher suite used by the session and the cipher strenght in bits. +.It Ic link-disconnect +This event is generated upon disconnection of the client. +.It Ic link-auth : Ar username result +This event is generated upon authentication attempt of the client. +.Pp +.Ar username +contains the username used for the authentication attempt. +.Pp +.Ar result +contains the string +.Dq pass , +.Dq fail +or +.Dq error +depending on the result of the authentication attempt. +.It Ic tx-reset : Op message-id +This event is generated when a transaction is reset. +.Pp +If reset happend while in a transaction, +.Ar message-id +contains the identifier of the transaction being reset. +.It Ic tx-begin : Ar message-id +This event is generated when a transaction is initiated. +.Pp +.Ar message-id +contains the identifier for the transaction. +.It Ic tx-mail : Ar message-id Ar result address +This event is generated when client emits +.Dq MAIL FROM . +.Pp +.Ar message-id +contains the identifier for the transaction. +.Pp +.Ar result +contains +.Dq ok +if the sender was accepted, +.Dq permfail +if it was rejected +or +.Dq tempfail +if it was rejected for a transient error. +.Pp +.Ar address +contains the e-mail address of the sender. +The address is normalized and sanitized, +the protocol +.Dq < +and +.Dq > +are removed and so are parameters to +.Dq MAIL FROM . +.It Ic tx-rcpt : Ar message-id Ar result address +This event is generated when client emits +.Dq RCPT TO . +.Pp +.Ar message-id +contains the identifier for the transaction. +.Pp +.Ar result +contains +.Dq ok +if the recipient was accepted, +.Dq permfail +if it was rejected +or +.Dq tempfail +if it was rejected for a transient error. +.Pp +.Ar address +contains the e-mail address of the recipient. +The address is normalized and sanitized, +the protocol +.Dq < +and +.Dq > +are removed and so are parameters to +.Dq RCPT TO . +.It Ic tx-envelope : Ar message-id Ar envelope-id +This event is generated when an envelope is accepted. +.Pp +.Ar envelope-id +contains the unique identifier for the envelope. +.It Ic tx-data : Ar message-id Ar result +This event is generated when client has emitted +.Dq DATA . +.Pp +.Ar message-id +contains the unique identifier for the transaction. +.Pp +.Ar result +contains +.Dq ok +if server accepted to process the message, +.Dq permfail +if it has not accepted and +.Dq tempfail +if a transient error is preventing the processing of message. +.It Ic tx-commit : Ar message-id Ar message-size +This event is generated when a transaction has been accepted by the server. +.Pp +.Ar message-id +contains the unique identifier for the SMTP transaction. +.Pp +.Ar message-size +contains the size of the message submitted in the +.Dq DATA +phase of the SMTP transaction. +.It Ic tx-rollback : Ar message-id +This event is generated when a transaction has been rejected by the server. +.Pp +.Ar message-id +contains the unique identifier for the SMTP transaction. +.It Ic protocol-client : Ar command +This event is generated for every command submitted by the client. +It contains the raw command as received by the server. +.Pp +.Ar command +contains the command emitted by the client to the server. +.It Ic protocol-server : Ar response +This event is generated for every response emitted by the server. +It contains the raw response as emitted by the server. +.Pp +.Ar response +contains the response emitted by the server to the client. +.It Ic filter-report : Ar filter-kind Ar name message +This event is generated when a filter emits a report. +.Pp +.Ar filter-kind may be either +.Dq builtin +or +.Dq proc +depending on if the filter is an +.Xr smtpd 8 +builtin filter or a proc filter implementing the API. +.Pp +.Ar name +is the name of the filter that generated the report. +.Pp +.Ar message +is a filter-specific message. +.It Ic filter-response : Ar phase response Op param +This event is generated when a filter responds to a filtering request. +.Pp +.Ar phase +contains the phase name for the request. +The phases are documented in the next section. +.Pp +.Ar response +contains the response of the filter to the request, +it is either one of +.Dq proceed , +.Dq report , +.Dq reject , +.Dq disconnect , +.Dq junk or +.Dq rewrite . +.Pp +If specified, +.Ar param +is the parameter to the response. +.It Ic timeout +This event is generated when a timeout happens for a session. +.El +.Sh FILTER REQUESTS +There is currently only one subsystem supported in the API: +smtp-in. +.Pp +The filter requests allow +.Xr smtpd 8 +to query +.Nm +about what to do with a session at a particular phase. +In addition, +they allow +.Nm +to alter the content of a message by adding, +modifying, +or suppressing lines of input in a way that is similar to what program like +.Xr sed 1 +or +.Xr grep 1 +would do. +.Pp +Each filter request is generated by +.Xr smtpd 8 +as a single line similar to the one below: +.Bd -literal -offset indent +filter|0.5|1576146008.006099|smtp-in|connect|7641df9771b4ed00|1ef1c203cc576e5d|mail.openbsd.org|pass|199.185.178.25:33174|45.77.67.80:25 +.Ed +.Pp +The format consists of a protocol prefix containing the stream, +the protocol version, +the timestamp, +the subsystem, +the filtering phase, +the unique session identifier and an opaque token separated by +.Dq | +that the filter should provide in its response: +.Bd -literal -offset indent +filter|0.5|1576146008.006099|smtp-in|connect|7641df9771b4ed00|1ef1c203cc576e5d +.Ed +.Pp +It is followed by a suffix containing the phase-specific parameters to the +filter request, +also separated by +.Dq | : +.Bd -literal -offset indent +mail.openbsd.org|pass|199.185.178.25:33174|45.77.67.80:25 +.Ed +.Pp +Unlike with report events, +.Xr smtpd 8 +expects answers from filter requests and will not allow a session to move +forward before the filter has instructed +.Xr smtpd 8 +what to do with it. +.Pp +For all phases, +excepted +.Dq data-line , +the responses must follow the same construct, +a message type +.Dq filter-result , +followed by the unique session id, +the opaque token, +a decision and optional decision-specific parameters: +.Bd -literal -offset indent +filter-result|7641df9771b4ed00|1ef1c203cc576e5d|proceed +filter-result|7641df9771b4ed00|1ef1c203cc576e5d|reject|550 nope +.Ed +.Pp +The possible decisions to a +.Dq filter-result +message will be described below. +.Pp +For the +.Dq data-line +phase, +.Nm +are fed with a stream of lines corresponding to the message to filter, +and terminated by a single dot: +.Bd -literal -offset indent +filter|0.5|1576146008.006099|smtp-in|data-line|7641df9771b4ed00|1ef1c203cc576e5d|line 1 +filter|0.5|1576146008.006103|smtp-in|data-line|7641df9771b4ed00|1ef1c203cc576e5d|line 2 +filter|0.5|1576146008.006105|smtp-in|data-line|7641df9771b4ed00|1ef1c203cc576e5d|. +.Ed +.Pp +They are expected to produce an output stream similarly terminate by a single +dot. +A filter may inject, +suppress, +modify or echo back the lines it receives. +Ultimately, +.Xr smtpd 8 +will assume that the message consists of the output from +.Nm . +.Pp +Note that filters may be chained and the lines that are input into a filter +are the lines that are output from previous filter. +.Pp +The response to +.Dq data-line +requests use their own construct. +A +.Dq filter-dataline +prefix, +followed by the unique session identifier, +the opaque token and the output line as follows: +.Bd -literal -offset indent +filter-dataline|7641df9771b4ed00|1ef1c203cc576e5d|line 1 +filter-dataline|7641df9771b4ed00|1ef1c203cc576e5d|line 2 +filter-dataline|7641df9771b4ed00|1ef1c203cc576e5d|. +.Ed +.Pp +The list of events and event-specific parameters are provided here for smtp-in: +.Bl -tag -width Ds +.It Ic connect : Ar rdns fcrdns src dest +This request is emitted after connection, +before the banner is displayed. +.It Ic helo : Ar identity +This request is emitted after the client has emitted +.Dq HELO . +.It Ic ehlo : Ar identity +This request is emitted after the client has emitted +.Dq EHLO . +.It Ic starttls : Ar tls-string +This request is emitted after the client has requested +.Dq STARTTLS . +.It Ic auth : Ar auth +This request is emitted after the client has requested +.Dq AUTH . +.It Ic mail-from : Ar address +This request is emitted after the client has requested +.Dq MAIL FROM . +.It Ic rcpt-to : Ar address +This request is emitted after the client has requested +.Dq RCPT TO . +.It Ic data +This request is emitted after the client has requested +.Dq DATA . +.It Ic data-line : Ar line +This request is emitted for each line of input in the +.Dq DATA +phase. +The lines are raw dot-escaped SMTP DATA input, +terminated with a single dot. +.It Ic commit +This request is emitted after the final single dot is received. +.El +.Pp +For every filtering phase, +excepted +.Dq data-line , +the following decisions may be taken by a filter: +.Bl -tag -width Ds +.It Ic proceed +No action is taken, +session or transaction may be passed to the next filter. +.It Ic junk +The session or transaction is marked as Spam. +.Xr smtpd 8 +will prepend a +.Dq X-Spam +header to the message. +.It Ic reject Ar error +The command is rejected with the message +.Ar error . +The message must be a valid SMTP message including status code, +5xx or 4xx. +.Pp +Messages starting with a 5xx status result in a permanent failure, +those starting with a 4xx status result in a temporary failure. +.Pp +Messages starting with a 421 status will result in a client disconnect. +.It Ic disconnect Ar error +The client is disconnected with the message +.Ar error . +The message must be a valid SMTP message including status code, +5xx or 4xx. +.Pp +Messages starting with a 5xx status result in a permanent failure, +those starting with a 4xx status result in a temporary failure. +.It Ic rewrite Ar parameter +The command parameter is rewritten. +.Pp +This decision allows a filter to perform a rewrite of client-submitted +commands before they are processed by the SMTP engine. +.Ar parameter +is expected to be a valid SMTP parameter for the command. +.It Ic report Ar parameter +Generates a report with +.Ar parameter +for this filter. +.El +.\".Sh EXAMPLES +.\"This example filter written in +.\".Xr sh 1 +.\"will echo back... +.\".Bd -literal -offset indent +.\"XXX +.\".Ed +.\".Pp +.\"This example filter will filter... +.\".Bd -literal -offset indent +.\"XXX +.\".Ed +.\".Pp +.\"Note that libraries may provide a simpler interface to +.\".Nm +.\"that does not require implementing the protocol itself. +.\".Ed +.Sh SEE ALSO +.Xr smtpd 8 +.Sh HISTORY +.Nm +first appeared in +.Ox 6.6 . diff --git a/smtpd/smtpd.8 b/smtpd/smtpd.8 new file mode 100644 index 00000000..e3429f07 --- /dev/null +++ b/smtpd/smtpd.8 @@ -0,0 +1,167 @@ +.\" $OpenBSD: smtpd.8,v 1.32 2017/01/03 22:11:39 jmc Exp $ +.\" +.\" Copyright (c) 2012, Eric Faurot <eric@openbsd.org> +.\" Copyright (c) 2008, Gilles Chehade <gilles@poolp.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: January 3 2017 $ +.Dt SMTPD 8 +.Os +.Sh NAME +.Nm smtpd +.Nd Simple Mail Transfer Protocol daemon +.Sh SYNOPSIS +.Nm +.Op Fl dFhnv +.Op Fl D Ar macro Ns = Ns Ar value +.Op Fl f Ar file +.Op Fl P Ar system +.Op Fl T Ar trace +.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 SMTP +transactions; it can also be fed messages through the standard +.Xr sendmail 8 +interface. +It 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 RFC 5321 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 +Do not daemonize. +If this option is specified, +.Nm +will run in the foreground and log to +.Xr syslogd 8 . +.It Fl f Ar file +Specify an alternative configuration file. +.It Fl h +Display version and usage. +.It Fl n +Configtest mode. +Only check the configuration file for validity. +.It Fl P Ar system +Pause a specific subsystem at startup. +Normal operation can be resumed using +.Xr smtpctl 8 . +This option can be used multiple times. +The accepted values are: +.Pp +.Bl -tag -width "smtpXXX" -compact +.It mda +Do not schedule local deliveries. +.It mta +Do not schedule remote transfers. +.It smtp +Do not listen on SMTP sockets. +.El +.It Fl T Ar trace +Enables real-time tracing at startup. +Normal operation can be resumed using +.Xr smtpctl 8 . +This option can be used multiple times. +The accepted values are: +.Pp +.Bl -bullet -compact +.It +imsg +.It +io +.It +smtp (incoming sessions) +.It +filters +.It +transfer (outgoing sessions) +.It +bounce +.It +scheduler +.It +expand (aliases/virtual/forward expansion) +.It +lookup (user/credentials lookups) +.It +stat +.It +rules (matched by incoming sessions) +.It +mproc +.It +all +.El +.It Fl v +Produce more verbose output. +.El +.Sh FILES +.Bl -tag -width "/etc/mail/smtpd.confXXX" -compact +.It Pa /etc/mail/mailname +Alternate server name to use. +.It Pa /etc/mail/smtpd.conf +Default +.Nm +configuration file. +.It Pa /var/run/smtpd.sock +.Ux Ns -domain +socket used for communication with +.Xr smtpctl 8 . +.It Pa /var/spool/smtpd/ +Spool directories for mail during processing. +.It Pa ~/.forward +User email forwarding information. +.El +.Sh SEE ALSO +.Xr forward 5 , +.Xr smtpd.conf 5 , +.Xr mailwrapper 8 , +.Xr smtpctl 8 +.Sh STANDARDS +.Rs +.%A J. Klensin +.%D October 2008 +.%R RFC 5321 +.%T Simple Mail Transfer Protocol +.Re +.Sh HISTORY +The +.Nm +program first appeared in +.Ox 4.6 . diff --git a/smtpd/smtpd.c b/smtpd/smtpd.c new file mode 100644 index 00000000..f18b1446 --- /dev/null +++ b/smtpd/smtpd.c @@ -0,0 +1,2328 @@ +/* $OpenBSD: smtpd.c,v 1.333 2020/05/06 16:03:30 millert Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#include <sys/file.h> /* Needed for flock */ +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <sys/stat.h> +#include <sys/uio.h> +#include <sys/mman.h> + +#ifdef BSD_AUTH +#include <bsd_auth.h> +#endif + +#ifdef USE_PAM +#if defined(HAVE_SECURITY_PAM_APPL_H) +#include <security/pam_appl.h> +#elif defined (HAVE_PAM_PAM_APPL_H) +#include <pam/pam_appl.h> +#endif +#endif + +#ifdef HAVE_CRYPT_H +#include <crypt.h> /* needed for crypt() */ +#endif +#include <dirent.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <grp.h> /* needed for setgroups */ +#include <fts.h> +#include <grp.h> +#include <imsg.h> +#include <inttypes.h> +#include <libgen.h> +#ifdef HAVE_LOGIN_CAP_H +#include <login_cap.h> +#endif +#ifdef HAVE_PATHS_H +#include <paths.h> +#endif +#include <poll.h> +#include <pwd.h> +#include <signal.h> +#ifdef HAVE_SHADOW_H +#include <shadow.h> /* needed for getspnam() */ +#endif +#include <stdio.h> +#include <syslog.h> +#include <limits.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <time.h> +#include <unistd.h> +#ifdef HAVE_UTIL_H +#include <util.h> +#endif + +#include <openssl/ssl.h> +#include <openssl/evp.h> + +#include "smtpd.h" +#include "log.h" +#include "ssl.h" + +extern char *__progname; + +#define SMTPD_MAXARG 32 + +static void parent_imsg(struct mproc *, struct imsg *); +static void usage(void); +static int smtpd(void); +static void parent_shutdown(void); +static void parent_send_config(int, short, void *); +static void parent_send_config_lka(void); +static void parent_send_config_pony(void); +static void parent_send_config_ca(void); +static void parent_sig_handler(int, short, void *); +static void forkmda(struct mproc *, uint64_t, struct deliver *); +static int parent_forward_open(char *, char *, uid_t, gid_t); +static struct child *child_add(pid_t, int, const char *); +static struct mproc *start_child(int, char **, char *); +static struct mproc *setup_peer(enum smtp_proc_type, pid_t, int); +static void setup_peers(struct mproc *, struct mproc *); +static void setup_done(struct mproc *); +static void setup_proc(void); +static struct mproc *setup_peer(enum smtp_proc_type, pid_t, int); +static int imsg_wait(struct imsgbuf *, struct imsg *, int); + +static void offline_scan(int, short, void *); +static int offline_add(char *, uid_t, gid_t); +static void offline_done(void); +static int offline_enqueue(char *, uid_t, gid_t); + +static void purge_task(void); +static int parent_auth_user(const char *, const char *); +static void load_pki_tree(void); +static void load_pki_keys(void); + +static void fork_filter_processes(void); +static void fork_filter_process(const char *, const char *, const char *, const char *, const char *, uint32_t); + +enum child_type { + CHILD_DAEMON, + CHILD_MDA, + CHILD_PROCESSOR, + CHILD_ENQUEUE_OFFLINE, +}; + +struct child { + pid_t pid; + enum child_type type; + const char *title; + int mda_out; + uint64_t mda_id; + char *path; + char *cause; +}; + +struct offline { + TAILQ_ENTRY(offline) entry; + uid_t uid; + gid_t gid; + char *path; +}; + +#define OFFLINE_READMAX 20 +#define OFFLINE_QUEUEMAX 5 +static size_t offline_running = 0; +TAILQ_HEAD(, offline) offline_q; + +static struct event config_ev; +static struct event offline_ev; +static struct timeval offline_timeout; + +static pid_t purge_pid = -1; + +extern char **environ; +void (*imsg_callback)(struct mproc *, struct imsg *); + +enum smtp_proc_type smtpd_process; + +struct smtpd *env = NULL; + +struct mproc *p_control = NULL; +struct mproc *p_lka = NULL; +struct mproc *p_parent = NULL; +struct mproc *p_queue = NULL; +struct mproc *p_scheduler = NULL; +struct mproc *p_pony = NULL; +struct mproc *p_ca = NULL; + +const char *backend_queue = "fs"; +const char *backend_scheduler = "ramqueue"; +const char *backend_stat = "ram"; + +int profiling = 0; +int debug = 0; +int foreground = 0; +int control_socket = -1; + +struct tree children; + +/* Saved arguments to main(). */ +char **saved_argv; +int saved_argc; + +static void +parent_imsg(struct mproc *p, struct imsg *imsg) +{ + struct forward_req *fwreq; + struct filter_proc *processor; + struct deliver deliver; + struct child *c; + struct msg m; + const void *data; + const char *username, *password, *cause, *procname; + uint64_t reqid; + size_t sz; + void *i; + int fd, n, v, ret; + + if (imsg == NULL) + fatalx("process %s socket closed", p->name); + + switch (imsg->hdr.type) { + case IMSG_LKA_OPEN_FORWARD: + CHECK_IMSG_DATA_SIZE(imsg, sizeof *fwreq); + fwreq = imsg->data; + fd = parent_forward_open(fwreq->user, fwreq->directory, + fwreq->uid, fwreq->gid); + fwreq->status = 0; + if (fd == -1 && errno != ENOENT) { + if (errno == EAGAIN) + fwreq->status = -1; + } + else + fwreq->status = 1; + m_compose(p, IMSG_LKA_OPEN_FORWARD, 0, 0, fd, + fwreq, sizeof *fwreq); + return; + + case IMSG_LKA_AUTHENTICATE: + /* + * If we reached here, it means we want root to lookup + * system user. + */ + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &username); + m_get_string(&m, &password); + m_end(&m); + + ret = parent_auth_user(username, password); + + m_create(p, IMSG_LKA_AUTHENTICATE, 0, 0, -1); + m_add_id(p, reqid); + m_add_int(p, ret); + m_close(p); + return; + + case IMSG_MDA_FORK: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_data(&m, &data, &sz); + m_end(&m); + if (sz != sizeof(deliver)) + fatalx("expected deliver"); + memmove(&deliver, data, sz); + forkmda(p, reqid, &deliver); + return; + + case IMSG_MDA_KILL: + m_msg(&m, imsg); + m_get_id(&m, &reqid); + m_get_string(&m, &cause); + m_end(&m); + + i = NULL; + while ((n = tree_iter(&children, &i, NULL, (void**)&c))) + if (c->type == CHILD_MDA && + c->mda_id == reqid && + c->cause == NULL) + break; + if (!n) { + log_debug("debug: smtpd: " + "kill request: proc not found"); + return; + } + + c->cause = xstrdup(cause); + log_debug("debug: smtpd: kill requested for %u: %s", + c->pid, c->cause); + kill(c->pid, SIGTERM); + return; + + case IMSG_CTL_VERBOSE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + log_trace_verbose(v); + return; + + case IMSG_CTL_PROFILE: + m_msg(&m, imsg); + m_get_int(&m, &v); + m_end(&m); + profiling = v; + return; + + case IMSG_LKA_PROCESSOR_ERRFD: + m_msg(&m, imsg); + m_get_string(&m, &procname); + m_end(&m); + + processor = dict_xget(env->sc_filter_processes_dict, procname); + m_create(p_lka, IMSG_LKA_PROCESSOR_ERRFD, 0, 0, processor->errfd); + m_add_string(p_lka, procname); + m_close(p_lka); + return; + } + + errx(1, "parent_imsg: unexpected %s imsg from %s", + imsg_to_str(imsg->hdr.type), proc_title(p->proc)); +} + +static void +usage(void) +{ + extern char *__progname; + + fprintf(stderr, "usage: %s [-dFhnv] [-D macro=value] " + "[-f file] [-P system] [-T trace]\n", __progname); + exit(1); +} + +static void +parent_shutdown(void) +{ + pid_t pid; + + mproc_clear(p_ca); + mproc_clear(p_pony); + mproc_clear(p_control); + mproc_clear(p_lka); + mproc_clear(p_scheduler); + mproc_clear(p_queue); + + do { + pid = waitpid(WAIT_MYPGRP, NULL, 0); + } while (pid != -1 || (pid == -1 && errno == EINTR)); + + unlink(SMTPD_SOCKET); + + log_info("Exiting"); + exit(0); +} + +static void +parent_send_config(int fd, short event, void *p) +{ + parent_send_config_lka(); + parent_send_config_pony(); + parent_send_config_ca(); + purge_config(PURGE_PKI); +} + +static void +parent_send_config_pony(void) +{ + log_debug("debug: parent_send_config: configuring pony process"); + m_compose(p_pony, IMSG_CONF_START, 0, 0, -1, NULL, 0); + m_compose(p_pony, IMSG_CONF_END, 0, 0, -1, NULL, 0); +} + +void +parent_send_config_lka() +{ + log_debug("debug: parent_send_config_ruleset: reloading"); + m_compose(p_lka, IMSG_CONF_START, 0, 0, -1, NULL, 0); + m_compose(p_lka, IMSG_CONF_END, 0, 0, -1, NULL, 0); +} + +static void +parent_send_config_ca(void) +{ + log_debug("debug: parent_send_config: configuring ca process"); + m_compose(p_ca, IMSG_CONF_START, 0, 0, -1, NULL, 0); + m_compose(p_ca, IMSG_CONF_END, 0, 0, -1, NULL, 0); +} + +static void +parent_sig_handler(int sig, short event, void *p) +{ + struct child *child; + int status, fail; + pid_t pid; + char *cause; + + switch (sig) { + case SIGTERM: + case SIGINT: + log_debug("debug: got signal %d", sig); + parent_shutdown(); + /* NOT REACHED */ + + case SIGCHLD: + do { + int len; + enum mda_resp_status mda_status; + int mda_sysexit; + + pid = waitpid(-1, &status, WNOHANG); + if (pid <= 0) + continue; + + fail = 0; + if (WIFSIGNALED(status)) { + fail = 1; + len = asprintf(&cause, "terminated; signal %d", + WTERMSIG(status)); + mda_status = MDA_TEMPFAIL; + mda_sysexit = 0; + } else if (WIFEXITED(status)) { + if (WEXITSTATUS(status) != 0) { + fail = 1; + len = asprintf(&cause, + "exited abnormally"); + mda_sysexit = WEXITSTATUS(status); + if (mda_sysexit == EX_OSERR || + mda_sysexit == EX_TEMPFAIL) + mda_status = MDA_TEMPFAIL; + else + mda_status = MDA_PERMFAIL; + } else { + len = asprintf(&cause, "exited okay"); + mda_status = MDA_OK; + mda_sysexit = 0; + } + } else + /* WIFSTOPPED or WIFCONTINUED */ + continue; + + if (len == -1) + fatal("asprintf"); + + if (pid == purge_pid) + purge_pid = -1; + + child = tree_pop(&children, pid); + if (child == NULL) + goto skip; + + switch (child->type) { + case CHILD_PROCESSOR: + if (fail) { + log_warnx("warn: lost processor: %s %s", + child->title, cause); + parent_shutdown(); + } + break; + + case CHILD_DAEMON: + if (fail) + log_warnx("warn: lost child: %s %s", + child->title, cause); + break; + + case CHILD_MDA: + if (WIFSIGNALED(status) && + WTERMSIG(status) == SIGALRM) { + char *tmp; + if (asprintf(&tmp, + "terminated; timeout") != -1) { + free(cause); + cause = tmp; + } + } + else if (child->cause && + WIFSIGNALED(status) && + WTERMSIG(status) == SIGTERM) { + free(cause); + cause = child->cause; + child->cause = NULL; + } + free(child->cause); + log_debug("debug: smtpd: mda process done " + "for session %016"PRIx64 ": %s", + child->mda_id, cause); + + m_create(p_pony, IMSG_MDA_DONE, 0, 0, + child->mda_out); + m_add_id(p_pony, child->mda_id); + m_add_int(p_pony, mda_status); + m_add_int(p_pony, mda_sysexit); + m_add_string(p_pony, cause); + m_close(p_pony); + + break; + + case CHILD_ENQUEUE_OFFLINE: + if (fail) + log_warnx("warn: smtpd: " + "couldn't enqueue offline " + "message %s; smtpctl %s", + child->path, cause); + else + unlink(child->path); + free(child->path); + offline_done(); + break; + + default: + fatalx("smtpd: unexpected child type"); + } + free(child); + skip: + free(cause); + } while (pid > 0 || (pid == -1 && errno == EINTR)); + + break; + default: + fatalx("smtpd: unexpected signal"); + } +} + +int +main(int argc, char *argv[]) +{ + int c, i; + int opts, flags; + const char *conffile = CONF_FILE; + int save_argc = argc; + char **save_argv = argv; + char *rexec = NULL; + struct smtpd *conf; + +#ifndef HAVE___PROGNAME + __progname = ssh_get_progname(argv[0]); +#endif + +#ifndef HAVE_SETPROCTITLE + /* Save argv. Duplicate so setproctitle emulation doesn't clobber it */ + saved_argc = argc; + saved_argv = xcalloc(argc + 1, sizeof(*saved_argv)); + for (i = 0; i < argc; i++) + saved_argv[i] = xstrdup(argv[i]); + saved_argv[i] = NULL; + + /* Prepare for later setproctitle emulation */ + compat_init_setproctitle(argc, argv); + argv = saved_argv; + + /* this is to work around GNU getopt + portable setproctitle() fuckery */ + save_argc = saved_argc; + save_argv = saved_argv; +#endif + + if ((conf = config_default()) == NULL) + err(1, NULL); + + env = conf; + + flags = 0; + opts = 0; + debug = 0; + tracing = 0; + + log_init(1, LOG_MAIL); + + TAILQ_INIT(&offline_q); + + while ((c = getopt(argc, argv, "B:dD:hnP:f:FT:vx:")) != -1) { + switch (c) { + case 'B': + if (strstr(optarg, "queue=") == optarg) + backend_queue = strchr(optarg, '=') + 1; + else if (strstr(optarg, "scheduler=") == optarg) + backend_scheduler = strchr(optarg, '=') + 1; + else if (strstr(optarg, "stat=") == optarg) + backend_stat = strchr(optarg, '=') + 1; + else + log_warnx("warn: " + "invalid backend specifier %s", + optarg); + break; + case 'd': + foreground = 1; + foreground_log = 1; + break; + case 'D': + if (cmdline_symset(optarg) < 0) + log_warnx("warn: " + "could not parse macro definition %s", + optarg); + break; + case 'h': + log_info("version: " SMTPD_NAME " " SMTPD_VERSION); + usage(); + break; + case 'n': + debug = 2; + opts |= SMTPD_OPT_NOACTION; + break; + case 'f': + conffile = optarg; + break; + case 'F': + foreground = 1; + break; + + case 'T': + if (!strcmp(optarg, "imsg")) + tracing |= TRACE_IMSG; + else if (!strcmp(optarg, "io")) + tracing |= TRACE_IO; + else if (!strcmp(optarg, "smtp")) + tracing |= TRACE_SMTP; + else if (!strcmp(optarg, "filters")) + tracing |= TRACE_FILTERS; + else if (!strcmp(optarg, "mta") || + !strcmp(optarg, "transfer")) + tracing |= TRACE_MTA; + else if (!strcmp(optarg, "bounce") || + !strcmp(optarg, "bounces")) + tracing |= TRACE_BOUNCE; + else if (!strcmp(optarg, "scheduler")) + tracing |= TRACE_SCHEDULER; + else if (!strcmp(optarg, "lookup")) + tracing |= TRACE_LOOKUP; + else if (!strcmp(optarg, "stat") || + !strcmp(optarg, "stats")) + tracing |= TRACE_STAT; + else if (!strcmp(optarg, "rules")) + tracing |= TRACE_RULES; + else if (!strcmp(optarg, "mproc")) + tracing |= TRACE_MPROC; + else if (!strcmp(optarg, "expand")) + tracing |= TRACE_EXPAND; + else if (!strcmp(optarg, "table") || + !strcmp(optarg, "tables")) + tracing |= TRACE_TABLES; + else if (!strcmp(optarg, "queue")) + tracing |= TRACE_QUEUE; + else if (!strcmp(optarg, "all")) + tracing |= ~TRACE_DEBUG; + else if (!strcmp(optarg, "profstat")) + profiling |= PROFILE_TOSTAT; + else if (!strcmp(optarg, "profile-imsg")) + profiling |= PROFILE_IMSG; + else if (!strcmp(optarg, "profile-queue")) + profiling |= PROFILE_QUEUE; + else + log_warnx("warn: unknown trace flag \"%s\"", + optarg); + break; + case 'P': + if (!strcmp(optarg, "smtp")) + flags |= SMTPD_SMTP_PAUSED; + else if (!strcmp(optarg, "mta")) + flags |= SMTPD_MTA_PAUSED; + else if (!strcmp(optarg, "mda")) + flags |= SMTPD_MDA_PAUSED; + break; + case 'v': + tracing |= TRACE_DEBUG; + break; + case 'x': + rexec = optarg; + break; + default: + usage(); + } + } + + argv += optind; + argc -= optind; + + if (argc || *argv) + usage(); + + env->sc_opts |= opts; + + ssl_init(); + + if (parse_config(conf, conffile, opts)) + exit(1); + + if (RAND_status() != 1) + errx(1, "PRNG is not seeded"); + + if (strlcpy(env->sc_conffile, conffile, PATH_MAX) + >= PATH_MAX) + errx(1, "config file exceeds PATH_MAX"); + + if (env->sc_opts & SMTPD_OPT_NOACTION) { + if (env->sc_queue_key && + crypto_setup(env->sc_queue_key, + strlen(env->sc_queue_key)) == 0) { + fatalx("crypto_setup:" + "invalid key for queue encryption"); + } + load_pki_tree(); + load_pki_keys(); + fprintf(stderr, "configuration OK\n"); + exit(0); + } + + env->sc_flags |= flags; + + /* check for root privileges */ + if (geteuid()) + errx(1, "need root privileges"); + + log_init(foreground_log, LOG_MAIL); + log_trace_verbose(tracing); + load_pki_tree(); + load_pki_keys(); + + log_debug("debug: using \"%s\" queue backend", backend_queue); + log_debug("debug: using \"%s\" scheduler backend", backend_scheduler); + log_debug("debug: using \"%s\" stat backend", backend_stat); + + if (env->sc_hostname[0] == '\0') + errx(1, "machine does not have a hostname set"); + env->sc_uptime = time(NULL); + + if (rexec == NULL) { + smtpd_process = PROC_PARENT; + + if (env->sc_queue_flags & QUEUE_ENCRYPTION) { + if (env->sc_queue_key == NULL) { + char *password; + + password = getpass("queue key: "); + if (password == NULL) + err(1, "getpass"); + + env->sc_queue_key = strdup(password); + explicit_bzero(password, strlen(password)); + if (env->sc_queue_key == NULL) + err(1, "strdup"); + } + else { + char *buf = NULL; + size_t sz = 0; + ssize_t len; + + if (strcasecmp(env->sc_queue_key, "stdin") == 0) { + if ((len = getline(&buf, &sz, stdin)) == -1) + err(1, "getline"); + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + env->sc_queue_key = buf; + } + } + } + + log_info("info: %s %s starting", SMTPD_NAME, SMTPD_VERSION); + + if (!foreground) + if (daemon(0, 0) == -1) + err(1, "failed to daemonize"); + + /* setup all processes */ + + p_ca = start_child(save_argc, save_argv, "ca"); + p_ca->proc = PROC_CA; + + p_control = start_child(save_argc, save_argv, "control"); + p_control->proc = PROC_CONTROL; + + p_lka = start_child(save_argc, save_argv, "lka"); + p_lka->proc = PROC_LKA; + + p_pony = start_child(save_argc, save_argv, "pony"); + p_pony->proc = PROC_PONY; + + p_queue = start_child(save_argc, save_argv, "queue"); + p_queue->proc = PROC_QUEUE; + + p_scheduler = start_child(save_argc, save_argv, "scheduler"); + p_scheduler->proc = PROC_SCHEDULER; + + setup_peers(p_control, p_ca); + setup_peers(p_control, p_lka); + setup_peers(p_control, p_pony); + setup_peers(p_control, p_queue); + setup_peers(p_control, p_scheduler); + setup_peers(p_pony, p_ca); + setup_peers(p_pony, p_lka); + setup_peers(p_pony, p_queue); + setup_peers(p_queue, p_lka); + setup_peers(p_queue, p_scheduler); + + if (env->sc_queue_key) { + if (imsg_compose(&p_queue->imsgbuf, IMSG_SETUP_KEY, 0, + 0, -1, env->sc_queue_key, strlen(env->sc_queue_key) + + 1) == -1) + fatal("imsg_compose"); + if (imsg_flush(&p_queue->imsgbuf) == -1) + fatal("imsg_flush"); + } + + setup_done(p_ca); + setup_done(p_control); + setup_done(p_lka); + setup_done(p_pony); + setup_done(p_queue); + setup_done(p_scheduler); + + log_debug("smtpd: setup done"); + + return smtpd(); + } + + if (!strcmp(rexec, "ca")) { + smtpd_process = PROC_CA; + setup_proc(); + + return ca(); + } + + else if (!strcmp(rexec, "control")) { + smtpd_process = PROC_CONTROL; + setup_proc(); + + /* the control socket ensures that only one smtpd instance is running */ + control_socket = control_create_socket(); + + env->sc_stat = stat_backend_lookup(backend_stat); + if (env->sc_stat == NULL) + errx(1, "could not find stat backend \"%s\"", backend_stat); + + return control(); + } + + else if (!strcmp(rexec, "lka")) { + smtpd_process = PROC_LKA; + setup_proc(); + + return lka(); + } + + else if (!strcmp(rexec, "pony")) { + smtpd_process = PROC_PONY; + setup_proc(); + + return pony(); + } + + else if (!strcmp(rexec, "queue")) { + smtpd_process = PROC_QUEUE; + setup_proc(); + + if (env->sc_queue_flags & QUEUE_COMPRESSION) + env->sc_comp = compress_backend_lookup("gzip"); + + if (!queue_init(backend_queue, 1)) + errx(1, "could not initialize queue backend"); + + return queue(); + } + + else if (!strcmp(rexec, "scheduler")) { + smtpd_process = PROC_SCHEDULER; + setup_proc(); + + for (i = 0; i < MAX_BOUNCE_WARN; i++) { + if (env->sc_bounce_warn[i] == 0) + break; + log_debug("debug: bounce warning after %s", + duration_to_text(env->sc_bounce_warn[i])); + } + + return scheduler(); + } + + fatalx("bad rexec: %s", rexec); + + return (1); +} + +static struct mproc * +start_child(int save_argc, char **save_argv, char *rexec) +{ + struct mproc *p; + char *argv[SMTPD_MAXARG]; + int sp[2], argc = 0; + pid_t pid; + + if (save_argc >= SMTPD_MAXARG - 2) + fatalx("too many arguments"); + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) + fatal("socketpair"); + + io_set_nonblocking(sp[0]); + io_set_nonblocking(sp[1]); + + switch (pid = fork()) { + case -1: + fatal("%s: fork", save_argv[0]); + case 0: + break; + default: + close(sp[0]); + p = calloc(1, sizeof(*p)); + if (p == NULL) + fatal("calloc"); + if((p->name = strdup(rexec)) == NULL) + fatal("strdup"); + mproc_init(p, sp[1]); + p->pid = pid; + p->handler = parent_imsg; + return p; + } + + if (sp[0] != 3) { + if (dup2(sp[0], 3) == -1) + fatal("%s: dup2", rexec); + } else if (fcntl(sp[0], F_SETFD, 0) == -1) + fatal("%s: fcntl", rexec); + + xclosefrom(4); + + for (argc = 0; argc < save_argc; argc++) + argv[argc] = save_argv[argc]; + argv[argc++] = "-x"; + argv[argc++] = rexec; + argv[argc++] = NULL; + + execvp(argv[0], argv); + fatal("%s: execvp", rexec); +} + +static void +setup_peers(struct mproc *a, struct mproc *b) +{ + int sp[2]; + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) + fatal("socketpair"); + + io_set_nonblocking(sp[0]); + io_set_nonblocking(sp[1]); + + if (imsg_compose(&a->imsgbuf, IMSG_SETUP_PEER, b->proc, b->pid, sp[0], + NULL, 0) == -1) + fatal("imsg_compose"); + if (imsg_flush(&a->imsgbuf) == -1) + fatal("imsg_flush"); + + if (imsg_compose(&b->imsgbuf, IMSG_SETUP_PEER, a->proc, a->pid, sp[1], + NULL, 0) == -1) + fatal("imsg_compose"); + if (imsg_flush(&b->imsgbuf) == -1) + fatal("imsg_flush"); +} + +static void +setup_done(struct mproc *p) +{ + struct imsg imsg; + + if (imsg_compose(&p->imsgbuf, IMSG_SETUP_DONE, 0, 0, -1, NULL, 0) == -1) + fatal("imsg_compose"); + if (imsg_flush(&p->imsgbuf) == -1) + fatal("imsg_flush"); + + if (imsg_wait(&p->imsgbuf, &imsg, 10000) == -1) + fatal("imsg_wait"); + + if (imsg.hdr.type != IMSG_SETUP_DONE) + fatalx("expect IMSG_SETUP_DONE"); + + log_debug("setup_done: %s[%d] done", p->name, p->pid); + + imsg_free(&imsg); +} + +static void +setup_proc(void) +{ + struct imsgbuf *ibuf; + struct imsg imsg; + int setup = 1; + + log_procinit(proc_title(smtpd_process)); + + p_parent = calloc(1, sizeof(*p_parent)); + if (p_parent == NULL) + fatal("calloc"); + if((p_parent->name = strdup("parent")) == NULL) + fatal("strdup"); + p_parent->proc = PROC_PARENT; + p_parent->handler = imsg_dispatch; + mproc_init(p_parent, 3); + + ibuf = &p_parent->imsgbuf; + + while (setup) { + if (imsg_wait(ibuf, &imsg, 10000) == -1) + fatal("imsg_wait"); + + switch (imsg.hdr.type) { + case IMSG_SETUP_KEY: + env->sc_queue_key = strdup(imsg.data); + break; + case IMSG_SETUP_PEER: + setup_peer(imsg.hdr.peerid, imsg.hdr.pid, imsg.fd); + break; + case IMSG_SETUP_DONE: + setup = 0; + break; + default: + fatal("bad imsg %d", imsg.hdr.type); + } + imsg_free(&imsg); + } + + if (imsg_compose(ibuf, IMSG_SETUP_DONE, 0, 0, -1, NULL, 0) == -1) + fatal("imsg_compose"); + + if (imsg_flush(ibuf) == -1) + fatal("imsg_flush"); + + log_debug("setup_proc: %s done", proc_title(smtpd_process)); +} + +static struct mproc * +setup_peer(enum smtp_proc_type proc, pid_t pid, int sock) +{ + struct mproc *p, **pp; + + log_debug("setup_peer: %s -> %s[%u] fd=%d", proc_title(smtpd_process), + proc_title(proc), pid, sock); + + if (sock == -1) + fatalx("peer socket not received"); + + switch (proc) { + case PROC_LKA: + pp = &p_lka; + break; + case PROC_QUEUE: + pp = &p_queue; + break; + case PROC_CONTROL: + pp = &p_control; + break; + case PROC_SCHEDULER: + pp = &p_scheduler; + break; + case PROC_PONY: + pp = &p_pony; + break; + case PROC_CA: + pp = &p_ca; + break; + default: + fatalx("unknown peer"); + } + + if (*pp) + fatalx("peer already set"); + + p = calloc(1, sizeof(*p)); + if (p == NULL) + fatal("calloc"); + if((p->name = strdup(proc_title(proc))) == NULL) + fatal("strdup"); + mproc_init(p, sock); + p->pid = pid; + p->proc = proc; + p->handler = imsg_dispatch; + + *pp = p; + + return p; +} + +static int +imsg_wait(struct imsgbuf *ibuf, struct imsg *imsg, int timeout) +{ + struct pollfd pfd[1]; + ssize_t n; + + pfd[0].fd = ibuf->fd; + pfd[0].events = POLLIN; + + while (1) { + if ((n = imsg_get(ibuf, imsg)) == -1) + return -1; + if (n) + return 1; + + n = poll(pfd, 1, timeout); + if (n == -1) + return -1; + if (n == 0) { + errno = ETIMEDOUT; + return -1; + } + + if (((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) || n == 0) + return -1; + } +} + +int +smtpd(void) { + struct event ev_sigint; + struct event ev_sigterm; + struct event ev_sigchld; + struct event ev_sighup; + struct timeval tv; + + imsg_callback = parent_imsg; + + tree_init(&children); + + child_add(p_queue->pid, CHILD_DAEMON, proc_title(PROC_QUEUE)); + child_add(p_control->pid, CHILD_DAEMON, proc_title(PROC_CONTROL)); + child_add(p_lka->pid, CHILD_DAEMON, proc_title(PROC_LKA)); + child_add(p_scheduler->pid, CHILD_DAEMON, proc_title(PROC_SCHEDULER)); + child_add(p_pony->pid, CHILD_DAEMON, proc_title(PROC_PONY)); + child_add(p_ca->pid, CHILD_DAEMON, proc_title(PROC_CA)); + + event_init(); + + signal_set(&ev_sigint, SIGINT, parent_sig_handler, NULL); + signal_set(&ev_sigterm, SIGTERM, parent_sig_handler, NULL); + signal_set(&ev_sigchld, SIGCHLD, parent_sig_handler, NULL); + signal_set(&ev_sighup, SIGHUP, parent_sig_handler, NULL); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal_add(&ev_sigchld, NULL); + signal_add(&ev_sighup, NULL); + signal(SIGPIPE, SIG_IGN); + + config_peer(PROC_CONTROL); + config_peer(PROC_LKA); + config_peer(PROC_QUEUE); + config_peer(PROC_CA); + config_peer(PROC_PONY); + + evtimer_set(&config_ev, parent_send_config, NULL); + memset(&tv, 0, sizeof(tv)); + evtimer_add(&config_ev, &tv); + + /* defer offline scanning for a second */ + evtimer_set(&offline_ev, offline_scan, NULL); + offline_timeout.tv_sec = 1; + offline_timeout.tv_usec = 0; + evtimer_add(&offline_ev, &offline_timeout); + + if (pidfile(NULL) < 0) + err(1, "pidfile"); + + fork_filter_processes(); + + purge_task(); + +#if HAVE_PLEDGE + if (pledge("stdio rpath wpath cpath fattr tmppath " + "getpw sendfd proc exec id inet chown unix", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + fatalx("exited event loop"); + + return (0); +} + +static void +load_pki_tree(void) +{ + struct pki *pki; + struct ca *sca; + const char *k; + void *iter_dict; + + log_debug("debug: init ssl-tree"); + iter_dict = NULL; + while (dict_iter(env->sc_pki_dict, &iter_dict, &k, (void **)&pki)) { + log_debug("info: loading pki information for %s", k); + if (pki->pki_cert_file == NULL) + fatalx("load_pki_tree: missing certificate file"); + if (pki->pki_key_file == NULL) + fatalx("load_pki_tree: missing key file"); + + if (!ssl_load_certificate(pki, pki->pki_cert_file)) + fatalx("load_pki_tree: failed to load certificate file"); + } + + log_debug("debug: init ca-tree"); + iter_dict = NULL; + while (dict_iter(env->sc_ca_dict, &iter_dict, &k, (void **)&sca)) { + log_debug("info: loading CA information for %s", k); + if (!ssl_load_cafile(sca, sca->ca_cert_file)) + fatalx("load_pki_tree: failed to load CA file"); + } +} + +void +load_pki_keys(void) +{ + struct pki *pki; + const char *k; + void *iter_dict; + + log_debug("debug: init ssl-tree"); + iter_dict = NULL; + while (dict_iter(env->sc_pki_dict, &iter_dict, &k, (void **)&pki)) { + log_debug("info: loading pki keys for %s", k); + + if (!ssl_load_keyfile(pki, pki->pki_key_file, k)) + fatalx("load_pki_keys: failed to load key file"); + } +} + +int +fork_proc_backend(const char *key, const char *conf, const char *procname) +{ + pid_t pid; + int sp[2]; + char path[PATH_MAX]; + char name[PATH_MAX]; + char *arg; + + if (strlcpy(name, conf, sizeof(name)) >= sizeof(name)) { + log_warnx("warn: %s-proc: conf too long", key); + return (0); + } + + arg = strchr(name, ':'); + if (arg) + *arg++ = '\0'; + + if (snprintf(path, sizeof(path), PATH_LIBEXEC "/%s-%s", key, name) >= + (ssize_t)sizeof(path)) { + log_warn("warn: %s-proc: exec path too long", key); + return (-1); + } + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) { + log_warn("warn: %s-proc: socketpair", key); + return (-1); + } + + if ((pid = fork()) == -1) { + log_warn("warn: %s-proc: fork", key); + close(sp[0]); + close(sp[1]); + return (-1); + } + + if (pid == 0) { + /* child process */ + dup2(sp[0], STDIN_FILENO); + closefrom(STDERR_FILENO + 1); + + if (procname == NULL) + procname = name; + + execl(path, procname, arg, (char *)NULL); + err(1, "execl: %s", path); + } + + /* parent process */ + close(sp[0]); + + return (sp[1]); +} + +struct child * +child_add(pid_t pid, int type, const char *title) +{ + struct child *child; + + if ((child = calloc(1, sizeof(*child))) == NULL) + fatal("smtpd: child_add: calloc"); + + child->pid = pid; + child->type = type; + child->title = title; + + tree_xset(&children, pid, child); + + return (child); +} + +static void +purge_task(void) +{ + struct passwd *pw; + DIR *d; + int n; + uid_t uid; + gid_t gid; + + n = 0; + if ((d = opendir(PATH_SPOOL PATH_PURGE))) { + while (readdir(d) != NULL) + n++; + closedir(d); + } else + log_warn("warn: purge_task: opendir"); + + if (n > 2) { + switch (purge_pid = fork()) { + case -1: + log_warn("warn: purge_task: fork"); + break; + case 0: + if ((pw = getpwnam(SMTPD_QUEUE_USER)) == NULL) + fatalx("unknown user " SMTPD_QUEUE_USER); + if (chroot(PATH_SPOOL PATH_PURGE) == -1) + fatal("smtpd: chroot"); + if (chdir("/") == -1) + fatal("smtpd: chdir"); + uid = pw->pw_uid; + gid = pw->pw_gid; + if (setgroups(1, &gid) || + setresgid(gid, gid, gid) || + setresuid(uid, uid, uid)) + fatal("smtpd: cannot drop privileges"); + rmtree("/", 1); + _exit(0); + break; + default: + break; + } + } +} + +static void +fork_filter_processes(void) +{ + const char *name; + void *iter; + const char *fn; + struct filter_config *fc; + struct filter_config *fcs; + struct filter_proc *fp; + size_t i; + + /* For each filter chain, assign the registered subsystem to subfilters */ + iter = NULL; + while (dict_iter(env->sc_filters_dict, &iter, (const char **)&fn, (void **)&fc)) { + if (fc->chain) { + for (i = 0; i < fc->chain_size; ++i) { + fcs = dict_xget(env->sc_filters_dict, fc->chain[i]); + fcs->filter_subsystem |= fc->filter_subsystem; + } + } + } + + /* For each filter, assign the registered subsystem to underlying proc */ + iter = NULL; + while (dict_iter(env->sc_filters_dict, &iter, (const char **)&fn, (void **)&fc)) { + if (fc->proc) { + fp = dict_xget(env->sc_filter_processes_dict, fc->proc); + fp->filter_subsystem |= fc->filter_subsystem; + } + } + + iter = NULL; + while (dict_iter(env->sc_filter_processes_dict, &iter, &name, (void **)&fp)) + fork_filter_process(name, fp->command, fp->user, fp->group, fp->chroot, fp->filter_subsystem); +} + +static void +fork_filter_process(const char *name, const char *command, const char *user, const char *group, const char *chroot_path, uint32_t subsystems) +{ + pid_t pid; + struct filter_proc *processor; + char buf; + int sp[2], errfd[2]; + struct passwd *pw; + struct group *gr; + char exec[_POSIX_ARG_MAX]; + int execr; + + if (user == NULL) + user = SMTPD_USER; + if ((pw = getpwnam(user)) == NULL) + err(1, "getpwnam"); + + if (group) { + if ((gr = getgrnam(group)) == NULL) + err(1, "getgrnam"); + } + else { + if ((gr = getgrgid(pw->pw_gid)) == NULL) + err(1, "getgrgid"); + } + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) + err(1, "socketpair"); + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, errfd) == -1) + err(1, "socketpair"); + + if ((pid = fork()) == -1) + err(1, "fork"); + + /* parent passes the child fd over to lka */ + if (pid > 0) { + processor = dict_xget(env->sc_filter_processes_dict, name); + processor->errfd = errfd[1]; + child_add(pid, CHILD_PROCESSOR, name); + close(sp[0]); + close(errfd[0]); + m_create(p_lka, IMSG_LKA_PROCESSOR_FORK, 0, 0, sp[1]); + m_add_string(p_lka, name); + m_add_u32(p_lka, (uint32_t)subsystems); + m_close(p_lka); + return; + } + + close(sp[1]); + close(errfd[1]); + dup2(sp[0], STDIN_FILENO); + dup2(sp[0], STDOUT_FILENO); + dup2(errfd[0], STDERR_FILENO); + + if (chroot_path) { + if (chroot(chroot_path) != 0 || chdir("/") != 0) + err(1, "chroot: %s", chroot_path); + } + + if (setgroups(1, &gr->gr_gid) || + setresgid(gr->gr_gid, gr->gr_gid, gr->gr_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) + err(1, "fork_filter_process: cannot drop privileges"); + + xclosefrom(STDERR_FILENO + 1); + + if (setsid() < 0) + err(1, "setsid"); + if (signal(SIGPIPE, SIG_DFL) == SIG_ERR || + signal(SIGINT, SIG_DFL) == SIG_ERR || + signal(SIGTERM, SIG_DFL) == SIG_ERR || + signal(SIGCHLD, SIG_DFL) == SIG_ERR || + signal(SIGHUP, SIG_DFL) == SIG_ERR) + err(1, "signal"); + + if (command[0] == '/') + execr = snprintf(exec, sizeof(exec), "exec %s", command); + else + execr = snprintf(exec, sizeof(exec), "exec %s/%s", + PATH_LIBEXEC, command); + if (execr >= (int) sizeof(exec)) + errx(1, "%s: exec path too long", name); + + /* + * Wait for lka to acknowledge that it received the fd. + * This prevents a race condition between the filter sending an error + * message, and exiting and lka not being able to log it because of + * SIGCHLD. + * (Ab)use read to determine if the fd is installed; since stderr is + * never going to be read from we can shutdown(2) the write-end in lka. + */ + if (read(STDERR_FILENO, &buf, 1) != 0) + errx(1, "lka didn't properly close write end of error socket"); + if (system(exec) == -1) + err(1, NULL); + + /* there's no successful exit from a processor */ + _exit(1); +} + +static void +forkmda(struct mproc *p, uint64_t id, struct deliver *deliver) +{ + char ebuf[128], sfn[32]; + struct dispatcher *dsp; + struct child *child; + pid_t pid; + int allout, pipefd[2]; + struct passwd *pw; + const char *pw_name; + uid_t pw_uid; + gid_t pw_gid; + const char *pw_dir; + + dsp = dict_xget(env->sc_dispatchers, deliver->dispatcher); + if (dsp->type != DISPATCHER_LOCAL) + fatalx("non-local dispatcher called from forkmda()"); + + log_debug("debug: smtpd: forking mda for session %016"PRIx64 + ": %s as %s", id, deliver->userinfo.username, + dsp->u.local.user ? dsp->u.local.user : deliver->userinfo.username); + + if (dsp->u.local.user) { + if ((pw = getpwnam(dsp->u.local.user)) == NULL) { + (void)snprintf(ebuf, sizeof ebuf, + "delivery user '%s' does not exist", + dsp->u.local.user); + m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1); + m_add_id(p_pony, id); + m_add_int(p_pony, MDA_PERMFAIL); + m_add_int(p_pony, EX_NOUSER); + m_add_string(p_pony, ebuf); + m_close(p_pony); + return; + } + pw_name = pw->pw_name; + pw_uid = pw->pw_uid; + pw_gid = pw->pw_gid; + pw_dir = pw->pw_dir; + } + else { + pw_name = deliver->userinfo.username; + pw_uid = deliver->userinfo.uid; + pw_gid = deliver->userinfo.gid; + pw_dir = deliver->userinfo.directory; + } + + if (pw_uid == 0 && deliver->mda_exec[0]) { + pw_name = deliver->userinfo.username; + pw_uid = deliver->userinfo.uid; + pw_gid = deliver->userinfo.gid; + pw_dir = deliver->userinfo.directory; + } + + if (pw_uid == 0 && !dsp->u.local.is_mbox) { + (void)snprintf(ebuf, sizeof ebuf, "not allowed to deliver to: %s", + deliver->userinfo.username); + m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1); + m_add_id(p_pony, id); + m_add_int(p_pony, MDA_PERMFAIL); + m_add_int(p_pony, EX_NOPERM); + m_add_string(p_pony, ebuf); + m_close(p_pony); + return; + } + + if (pipe(pipefd) == -1) { + (void)snprintf(ebuf, sizeof ebuf, "pipe: %s", strerror(errno)); + m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1); + m_add_id(p_pony, id); + m_add_int(p_pony, MDA_TEMPFAIL); + m_add_int(p_pony, EX_OSERR); + m_add_string(p_pony, ebuf); + m_close(p_pony); + return; + } + + /* prepare file which captures stdout and stderr */ + (void)strlcpy(sfn, "/tmp/smtpd.out.XXXXXXXXXXX", sizeof(sfn)); + allout = mkstemp(sfn); + if (allout == -1) { + (void)snprintf(ebuf, sizeof ebuf, "mkstemp: %s", strerror(errno)); + m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1); + m_add_id(p_pony, id); + m_add_int(p_pony, MDA_TEMPFAIL); + m_add_int(p_pony, EX_OSERR); + m_add_string(p_pony, ebuf); + m_close(p_pony); + close(pipefd[0]); + close(pipefd[1]); + return; + } + unlink(sfn); + + pid = fork(); + if (pid == -1) { + (void)snprintf(ebuf, sizeof ebuf, "fork: %s", strerror(errno)); + m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1); + m_add_id(p_pony, id); + m_add_int(p_pony, MDA_TEMPFAIL); + m_add_int(p_pony, EX_OSERR); + m_add_string(p_pony, ebuf); + m_close(p_pony); + close(pipefd[0]); + close(pipefd[1]); + close(allout); + return; + } + + /* parent passes the child fd over to mda */ + if (pid > 0) { + child = child_add(pid, CHILD_MDA, NULL); + child->mda_out = allout; + child->mda_id = id; + close(pipefd[0]); + m_create(p, IMSG_MDA_FORK, 0, 0, pipefd[1]); + m_add_id(p, id); + m_close(p); + return; + } + + /* mbox helper, create mailbox before privdrop if it doesn't exist */ + if (dsp->u.local.is_mbox) + mda_mbox_init(deliver); + + if (chdir(pw_dir) == -1 && chdir("/") == -1) + err(1, "chdir"); + if (setgroups(1, &pw_gid) || + setresgid(pw_gid, pw_gid, pw_gid) || + setresuid(pw_uid, pw_uid, pw_uid)) + err(1, "forkmda: cannot drop privileges"); + if (dup2(pipefd[0], STDIN_FILENO) == -1 || + dup2(allout, STDOUT_FILENO) == -1 || + dup2(allout, STDERR_FILENO) == -1) + err(1, "forkmda: dup2"); + closefrom(STDERR_FILENO + 1); + if (setsid() < 0) + err(1, "setsid"); + if (signal(SIGPIPE, SIG_DFL) == SIG_ERR || + signal(SIGINT, SIG_DFL) == SIG_ERR || + signal(SIGTERM, SIG_DFL) == SIG_ERR || + signal(SIGCHLD, SIG_DFL) == SIG_ERR || + signal(SIGHUP, SIG_DFL) == SIG_ERR) + err(1, "signal"); + + /* avoid hangs by setting 5m timeout */ + alarm(300); + + if (dsp->u.local.is_mbox && + dsp->u.local.mda_wrapper == NULL && + deliver->mda_exec[0] == '\0') + mda_mbox(deliver); + else + mda_unpriv(dsp, deliver, pw_name, pw_dir); +} + +static void +offline_scan(int fd, short ev, void *arg) +{ + char *path_argv[2]; + FTS *fts = arg; + FTSENT *e; + int n = 0; + + path_argv[0] = PATH_SPOOL PATH_OFFLINE; + path_argv[1] = NULL; + + if (fts == NULL) { + log_debug("debug: smtpd: scanning offline queue..."); + fts = fts_open(path_argv, FTS_PHYSICAL | FTS_NOCHDIR, NULL); + if (fts == NULL) { + log_warn("fts_open: %s", path_argv[0]); + return; + } + } + + while ((e = fts_read(fts)) != NULL) { + if (e->fts_info != FTS_F) + continue; + + /* offline files must be at depth 1 */ + if (e->fts_level != 1) + continue; + + /* offline file group must match parent directory group */ + if (e->fts_statp->st_gid != e->fts_parent->fts_statp->st_gid) + continue; + + if (e->fts_statp->st_size == 0) { + if (unlink(e->fts_accpath) == -1) + log_warnx("warn: smtpd: could not unlink %s", e->fts_accpath); + continue; + } + + if (offline_add(e->fts_name, e->fts_statp->st_uid, + e->fts_statp->st_gid)) { + log_warnx("warn: smtpd: " + "could not add offline message %s", e->fts_name); + continue; + } + + if ((n++) == OFFLINE_READMAX) { + evtimer_set(&offline_ev, offline_scan, fts); + offline_timeout.tv_sec = 0; + offline_timeout.tv_usec = 100000; + evtimer_add(&offline_ev, &offline_timeout); + return; + } + } + + log_debug("debug: smtpd: offline scanning done"); + fts_close(fts); +} + +static int +offline_enqueue(char *name, uid_t uid, gid_t gid) +{ + char *path; + struct stat sb; + pid_t pid; + struct child *child; + struct passwd *pw; + int pathlen; + + pathlen = asprintf(&path, "%s/%s", PATH_SPOOL PATH_OFFLINE, name); + if (pathlen == -1) { + log_warnx("warn: smtpd: asprintf"); + return (-1); + } + + if (pathlen >= PATH_MAX) { + log_warnx("warn: smtpd: pathname exceeds PATH_MAX"); + free(path); + return (-1); + } + + log_debug("debug: smtpd: enqueueing offline message %s", path); + + if ((pid = fork()) == -1) { + log_warn("warn: smtpd: fork"); + free(path); + return (-1); + } + + if (pid == 0) { + char *envp[2], *p = NULL, *tmp; + int fd; + FILE *fp; + size_t sz = 0; + ssize_t len; + arglist args; + + closefrom(STDERR_FILENO + 1); + + memset(&args, 0, sizeof(args)); + + if ((fd = open(path, O_RDONLY|O_NOFOLLOW|O_NONBLOCK)) == -1) { + log_warn("warn: smtpd: open: %s", path); + _exit(1); + } + + if (fstat(fd, &sb) == -1) { + log_warn("warn: smtpd: fstat: %s", path); + _exit(1); + } + + if (!S_ISREG(sb.st_mode)) { + log_warnx("warn: smtpd: file %s (uid %d) not regular", + path, sb.st_uid); + _exit(1); + } + + if (sb.st_nlink != 1) { + log_warnx("warn: smtpd: file %s is hard-link", path); + _exit(1); + } + + if (sb.st_uid != uid) { + log_warnx("warn: smtpd: file %s has bad uid %d", + path, sb.st_uid); + _exit(1); + } + + if (sb.st_gid != gid) { + log_warnx("warn: smtpd: file %s has bad gid %d", + path, sb.st_gid); + _exit(1); + } + + pw = getpwuid(sb.st_uid); + if (pw == NULL) { + log_warnx("warn: smtpd: getpwuid for uid %d failed", + sb.st_uid); + _exit(1); + } + + if (setgroups(1, &pw->pw_gid) || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) + _exit(1); + + if ((fp = fdopen(fd, "r")) == NULL) + _exit(1); + + if (chdir(pw->pw_dir) == -1 && chdir("/") == -1) + _exit(1); + + if (setsid() == -1 || + signal(SIGPIPE, SIG_DFL) == SIG_ERR || + dup2(fileno(fp), STDIN_FILENO) == -1) + _exit(1); + + if ((len = getline(&p, &sz, fp)) == -1) + _exit(1); + + if (p[len - 1] != '\n') + _exit(1); + p[len - 1] = '\0'; + + addargs(&args, "%s", "sendmail"); + addargs(&args, "%s", "-S"); + + while ((tmp = strsep(&p, "|")) != NULL) + addargs(&args, "%s", tmp); + + free(p); + if (lseek(fileno(fp), len, SEEK_SET) == -1) + _exit(1); + + envp[0] = "PATH=" _PATH_DEFPATH; + envp[1] = (char *)NULL; + environ = envp; + + execvp(PATH_SMTPCTL, args.list); + _exit(1); + } + + offline_running++; + child = child_add(pid, CHILD_ENQUEUE_OFFLINE, NULL); + child->path = path; + + return (0); +} + +static int +offline_add(char *path, uid_t uid, gid_t gid) +{ + struct offline *q; + + if (offline_running < OFFLINE_QUEUEMAX) + /* skip queue */ + return offline_enqueue(path, uid, gid); + + q = malloc(sizeof(*q) + strlen(path) + 1); + if (q == NULL) + return (-1); + q->uid = uid; + q->gid = gid; + q->path = (char *)q + sizeof(*q); + memmove(q->path, path, strlen(path) + 1); + TAILQ_INSERT_TAIL(&offline_q, q, entry); + + return (0); +} + +static void +offline_done(void) +{ + struct offline *q; + + offline_running--; + + while (offline_running < OFFLINE_QUEUEMAX) { + if ((q = TAILQ_FIRST(&offline_q)) == NULL) + break; /* all done */ + TAILQ_REMOVE(&offline_q, q, entry); + offline_enqueue(q->path, q->uid, q->gid); + free(q); + } +} + +static int +parent_forward_open(char *username, char *directory, uid_t uid, gid_t gid) +{ + char pathname[PATH_MAX]; + int fd; + struct stat sb; + + if (!bsnprintf(pathname, sizeof (pathname), "%s/.forward", + directory)) { + log_warnx("warn: smtpd: %s: pathname too large", pathname); + return -1; + } + + if (stat(directory, &sb) == -1) { + log_warn("warn: smtpd: parent_forward_open: %s", directory); + return -1; + } + if (sb.st_mode & S_ISVTX) { + log_warnx("warn: smtpd: parent_forward_open: %s is sticky", + directory); + errno = EAGAIN; + return -1; + } + + do { + fd = open(pathname, O_RDONLY|O_NOFOLLOW|O_NONBLOCK); + } while (fd == -1 && errno == EINTR); + if (fd == -1) { + if (errno == ENOENT) + return -1; + if (errno == EMFILE || errno == ENFILE || errno == EIO) { + errno = EAGAIN; + return -1; + } + if (errno == ELOOP) + log_warnx("warn: smtpd: parent_forward_open: %s: " + "cannot follow symbolic links", pathname); + else + log_warn("warn: smtpd: parent_forward_open: %s", pathname); + return -1; + } + + if (!secure_file(fd, pathname, directory, uid, 1)) { + log_warnx("warn: smtpd: %s: unsecure file", pathname); + close(fd); + return -1; + } + + return fd; +} + +void +imsg_dispatch(struct mproc *p, struct imsg *imsg) +{ + struct timespec t0, t1, dt; + int msg; + + if (imsg == NULL) { + imsg_callback(p, imsg); + return; + } + + log_imsg(smtpd_process, p->proc, imsg); + + if (profiling & PROFILE_IMSG) + clock_gettime(CLOCK_MONOTONIC, &t0); + + msg = imsg->hdr.type; + imsg_callback(p, imsg); + + if (profiling & PROFILE_IMSG) { + clock_gettime(CLOCK_MONOTONIC, &t1); + timespecsub(&t1, &t0, &dt); + + log_debug("profile-imsg: %s %s %s %d %lld.%09ld", + proc_name(smtpd_process), + proc_name(p->proc), + imsg_to_str(msg), + (int)imsg->hdr.len, + (long long)dt.tv_sec, + dt.tv_nsec); + + if (profiling & PROFILE_TOSTAT) { + char key[STAT_KEY_SIZE]; + /* can't profstat control process yet */ + if (smtpd_process == PROC_CONTROL) + return; + + if (!bsnprintf(key, sizeof key, + "profiling.imsg.%s.%s.%s", + proc_name(smtpd_process), + proc_name(p->proc), + imsg_to_str(msg))) + return; + stat_set(key, stat_timespec(&dt)); + } + } +} + +void +log_imsg(int to, int from, struct imsg *imsg) +{ + + if (to == PROC_CONTROL && imsg->hdr.type == IMSG_STAT_SET) + return; + + if (imsg->fd != -1) + log_trace(TRACE_IMSG, "imsg: %s <- %s: %s (len=%zu, fd=%d)", + proc_name(to), + proc_name(from), + imsg_to_str(imsg->hdr.type), + imsg->hdr.len - IMSG_HEADER_SIZE, + imsg->fd); + else + log_trace(TRACE_IMSG, "imsg: %s <- %s: %s (len=%zu)", + proc_name(to), + proc_name(from), + imsg_to_str(imsg->hdr.type), + imsg->hdr.len - IMSG_HEADER_SIZE); +} + +const char * +proc_title(enum smtp_proc_type proc) +{ + switch (proc) { + case PROC_PARENT: + return "[priv]"; + case PROC_LKA: + return "lookup"; + case PROC_QUEUE: + return "queue"; + case PROC_CONTROL: + return "control"; + case PROC_SCHEDULER: + return "scheduler"; + case PROC_PONY: + return "pony express"; + case PROC_CA: + return "klondike"; + case PROC_CLIENT: + return "client"; + case PROC_PROCESSOR: + return "processor"; + } + return "unknown"; +} + +const char * +proc_name(enum smtp_proc_type proc) +{ + switch (proc) { + case PROC_PARENT: + return "parent"; + case PROC_LKA: + return "lka"; + case PROC_QUEUE: + return "queue"; + case PROC_CONTROL: + return "control"; + case PROC_SCHEDULER: + return "scheduler"; + case PROC_PONY: + return "pony"; + case PROC_CA: + return "ca"; + case PROC_CLIENT: + return "client-proc"; + default: + return "unknown"; + } +} + +#define CASE(x) case x : return #x + +const char * +imsg_to_str(int type) +{ + static char buf[32]; + + switch (type) { + CASE(IMSG_NONE); + + CASE(IMSG_CTL_OK); + CASE(IMSG_CTL_FAIL); + + CASE(IMSG_CTL_GET_DIGEST); + CASE(IMSG_CTL_GET_STATS); + CASE(IMSG_CTL_LIST_MESSAGES); + CASE(IMSG_CTL_LIST_ENVELOPES); + CASE(IMSG_CTL_MTA_SHOW_HOSTS); + CASE(IMSG_CTL_MTA_SHOW_RELAYS); + CASE(IMSG_CTL_MTA_SHOW_ROUTES); + CASE(IMSG_CTL_MTA_SHOW_HOSTSTATS); + CASE(IMSG_CTL_MTA_BLOCK); + CASE(IMSG_CTL_MTA_UNBLOCK); + CASE(IMSG_CTL_MTA_SHOW_BLOCK); + CASE(IMSG_CTL_PAUSE_EVP); + CASE(IMSG_CTL_PAUSE_MDA); + CASE(IMSG_CTL_PAUSE_MTA); + CASE(IMSG_CTL_PAUSE_SMTP); + CASE(IMSG_CTL_PROFILE); + CASE(IMSG_CTL_PROFILE_DISABLE); + CASE(IMSG_CTL_PROFILE_ENABLE); + CASE(IMSG_CTL_RESUME_EVP); + CASE(IMSG_CTL_RESUME_MDA); + CASE(IMSG_CTL_RESUME_MTA); + CASE(IMSG_CTL_RESUME_SMTP); + CASE(IMSG_CTL_RESUME_ROUTE); + CASE(IMSG_CTL_REMOVE); + CASE(IMSG_CTL_SCHEDULE); + CASE(IMSG_CTL_SHOW_STATUS); + CASE(IMSG_CTL_TRACE_DISABLE); + CASE(IMSG_CTL_TRACE_ENABLE); + CASE(IMSG_CTL_UPDATE_TABLE); + CASE(IMSG_CTL_VERBOSE); + CASE(IMSG_CTL_DISCOVER_EVPID); + CASE(IMSG_CTL_DISCOVER_MSGID); + + CASE(IMSG_CTL_SMTP_SESSION); + + CASE(IMSG_GETADDRINFO); + CASE(IMSG_GETADDRINFO_END); + CASE(IMSG_GETNAMEINFO); + CASE(IMSG_RES_QUERY); + + CASE(IMSG_CERT_INIT); + CASE(IMSG_CERT_CERTIFICATE); + CASE(IMSG_CERT_VERIFY); + + CASE(IMSG_SETUP_KEY); + CASE(IMSG_SETUP_PEER); + CASE(IMSG_SETUP_DONE); + + CASE(IMSG_CONF_START); + CASE(IMSG_CONF_END); + + CASE(IMSG_STAT_INCREMENT); + CASE(IMSG_STAT_DECREMENT); + CASE(IMSG_STAT_SET); + + CASE(IMSG_LKA_AUTHENTICATE); + CASE(IMSG_LKA_OPEN_FORWARD); + CASE(IMSG_LKA_ENVELOPE_SUBMIT); + CASE(IMSG_LKA_ENVELOPE_COMMIT); + + CASE(IMSG_QUEUE_DELIVER); + CASE(IMSG_QUEUE_DELIVERY_OK); + CASE(IMSG_QUEUE_DELIVERY_TEMPFAIL); + CASE(IMSG_QUEUE_DELIVERY_PERMFAIL); + CASE(IMSG_QUEUE_DELIVERY_LOOP); + CASE(IMSG_QUEUE_DISCOVER_EVPID); + CASE(IMSG_QUEUE_DISCOVER_MSGID); + CASE(IMSG_QUEUE_ENVELOPE_ACK); + CASE(IMSG_QUEUE_ENVELOPE_COMMIT); + CASE(IMSG_QUEUE_ENVELOPE_REMOVE); + CASE(IMSG_QUEUE_ENVELOPE_SCHEDULE); + CASE(IMSG_QUEUE_ENVELOPE_SUBMIT); + CASE(IMSG_QUEUE_HOLDQ_HOLD); + CASE(IMSG_QUEUE_HOLDQ_RELEASE); + CASE(IMSG_QUEUE_MESSAGE_COMMIT); + CASE(IMSG_QUEUE_MESSAGE_ROLLBACK); + CASE(IMSG_QUEUE_SMTP_SESSION); + CASE(IMSG_QUEUE_TRANSFER); + + CASE(IMSG_MDA_DELIVERY_OK); + CASE(IMSG_MDA_DELIVERY_TEMPFAIL); + CASE(IMSG_MDA_DELIVERY_PERMFAIL); + CASE(IMSG_MDA_DELIVERY_LOOP); + CASE(IMSG_MDA_DELIVERY_HOLD); + CASE(IMSG_MDA_DONE); + CASE(IMSG_MDA_FORK); + CASE(IMSG_MDA_HOLDQ_RELEASE); + CASE(IMSG_MDA_LOOKUP_USERINFO); + CASE(IMSG_MDA_KILL); + CASE(IMSG_MDA_OPEN_MESSAGE); + + CASE(IMSG_MTA_DELIVERY_OK); + CASE(IMSG_MTA_DELIVERY_TEMPFAIL); + CASE(IMSG_MTA_DELIVERY_PERMFAIL); + CASE(IMSG_MTA_DELIVERY_LOOP); + CASE(IMSG_MTA_DELIVERY_HOLD); + CASE(IMSG_MTA_DNS_HOST); + CASE(IMSG_MTA_DNS_HOST_END); + CASE(IMSG_MTA_DNS_MX); + CASE(IMSG_MTA_DNS_MX_PREFERENCE); + CASE(IMSG_MTA_HOLDQ_RELEASE); + CASE(IMSG_MTA_LOOKUP_CREDENTIALS); + CASE(IMSG_MTA_LOOKUP_SOURCE); + CASE(IMSG_MTA_LOOKUP_HELO); + CASE(IMSG_MTA_LOOKUP_SMARTHOST); + CASE(IMSG_MTA_OPEN_MESSAGE); + CASE(IMSG_MTA_SCHEDULE); + + CASE(IMSG_SCHED_ENVELOPE_BOUNCE); + CASE(IMSG_SCHED_ENVELOPE_DELIVER); + CASE(IMSG_SCHED_ENVELOPE_EXPIRE); + CASE(IMSG_SCHED_ENVELOPE_INJECT); + CASE(IMSG_SCHED_ENVELOPE_REMOVE); + CASE(IMSG_SCHED_ENVELOPE_TRANSFER); + + CASE(IMSG_SMTP_AUTHENTICATE); + CASE(IMSG_SMTP_MESSAGE_COMMIT); + CASE(IMSG_SMTP_MESSAGE_CREATE); + CASE(IMSG_SMTP_MESSAGE_ROLLBACK); + CASE(IMSG_SMTP_MESSAGE_OPEN); + CASE(IMSG_SMTP_CHECK_SENDER); + CASE(IMSG_SMTP_EXPAND_RCPT); + CASE(IMSG_SMTP_LOOKUP_HELO); + + CASE(IMSG_SMTP_REQ_CONNECT); + CASE(IMSG_SMTP_REQ_HELO); + CASE(IMSG_SMTP_REQ_MAIL); + CASE(IMSG_SMTP_REQ_RCPT); + CASE(IMSG_SMTP_REQ_DATA); + CASE(IMSG_SMTP_REQ_EOM); + CASE(IMSG_SMTP_EVENT_RSET); + CASE(IMSG_SMTP_EVENT_COMMIT); + CASE(IMSG_SMTP_EVENT_ROLLBACK); + CASE(IMSG_SMTP_EVENT_DISCONNECT); + + CASE(IMSG_LKA_PROCESSOR_FORK); + CASE(IMSG_LKA_PROCESSOR_ERRFD); + + CASE(IMSG_REPORT_SMTP_LINK_CONNECT); + CASE(IMSG_REPORT_SMTP_LINK_DISCONNECT); + CASE(IMSG_REPORT_SMTP_LINK_TLS); + CASE(IMSG_REPORT_SMTP_LINK_GREETING); + CASE(IMSG_REPORT_SMTP_LINK_IDENTIFY); + CASE(IMSG_REPORT_SMTP_LINK_AUTH); + + CASE(IMSG_REPORT_SMTP_TX_RESET); + CASE(IMSG_REPORT_SMTP_TX_BEGIN); + CASE(IMSG_REPORT_SMTP_TX_ENVELOPE); + CASE(IMSG_REPORT_SMTP_TX_COMMIT); + CASE(IMSG_REPORT_SMTP_TX_ROLLBACK); + + CASE(IMSG_REPORT_SMTP_PROTOCOL_CLIENT); + CASE(IMSG_REPORT_SMTP_PROTOCOL_SERVER); + + CASE(IMSG_FILTER_SMTP_BEGIN); + CASE(IMSG_FILTER_SMTP_END); + CASE(IMSG_FILTER_SMTP_PROTOCOL); + CASE(IMSG_FILTER_SMTP_DATA_BEGIN); + CASE(IMSG_FILTER_SMTP_DATA_END); + + CASE(IMSG_CA_RSA_PRIVENC); + CASE(IMSG_CA_RSA_PRIVDEC); + CASE(IMSG_CA_ECDSA_SIGN); + default: + (void)snprintf(buf, sizeof(buf), "IMSG_??? (%d)", type); + + return buf; + } +} + +#ifdef BSD_AUTH +int +parent_auth_bsd(const char *username, const char *password) +{ + char user[LOGIN_NAME_MAX]; + char pass[LINE_MAX]; + int ret; + + (void)strlcpy(user, username, sizeof(user)); + (void)strlcpy(pass, password, sizeof(pass)); + + ret = auth_userokay(user, NULL, "auth-smtp", pass); + if (ret) + return LKA_OK; + return LKA_PERMFAIL; +} +#endif + +#ifdef USE_PAM +int +pam_conv_password(int num_msg, const struct pam_message **msg, + struct pam_response **respp, void *password) +{ + struct pam_response *response; + + if (num_msg != 1) + return PAM_CONV_ERR; + + response = calloc(1, sizeof(struct pam_response)); + if (response == NULL || (response->resp = strdup(password)) == NULL) { + free(response); + return PAM_BUF_ERR; + } + + *respp = response; + return PAM_SUCCESS; +} +int +parent_auth_pam(const char *username, const char *password) +{ + int rc; + pam_handle_t *pamh = NULL; + struct pam_conv conv = { pam_conv_password, (char *)password }; + + if ((rc = pam_start(USE_PAM_SERVICE, username, &conv, &pamh)) != PAM_SUCCESS) + goto end; + if ((rc = pam_authenticate(pamh, 0)) != PAM_SUCCESS) + goto end; + if ((rc = pam_acct_mgmt(pamh, 0)) != PAM_SUCCESS) + goto end; + +end: + pam_end(pamh, rc); + + switch (rc) { + case PAM_SUCCESS: + return LKA_OK; + case PAM_SYSTEM_ERR: + case PAM_ABORT: + case PAM_AUTHINFO_UNAVAIL: + return LKA_TEMPFAIL; + default: + return LKA_PERMFAIL; + } +} +#endif + +#ifdef HAVE_GETSPNAM +int +parent_auth_getspnam(const char *username, const char *password) +{ + struct spwd *pw; + char *ep; + + errno = 0; + do { + pw = getspnam(username); + } while (pw == NULL && errno == EINTR); + + if (pw == NULL) { + if (errno) + return LKA_TEMPFAIL; + return LKA_PERMFAIL; + } + + if ((ep = crypt(password, pw->sp_pwdp)) == NULL) + return LKA_PERMFAIL; + + if (strcmp(pw->sp_pwdp, ep) == 0) + return LKA_OK; + + return LKA_PERMFAIL; +} +#endif + +int +parent_auth_pwd(const char *username, const char *password) +{ + struct passwd *pw; + char *ep; + + errno = 0; + do { + pw = getpwnam(username); + } while (pw == NULL && errno == EINTR); + + if (pw == NULL) { + if (errno) + return LKA_TEMPFAIL; + return LKA_PERMFAIL; + } + + if ((ep = crypt(password, pw->pw_passwd)) == NULL) + return LKA_PERMFAIL; + + if (strcmp(pw->pw_passwd, ep) == 0) + return LKA_OK; + + return LKA_PERMFAIL; +} + +int +parent_auth_user(const char *username, const char *password) +{ +#if defined(BSD_AUTH) + return (parent_auth_bsd(username, password)); +#elif defined(USE_PAM) + return (parent_auth_pam(username, password)); +#elif defined(HAVE_GETSPNAM) + return (parent_auth_getspnam(username, password)); +#else + return (parent_auth_pwd(username, password)); +#endif +} diff --git a/smtpd/smtpd.conf b/smtpd/smtpd.conf new file mode 100644 index 00000000..a7ba6c64 --- /dev/null +++ b/smtpd/smtpd.conf @@ -0,0 +1,19 @@ +# $OpenBSD: smtpd.conf,v 1.10 2018/05/24 11:40:17 gilles Exp $ + +# This is the smtpd server system-wide configuration file. +# See smtpd.conf(5) for more information. + +table aliases file:/etc/mail/aliases + +# To accept external mail, replace with: listen on all +# +listen on localhost + +action "local" maildir alias <aliases> +action "relay" relay + +# Uncomment the following to accept external mail for domain "example.org" +# +# match from any for domain "example.org" action "local" +match for local action "local" +match from local for any action "relay" diff --git a/smtpd/smtpd.conf.5 b/smtpd/smtpd.conf.5 new file mode 100644 index 00000000..c543c662 --- /dev/null +++ b/smtpd/smtpd.conf.5 @@ -0,0 +1,1240 @@ +.\" $OpenBSD: smtpd.conf.5,v 1.250 2020/04/25 09:20:38 eric Exp $ +.\" +.\" Copyright (c) 2008 Janne Johansson <jj@openbsd.org> +.\" Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> +.\" Copyright (c) 2012 Gilles Chehade <gilles@poolp.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: April 25 2020 $ +.Dt SMTPD.CONF 5 +.Os +.Sh NAME +.Nm smtpd.conf +.Nd Simple Mail Transfer Protocol daemon configuration file +.Sh DESCRIPTION +.Nm +is the configuration file for the mail daemon +.Xr smtpd 8 . +.Pp +When mail arrives, +each +.Dq RCPT TO: +command generates a mail envelope. +If an envelope matches +any of a pre-designated set of criteria +(using the +.Ic match +directive), +the message is accepted for delivery. +A copy of the message, as well as its associated envelopes, +is saved in the mail queue and later dispatched +according to an associated set of actions +(using the +.Ic action +directive). +If an envelope does not match any options, +it is rejected. +The match rules are evaluated sequentially, +with the first match winning. +.Pp +The format of the configuration file is fairly flexible. +The current line can be extended over multiple lines using a backslash +.Pq Sq \e . +Comments can be put anywhere in the file using a hash mark +.Pq Sq # , +and extend to the end of the current line. +Care should be taken when commenting out multi-line text: +the comment is effective until the end of the entire block. +Argument names not beginning with a letter, digit, or underscore, +as well as reserved words +(such as +.Ic listen , +.Ic match , +and +.Cm port ) , +must be quoted. +Arguments containing whitespace should be surrounded by double quotes +.Pq \&" . +.Pp +Macros can be defined that are later expanded in context. +Macro names must start with a letter, digit, or underscore, +and may contain any of those characters, +but may not be reserved words. +Macros are not expanded inside quotes. +For example: +.Bd -literal -offset indent +lan_addr = "192.168.0.1" +listen on $lan_addr +listen on $lan_addr tls auth +.Ed +.Pp +The syntax of +.Nm +is described below. +.Bl -tag -width Ds +.It Ic action Ar name method Op Ar options +When the queue runner processes an envelope from the mail queue, +it carries out the +.Ic action +.Ar name , +selected by the +.Ic match No ... Cm action +directive when the message was received. +The +.Ic action +directive provides configuration data for delivery attempts. +Required lookups are performed at the time of each delivery attempt. +Consequently, changing an +.Ic action +directive or the files it references and restarting the +.Xr smtpd 8 +daemon causes the changes to take effect for subsequent delivery +attempts for the respective dispatcher +.Ar name , +even for messages that were already stuck in the queue +prior to the configuration changes. +.Pp +The delivery +.Ar method +parameter may be one of the following: +.Bl -tag -width Ds +.It Cm expand-only +Only accept the message if a delivery method was specified +in an aliases or +.Pa .forward +file. +.It Cm forward-only +Only accept the message if the recipient results in a remote address +after the processing of aliases or forward file. +.It Cm lmtp Ar destination Op Ar rcpt-to +Deliver the message to an LMTP server at +.Ar destination . +The location may be expressed as host:port or as a UNIX socket. +.Pp +Optionally, +.Ar rcpt-to +might be specified to use the +recipient email address (after expansion) instead of the +local user in the LMTP session as RCPT TO. +.It Cm maildir Op Ar pathname Op Cm junk +Deliver the message to the maildir in +.Ar pathname +if specified, or by default to +.Pa ~/Maildir . +.Pp +The +.Ar pathname +may contain format specifiers that are expanded before use +.Pq see Sx FORMAT SPECIFIERS . +.Pp +If the +.Cm junk +argument is provided, the message will be moved to the +.Ql Junk +folder if it contains a positive +.Ql X-Spam +header. +This folder will be created under +.Ar pathname +if it does not yet exist. +.It Cm mbox +Deliver the message to the user's mbox with +.Xr mail.local 8 . +.It Cm mda Ar command +Delegate the delivery to a +.Ar command +that receives the message on its standard input. +.Pp +The +.Ar command +may contain format specifiers that are expanded before use +.Pq see Sx FORMAT SPECIFIERS . +.It Cm relay +Relay the message to another SMTP server. +.El +.Pp +The local delivery methods support additional options: +.Bl -tag -width Ds +.It Cm alias Pf < Ar table Ns > +Use the mapping +.Ar table +for +.Xr aliases 5 +expansion. +.It Xo +.Cm ttl +.Sm off +.Ar n +.Brq Cm s | m | h | d +.Sm on +.Xc +Specify how long a message may remain in the queue. +.It Cm user Ar username +Specify the +.Ar username +for performing the delivery, to be looked up with +.Xr getpwnam 3 . +.Pp +This is used for virtual hosting where a single username +is in charge of handling delivery for all virtual users. +.Pp +This option is not usable with the +.Cm mbox +delivery method. +.It Cm userbase Pf < Ar table Ns > +Use the mapping +.Ar table +for user lookups instead of the +.Xr getpwnam 3 +function. +.Pp +The +.Cm userbase +does not apply for the +.Cm user +option. +.It Cm virtual Pf < Ar table Ns > +Use the mapping +.Ar table +for virtual expansion. +The aliasing table format is described in +.Xr table 5 . +.It Cm wrapper Ar name +Use the wrapper specified in +.Cm mda wrapper . +.El +.Pp +The relay delivery methods also support additional options: +.Bl -tag -width Ds +.It Cm backup +Operate as a backup mail exchanger delivering messages to any mail exchanger +with higher priority. +.It Cm backup mx Ar name +Operate as a backup mail exchanger delivering messages to any mail exchanger +with higher priority than mail exchanger identified as +.Ar name . +.It Cm helo Ar heloname +Advertise +.Ar heloname +as the hostname to other mail exchangers during the HELO phase. +.It Cm helo-src Pf < Ar table Ns > +Use the mapping +.Ar table +to look up a hostname matching the source address, +to advertise during the HELO phase. +.It Cm domain Pf < Ar domains Ns > +Do not perform MX lookups but look up destination domain in +.Ar domains +and use matching relay url as relay host. +.It Cm host Ar relay-url +Do not perform MX lookups but relay messages to the relay host described by +.Ar relay-url . +The format for +.Ar relay-url +is +.Sm off +.Op Ar proto No :// Op Ar label No @ +.Ar host Op : Ar port . +.Sm on +The following protocols are available: +.Pp +.Bl -tag -width "smtp+notls" -compact +.It smtp +Normal SMTP session with opportunistic STARTTLS +(the default). +.It smtp+tls +Normal SMTP session with mandatory STARTTLS. +.It smtp+notls +Plain text SMTP session without TLS. +.It lmtp +LMTP session. +.Ar port +is required. +.It smtps +SMTP session with forced TLS on connection, default port is 465. +.El +Unless noted, +.Ar port +defaults to 25. +.Pp +The +.Ar label +corresponds to an entry in a credentials table, +as documented in +.Xr table 5 . +It is used with the +.Dq smtp+tls +and +.Dq smtps +protocols for authentication. +Server certificates for those protocols are verified by default. +.It Cm srs +When relaying a mail resulting from a forward, +use the Sender Rewriting Scheme to rewrite sender address. +.It Cm tls Op Cm no-verify +Require TLS to be used when relaying, using mandatory STARTTLS by default. +When used with a smarthost, the protocol must not be +.Dq smtp+notls:// . +If +.Cm no-verify +is specified, do not require a valid certificate. +.It Cm auth Pf < Ar table Ns > +Use the mapping +.Ar table +for connecting to +.Ar relay-url +using credentials. +This option is usable only with +.Cm host +option. +The credential table format is described in +.Xr table 5 . +.It Cm mail-from Ar mailaddr +Use +.Ar mailaddr +as the MAIL FROM address within the SMTP transaction. +.It Cm src Ar sourceaddr | Pf < Ar sourceaddr Ns > +Use the string or list table +.Ar sourceaddr +for the source IP address, +which is useful on machines with multiple interfaces. +If the list contains more than one address, all of them are used +in such a way that traffic is routed as efficiently as possible. +.El +.It Ic bounce Cm warn-interval Ar delay Op , Ar delay ... +Send warning messages to the envelope sender when temporary delivery +failures cause a message to remain on the queue for longer than +.Ar delay . +Each +.Ar delay +parameter consists of a positive decimal integer and a unit +.Cm s , m , h , +or +.Cm d . +At most four +.Ar delay +parameters can be specified. +The default is +.Qq Ic bounce Cm warn-interval No 4h , +sending a single warning after four hours. +.It Ic ca Ar caname Cm cert Ar cafile +Associate the Certificate Authority (CA) certificate file +.Ar cafile +with host +.Ar caname , +and use that file as the CA certificate for that host. +.Ar caname +is the server's name, +derived from the default hostname +or set using either +.Pa /etc/mail/mailname +or using the +.Ic hostname +directive. +.It Ic filter Ar chain-name Ic chain Brq Ar filter-name Op , Ar ... +Register a chain of filters +.Ar chain-name , +consisting of the filters listed from +.Ar filter-name . +Filters part of a filter chain are executed in order of declaration for +each phase that they are registered for. +A filter chain may be used in place of a filter for any directive but +filter chains themselves. +.It Ic filter Ar filter-name Ic phase Ar phase-name Ic match Ar conditions decision +Register a filter +.Ar filter-name . +A +.Ar decision +about what to do with the mail is taken at phase +.Ar phase-name +when matching +.Ar conditions . +Phases, matching conditions, and decisions are described in +.Sx MAIL FILTERING , +below. +.It Ic filter Ar filter-name Ic proc Ar proc-name +Register +.Qq proc +filter +.Ar filter-name +backed by the +.Ar proc-name +process. +.It Ic filter Ar filter-name Ic proc-exec Ar command +Register and execute +.Qq proc +filter +.Ar filter-name +from +.Ar command . +If +.Ar command +starts with a slash it is executed with an absolute path, +else it will be run from +.Dq /usr/local/libexec/smtpd/ . +.It Ic include Qq Ar pathname +Replace this directive with the content of the additional configuration +file at the absolute +.Ar pathname . +.It Ic listen on Ar interface Oo Ar family Oc Op Ar options +Listen on the +.Ar interface +for incoming connections, using the same syntax as for +.Xr ifconfig 8 . +The +.Ar interface +parameter may also be an interface group, an IP address, or a domain name. +Listening can optionally be restricted to a specific address +.Ar family , +which can be either +.Cm inet4 +or +.Cm inet6 . +.Pp +The +.Ar options +are as follows: +.Bl -tag -width Ds +.It Cm auth Op Pf < Ar authtable Ns > +Support SMTPAUTH: clients may only start SMTP transactions +after successful authentication. +Users are authenticated against either their own normal login credentials +or a credentials table +.Ar authtable , +the format of which is described in +.Xr table 5 . +.It Cm auth-optional Op Pf < Ar authtable Ns > +Support SMTPAUTH optionally: +clients need not authenticate, but may do so. +This allows a +.Ic listen on +directive to both accept incoming mail from untrusted senders +and permit outgoing mail from authenticated users +(using +.Cm match auth ) . +It can be used in situations where it is not possible to listen on a separate port +(usually the submission port, 587) +for users to authenticate. +.It Ic ca Ar caname +For secure connections, +use the CA certificate associated with +.Ar caname +(declared in a +.Ic ca +directive) +as the CA certificate when verifying client certificates. +.It Ic filter Ar name +Apply filter +.Ar name +on connections handled by this listener. +.It Cm hostname Ar hostname +Use +.Ar hostname +in the greeting banner instead of the default server name. +.It Cm hostnames Pf < Ar names Ns > +Override the server name for specific addresses. +The +.Ar names +table contains a mapping of IP addresses to hostnames. +If the address on which the connection arrives appears in the mapping, +the associated hostname is used. +.It Cm mask-src +Omit the +.Sy from +part when prepending +.Dq Received +headers. +.It Cm no-dsn +Disable the DSN (Delivery Status Notification) extension. +.It Cm pki Ar pkiname +For secure connections, +use the certificate associated with +.Ar pkiname +(declared in a +.Ic pki +directive) +to prove a mail server's identity. +.It Cm port Op Ar port +Listen on the given +.Ar port +instead of the default port 25. +.It Cm proxy-v2 +Support the PROXYv2 protocol, +rewriting appropriately source address received from proxy. +.It Cm received-auth +In +.Dq Received +headers, report whether the session was authenticated +and by which local user. +.It Cm senders Pf < Ar users Ns > Op Cm masquerade +Look up the authenticated user in the +.Ar users +mapping table to find the email addresses that user is allowed +to submit mail as. +In addition, if the +.Cm masquerade +option is provided, +the From header is rewritten +to match the sender provided in the SMTP session. +.It Cm smtps +Support SMTPS, by default on port 465. +Mutually exclusive with +.Cm tls . +.It Cm tag Ar tag +Clients connecting to the listener are tagged with the given +.Ar tag . +.It Cm tls +Support STARTTLS, by default on port 25. +Mutually exclusive with +.Cm smtps . +.It Cm tls-require Op Cm verify +Like +.Cm tls , +but force clients to establish a secure connection +before being allowed to start an SMTP transaction. +With the +.Cm verify +option, clients must also provide a valid certificate +to establish an SMTP session. +.El +.It Ic listen on Cm socket Op Ar options +Listen for incoming SMTP connections on the Unix domain socket +.Pa /var/run/smtpd.sock . +This is done by default, even if the directive is absent. +.Pp +The +.Ar options +are as follows: +.Bl -tag -width Ds +.It Ic filter Ar name +Apply filter +.Ar name +on connections handled by this listener. +.It Cm mask-src +Omit the +.Sy from +part when prepending +.Dq Received +headers. +.It Cm tag Ar tag +Clients connecting to the listener are tagged with the given +.Ar tag . +.El +.It Ic match Ar options Cm action Ar name +If at least one mail envelope matches the +.Ar options +of one +.Ic match Cm action +directive, receive the incoming message, put a copy into each +matching envelope, and atomically save the envelopes to the mail +spool for later processing by the respective dispatcher +.Ar name . +.Pp +The following matching options are supported and can all be negated: +.Bl -tag -width Ds +.It Xo +.Op Ic \&! +.Cm for any +.Xc +Specify that session may address any destination. +.It Xo +.Op Ic \&! +.Cm for local +.Xc +Specify that session may address any local domain. +This is the default, and may be omitted. +.It Xo +.Op Ic \&! +.Cm for domain +.Ar domain | Pf < Ar domain Ns > +.Xc +Specify that session may address the string or list table +.Ar domain . +.It Xo +.Op Ic \&! +.Cm for domain regex +.Ar domain | Pf < Ar domain Ns > +.Xc +Specify that session may address the regex or regex table +.Ar domain . +.It Xo +.Op Ic \&! +.Cm for rcpt-to +.Ar recipient | Pf < Ar recipient Ns > +.Xc +Specify that session may address the string or list table +.Ar recipient . +.It Xo +.Op Ic \&! +.Cm for rcpt-to regex +.Ar recipient | Pf < Ar recipient Ns > +.Xc +Specify that session may address the regex or regex table +.Ar recipient . +.It Xo +.Op Ic \&! +.Cm from any +.Xc +Specify that session may originate from any source. +.It Xo +.Op Ic \&! +.Cm from auth +.Xc +Specify that session may originate from any authenticated user, +no matter the source IP address. +.It Xo +.Op Ic \&! +.Cm from auth +.Ar user | Pf < Ar user Ns > +.Xc +Specify that session may originate from authenticated user or user list +.Ar user , +no matter the source IP address. +.It Xo +.Op Ic \&! +.Cm from auth +.Ar user | Pf < Ar user Ns > +.Xc +Specify that session may originate from authenticated regex or regex list +.Ar user , +no matter the source IP address. +.It Xo +.Op Ic \&! +.Cm from local +.Xc +Specify that session may only originate from a local IP address, +or from the local enqueuer. +This is the default, and may be omitted. +.It Xo +.Op Ic \&! +.Cm from mail-from +.Ar sender | Pf < Ar sender Ns > +.Xc +Specify that session may originate from sender or sender list +.Ar sender , +no matter the source IP address. +.It Xo +.Op Ic \&! +.Cm from mail-from regex +.Ar sender | Pf < Ar sender Ns > +.Xc +Specify that session may originate from regex or regex list +.Ar sender , +no matter the source IP address. +.It Xo +.Op Ic \&! +.Cm from rdns +.Xc +Specify that session may only originate from an IP address that +resolves to a reverse DNS. +.It Xo +.Op Ic \&! +.Cm from rdns +.Ar hostname | Pf < Ar hostname Ns > +.Xc +Specify that session may only originate from an IP address that +resolves to a reverse DNS matching string or list string +.Ar hostname . +.It Xo +.Op Ic \&! +.Cm from rdns regex +.Ar hostname | Pf < Ar hostname Ns > +.Xc +Specify that session may only originate from an IP address that +resolves to a reverse DNS matching regex or list regex +.Ar hostname . +.It Xo +.Op Ic \&! +.Cm from socket +.Xc +Specify that session may only originate from the local enqueuer. +.It Xo +.Op Ic \&! +.Cm from src +.Ar address | Pf < Ar address Ns > +.Xc +Specify that session may only originate from string or list table +.Ar address +which can be a specific address or a subnet expressed in CIDR-notation. +.It Xo +.Op Ic \&! +.Cm from src regex +.Ar address | Pf < Ar address Ns > +.Xc +Specify that session may only originate from regex or regex table +.Ar address +which can be a specific address or a subnet expressed in CIDR-notation. +.El +.Pp +In addition, the following transaction options: +.Bl -tag -width Ds +.It Xo +.Op Ic \&! +.Cm auth +.Xc +Matches transactions which have been authenticated. +.It Xo +.Op Ic \&! +.Cm auth +.Ar username | Pf < Ar username Ns > +.Xc +Matches transactions which have been authenticated for user or user list +.Ar username . +.It Xo +.Op Ic \&! +.Cm auth regex +.Ar username | Pf < Ar username Ns > +.Xc +Matches transactions which have been authenticated for regex or regex list +.Ar username . +.It Xo +.Op Ic \&! +.Cm helo +.Ar helo-name | Pf < Ar helo-name Ns > +.Xc +Specify that session's HELO / EHLO should match the string or list table +.Ar helo-name . +.It Xo +.Op Ic \&! +.Cm helo regex +.Ar helo-name | Pf < Ar helo-name Ns > +.Xc +Specify that session's HELO / EHLO should match the regex or regex table +.Ar helo-name . +.It Xo +.Op Ic \&! +.Cm mail-from +.Ar sender | Pf < Ar sender Ns > +.Xc +Specify that transactions's MAIL FROM should match the string or list table +.Ar sender . +.It Xo +.Op Ic \&! +.Cm mail-from regex +.Ar sender | Pf < Ar sender Ns > +.Xc +Specify that transactions's MAIL FROM should match the regex or regex table +.Ar sender . +.It Xo +.Op Ic \&! +.Cm rcpt-to +.Ar recipient | Pf < Ar recipient Ns > +.Xc +Specify that transaction's RCPT TO should match the string or list table +.Ar recipient . +.It Xo +.Op Ic \&! +.Cm rcpt-to regex +.Ar recipient | Pf < Ar recipient Ns > +.Xc +Specify that transaction's RCPT TO should match the regex or regex table +.Ar recipient . +.It Xo +.Op Ic \&! +.Cm tag Ar tag +.Xc +Matches transactions tagged with the given +.Ar tag . +.It Xo +.Op Ic \&! +.Cm tag regex Ar tag +.Xc +Matches transactions tagged with the given +.Ar tag +regex. +.It Xo +.Op Ic \&! +.Cm tls +.Xc +Specify that transaction should take place in a TLS channel. +.El +.It Ic match Ar options Cm reject +Reject the incoming message during the SMTP dialogue. +The same +.Ar options +are supported as for the +.Ic match Cm action +directive. +.It Ic mda Cm wrapper Ar name command +Associate +.Ar command +with the mail delivery agent wrapper named +.Ar name . +When a local delivery specifies a wrapper, the +.Ar command +associated with the wrapper will be executed instead. +The command may contain format specifiers +.Pq see Sx FORMAT SPECIFIERS . +.It Ic mta Cm max-deferred Ar number +When delivery to a given host is suspended due to temporary failures, +cache at most +.Ar number +envelopes for that host such that they can be delivered +as soon as another delivery succeeds to that host. +The default is 100. +.It Ic pki Ar pkiname Cm cert Ar certfile +Associate certificate file +.Ar certfile +with host +.Ar pkiname , +and use that file to prove the identity of the mail server to clients. +.Ar pkiname +is the server's name, +derived from the default hostname +or set using either +.Pa /etc/mail/mailname +or using the +.Ic hostname +directive. +If a fallback certificate or SNI is wanted, the +.Sq * +wildcard may be used as +.Ar pkiname . +.Pp +A certificate chain may be created by appending one or many certificates, +including a Certificate Authority certificate, +to +.Ar certfile . +The creation of certificates is documented in +.Xr starttls 8 . +.It Ic pki Ar pkiname Cm key Ar keyfile +Associate the key located in +.Ar keyfile +with host +.Ar pkiname . +.It Ic pki Ar pkiname Cm dhe Ar params +Specify the DHE parameters to use for DHE cipher suites with host +.Ar pkiname . +Valid parameter values are +.Cm none , +.Cm legacy , +and +.Cm auto . +For +.Cm legacy , +a fixed key length of 1024 bits is used, whereas for +.Cm auto , +the key length is determined automatically. +The default is +.Cm none , +which disables DHE cipher suites. +.It Ic proc Ar proc-name Ar command +Register an external process named +.Ar proc-name +from +.Ar command . +Such processes may be used to share the same instance between multiple filters. +If +.Ar command +starts with a slash it is executed with an absolute path, +else it will be run from +.Dq /usr/local/libexec/smtpd/ . +.It Ic queue Cm compression +Store queue files in a compressed format. +This may be useful to save disk space. +.It Ic queue Cm encryption Op Ar key +Encrypt queue files with +.Xr EVP_aes_256_gcm 3 . +If no +.Ar key +is specified, it is read with +.Xr getpass 3 . +If the string +.Cm stdin +or a single dash +.Pq Ql - +is given instead of a +.Ar key , +the key is read from the standard input. +.It Ic queue Cm ttl Ar delay +Set the default expiration time for temporarily undeliverable +messages, given as a positive decimal integer followed by a unit +.Cm s , m , h , +or +.Cm d . +The default is four days +.Pq 4d . +.It Ic smtp Cm ciphers Ar control +Set the +.Ar control +string for +.Xr SSL_CTX_set_cipher_list 3 . +The default is +.Qq HIGH:!aNULL:!MD5 . +.It Ic smtp limit Cm max-mails Ar count +Limit the number of messages to +.Ar count +for each session. +The default is 100. +.It Ic smtp limit Cm max-rcpt Ar count +Limit the number of recipients to +.Ar count +for each transaction. +The default is 1000. +.It Ic smtp Cm max-message-size Ar size +Reject messages larger than +.Ar size , +given as a positive number of bytes or as a string to be parsed with +.Xr scan_scaled 3 . +The default is +.Qq 35M . +.It Ic smtp Cm sub-addr-delim Ar character +When resolving the local part of a local email address, ignore the ASCII +.Ar character +and all characters following it. +The default is +.Ql + . +.It Ic srs Cm key Ar secret +Set the secret key to use for SRS, +the Sender Rewriting Scheme. +.It Ic srs Cm key backup Ar secret +Set a backup secret key to use as a fallback for SRS. +This can be used to implement SRS key rotation. +.It Ic srs Cm ttl Ar delay +Set the time-to-live delay for SRS envelopes. +After this delay, +a bounce reply to the SRS address will be discarded to limit risks of forged addresses. +The default is four days +.Pq 4d . +.It Ic table Ar name Oo Ar type : Oc Ns Ar pathname +Tables provide additional configuration information for +.Xr smtpd 8 +in the form of lists or key-value mappings. +The format of the entries depends on what the table is used for. +Refer to +.Xr table 5 +for the exhaustive documentation. +.Pp +Each table is identified by an arbitrary, unique +.Ar name . +.Pp +If the +.Ar type +is +.Cm db , +information is stored in a file created with +.Xr makemap 8 ; +if it is +.Cm file +or omitted, information is stored in a plain text file +using the format described in +.Xr table 5 . +The +.Ar pathname +to the file must be absolute. +.It Ic table Ar name Brq Ar value Op , Ar ... +Instead of using a separate file, declare a list table +containing the given static +.Ar value Ns s . +The table must contain at least one value and may declare multiple values as a +comma-separated (whitespace optional) list. +.It Ic table Ar name Brq Ar key Ns = Ns Ar value Op , Ar ... +Instead of using a separate file, declare a mapping table +containing the given static +.Ar key Ns - Ns Ar value +pairs. +The table must contain at least one key-value pair and may declare +multiple pairs as a comma-separated (whitespace optional) list. +.El +.Ss MAIL FILTERING +In a regular workflow, +.Xr smtpd 8 +may accept or reject a message based only on the content of envelopes. +Its decisions are about the handling of the message, +not about the handling of an active session. +.Pp +Filtering extends the decision making process by allowing +.Xr smtpd 8 +to stop at each phase of an SMTP session, +check that conditions are met, +then decide if a session is allowed to move forward. +.Pp +With filtering, +a session may be interrupted at any phase before an envelope is complete. +A message may also be rejected after being submitted, +regardless of whether the envelope was accepted or not. +.Pp +The following phases are currently supported: +.Bl -column mail-from -offset indent +.It connect Ta upon connection, before a banner is displayed +.It helo Ta after HELO command is submitted +.It ehlo Ta after EHLO command is submitted +.It mail-from Ta after MAIL FROM command is submitted +.It rcpt-to Ta after RCPT TO command is submitted +.It data Ta after DATA command is submitted +.It commit Ta after message is fully is submitted +.El +.Pp +At each phase, various conditions may be matched. +The fcrdns, rdns, and src data are available in all phases, +but other data must have been already submitted before they are available. +.Bl -column XXXXXXXXXXXXXXXXXXXXX -offset indent +.It fcrdns Ta forward-confirmed reverse DNS is valid +.It rdns Ta session has a reverse DNS +.It rdns Pf < Ar table Ns > Ta session has a reverse DNS in table +.It src Pf < Ar table Ns > Ta source address is in table +.It helo Pf < Ar table Ns > Ta helo name is in table +.It auth Ta session is authenticated +.It auth Pf < Ar table Ns > Ta session username is in table +.It mail-from Pf < Ar table Ns > Ta sender address is in table +.It rcpt-to Pf < Ar table Ns > Ta recipient address is in table +.El +.Pp +These conditions may all be negated by prefixing them with an exclamation mark: +.Bl -column XXXXXXXXXXXXXXXXXXXXX -offset indent +.It !fcrdns Ta forward-confirmed reverse DNS is invalid +.El +.Pp +Any conditions using a table may indicate that tables hold regex by +prefixing the table name with the keyword regex. +.Bl -column XXXXXXXXXXXXXXXXXXXXX -offset indent +.It helo regex Pf < Ar table Ns > Ta helo name matches a regex in table +.El +.Pp +Finally, a number of decisions may be taken: +.Bl -column XXXXXXXXXXXXXXXXXXXXX -offset indent +.It bypass Ta the session or transaction bypasses filters +.It disconnect Ar message Ta the session is disconnected with message +.It junk Ta the session or transaction is junked, i.e., an +.Ql X-Spam: yes +header is added to any messages +.It reject Ar message Ta the command is rejected with message +.It rewrite Ar value Ta the command parameter is rewritten with value +.El +.Pp +Decisions that involve a message require that the message be RFC valid, +meaning that they should either start with a 4xx or 5xx status code. +Descisions can be taken at any phase, +though junking can only happen before a message is committed. +.Ss FORMAT SPECIFIERS +Some configuration directives support expansion of their parameters at runtime. +Such directives (for example +.Ic action Cm maildir , +.Ic action Cm mda ) +may use format specifiers which are expanded before delivery or +relaying. +The following formats are currently supported: +.Bl -column %{user.directory} -offset indent +.It %{sender} Ta sender email address, may be empty string +.It %{sender.user} Ta user part of the sender email address, may be empty +.It %{sender.domain} Ta domain part of the sender email address, may be empty +.It %{rcpt} Ta recipient email address +.It %{rcpt.user} Ta user part of the recipient email address +.It %{rcpt.domain} Ta domain part of the recipient email address +.It %{dest} Ta recipient email address after expansion +.It %{dest.user} Ta user part after expansion +.It %{dest.domain} Ta domain part after expansion +.It %{user.username} Ta local user +.It %{user.directory} Ta home directory of the local user +.It %{mbox.from} Ta name used in mbox From separator lines +.It %{mda} Ta mda command, only available for mda wrappers +.El +.Pp +Expansion formats also support partial expansion using the optional +bracket notations with substring offset. +For example, with recipient domain +.Dq example.org : +.Bl -column %{rcpt.domain[0:-4]} -offset indent +.It %{rcpt.domain[0]} Ta expands to Dq e +.It %{rcpt.domain[1]} Ta expands to Dq x +.It %{rcpt.domain[8:]} Ta expands to Dq org +.It %{rcpt.domain[-3:]} Ta expands to Dq org +.It %{rcpt.domain[0:6]} Ta expands to Dq example +.It %{rcpt.domain[0:-4]} Ta expands to Dq example +.El +.Pp +In addition, modifiers may be applied to the token. +For example, with recipient +.Dq User+Tag@Example.org : +.Bl -column %{rcpt:lowercase|strip} -offset indent +.It %{rcpt:lowercase} Ta expands to Dq user+tag@example.org +.It %{rcpt:uppercase} Ta expands to Dq USER+TAG@EXAMPLE.ORG +.It %{rcpt:strip} Ta expands to Dq User@Example.org +.It %{rcpt:lowercase|strip} Ta expands to Dq user@example.org +.El +.Pp +For security concerns, expanded values are sanitized and potentially +dangerous characters are replaced with +.Sq \&: . +In situations where they are desirable, the +.Dq raw +modifier may be applied. +For example, with recipient +.Dq user+t?g@example.org : +.Bl -column %{rcpt:raw} -offset indent +.It %{rcpt} Ta expands to Dq user+t:g@example.org +.It %{rcpt:raw} Ta expands to Dq user+t?g@example.org +.El +.Sh FILES +.Bl -tag -width "/etc/mail/smtpd.confXXX" -compact +.It Pa /etc/mail/smtpd.conf +Default +.Xr smtpd 8 +configuration file. +.It Pa /etc/mail/mailname +If this file exists, +the first line is used as the server name. +Otherwise, the server name is derived from the local hostname returned by +.Xr gethostname 3 , +either directly if it is a fully qualified domain name, +or by retrieving the associated canonical name through +.Xr getaddrinfo 3 . +.It Pa /var/run/smtpd.sock +Unix domain socket for incoming SMTP connections. +.It Pa /var/spool/smtpd/ +Spool directories for mail during processing. +.El +.Sh EXAMPLES +The default +.Nm +file which ships with +.Ox +listens on the loopback network interface +.Pq Pa lo0 +and allows for mail from users and daemons on the local machine, +as well as permitting email to remote servers. +Some more complex configurations are given below. +.Pp +This first example is the same as the default configuration, +but all outgoing mail is forwarded to a remote SMTP server. +A secrets file is needed to specify a username and password: +.Bd -literal -offset indent +# touch /etc/mail/secrets +# chmod 640 /etc/mail/secrets +# chown root:_smtpd /etc/mail/secrets +# echo "bob username:password" > /etc/mail/secrets +.Ed +.Pp +.Nm +would look like this: +.Bd -literal -offset indent +table aliases file:/etc/mail/aliases +table secrets file:/etc/mail/secrets + +listen on lo0 + +action "local_mail" mbox alias <aliases> +action "outbound" relay host smtp+tls://bob@smtp.example.com \e + auth <secrets> + +match from local for local action "local_mail" +match from local for any action "outbound" +.Ed +.Pp +In this second example, +the aim is to permit mail delivery and relaying only for users that can authenticate +(using their normal login credentials). +An RSA certificate must be provided to prove the server's identity. +The mail server listens on all interfaces the default routes point to. +Mail with a local destination is sent to an external MDA. +First, the RSA certificate is created: +.Bd -literal -offset indent +# openssl genrsa \-out /etc/ssl/private/mail.example.com.key 4096 +# openssl req \-new \-x509 \-key /etc/ssl/private/mail.example.com.key \e + \-out /etc/ssl/mail.example.com.crt \-days 365 +# chmod 600 /etc/ssl/mail.example.com.crt +# chmod 600 /etc/ssl/private/mail.example.com.key +.Ed +.Pp +In the example above, +a certificate valid for one year was created. +The configuration file would look like this: +.Bd -literal -offset indent +pki mail.example.com cert "/etc/ssl/mail.example.com.crt" +pki mail.example.com key "/etc/ssl/private/mail.example.com.key" + +table aliases file:/etc/mail/aliases + +listen on lo0 +listen on egress tls pki mail.example.com auth + +action mda_with_aliases mda "/path/to/mda \-f \-" alias <aliases> +action mda_without_aliases mda "/path/to/mda \-f \-" +action "outbound" relay + +match for local action mda_with_aliases +match from any for domain example.com action mda_without_aliases +match for any action "outbound" +match auth from any for any action "outbound" +.Ed +.Pp +For sites that wish to sign messages using DKIM, +the following example uses +.Sy opensmtpd-filter-dkimsign +for DKIM signing: +.Bd -literal -offset indent +table aliases file:/etc/mail/aliases + +filter "dkimsign" proc-exec "filter-dkimsign -d <domain> -s <selector> \e + -k /etc/mail/dkim/private.key" user _dkimsign group _dkimsign + +listen on socket filter "dkimsign" +listen on lo0 filter "dkimsign" + +action "local_mail" mbox alias <aliases> +action "outbound" relay + +match for local action "local_mail" +match for any action "outbound" +.Ed +.Pp +Alternatively, the +.Sy opensmtpd-filter-rspamd +package may be used to provide integration with +.Sy rspamd , +a third-party daemon which provides multiple antispam features +as well as DKIM signing. +As well as configuring +.Sy rspamd +itself, +it requires use of the +.Cm proc-exec +keyword: +.Bd -literal -offset indent +filter "rspamd" proc-exec "filter-rspamd" +.Ed +.Pp +Sites that accept non-local messages may be able to cut down on the +volume of spam received by rejecting forged messages that claim +to be from the local domain. +The following example uses a list table +.Em other-relays +to specify the IP addresses of relays that may legitimately +originate mail with the owner's domain as the sender. +.Bd -literal -offset indent +table aliases file:/etc/mail/aliases +table other-relays file:/etc/mail/other-relays + +listen on lo0 +listen on egress + +action "local_mail" mbox alias <aliases> +action "outbound" relay + +match for local action "local_mail" +match for any action "outbound" +match !from src <other-relays> mail\-from "@example.com" for any \e + reject +match from any for domain example.com action "local_mail" +.Ed +.Sh SEE ALSO +.Xr mailer.conf 5 , +.Xr table 5 , +.Xr makemap 8 , +.Xr smtpd 8 +.Sh HISTORY +.Xr smtpd 8 +first appeared in +.Ox 4.6 . diff --git a/smtpd/smtpd.h b/smtpd/smtpd.h new file mode 100644 index 00000000..4385f747 --- /dev/null +++ b/smtpd/smtpd.h @@ -0,0 +1,1784 @@ +/* $OpenBSD: smtpd.h,v 1.656 2020/04/08 07:30:44 eric Exp $ */ + +/* + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2012 Eric Faurot <eric@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 <event.h> + +#include <imsg.h> + +#include "openbsd-compat.h" + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +#include <netinet/in.h> +#include <netdb.h> +#include <event.h> + +#include "smtpd-defines.h" +#include "smtpd-api.h" +#include "ioev.h" + +#define CHECK_IMSG_DATA_SIZE(imsg, expected_sz) do { \ + if ((imsg)->hdr.len - IMSG_HEADER_SIZE != (expected_sz)) \ + fatalx("smtpd: imsg %d: data size expected %zd got %zd",\ + (imsg)->hdr.type, \ + (expected_sz), (imsg)->hdr.len - IMSG_HEADER_SIZE); \ +} while (0) + +#ifndef SMTPD_CONFDIR +#define SMTPD_CONFDIR "/etc" +#endif +#define CONF_FILE SMTPD_CONFDIR "/smtpd.conf" +#define MAILNAME_FILE SMTPD_CONFDIR "/mailname" +#ifndef CA_FILE +#define CA_FILE "/etc/ssl/cert.pem" +#endif + +#define PROC_COUNT 7 + +#define MAX_HOPS_COUNT 100 +#define DEFAULT_MAX_BODY_SIZE (35*1024*1024) + +#define EXPAND_BUFFER 1024 + +#define SMTPD_QUEUE_EXPIRY (4 * 24 * 60 * 60) +#ifndef SMTPD_USER +#define SMTPD_USER "_smtpd" +#endif +#ifndef SMTPD_QUEUE_USER +#define SMTPD_QUEUE_USER "_smtpq" +#endif +#ifndef SMTPD_SOCKDIR +#define SMTPD_SOCKDIR "/var/run" +#endif +#define SMTPD_SOCKET SMTPD_SOCKDIR "/smtpd.sock" +#ifndef SMTPD_NAME +#define SMTPD_NAME "OpenSMTPD" +#endif +#define SMTPD_VERSION "6.7.0-portable" +#define SMTPD_SESSION_TIMEOUT 300 +#define SMTPD_BACKLOG 5 + +#ifndef PATH_SMTPCTL +#define PATH_SMTPCTL "/usr/sbin/smtpctl" +#endif + +#define PATH_OFFLINE "/offline" +#define PATH_PURGE "/purge" +#define PATH_TEMPORARY "/temporary" + +#ifndef PATH_LIBEXEC +#define PATH_LIBEXEC "/usr/local/libexec/smtpd" +#endif + + +/* + * RFC 5322 defines these characters as valid, some of them are + * potentially dangerous and need to be escaped. + */ +#define MAILADDR_ALLOWED "!#$%&'*/?^`{|}~+-=_" +#define MAILADDR_ESCAPE "!#$%&'*?`{|}~" + + +#define F_STARTTLS 0x01 +#define F_SMTPS 0x02 +#define F_SSL (F_STARTTLS | F_SMTPS) +#define F_AUTH 0x08 +#define F_STARTTLS_REQUIRE 0x20 +#define F_AUTH_REQUIRE 0x40 +#define F_MASK_SOURCE 0x100 +#define F_TLS_VERIFY 0x200 +#define F_EXT_DSN 0x400 +#define F_RECEIVEDAUTH 0x800 +#define F_MASQUERADE 0x1000 +#define F_FILTERED 0x2000 +#define F_PROXY 0x4000 + +#define RELAY_TLS_OPPORTUNISTIC 0 +#define RELAY_TLS_STARTTLS 1 +#define RELAY_TLS_SMTPS 2 +#define RELAY_TLS_NO 3 + +#define RELAY_AUTH 0x08 +#define RELAY_LMTP 0x80 +#define RELAY_TLS_VERIFY 0x200 + +#define MTA_EXT_DSN 0x400 + + +#define P_SENDMAIL 0 +#define P_NEWALIASES 1 +#define P_MAKEMAP 2 + +#define CERT_ERROR -1 +#define CERT_OK 0 +#define CERT_NOCA 1 +#define CERT_NOCERT 2 +#define CERT_INVALID 3 + +struct userinfo { + char username[SMTPD_VUSERNAME_SIZE]; + char directory[PATH_MAX]; + uid_t uid; + gid_t gid; +}; + +struct netaddr { + struct sockaddr_storage ss; + int bits; +}; + +struct relayhost { + uint16_t flags; + int tls; + char hostname[HOST_NAME_MAX+1]; + uint16_t port; + char authlabel[PATH_MAX]; +}; + +struct credentials { + char username[LINE_MAX]; + char password[LINE_MAX]; +}; + +struct destination { + char name[HOST_NAME_MAX+1]; +}; + +struct source { + struct sockaddr_storage addr; +}; + +struct addrname { + struct sockaddr_storage addr; + char name[HOST_NAME_MAX+1]; +}; + +union lookup { + struct expand *expand; + struct credentials creds; + struct netaddr netaddr; + struct source source; + struct destination domain; + struct userinfo userinfo; + struct mailaddr mailaddr; + struct addrname addrname; + struct maddrmap *maddrmap; + char relayhost[LINE_MAX]; +}; + +/* + * Bump IMSG_VERSION whenever a change is made to enum imsg_type. + * This will ensure that we can never use a wrong version of smtpctl with smtpd. + */ +#define IMSG_VERSION 16 + +enum imsg_type { + IMSG_NONE, + + IMSG_CTL_OK, + IMSG_CTL_FAIL, + + IMSG_CTL_GET_DIGEST, + IMSG_CTL_GET_STATS, + IMSG_CTL_LIST_MESSAGES, + IMSG_CTL_LIST_ENVELOPES, + IMSG_CTL_MTA_SHOW_HOSTS, + IMSG_CTL_MTA_SHOW_RELAYS, + IMSG_CTL_MTA_SHOW_ROUTES, + IMSG_CTL_MTA_SHOW_HOSTSTATS, + IMSG_CTL_MTA_BLOCK, + IMSG_CTL_MTA_UNBLOCK, + IMSG_CTL_MTA_SHOW_BLOCK, + IMSG_CTL_PAUSE_EVP, + IMSG_CTL_PAUSE_MDA, + IMSG_CTL_PAUSE_MTA, + IMSG_CTL_PAUSE_SMTP, + IMSG_CTL_PROFILE, + IMSG_CTL_PROFILE_DISABLE, + IMSG_CTL_PROFILE_ENABLE, + IMSG_CTL_RESUME_EVP, + IMSG_CTL_RESUME_MDA, + IMSG_CTL_RESUME_MTA, + IMSG_CTL_RESUME_SMTP, + IMSG_CTL_RESUME_ROUTE, + IMSG_CTL_REMOVE, + IMSG_CTL_SCHEDULE, + IMSG_CTL_SHOW_STATUS, + IMSG_CTL_TRACE_DISABLE, + IMSG_CTL_TRACE_ENABLE, + IMSG_CTL_UPDATE_TABLE, + IMSG_CTL_VERBOSE, + IMSG_CTL_DISCOVER_EVPID, + IMSG_CTL_DISCOVER_MSGID, + + IMSG_CTL_SMTP_SESSION, + + IMSG_GETADDRINFO, + IMSG_GETADDRINFO_END, + IMSG_GETNAMEINFO, + IMSG_RES_QUERY, + + IMSG_CERT_INIT, + IMSG_CERT_CERTIFICATE, + IMSG_CERT_VERIFY, + + IMSG_SETUP_KEY, + IMSG_SETUP_PEER, + IMSG_SETUP_DONE, + + IMSG_CONF_START, + IMSG_CONF_END, + + IMSG_STAT_INCREMENT, + IMSG_STAT_DECREMENT, + IMSG_STAT_SET, + + IMSG_LKA_AUTHENTICATE, + IMSG_LKA_OPEN_FORWARD, + IMSG_LKA_ENVELOPE_SUBMIT, + IMSG_LKA_ENVELOPE_COMMIT, + + IMSG_QUEUE_DELIVER, + IMSG_QUEUE_DELIVERY_OK, + IMSG_QUEUE_DELIVERY_TEMPFAIL, + IMSG_QUEUE_DELIVERY_PERMFAIL, + IMSG_QUEUE_DELIVERY_LOOP, + IMSG_QUEUE_DISCOVER_EVPID, + IMSG_QUEUE_DISCOVER_MSGID, + IMSG_QUEUE_ENVELOPE_ACK, + IMSG_QUEUE_ENVELOPE_COMMIT, + IMSG_QUEUE_ENVELOPE_REMOVE, + IMSG_QUEUE_ENVELOPE_SCHEDULE, + IMSG_QUEUE_ENVELOPE_SUBMIT, + IMSG_QUEUE_HOLDQ_HOLD, + IMSG_QUEUE_HOLDQ_RELEASE, + IMSG_QUEUE_MESSAGE_COMMIT, + IMSG_QUEUE_MESSAGE_ROLLBACK, + IMSG_QUEUE_SMTP_SESSION, + IMSG_QUEUE_TRANSFER, + + IMSG_MDA_DELIVERY_OK, + IMSG_MDA_DELIVERY_TEMPFAIL, + IMSG_MDA_DELIVERY_PERMFAIL, + IMSG_MDA_DELIVERY_LOOP, + IMSG_MDA_DELIVERY_HOLD, + IMSG_MDA_DONE, + IMSG_MDA_FORK, + IMSG_MDA_HOLDQ_RELEASE, + IMSG_MDA_LOOKUP_USERINFO, + IMSG_MDA_KILL, + IMSG_MDA_OPEN_MESSAGE, + + IMSG_MTA_DELIVERY_OK, + IMSG_MTA_DELIVERY_TEMPFAIL, + IMSG_MTA_DELIVERY_PERMFAIL, + IMSG_MTA_DELIVERY_LOOP, + IMSG_MTA_DELIVERY_HOLD, + IMSG_MTA_DNS_HOST, + IMSG_MTA_DNS_HOST_END, + IMSG_MTA_DNS_MX, + IMSG_MTA_DNS_MX_PREFERENCE, + IMSG_MTA_HOLDQ_RELEASE, + IMSG_MTA_LOOKUP_CREDENTIALS, + IMSG_MTA_LOOKUP_SOURCE, + IMSG_MTA_LOOKUP_HELO, + IMSG_MTA_LOOKUP_SMARTHOST, + IMSG_MTA_OPEN_MESSAGE, + IMSG_MTA_SCHEDULE, + + IMSG_SCHED_ENVELOPE_BOUNCE, + IMSG_SCHED_ENVELOPE_DELIVER, + IMSG_SCHED_ENVELOPE_EXPIRE, + IMSG_SCHED_ENVELOPE_INJECT, + IMSG_SCHED_ENVELOPE_REMOVE, + IMSG_SCHED_ENVELOPE_TRANSFER, + + IMSG_SMTP_AUTHENTICATE, + IMSG_SMTP_MESSAGE_COMMIT, + IMSG_SMTP_MESSAGE_CREATE, + IMSG_SMTP_MESSAGE_ROLLBACK, + IMSG_SMTP_MESSAGE_OPEN, + IMSG_SMTP_CHECK_SENDER, + IMSG_SMTP_EXPAND_RCPT, + IMSG_SMTP_LOOKUP_HELO, + + IMSG_SMTP_REQ_CONNECT, + IMSG_SMTP_REQ_HELO, + IMSG_SMTP_REQ_MAIL, + IMSG_SMTP_REQ_RCPT, + IMSG_SMTP_REQ_DATA, + IMSG_SMTP_REQ_EOM, + IMSG_SMTP_EVENT_RSET, + IMSG_SMTP_EVENT_COMMIT, + IMSG_SMTP_EVENT_ROLLBACK, + IMSG_SMTP_EVENT_DISCONNECT, + + IMSG_LKA_PROCESSOR_FORK, + IMSG_LKA_PROCESSOR_ERRFD, + + IMSG_REPORT_SMTP_LINK_CONNECT, + IMSG_REPORT_SMTP_LINK_DISCONNECT, + IMSG_REPORT_SMTP_LINK_GREETING, + IMSG_REPORT_SMTP_LINK_IDENTIFY, + IMSG_REPORT_SMTP_LINK_TLS, + IMSG_REPORT_SMTP_LINK_AUTH, + IMSG_REPORT_SMTP_TX_RESET, + IMSG_REPORT_SMTP_TX_BEGIN, + IMSG_REPORT_SMTP_TX_MAIL, + IMSG_REPORT_SMTP_TX_RCPT, + IMSG_REPORT_SMTP_TX_ENVELOPE, + IMSG_REPORT_SMTP_TX_DATA, + IMSG_REPORT_SMTP_TX_COMMIT, + IMSG_REPORT_SMTP_TX_ROLLBACK, + IMSG_REPORT_SMTP_PROTOCOL_CLIENT, + IMSG_REPORT_SMTP_PROTOCOL_SERVER, + IMSG_REPORT_SMTP_FILTER_RESPONSE, + IMSG_REPORT_SMTP_TIMEOUT, + + IMSG_FILTER_SMTP_BEGIN, + IMSG_FILTER_SMTP_END, + IMSG_FILTER_SMTP_PROTOCOL, + IMSG_FILTER_SMTP_DATA_BEGIN, + IMSG_FILTER_SMTP_DATA_END, + + IMSG_CA_RSA_PRIVENC, + IMSG_CA_RSA_PRIVDEC, + IMSG_CA_ECDSA_SIGN, +}; + +enum smtp_proc_type { + PROC_PARENT = 0, + PROC_LKA, + PROC_QUEUE, + PROC_CONTROL, + PROC_SCHEDULER, + PROC_PONY, + PROC_CA, + PROC_PROCESSOR, + PROC_CLIENT, +}; + +enum table_type { + T_NONE = 0, + T_DYNAMIC = 0x01, /* table with external source */ + T_LIST = 0x02, /* table holding a list */ + T_HASH = 0x04, /* table holding a hash table */ +}; + +struct table { + char t_name[LINE_MAX]; + enum table_type t_type; + char t_config[PATH_MAX]; + + void *t_handle; + struct table_backend *t_backend; +}; + +struct table_backend { + const char *name; + const unsigned int services; + int (*config)(struct table *); + int (*add)(struct table *, const char *, const char *); + void (*dump)(struct table *); + int (*open)(struct table *); + int (*update)(struct table *); + void (*close)(struct table *); + int (*lookup)(struct table *, enum table_service, const char *, char **); + int (*fetch)(struct table *, enum table_service, char **); +}; + + +enum bounce_type { + B_FAILED, + B_DELAYED, + B_DELIVERED +}; + +enum dsn_ret { + DSN_RETFULL = 1, + DSN_RETHDRS +}; + +struct delivery_bounce { + enum bounce_type type; + time_t delay; + time_t ttl; + enum dsn_ret dsn_ret; + int mta_without_dsn; +}; + +enum expand_type { + EXPAND_INVALID, + EXPAND_USERNAME, + EXPAND_FILENAME, + EXPAND_FILTER, + EXPAND_INCLUDE, + EXPAND_ADDRESS, + EXPAND_ERROR, +}; + +enum filter_phase { + FILTER_CONNECT, + FILTER_HELO, + FILTER_EHLO, + FILTER_STARTTLS, + FILTER_AUTH, + FILTER_MAIL_FROM, + FILTER_RCPT_TO, + FILTER_DATA, + FILTER_DATA_LINE, + FILTER_RSET, + FILTER_QUIT, + FILTER_NOOP, + FILTER_HELP, + FILTER_WIZ, + FILTER_COMMIT, + FILTER_PHASES_COUNT /* must be last */ +}; + +struct expandnode { + RB_ENTRY(expandnode) entry; + TAILQ_ENTRY(expandnode) tq_entry; + enum expand_type type; + int sameuser; + int realuser; + int forwarded; + struct rule *rule; + struct expandnode *parent; + unsigned int depth; + union { + /* + * user field handles both expansion user and system user + * so we MUST make it large enough to fit a mailaddr user + */ + char user[SMTPD_MAXLOCALPARTSIZE]; + char buffer[EXPAND_BUFFER]; + struct mailaddr mailaddr; + } u; + char subaddress[SMTPD_SUBADDRESS_SIZE]; +}; + +struct expand { + RB_HEAD(expandtree, expandnode) tree; + TAILQ_HEAD(xnodes, expandnode) *queue; + size_t nb_nodes; + struct rule *rule; + struct expandnode *parent; +}; + +struct maddrnode { + TAILQ_ENTRY(maddrnode) entries; + struct mailaddr mailaddr; +}; + +struct maddrmap { + TAILQ_HEAD(xmaddr, maddrnode) queue; +}; + +#define DSN_SUCCESS 0x01 +#define DSN_FAILURE 0x02 +#define DSN_DELAY 0x04 +#define DSN_NEVER 0x08 + +#define DSN_ENVID_LEN 100 + +#define SMTPD_ENVELOPE_VERSION 3 +struct envelope { + TAILQ_ENTRY(envelope) entry; + + char dispatcher[HOST_NAME_MAX+1]; + + char tag[SMTPD_TAG_SIZE]; + + uint32_t version; + uint64_t id; + enum envelope_flags flags; + + char smtpname[HOST_NAME_MAX+1]; + char helo[HOST_NAME_MAX+1]; + char hostname[HOST_NAME_MAX+1]; + char username[SMTPD_MAXMAILADDRSIZE]; + char errorline[LINE_MAX]; + struct sockaddr_storage ss; + + struct mailaddr sender; + struct mailaddr rcpt; + struct mailaddr dest; + + char mda_user[SMTPD_VUSERNAME_SIZE]; + char mda_subaddress[SMTPD_SUBADDRESS_SIZE]; + char mda_exec[LINE_MAX]; + + enum delivery_type type; + union { + struct delivery_bounce bounce; + } agent; + + uint16_t retry; + time_t creation; + time_t ttl; + time_t lasttry; + time_t nexttry; + time_t lastbounce; + + struct mailaddr dsn_orcpt; + char dsn_envid[DSN_ENVID_LEN+1]; + uint8_t dsn_notify; + enum dsn_ret dsn_ret; + + uint8_t esc_class; + uint8_t esc_code; +}; + +struct listener { + uint16_t flags; + int fd; + struct sockaddr_storage ss; + in_port_t port; + struct timeval timeout; + struct event ev; + char filter_name[PATH_MAX]; + char pki_name[PATH_MAX]; + char ca_name[PATH_MAX]; + char tag[SMTPD_TAG_SIZE]; + char authtable[LINE_MAX]; + char hostname[HOST_NAME_MAX+1]; + char hostnametable[PATH_MAX]; + char sendertable[PATH_MAX]; + + TAILQ_ENTRY(listener) entry; + + int local; /* there must be a better way */ +}; + +struct smtpd { + char sc_conffile[PATH_MAX]; + size_t sc_maxsize; + +#define SMTPD_OPT_VERBOSE 0x00000001 +#define SMTPD_OPT_NOACTION 0x00000002 + uint32_t sc_opts; + +#define SMTPD_EXITING 0x00000001 /* unused */ +#define SMTPD_MDA_PAUSED 0x00000002 +#define SMTPD_MTA_PAUSED 0x00000004 +#define SMTPD_SMTP_PAUSED 0x00000008 +#define SMTPD_MDA_BUSY 0x00000010 +#define SMTPD_MTA_BUSY 0x00000020 +#define SMTPD_BOUNCE_BUSY 0x00000040 +#define SMTPD_SMTP_DISABLED 0x00000080 + uint32_t sc_flags; + +#define QUEUE_COMPRESSION 0x00000001 +#define QUEUE_ENCRYPTION 0x00000002 +#define QUEUE_EVPCACHE 0x00000004 + uint32_t sc_queue_flags; + char *sc_queue_key; + size_t sc_queue_evpcache_size; + + size_t sc_session_max_rcpt; + size_t sc_session_max_mails; + + struct dict *sc_mda_wrappers; + size_t sc_mda_max_session; + size_t sc_mda_max_user_session; + size_t sc_mda_task_hiwat; + size_t sc_mda_task_lowat; + size_t sc_mda_task_release; + + size_t sc_mta_max_deferred; + + size_t sc_scheduler_max_inflight; + size_t sc_scheduler_max_evp_batch_size; + size_t sc_scheduler_max_msg_batch_size; + size_t sc_scheduler_max_schedule; + + struct dict *sc_filter_processes_dict; + + int sc_ttl; +#define MAX_BOUNCE_WARN 4 + time_t sc_bounce_warn[MAX_BOUNCE_WARN]; + char sc_hostname[HOST_NAME_MAX+1]; + struct stat_backend *sc_stat; + struct compress_backend *sc_comp; + + time_t sc_uptime; + + /* This is a listener for a local socket used by smtp_enqueue(). */ + struct listener *sc_sock_listener; + + TAILQ_HEAD(listenerlist, listener) *sc_listeners; + + TAILQ_HEAD(rulelist, rule) *sc_rules; + + + struct dict *sc_filters_dict; + struct dict *sc_dispatchers; + struct dispatcher *sc_dispatcher_bounce; + + struct dict *sc_ca_dict; + struct dict *sc_pki_dict; + struct dict *sc_ssl_dict; + + struct dict *sc_tables_dict; /* keyed lookup */ + + struct dict *sc_limits_dict; + + char *sc_tls_ciphers; + + char *sc_subaddressing_delim; + + char *sc_srs_key; + char *sc_srs_key_backup; + int sc_srs_ttl; +}; + +#define TRACE_DEBUG 0x0001 +#define TRACE_IMSG 0x0002 +#define TRACE_IO 0x0004 +#define TRACE_SMTP 0x0008 +#define TRACE_FILTERS 0x0010 +#define TRACE_MTA 0x0020 +#define TRACE_BOUNCE 0x0040 +#define TRACE_SCHEDULER 0x0080 +#define TRACE_LOOKUP 0x0100 +#define TRACE_STAT 0x0200 +#define TRACE_RULES 0x0400 +#define TRACE_MPROC 0x0800 +#define TRACE_EXPAND 0x1000 +#define TRACE_TABLES 0x2000 +#define TRACE_QUEUE 0x4000 + +#define PROFILE_TOSTAT 0x0001 +#define PROFILE_IMSG 0x0002 +#define PROFILE_QUEUE 0x0004 + +struct forward_req { + uint64_t id; + uint8_t status; + + char user[SMTPD_VUSERNAME_SIZE]; + uid_t uid; + gid_t gid; + char directory[PATH_MAX]; +}; + +struct deliver { + char dispatcher[EXPAND_BUFFER]; + + struct mailaddr sender; + struct mailaddr rcpt; + struct mailaddr dest; + + char mda_subaddress[SMTPD_SUBADDRESS_SIZE]; + char mda_exec[LINE_MAX]; + + struct userinfo userinfo; +}; + +struct mta_host { + SPLAY_ENTRY(mta_host) entry; + struct sockaddr *sa; + char *ptrname; + int refcount; + size_t nconn; + time_t lastconn; + time_t lastptrquery; + +#define HOST_IGNORE 0x01 + int flags; +}; + +struct mta_mx { + TAILQ_ENTRY(mta_mx) entry; + struct mta_host *host; + char *mxname; + int preference; +}; + +struct mta_domain { + SPLAY_ENTRY(mta_domain) entry; + char *name; + int as_host; + TAILQ_HEAD(, mta_mx) mxs; + int mxstatus; + int refcount; + size_t nconn; + time_t lastconn; + time_t lastmxquery; +}; + +struct mta_source { + SPLAY_ENTRY(mta_source) entry; + struct sockaddr *sa; + int refcount; + size_t nconn; + time_t lastconn; +}; + +struct mta_connector { + struct mta_source *source; + struct mta_relay *relay; + +#define CONNECTOR_ERROR_FAMILY 0x0001 +#define CONNECTOR_ERROR_SOURCE 0x0002 +#define CONNECTOR_ERROR_MX 0x0004 +#define CONNECTOR_ERROR_ROUTE_NET 0x0008 +#define CONNECTOR_ERROR_ROUTE_SMTP 0x0010 +#define CONNECTOR_ERROR_ROUTE 0x0018 +#define CONNECTOR_ERROR_BLOCKED 0x0020 +#define CONNECTOR_ERROR 0x00ff + +#define CONNECTOR_LIMIT_HOST 0x0100 +#define CONNECTOR_LIMIT_ROUTE 0x0200 +#define CONNECTOR_LIMIT_SOURCE 0x0400 +#define CONNECTOR_LIMIT_RELAY 0x0800 +#define CONNECTOR_LIMIT_CONN 0x1000 +#define CONNECTOR_LIMIT_DOMAIN 0x2000 +#define CONNECTOR_LIMIT 0xff00 + +#define CONNECTOR_NEW 0x10000 +#define CONNECTOR_WAIT 0x20000 + int flags; + + int refcount; + size_t nconn; + time_t lastconn; +}; + +struct mta_route { + SPLAY_ENTRY(mta_route) entry; + uint64_t id; + struct mta_source *src; + struct mta_host *dst; +#define ROUTE_NEW 0x01 +#define ROUTE_RUNQ 0x02 +#define ROUTE_KEEPALIVE 0x04 +#define ROUTE_DISABLED 0xf0 +#define ROUTE_DISABLED_NET 0x10 +#define ROUTE_DISABLED_SMTP 0x20 + int flags; + int nerror; + int penalty; + int refcount; + size_t nconn; + time_t lastconn; + time_t lastdisc; + time_t lastpenalty; +}; + +struct mta_limits { + size_t maxconn_per_host; + size_t maxconn_per_route; + size_t maxconn_per_source; + size_t maxconn_per_connector; + size_t maxconn_per_relay; + size_t maxconn_per_domain; + + time_t conndelay_host; + time_t conndelay_route; + time_t conndelay_source; + time_t conndelay_connector; + time_t conndelay_relay; + time_t conndelay_domain; + + time_t discdelay_route; + + size_t max_mail_per_session; + time_t sessdelay_transaction; + time_t sessdelay_keepalive; + + size_t max_failures_per_session; + + int family; + + int task_hiwat; + int task_lowat; + int task_release; +}; + +struct mta_relay { + SPLAY_ENTRY(mta_relay) entry; + uint64_t id; + + struct dispatcher *dispatcher; + struct mta_domain *domain; + struct mta_limits *limits; + int tls; + int flags; + char *backupname; + int backuppref; + char *sourcetable; + uint16_t port; + char *pki_name; + char *ca_name; + char *authtable; + char *authlabel; + char *helotable; + char *heloname; + char *secret; + int srs; + + int state; + size_t ntask; + TAILQ_HEAD(, mta_task) tasks; + + struct tree connectors; + size_t sourceloop; + time_t lastsource; + time_t nextsource; + + int fail; + char *failstr; + +#define RELAY_WAIT_MX 0x01 +#define RELAY_WAIT_PREFERENCE 0x02 +#define RELAY_WAIT_SECRET 0x04 +#define RELAY_WAIT_LIMITS 0x08 +#define RELAY_WAIT_SOURCE 0x10 +#define RELAY_WAIT_CONNECTOR 0x20 +#define RELAY_WAIT_SMARTHOST 0x40 +#define RELAY_WAITMASK 0x7f + int status; + + int refcount; + size_t nconn; + size_t nconn_ready; + time_t lastconn; +}; + +struct mta_envelope { + TAILQ_ENTRY(mta_envelope) entry; + uint64_t id; + uint64_t session; + time_t creation; + char *smtpname; + char *dest; + char *rcpt; + struct mta_task *task; + int delivery; + + int ext; + char *dsn_orcpt; + char dsn_envid[DSN_ENVID_LEN+1]; + uint8_t dsn_notify; + enum dsn_ret dsn_ret; + + char status[LINE_MAX]; +}; + +struct mta_task { + TAILQ_ENTRY(mta_task) entry; + struct mta_relay *relay; + uint32_t msgid; + TAILQ_HEAD(, mta_envelope) envelopes; + char *sender; +}; + +struct passwd; + +struct queue_backend { + int (*init)(struct passwd *, int, const char *); +}; + +struct compress_backend { + size_t (*compress_chunk)(void *, size_t, void *, size_t); + size_t (*uncompress_chunk)(void *, size_t, void *, size_t); + int (*compress_file)(FILE *, FILE *); + int (*uncompress_file)(FILE *, FILE *); +}; + +/* auth structures */ +enum auth_type { + AUTH_BSD, + AUTH_PWD, +}; + +struct auth_backend { + int (*authenticate)(char *, char *); +}; + +struct scheduler_backend { + int (*init)(const char *); + + int (*insert)(struct scheduler_info *); + size_t (*commit)(uint32_t); + size_t (*rollback)(uint32_t); + + int (*update)(struct scheduler_info *); + int (*delete)(uint64_t); + int (*hold)(uint64_t, uint64_t); + int (*release)(int, uint64_t, int); + + int (*batch)(int, int*, size_t*, uint64_t*, int*); + + size_t (*messages)(uint32_t, uint32_t *, size_t); + size_t (*envelopes)(uint64_t, struct evpstate *, size_t); + int (*schedule)(uint64_t); + int (*remove)(uint64_t); + int (*suspend)(uint64_t); + int (*resume)(uint64_t); + int (*query)(uint64_t); +}; + +enum stat_type { + STAT_COUNTER, + STAT_TIMESTAMP, + STAT_TIMEVAL, + STAT_TIMESPEC, +}; + +struct stat_value { + enum stat_type type; + union stat_v { + size_t counter; + time_t timestamp; + struct timeval tv; + struct timespec ts; + } u; +}; + +#define STAT_KEY_SIZE 1024 +struct stat_kv { + void *iter; + char key[STAT_KEY_SIZE]; + struct stat_value val; +}; + +struct stat_backend { + void (*init)(void); + void (*close)(void); + void (*increment)(const char *, size_t); + void (*decrement)(const char *, size_t); + void (*set)(const char *, const struct stat_value *); + int (*iter)(void **, char **, struct stat_value *); +}; + +struct stat_digest { + time_t startup; + time_t timestamp; + + size_t clt_connect; + size_t clt_disconnect; + + size_t evp_enqueued; + size_t evp_dequeued; + + size_t evp_expired; + size_t evp_removed; + size_t evp_bounce; + + size_t dlv_ok; + size_t dlv_permfail; + size_t dlv_tempfail; + size_t dlv_loop; +}; + + +struct mproc { + pid_t pid; + char *name; + int proc; + void (*handler)(struct mproc *, struct imsg *); + struct imsgbuf imsgbuf; + + char *m_buf; + size_t m_alloc; + size_t m_pos; + uint32_t m_type; + uint32_t m_peerid; + pid_t m_pid; + int m_fd; + + int enable; + short events; + struct event ev; + void *data; +}; + +struct msg { + const uint8_t *pos; + const uint8_t *end; +}; + +extern enum smtp_proc_type smtpd_process; + +extern int tracing; +extern int foreground_log; +extern int profiling; + +extern struct mproc *p_control; +extern struct mproc *p_parent; +extern struct mproc *p_lka; +extern struct mproc *p_queue; +extern struct mproc *p_scheduler; +extern struct mproc *p_pony; +extern struct mproc *p_ca; + +extern struct smtpd *env; +extern void (*imsg_callback)(struct mproc *, struct imsg *); + +/* inter-process structures */ + +struct bounce_req_msg { + uint64_t evpid; + time_t timestamp; + struct delivery_bounce bounce; +}; + +enum dns_error { + DNS_OK = 0, + DNS_RETRY, + DNS_EINVAL, + DNS_ENONAME, + DNS_ENOTFOUND, +}; + +enum lka_resp_status { + LKA_OK, + LKA_TEMPFAIL, + LKA_PERMFAIL +}; + +enum filter_type { + FILTER_TYPE_BUILTIN, + FILTER_TYPE_PROC, + FILTER_TYPE_CHAIN, +}; + +enum filter_subsystem { + FILTER_SUBSYSTEM_SMTP_IN = 1<<0, + FILTER_SUBSYSTEM_SMTP_OUT = 1<<1, +}; + +struct filter_proc { + const char *command; + const char *user; + const char *group; + const char *chroot; + int errfd; + enum filter_subsystem filter_subsystem; +}; + +struct filter_config { + char *name; + enum filter_subsystem filter_subsystem; + enum filter_type filter_type; + enum filter_phase phase; + char *reject; + char *disconnect; + char *rewrite; + char *report; + uint8_t junk; + uint8_t bypass; + char *proc; + + const char **chain; + size_t chain_size; + struct dict chain_procs; + + int8_t not_fcrdns; + int8_t fcrdns; + + int8_t not_rdns; + int8_t rdns; + + int8_t not_rdns_table; + struct table *rdns_table; + + int8_t not_rdns_regex; + struct table *rdns_regex; + + int8_t not_src_table; + struct table *src_table; + + int8_t not_src_regex; + struct table *src_regex; + + int8_t not_helo_table; + struct table *helo_table; + + int8_t not_helo_regex; + struct table *helo_regex; + + int8_t not_auth; + int8_t auth; + + int8_t not_auth_table; + struct table *auth_table; + + int8_t not_auth_regex; + struct table *auth_regex; + + int8_t not_mail_from_table; + struct table *mail_from_table; + + int8_t not_mail_from_regex; + struct table *mail_from_regex; + + int8_t not_rcpt_to_table; + struct table *rcpt_to_table; + + int8_t not_rcpt_to_regex; + struct table *rcpt_to_regex; + +}; + +enum filter_status { + FILTER_PROCEED, + FILTER_REWRITE, + FILTER_REJECT, + FILTER_DISCONNECT, + FILTER_JUNK, +}; + +enum ca_resp_status { + CA_OK, + CA_FAIL +}; + +enum mda_resp_status { + MDA_OK, + MDA_TEMPFAIL, + MDA_PERMFAIL +}; + +struct msg_walkinfo { + struct event ev; + uint32_t msgid; + uint32_t peerid; + size_t n_evp; + void *data; + int done; +}; + + +enum dispatcher_type { + DISPATCHER_LOCAL, + DISPATCHER_REMOTE, + DISPATCHER_BOUNCE, +}; + +struct dispatcher_local { + uint8_t is_mbox; /* only for MBOX */ + + uint8_t expand_only; + uint8_t forward_only; + + char *mda_wrapper; + char *command; + + char *table_alias; + char *table_virtual; + char *table_userbase; + + char *user; +}; + +struct dispatcher_remote { + char *helo; + char *helo_source; + + char *source; + + char *ca; + char *pki; + + char *mail_from; + + char *smarthost; + int smarthost_domain; + + char *auth; + int tls_required; + int tls_noverify; + + int backup; + char *backupmx; + + char *filtername; + + int srs; +}; + +struct dispatcher_bounce { +}; + +struct dispatcher { + enum dispatcher_type type; + union dispatcher_agent { + struct dispatcher_local local; + struct dispatcher_remote remote; + struct dispatcher_bounce bounce; + } u; + + time_t ttl; +}; + +struct rule { + TAILQ_ENTRY(rule) r_entry; + + uint8_t reject; + + int8_t flag_tag; + int8_t flag_from; + int8_t flag_for; + int8_t flag_from_rdns; + int8_t flag_from_socket; + + int8_t flag_tag_regex; + int8_t flag_from_regex; + int8_t flag_for_regex; + + int8_t flag_smtp_helo; + int8_t flag_smtp_starttls; + int8_t flag_smtp_auth; + int8_t flag_smtp_mail_from; + int8_t flag_smtp_rcpt_to; + + int8_t flag_smtp_helo_regex; + int8_t flag_smtp_starttls_regex; + int8_t flag_smtp_auth_regex; + int8_t flag_smtp_mail_from_regex; + int8_t flag_smtp_rcpt_to_regex; + + + char *table_tag; + char *table_from; + char *table_for; + + char *table_smtp_helo; + char *table_smtp_auth; + char *table_smtp_mail_from; + char *table_smtp_rcpt_to; + + char *dispatcher; +}; + + +/* aliases.c */ +int aliases_get(struct expand *, const char *); +int aliases_virtual_get(struct expand *, const struct mailaddr *); +int alias_parse(struct expandnode *, const char *); + + +/* auth.c */ +struct auth_backend *auth_backend_lookup(enum auth_type); + + +/* bounce.c */ +void bounce_add(uint64_t); +void bounce_fd(int); + + +/* ca.c */ +int ca(void); +int ca_X509_verify(void *, void *, const char *, const char *, const char **); +void ca_imsg(struct mproc *, struct imsg *); +void ca_init(void); +void ca_engine_init(void); + + +/* cert.c */ +int cert_init(const char *, int, + void (*)(void *, int, const char *, const void *, size_t), void *); +int cert_verify(const void *, const char *, int, void (*)(void *, int), void *); +void cert_dispatch_request(struct mproc *, struct imsg *); +void cert_dispatch_result(struct mproc *, struct imsg *); + + +/* compress_backend.c */ +struct compress_backend *compress_backend_lookup(const char *); +size_t compress_chunk(void *, size_t, void *, size_t); +size_t uncompress_chunk(void *, size_t, void *, size_t); +int compress_file(FILE *, FILE *); +int uncompress_file(FILE *, FILE *); + +/* config.c */ +#define PURGE_LISTENERS 0x01 +#define PURGE_TABLES 0x02 +#define PURGE_RULES 0x04 +#define PURGE_PKI 0x08 +#define PURGE_PKI_KEYS 0x10 +#define PURGE_DISPATCHERS 0x20 +#define PURGE_EVERYTHING 0xff +struct smtpd *config_default(void); +void purge_config(uint8_t); +void config_process(enum smtp_proc_type); +void config_peer(enum smtp_proc_type); + + +/* control.c */ +int control(void); +int control_create_socket(void); + + +/* crypto.c */ +int crypto_setup(const char *, size_t); +int crypto_encrypt_file(FILE *, FILE *); +int crypto_decrypt_file(FILE *, FILE *); +size_t crypto_encrypt_buffer(const char *, size_t, char *, size_t); +size_t crypto_decrypt_buffer(const char *, size_t, char *, size_t); + + +/* dns.c */ +void dns_imsg(struct mproc *, struct imsg *); + + +/* enqueue.c */ +int enqueue(int, char **, FILE *); + + +/* envelope.c */ +void envelope_set_errormsg(struct envelope *, char *, ...); +void envelope_set_esc_class(struct envelope *, enum enhanced_status_class); +void envelope_set_esc_code(struct envelope *, enum enhanced_status_code); +int envelope_load_buffer(struct envelope *, const char *, size_t); +int envelope_dump_buffer(const struct envelope *, char *, size_t); + + +/* expand.c */ +int expand_cmp(struct expandnode *, struct expandnode *); +void expand_insert(struct expand *, struct expandnode *); +struct expandnode *expand_lookup(struct expand *, struct expandnode *); +void expand_clear(struct expand *); +void expand_free(struct expand *); +int expand_line(struct expand *, const char *, int); +int expand_to_text(struct expand *, char *, size_t); +RB_PROTOTYPE(expandtree, expandnode, nodes, expand_cmp); + + +/* forward.c */ +int forwards_get(int, struct expand *); + + +/* limit.c */ +void limit_mta_set_defaults(struct mta_limits *); +int limit_mta_set(struct mta_limits *, const char*, int64_t); + + +/* lka.c */ +int lka(void); + + +/* lka_proc.c */ +int lka_proc_ready(void); +void lka_proc_forked(const char *, uint32_t, int); +void lka_proc_errfd(const char *, int); +struct io *lka_proc_get_io(const char *); + + +/* lka_report.c */ +void lka_report_init(void); +void lka_report_register_hook(const char *, const char *); +void lka_report_smtp_link_connect(const char *, struct timeval *, uint64_t, const char *, int, + const struct sockaddr_storage *, const struct sockaddr_storage *); +void lka_report_smtp_link_disconnect(const char *, struct timeval *, uint64_t); +void lka_report_smtp_link_greeting(const char *, uint64_t, struct timeval *, + const char *); +void lka_report_smtp_link_identify(const char *, struct timeval *, uint64_t, const char *, const char *); +void lka_report_smtp_link_tls(const char *, struct timeval *, uint64_t, const char *); +void lka_report_smtp_link_auth(const char *, struct timeval *, uint64_t, const char *, const char *); +void lka_report_smtp_tx_reset(const char *, struct timeval *, uint64_t, uint32_t); +void lka_report_smtp_tx_begin(const char *, struct timeval *, uint64_t, uint32_t); +void lka_report_smtp_tx_mail(const char *, struct timeval *, uint64_t, uint32_t, const char *, int); +void lka_report_smtp_tx_rcpt(const char *, struct timeval *, uint64_t, uint32_t, const char *, int); +void lka_report_smtp_tx_envelope(const char *, struct timeval *, uint64_t, uint32_t, uint64_t); +void lka_report_smtp_tx_commit(const char *, struct timeval *, uint64_t, uint32_t, size_t); +void lka_report_smtp_tx_data(const char *, struct timeval *, uint64_t, uint32_t, int); +void lka_report_smtp_tx_rollback(const char *, struct timeval *, uint64_t, uint32_t); +void lka_report_smtp_protocol_client(const char *, struct timeval *, uint64_t, const char *); +void lka_report_smtp_protocol_server(const char *, struct timeval *, uint64_t, const char *); +void lka_report_smtp_filter_response(const char *, struct timeval *, uint64_t, + int, int, const char *); +void lka_report_smtp_timeout(const char *, struct timeval *, uint64_t); +void lka_report_filter_report(uint64_t, const char *, int, const char *, + struct timeval *, const char *); +void lka_report_proc(const char *, const char *); + + +/* lka_filter.c */ +void lka_filter_init(void); +void lka_filter_register_hook(const char *, const char *); +void lka_filter_ready(void); +int lka_filter_proc_in_session(uint64_t, const char *); +void lka_filter_begin(uint64_t, const char *); +void lka_filter_end(uint64_t); +void lka_filter_protocol(uint64_t, enum filter_phase, const char *); +void lka_filter_data_begin(uint64_t); +void lka_filter_data_end(uint64_t); +int lka_filter_response(uint64_t, const char *, const char *); + + +/* lka_session.c */ +void lka_session(uint64_t, struct envelope *); +void lka_session_forward_reply(struct forward_req *, int); + + +/* log.c */ +void vlog(int, const char *, va_list); +void logit(int, const char *, ...) __attribute__((format (printf, 2, 3))); + + +/* mda.c */ +void mda_postfork(void); +void mda_postprivdrop(void); +void mda_imsg(struct mproc *, struct imsg *); + + +/* mda_mbox.c */ +void mda_mbox_init(struct deliver *); +void mda_mbox(struct deliver *); + + +/* mda_unpriv.c */ +void mda_unpriv(struct dispatcher *, struct deliver *, const char *, const char *); + + +/* mda_variables.c */ +ssize_t mda_expand_format(char *, size_t, const struct deliver *, + const struct userinfo *, const char *); + + +/* makemap.c */ +int makemap(int, int, char **); + + +/* mailaddr.c */ +int mailaddr_line(struct maddrmap *, const char *); +void maddrmap_init(struct maddrmap *); +void maddrmap_insert(struct maddrmap *, struct maddrnode *); +void maddrmap_free(struct maddrmap *); + + +/* mproc.c */ +int mproc_fork(struct mproc *, const char*, char **); +void mproc_init(struct mproc *, int); +void mproc_clear(struct mproc *); +void mproc_enable(struct mproc *); +void mproc_disable(struct mproc *); +void mproc_event_add(struct mproc *); +void m_compose(struct mproc *, uint32_t, uint32_t, pid_t, int, void *, size_t); +void m_composev(struct mproc *, uint32_t, uint32_t, pid_t, int, + const struct iovec *, int); +void m_forward(struct mproc *, struct imsg *); +void m_create(struct mproc *, uint32_t, uint32_t, pid_t, int); +void m_add(struct mproc *, const void *, size_t); +void m_add_int(struct mproc *, int); +void m_add_u32(struct mproc *, uint32_t); +void m_add_size(struct mproc *, size_t); +void m_add_time(struct mproc *, time_t); +void m_add_timeval(struct mproc *, struct timeval *tv); +void m_add_string(struct mproc *, const char *); +void m_add_data(struct mproc *, const void *, size_t); +void m_add_evpid(struct mproc *, uint64_t); +void m_add_msgid(struct mproc *, uint32_t); +void m_add_id(struct mproc *, uint64_t); +void m_add_sockaddr(struct mproc *, const struct sockaddr *); +void m_add_mailaddr(struct mproc *, const struct mailaddr *); +void m_add_envelope(struct mproc *, const struct envelope *); +void m_add_params(struct mproc *, struct dict *); +void m_close(struct mproc *); +void m_flush(struct mproc *); + +void m_msg(struct msg *, struct imsg *); +int m_is_eom(struct msg *); +void m_end(struct msg *); +void m_get_int(struct msg *, int *); +void m_get_size(struct msg *, size_t *); +void m_get_u32(struct msg *, uint32_t *); +void m_get_time(struct msg *, time_t *); +void m_get_timeval(struct msg *, struct timeval *); +void m_get_string(struct msg *, const char **); +void m_get_data(struct msg *, const void **, size_t *); +void m_get_evpid(struct msg *, uint64_t *); +void m_get_msgid(struct msg *, uint32_t *); +void m_get_id(struct msg *, uint64_t *); +void m_get_sockaddr(struct msg *, struct sockaddr *); +void m_get_mailaddr(struct msg *, struct mailaddr *); +void m_get_envelope(struct msg *, struct envelope *); +void m_get_params(struct msg *, struct dict *); +void m_clear_params(struct dict *); + + +/* mta.c */ +void mta_postfork(void); +void mta_postprivdrop(void); +void mta_imsg(struct mproc *, struct imsg *); +void mta_route_ok(struct mta_relay *, struct mta_route *); +void mta_route_error(struct mta_relay *, struct mta_route *); +void mta_route_down(struct mta_relay *, struct mta_route *); +void mta_route_collect(struct mta_relay *, struct mta_route *); +void mta_source_error(struct mta_relay *, struct mta_route *, const char *); +void mta_delivery_log(struct mta_envelope *, const char *, const char *, int, const char *); +void mta_delivery_notify(struct mta_envelope *); +struct mta_task *mta_route_next_task(struct mta_relay *, struct mta_route *); +const char *mta_host_to_text(struct mta_host *); +const char *mta_relay_to_text(struct mta_relay *); + + +/* mta_session.c */ +void mta_session(struct mta_relay *, struct mta_route *, const char *); +void mta_session_imsg(struct mproc *, struct imsg *); + + +/* parse.y */ +int parse_config(struct smtpd *, const char *, int); +int cmdline_symset(char *); + + +/* queue.c */ +int queue(void); + + +/* queue_backend.c */ +uint32_t queue_generate_msgid(void); +uint64_t queue_generate_evpid(uint32_t); +int queue_init(const char *, int); +int queue_close(void); +int queue_message_create(uint32_t *); +int queue_message_delete(uint32_t); +int queue_message_commit(uint32_t); +int queue_message_fd_r(uint32_t); +int queue_message_fd_rw(uint32_t); +int queue_envelope_create(struct envelope *); +int queue_envelope_delete(uint64_t); +int queue_envelope_load(uint64_t, struct envelope *); +int queue_envelope_update(struct envelope *); +int queue_envelope_walk(struct envelope *); +int queue_message_walk(struct envelope *, uint32_t, int *, void **); + + +/* report_smtp.c */ +void report_smtp_link_connect(const char *, uint64_t, const char *, int, + const struct sockaddr_storage *, const struct sockaddr_storage *); +void report_smtp_link_disconnect(const char *, uint64_t); +void report_smtp_link_greeting(const char *, uint64_t, const char *); +void report_smtp_link_identify(const char *, uint64_t, const char *, const char *); +void report_smtp_link_tls(const char *, uint64_t, const char *); +void report_smtp_link_auth(const char *, uint64_t, const char *, const char *); +void report_smtp_tx_reset(const char *, uint64_t, uint32_t); +void report_smtp_tx_begin(const char *, uint64_t, uint32_t); +void report_smtp_tx_mail(const char *, uint64_t, uint32_t, const char *, int); +void report_smtp_tx_rcpt(const char *, uint64_t, uint32_t, const char *, int); +void report_smtp_tx_envelope(const char *, uint64_t, uint32_t, uint64_t); +void report_smtp_tx_data(const char *, uint64_t, uint32_t, int); +void report_smtp_tx_commit(const char *, uint64_t, uint32_t, size_t); +void report_smtp_tx_rollback(const char *, uint64_t, uint32_t); +void report_smtp_protocol_client(const char *, uint64_t, const char *); +void report_smtp_protocol_server(const char *, uint64_t, const char *); +void report_smtp_filter_response(const char *, uint64_t, int, int, const char *); +void report_smtp_timeout(const char *, uint64_t); + + +/* ruleset.c */ +struct rule *ruleset_match(const struct envelope *); + + +/* scheduler.c */ +int scheduler(void); + + +/* scheduler_bakend.c */ +struct scheduler_backend *scheduler_backend_lookup(const char *); +void scheduler_info(struct scheduler_info *, struct envelope *); + + +/* pony.c */ +int pony(void); +void pony_imsg(struct mproc *, struct imsg *); + + +/* resolver.c */ +void resolver_getaddrinfo(const char *, const char *, const struct addrinfo *, + void(*)(void *, int, struct addrinfo*), void *); +void resolver_getnameinfo(const struct sockaddr *, int, + void(*)(void *, int, const char *, const char *), void *); +void resolver_res_query(const char *, int, int, + void (*cb)(void *, int, int, int, const void *, int), void *); +void resolver_dispatch_request(struct mproc *, struct imsg *); +void resolver_dispatch_result(struct mproc *, struct imsg *); + + +/* smtp.c */ +void smtp_postfork(void); +void smtp_postprivdrop(void); +void smtp_imsg(struct mproc *, struct imsg *); +void smtp_configure(void); +void smtp_collect(void); + + +/* smtp_session.c */ +int smtp_session(struct listener *, int, const struct sockaddr_storage *, + const char *, struct io *); +void smtp_session_imsg(struct mproc *, struct imsg *); + + +/* smtpf_session.c */ +int smtpf_session(struct listener *, int, const struct sockaddr_storage *, + const char *); +void smtpf_session_imsg(struct mproc *, struct imsg *); + + +/* smtpd.c */ +void imsg_dispatch(struct mproc *, struct imsg *); +const char *proc_name(enum smtp_proc_type); +const char *proc_title(enum smtp_proc_type); +const char *imsg_to_str(int); +void log_imsg(int, int, struct imsg *); +int fork_proc_backend(const char *, const char *, const char *); + + +/* srs.c */ +const char *srs_encode(const char *, const char *); +const char *srs_decode(const char *); + + +/* ssl_smtpd.c */ +void *ssl_mta_init(void *, char *, off_t, const char *); +void *ssl_smtp_init(void *, int); + + +/* stat_backend.c */ +struct stat_backend *stat_backend_lookup(const char *); +void stat_increment(const char *, size_t); +void stat_decrement(const char *, size_t); +void stat_set(const char *, const struct stat_value *); +struct stat_value *stat_counter(size_t); +struct stat_value *stat_timestamp(time_t); +struct stat_value *stat_timeval(struct timeval *); +struct stat_value *stat_timespec(struct timespec *); + + +/* table.c */ +struct table *table_find(struct smtpd *, const char *); +struct table *table_create(struct smtpd *, const char *, const char *, + const char *); +int table_config(struct table *); +int table_open(struct table *); +int table_update(struct table *); +void table_close(struct table *); +void table_dump(struct table *); +int table_check_use(struct table *, uint32_t, uint32_t); +int table_check_type(struct table *, uint32_t); +int table_check_service(struct table *, uint32_t); +int table_match(struct table *, enum table_service, const char *); +int table_lookup(struct table *, enum table_service, const char *, + union lookup *); +int table_fetch(struct table *, enum table_service, union lookup *); +void table_destroy(struct smtpd *, struct table *); +void table_add(struct table *, const char *, const char *); +int table_domain_match(const char *, const char *); +int table_netaddr_match(const char *, const char *); +int table_mailaddr_match(const char *, const char *); +int table_regex_match(const char *, const char *); +void table_open_all(struct smtpd *); +void table_dump_all(struct smtpd *); +void table_close_all(struct smtpd *); + + +/* to.c */ +int email_to_mailaddr(struct mailaddr *, char *); +int text_to_netaddr(struct netaddr *, const char *); +int text_to_mailaddr(struct mailaddr *, const char *); +int text_to_relayhost(struct relayhost *, const char *); +int text_to_userinfo(struct userinfo *, const char *); +int text_to_credentials(struct credentials *, const char *); +int text_to_expandnode(struct expandnode *, const char *); +uint64_t text_to_evpid(const char *); +uint32_t text_to_msgid(const char *); +const char *sa_to_text(const struct sockaddr *); +const char *ss_to_text(const struct sockaddr_storage *); +const char *time_to_text(time_t); +const char *duration_to_text(time_t); +const char *rule_to_text(struct rule *); +const char *sockaddr_to_text(struct sockaddr *); +const char *mailaddr_to_text(const struct mailaddr *); +const char *expandnode_to_text(struct expandnode *); + + +/* util.c */ +typedef struct arglist arglist; +struct arglist { + char **list; + uint num; + uint nalloc; +}; +void addargs(arglist *, char *, ...) + __attribute__((format(printf, 2, 3))); +int bsnprintf(char *, size_t, const char *, ...) + __attribute__((format (printf, 3, 4))); +int safe_fclose(FILE *); +int hostname_match(const char *, const char *); +int mailaddr_match(const struct mailaddr *, const struct mailaddr *); +int valid_localpart(const char *); +int valid_domainpart(const char *); +int valid_domainname(const char *); +int valid_smtp_response(const char *); +int secure_file(int, char *, char *, uid_t, int); +int lowercase(char *, const char *, size_t); +void xlowercase(char *, const char *, size_t); +int uppercase(char *, const char *, size_t); +uint64_t generate_uid(void); +int availdesc(void); +int ckdir(const char *, mode_t, uid_t, gid_t, int); +int rmtree(char *, int); +int mvpurge(char *, char *); +int mktmpfile(void); +const char *parse_smtp_response(char *, size_t, char **, int *); +int xasprintf(char **, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void *xmalloc(size_t); +void *xcalloc(size_t, size_t); +char *xstrdup(const char *); +void *xmemdup(const void *, size_t); +char *strip(char *); +int io_xprint(struct io *, const char *); +int io_xprintf(struct io *, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void log_envelope(const struct envelope *, const char *, const char *, + const char *); +int session_socket_error(int); +int getmailname(char *, size_t); +int base64_encode(unsigned char const *, size_t, char *, size_t); +int base64_decode(char const *, unsigned char *, size_t); +int base64_encode_rfc3548(unsigned char const *, size_t, + char *, size_t); +void xclosefrom(int); + +void log_trace_verbose(int); +void log_trace(int, const char *, ...) + __attribute__((format (printf, 2, 3))); + +/* waitq.c */ +int waitq_wait(void *, void (*)(void *, void *, void *), void *); +void waitq_run(void *, void *); + + +/* runq.c */ +struct runq; + +int runq_init(struct runq **, void (*)(struct runq *, void *)); +int runq_schedule(struct runq *, time_t, void *); +int runq_schedule_at(struct runq *, time_t, void *); +int runq_cancel(struct runq *, void *); +int runq_pending(struct runq *, void *, time_t *); diff --git a/smtpd/smtpd/Makefile b/smtpd/smtpd/Makefile new file mode 100644 index 00000000..34a4dc74 --- /dev/null +++ b/smtpd/smtpd/Makefile @@ -0,0 +1,102 @@ +# $OpenBSD: Makefile,v 1.91 2018/06/03 14:04:06 gilles Exp $ + +.PATH: ${.CURDIR}/.. + +PROG= smtpd + +SRCS= aliases.c +SRCS+= bounce.c +SRCS+= ca.c +SRCS+= cert.c +SRCS+= compress_backend.c +SRCS+= config.c +SRCS+= control.c +SRCS+= crypto.c +SRCS+= dict.c +SRCS+= dns.c +SRCS+= unpack_dns.c +SRCS+= envelope.c +SRCS+= esc.c +SRCS+= expand.c +SRCS+= forward.c +SRCS+= iobuf.c +SRCS+= ioev.c +SRCS+= limit.c +SRCS+= lka.c +SRCS+= lka_filter.c +SRCS+= lka_session.c +SRCS+= log.c +SRCS+= mailaddr.c +SRCS+= mda.c +SRCS+= mda_mbox.c +SRCS+= mda_unpriv.c +SRCS+= mda_variables.c +SRCS+= mproc.c +SRCS+= mta.c +SRCS+= mta_session.c +SRCS+= parse.y +SRCS+= pony.c +SRCS+= proxy.c +SRCS+= queue.c +SRCS+= queue_backend.c +SRCS+= report_smtp.c +SRCS+= resolver.c +SRCS+= ruleset.c +SRCS+= runq.c +SRCS+= scheduler.c +SRCS+= scheduler_backend.c +SRCS+= smtp.c +SRCS+= smtp_session.c +SRCS+= smtpd.c +SRCS+= srs.c +SRCS+= ssl.c +SRCS+= ssl_smtpd.c +SRCS+= ssl_verify.c +SRCS+= stat_backend.c +SRCS+= table.c +SRCS+= to.c +SRCS+= tree.c +SRCS+= util.c +SRCS+= waitq.c + +# RFC parsers +SRCS+= rfc5322.c + +# backends +SRCS+= compress_gzip.c + +SRCS+= table_db.c +SRCS+= table_getpwnam.c +SRCS+= table_proc.c +SRCS+= table_static.c + +SRCS+= queue_fs.c +SRCS+= queue_null.c +SRCS+= queue_proc.c +SRCS+= queue_ram.c + +SRCS+= scheduler_ramqueue.c +SRCS+= scheduler_null.c +SRCS+= scheduler_proc.c + +SRCS+= stat_ramstat.c + +MAN= sendmail.8 smtpd.8 smtpd.conf.5 table.5 +BINDIR= /usr/sbin + +LDADD+= -levent -lutil -lssl -lcrypto -lm -lz +DPADD+= ${LIBEVENT} ${LIBUTIL} ${LIBSSL} ${LIBCRYPTO} ${LIBM} ${LIBZ} + +CFLAGS+= -fstack-protector-all +CFLAGS+= -I${.CURDIR}/.. +CFLAGS+= -Wall -Wstrict-prototypes -Wmissing-prototypes +CFLAGS+= -Wmissing-declarations +CFLAGS+= -Wshadow -Wpointer-arith -Wcast-qual +CFLAGS+= -Wsign-compare +CFLAGS+= -Werror-implicit-function-declaration +#CFLAGS+= -Werror # during development phase (breaks some archs) +CFLAGS+= -DIO_TLS +CFLAGS+= -DQUEUE_PROFILING +YFLAGS= + +.include <bsd.prog.mk> diff --git a/smtpd/spfwalk.c b/smtpd/spfwalk.c new file mode 100644 index 00000000..0832d1bc --- /dev/null +++ b/smtpd/spfwalk.c @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2017 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/tree.h> + +#ifdef HAVE_ARPA_NAMESER_COMPAT_H +#include <arpa/nameser_compat.h> +#endif +#include <arpa/inet.h> +#include <arpa/nameser.h> +#include <netinet/in.h> +#include <netdb.h> + +#include <asr.h> +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <unistd.h> + +#include "smtpd-defines.h" +#include "smtpd-api.h" +#include "unpack_dns.h" +#include "parser.h" + +struct target { + void (*dispatch)(struct dns_rr *, struct target *); + int cidr4; + int cidr6; +}; + +int spfwalk(int, struct parameter *); + +static void dispatch_txt(struct dns_rr *, struct target *); +static void dispatch_mx(struct dns_rr *, struct target *); +static void dispatch_a(struct dns_rr *, struct target *); +static void dispatch_aaaa(struct dns_rr *, struct target *); +static void lookup_record(int, const char *, struct target *); +static void dispatch_record(struct asr_result *, void *); +static ssize_t parse_txt(const char *, size_t, char *, size_t); +static int parse_target(char *, struct target *); +void *xmalloc(size_t size); + +int ip_v4 = 0; +int ip_v6 = 0; +int ip_both = 1; + +struct dict seen; + +int +spfwalk(int argc, struct parameter *argv) +{ + struct target tgt; + const char *ip_family = NULL; + char *line = NULL; + size_t linesize = 0; + ssize_t linelen; + + if (argv) + ip_family = argv[0].u.u_str; + + if (ip_family) { + if (strcmp(ip_family, "-4") == 0) { + ip_both = 0; + ip_v4 = 1; + } else if (strcmp(ip_family, "-6") == 0) { + ip_both = 0; + ip_v6 = 1; + } else + errx(1, "invalid ip_family"); + } + + dict_init(&seen); + event_init(); + + tgt.cidr4 = tgt.cidr6 = -1; + tgt.dispatch = dispatch_txt; + + while ((linelen = getline(&line, &linesize, stdin)) != -1) { + while (linelen-- > 0 && isspace((unsigned char)line[linelen])) + line[linelen] = '\0'; + + if (linelen > 0) + lookup_record(T_TXT, line, &tgt); + } + + free(line); + +#if HAVE_PLEDGE + if (pledge("dns stdio", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + + return 0; +} + +void +lookup_record(int type, const char *record, struct target *tgt) +{ + struct asr_query *as; + struct target *ntgt; + + as = res_query_async(record, C_IN, type, NULL); + if (as == NULL) + err(1, "res_query_async"); + ntgt = xmalloc(sizeof(*ntgt)); + *ntgt = *tgt; + event_asr_run(as, dispatch_record, (void *)ntgt); +} + +void +dispatch_record(struct asr_result *ar, void *arg) +{ + struct target *tgt = arg; + struct unpack pack; + struct dns_header h; + struct dns_query q; + struct dns_rr rr; + + /* best effort */ + if (ar->ar_h_errno && ar->ar_h_errno != NO_DATA) + goto end; + + unpack_init(&pack, ar->ar_data, ar->ar_datalen); + unpack_header(&pack, &h); + unpack_query(&pack, &q); + + for (; h.ancount; h.ancount--) { + unpack_rr(&pack, &rr); + /**/ + tgt->dispatch(&rr, tgt); + } +end: + free(tgt); +} + +void +dispatch_txt(struct dns_rr *rr, struct target *tgt) +{ + char buf[4096]; + char *argv[512]; + char buf2[512]; + struct target ltgt; + struct in6_addr ina; + char **ap = argv; + char *in = buf; + char *record, *end; + ssize_t n; + + if (rr->rr_type != T_TXT) + return; + n = parse_txt(rr->rr.other.rdata, rr->rr.other.rdlen, buf, sizeof(buf)); + if (n == -1 || n == sizeof(buf)) + return; + buf[n] = '\0'; + + if (strncasecmp("v=spf1 ", buf, 7)) + return; + + while ((*ap = strsep(&in, " ")) != NULL) { + if (strcasecmp(*ap, "v=spf1") == 0) + continue; + + end = *ap + strlen(*ap)-1; + if (*end == '.') + *end = '\0'; + + if (dict_set(&seen, *ap, &seen)) + continue; + + if (**ap == '-' || **ap == '~') + continue; + + if (**ap == '+' || **ap == '?') + (*ap)++; + + ltgt.cidr4 = ltgt.cidr6 = -1; + + if (strncasecmp("ip4:", *ap, 4) == 0) { + if ((ip_v4 == 1 || ip_both == 1) && + inet_net_pton(AF_INET, *(ap) + 4, + &ina, sizeof(ina)) != -1) + printf("%s\n", *(ap) + 4); + continue; + } + if (strncasecmp("ip6:", *ap, 4) == 0) { + if ((ip_v6 == 1 || ip_both == 1) && + inet_net_pton(AF_INET6, *(ap) + 4, + &ina, sizeof(ina)) != -1) + printf("%s\n", *(ap) + 4); + continue; + } + if (strcasecmp("a", *ap) == 0) { + print_dname(rr->rr_dname, buf2, sizeof(buf2)); + buf2[strlen(buf2) - 1] = '\0'; + ltgt.dispatch = dispatch_a; + lookup_record(T_A, buf2, <gt); + ltgt.dispatch = dispatch_aaaa; + lookup_record(T_AAAA, buf2, <gt); + continue; + } + if (strncasecmp("a:", *ap, 2) == 0) { + record = *(ap) + 2; + if (parse_target(record, <gt) < 0) + continue; + ltgt.dispatch = dispatch_a; + lookup_record(T_A, record, <gt); + ltgt.dispatch = dispatch_aaaa; + lookup_record(T_AAAA, record, <gt); + continue; + } + if (strncasecmp("exists:", *ap, 7) == 0) { + ltgt.dispatch = dispatch_a; + lookup_record(T_A, *(ap) + 7, <gt); + continue; + } + if (strncasecmp("include:", *ap, 8) == 0) { + ltgt.dispatch = dispatch_txt; + lookup_record(T_TXT, *(ap) + 8, <gt); + continue; + } + if (strncasecmp("redirect=", *ap, 9) == 0) { + ltgt.dispatch = dispatch_txt; + lookup_record(T_TXT, *(ap) + 9, <gt); + continue; + } + if (strcasecmp("mx", *ap) == 0) { + print_dname(rr->rr_dname, buf2, sizeof(buf2)); + buf2[strlen(buf2) - 1] = '\0'; + ltgt.dispatch = dispatch_mx; + lookup_record(T_MX, buf2, <gt); + continue; + } + if (strncasecmp("mx:", *ap, 3) == 0) { + record = *(ap) + 3; + if (parse_target(record, <gt) < 0) + continue; + ltgt.dispatch = dispatch_mx; + lookup_record(T_MX, record, <gt); + continue; + } + } + *ap = NULL; +} + +void +dispatch_mx(struct dns_rr *rr, struct target *tgt) +{ + char buf[512]; + struct target ltgt; + + if (rr->rr_type != T_MX) + return; + + print_dname(rr->rr.mx.exchange, buf, sizeof(buf)); + buf[strlen(buf) - 1] = '\0'; + if (buf[strlen(buf) - 1] == '.') + buf[strlen(buf) - 1] = '\0'; + + ltgt = *tgt; + ltgt.dispatch = dispatch_a; + lookup_record(T_A, buf, <gt); + ltgt.dispatch = dispatch_aaaa; + lookup_record(T_AAAA, buf, <gt); +} + +void +dispatch_a(struct dns_rr *rr, struct target *tgt) +{ + char buffer[512]; + const char *ptr; + + if (rr->rr_type != T_A) + return; + + if ((ptr = inet_ntop(AF_INET, &rr->rr.in_a.addr, + buffer, sizeof buffer))) { + if (tgt->cidr4 >= 0) + printf("%s/%d\n", ptr, tgt->cidr4); + else + printf("%s\n", ptr); + } +} + +void +dispatch_aaaa(struct dns_rr *rr, struct target *tgt) +{ + char buffer[512]; + const char *ptr; + + if (rr->rr_type != T_AAAA) + return; + + if ((ptr = inet_ntop(AF_INET6, &rr->rr.in_aaaa.addr6, + buffer, sizeof buffer))) { + if (tgt->cidr6 >= 0) + printf("%s/%d\n", ptr, tgt->cidr6); + else + printf("%s\n", ptr); + } +} + +ssize_t +parse_txt(const char *rdata, size_t rdatalen, char *dst, size_t dstsz) +{ + size_t len; + ssize_t r = 0; + + while (rdatalen) { + len = *(const unsigned char *)rdata; + if (len >= rdatalen) { + errno = EINVAL; + return -1; + } + + rdata++; + rdatalen--; + + if (len == 0) + continue; + + if (len >= dstsz) { + errno = EOVERFLOW; + return -1; + } + memmove(dst, rdata, len); + dst += len; + dstsz -= len; + + rdata += len; + rdatalen -= len; + r += len; + } + + return r; +} + +int +parse_target(char *record, struct target *tgt) +{ + const char *err; + char *m4, *m6; + + m4 = record; + strsep(&m4, "/"); + if (m4 == NULL) + return 0; + + m6 = m4; + strsep(&m6, "/"); + + if (*m4) { + tgt->cidr4 = strtonum(m4, 0, 32, &err); + if (err) + return tgt->cidr4 = -1; + } + + if (m6 == NULL) + return 0; + + tgt->cidr6 = strtonum(m6, 0, 128, &err); + if (err) + return tgt->cidr6 = -1; + + return 0; +} diff --git a/smtpd/srs.c b/smtpd/srs.c new file mode 100644 index 00000000..bb4f4d9e --- /dev/null +++ b/smtpd/srs.c @@ -0,0 +1,379 @@ +/* $OpenBSD: srs.c,v 1.3 2019/09/29 10:03:49 gilles Exp $ */ + +/* + * Copyright (c) 2019 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <inttypes.h> +#include <netdb.h> +#include <limits.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include <openssl/sha.h> + +#include "smtpd.h" +#include "log.h" + +static uint8_t base32[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + +static int +minrange(uint16_t tref, uint16_t t2, int drift, int mod) +{ + if (tref > drift) { + /* t2 must fall in between tref and tref - drift */ + if (t2 <= tref && t2>= tref - drift) + return 1; + } + else { + /* t2 must fall in between 0 and tref, or wrap */ + if (t2 <= tref || t2 >= mod - (drift - tref)) + return 1; + } + return 0; +} + +static int +maxrange(uint16_t tref, uint16_t t2, int drift, int mod) +{ + if (tref + drift < 1024) { + /* t2 must fall in between tref and tref + drift */ + if (t2 >= tref && t2 <= tref + drift) + return 1; + } + else { + /* t2 must fall in between tref + drift, or wrap */ + if (t2 >= tref || t2 <= (tref + drift) % 1024) + return 1; + } + return 0; +} + +static int +timestamp_check_range(uint16_t tref, uint16_t t2) +{ + if (! minrange(tref, t2, env->sc_srs_ttl, 1024) && + ! maxrange(tref, t2, 1, 1024)) + return 0; + + return 1; +} + +static const unsigned char * +srs_hash(const char *key, const char *value) +{ + SHA_CTX c; + static unsigned char md[SHA_DIGEST_LENGTH]; + + SHA1_Init(&c); + SHA1_Update(&c, key, strlen(key)); + SHA1_Update(&c, value, strlen(value)); + SHA1_Final(md, &c); + return md; +} + +static const char * +srs0_encode(const char *sender, const char *rcpt_domain) +{ + static char dest[SMTPD_MAXMAILADDRSIZE]; + char tmp[SMTPD_MAXMAILADDRSIZE]; + char md[SHA_DIGEST_LENGTH*4+1]; + struct mailaddr maddr; + uint16_t timestamp; + int ret; + + /* compute 10 bits timestamp according to spec */ + timestamp = (time(NULL) / (60 * 60 * 24)) % 1024; + + /* parse sender into user and domain */ + if (! text_to_mailaddr(&maddr, sender)) + return sender; + + /* TT=<orig_domainpart>=<orig_userpart>@<new_domainpart> */ + ret = snprintf(tmp, sizeof tmp, "%c%c=%s=%s@%s", + base32[(timestamp>>5) & 0x1F], + base32[timestamp & 0x1F], + maddr.domain, maddr.user, rcpt_domain); + if (ret == -1 || ret >= (int)sizeof tmp) + return sender; + + /* compute HHHH */ + base64_encode_rfc3548(srs_hash(env->sc_srs_key, tmp), SHA_DIGEST_LENGTH, + md, sizeof md); + + /* prepend SRS0=HHHH= prefix */ + ret = snprintf(dest, sizeof dest, "SRS0=%c%c%c%c=%s", + md[0], md[1], md[2], md[3], tmp); + if (ret == -1 || ret >= (int)sizeof dest) + return sender; + + return dest; +} + +static const char * +srs1_encode_srs0(const char *sender, const char *rcpt_domain) +{ + static char dest[SMTPD_MAXMAILADDRSIZE]; + char tmp[SMTPD_MAXMAILADDRSIZE]; + char md[SHA_DIGEST_LENGTH*4+1]; + struct mailaddr maddr; + int ret; + + /* parse sender into user and domain */ + if (! text_to_mailaddr(&maddr, sender)) + return sender; + + /* <last_domainpart>==<SRS0_userpart>@<new_domainpart> */ + ret = snprintf(tmp, sizeof tmp, "%s==%s@%s", + maddr.domain, maddr.user, rcpt_domain); + if (ret == -1 || ret >= (int)sizeof tmp) + return sender; + + /* compute HHHH */ + base64_encode_rfc3548(srs_hash(env->sc_srs_key, tmp), SHA_DIGEST_LENGTH, + md, sizeof md); + + /* prepend SRS1=HHHH= prefix */ + ret = snprintf(dest, sizeof dest, "SRS1=%c%c%c%c=%s", + md[0], md[1], md[2], md[3], tmp); + if (ret == -1 || ret >= (int)sizeof dest) + return sender; + + return dest; +} + +static const char * +srs1_encode_srs1(const char *sender, const char *rcpt_domain) +{ + static char dest[SMTPD_MAXMAILADDRSIZE]; + char tmp[SMTPD_MAXMAILADDRSIZE]; + char md[SHA_DIGEST_LENGTH*4+1]; + struct mailaddr maddr; + int ret; + + /* parse sender into user and domain */ + if (! text_to_mailaddr(&maddr, sender)) + return sender; + + /* <SRS1_userpart>@<new_domainpart> */ + ret = snprintf(tmp, sizeof tmp, "%s@%s", maddr.user, rcpt_domain); + if (ret == -1 || ret >= (int)sizeof tmp) + return sender; + + /* sanity check: there's at least room for a checksum + * with allowed delimiter =, + or - + */ + if (strlen(tmp) < 5) + return sender; + if (tmp[4] != '=' && tmp[4] != '+' && tmp[4] != '-') + return sender; + + /* compute HHHH */ + base64_encode_rfc3548(srs_hash(env->sc_srs_key, tmp + 5), SHA_DIGEST_LENGTH, + md, sizeof md); + + /* prepend SRS1=HHHH= prefix skipping previous hops' HHHH */ + ret = snprintf(dest, sizeof dest, "SRS1=%c%c%c%c=%s", + md[0], md[1], md[2], md[3], tmp + 5); + if (ret == -1 || ret >= (int)sizeof dest) + return sender; + + return dest; +} + +const char * +srs_encode(const char *sender, const char *rcpt_domain) +{ + if (strncasecmp(sender, "SRS0=", 5) == 0) + return srs1_encode_srs0(sender+5, rcpt_domain); + if (strncasecmp(sender, "SRS1=", 5) == 0) + return srs1_encode_srs1(sender+5, rcpt_domain); + return srs0_encode(sender, rcpt_domain); +} + +static const char * +srs0_decode(const char *rcpt) +{ + static char dest[SMTPD_MAXMAILADDRSIZE]; + char md[SHA_DIGEST_LENGTH*4+1]; + struct mailaddr maddr; + char *p; + uint8_t *idx; + int ret; + uint16_t timestamp, srs_timestamp; + + /* sanity check: we have room for a checksum and delimiter */ + if (strlen(rcpt) < 5) + return NULL; + + /* compute checksum */ + base64_encode_rfc3548(srs_hash(env->sc_srs_key, rcpt+5), SHA_DIGEST_LENGTH, + md, sizeof md); + + /* compare prefix checksum with computed checksum */ + if (strncmp(md, rcpt, 4) != 0) { + if (env->sc_srs_key_backup == NULL) + return NULL; + base64_encode_rfc3548(srs_hash(env->sc_srs_key_backup, rcpt+5), + SHA_DIGEST_LENGTH, md, sizeof md); + if (strncmp(md, rcpt, 4) != 0) + return NULL; + } + rcpt += 5; + + /* sanity check: we have room for a timestamp and delimiter */ + if (strlen(rcpt) < 3) + return NULL; + + /* decode timestamp */ + if ((idx = strchr(base32, rcpt[0])) == NULL) + return NULL; + srs_timestamp = ((idx - base32) << 5); + + if ((idx = strchr(base32, rcpt[1])) == NULL) + return NULL; + srs_timestamp |= (idx - base32); + rcpt += 3; + + /* compute current 10 bits timestamp */ + timestamp = (time(NULL) / (60 * 60 * 24)) % 1024; + + /* check that SRS timestamp isn't too far from current */ + if (timestamp != srs_timestamp) + if (! timestamp_check_range(timestamp, srs_timestamp)) + return NULL; + + if (! text_to_mailaddr(&maddr, rcpt)) + return NULL; + + /* sanity check: we have at least one SRS separator */ + if ((p = strchr(maddr.user, '=')) == NULL) + return NULL; + *p++ = '\0'; + + /* maddr.user holds "domain\0user", with p pointing at user */ + ret = snprintf(dest, sizeof dest, "%s@%s", p, maddr.user); + if (ret == -1 || ret >= (int)sizeof dest) + return NULL; + + return dest; +} + +static const char * +srs1_decode(const char *rcpt) +{ + static char dest[SMTPD_MAXMAILADDRSIZE]; + char md[SHA_DIGEST_LENGTH*4+1]; + struct mailaddr maddr; + char *p; + uint8_t *idx; + int ret; + uint16_t timestamp, srs_timestamp; + + /* sanity check: we have room for a checksum and delimiter */ + if (strlen(rcpt) < 5) + return NULL; + + /* compute checksum */ + base64_encode_rfc3548(srs_hash(env->sc_srs_key, rcpt+5), SHA_DIGEST_LENGTH, + md, sizeof md); + + /* compare prefix checksum with computed checksum */ + if (strncmp(md, rcpt, 4) != 0) { + if (env->sc_srs_key_backup == NULL) + return NULL; + base64_encode_rfc3548(srs_hash(env->sc_srs_key_backup, rcpt+5), + SHA_DIGEST_LENGTH, md, sizeof md); + if (strncmp(md, rcpt, 4) != 0) + return NULL; + } + rcpt += 5; + + if (! text_to_mailaddr(&maddr, rcpt)) + return NULL; + + /* sanity check: we have at least one SRS separator */ + if ((p = strchr(maddr.user, '=')) == NULL) + return NULL; + *p++ = '\0'; + + /* maddr.user holds "domain\0user", with p pointing at user */ + ret = snprintf(dest, sizeof dest, "SRS0%s@%s", p, maddr.user); + if (ret == -1 || ret >= (int)sizeof dest) + return NULL; + + + /* we're ready to return decoded address, but let's check if + * SRS0 timestamp is valid. + */ + + /* first, get rid of SRS0 checksum (=HHHH=), we can't check it */ + if (strlen(p) < 6) + return NULL; + p += 6; + + /* we should be pointing to a timestamp, check that we're indeed */ + if (strlen(p) < 3) + return NULL; + if (p[2] != '=' && p[2] != '+' && p[2] != '-') + return NULL; + p[2] = '\0'; + + if ((idx = strchr(base32, p[0])) == NULL) + return NULL; + srs_timestamp = ((idx - base32) << 5); + + if ((idx = strchr(base32, p[1])) == NULL) + return NULL; + srs_timestamp |= (idx - base32); + + /* compute current 10 bits timestamp */ + timestamp = (time(NULL) / (60 * 60 * 24)) % 1024; + + /* check that SRS timestamp isn't too far from current */ + if (timestamp != srs_timestamp) + if (! timestamp_check_range(timestamp, srs_timestamp)) + return NULL; + + return dest; +} + +const char * +srs_decode(const char *rcpt) +{ + if (strncasecmp(rcpt, "SRS0=", 5) == 0) + return srs0_decode(rcpt + 5); + if (strncasecmp(rcpt, "SRS1=", 5) == 0) + return srs1_decode(rcpt + 5); + + return NULL; +} diff --git a/smtpd/ssl.c b/smtpd/ssl.c new file mode 100644 index 00000000..a37d6fea --- /dev/null +++ b/smtpd/ssl.c @@ -0,0 +1,458 @@ +/* $OpenBSD: ssl.c,v 1.93 2019/06/05 06:40:13 gilles Exp $ */ + +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Reyk Floeter <reyk@openbsd.org> + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <limits.h> +#include <pwd.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 <openssl/rsa.h> +#include <openssl/ecdsa.h> +#include <openssl/dh.h> +#include <openssl/bn.h> + +#include "log.h" +#include "ssl.h" + +void +ssl_init(void) +{ + static int inited = 0; + + if (inited) + return; + + SSL_library_init(); + SSL_load_error_strings(); + + OpenSSL_add_all_algorithms(); + + /* Init hardware crypto engines. */ + ENGINE_load_builtin_engines(); + ENGINE_register_all_complete(); + inited = 1; +} + +int +ssl_setup(SSL_CTX **ctxp, struct pki *pki, + int (*sni_cb)(SSL *,int *,void *), const char *ciphers) +{ + SSL_CTX *ctx; + uint8_t sid[SSL_MAX_SID_CTX_LENGTH]; + + ctx = ssl_ctx_create(pki->pki_name, pki->pki_cert, pki->pki_cert_len, ciphers); + + /* + * Set session ID context to a random value. We don't support + * persistent caching of sessions so it is OK to set a temporary + * session ID context that is valid during run time. + */ + arc4random_buf(sid, sizeof(sid)); + if (!SSL_CTX_set_session_id_context(ctx, sid, sizeof(sid))) + goto err; + + if (sni_cb) + SSL_CTX_set_tlsext_servername_callback(ctx, sni_cb); + + SSL_CTX_set_dh_auto(ctx, 0); + + SSL_CTX_set_ecdh_auto(ctx, 1); + + *ctxp = ctx; + return 1; + +err: + SSL_CTX_free(ctx); + ssl_error("ssl_setup"); + return 0; +} + +char * +ssl_load_file(const char *name, off_t *len, mode_t perm) +{ + struct stat st; + off_t size; + char *buf = NULL; + int fd, saved_errno; + char mode[12]; + + if ((fd = open(name, O_RDONLY)) == -1) + return (NULL); + if (fstat(fd, &st) != 0) + goto fail; + if (st.st_uid != 0) { + log_warnx("warn: %s: not owned by uid 0", name); + errno = EACCES; + goto fail; + } + if (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO) & ~perm) { + strmode(perm, mode); + log_warnx("warn: %s: insecure permissions: must be at most %s", + name, &mode[1]); + errno = EACCES; + 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: + free(buf); + saved_errno = errno; + close(fd); + errno = saved_errno; + return (NULL); +} + +#if 0 +static int +ssl_password_cb(char *buf, int size, int rwflag, void *u) +{ + size_t len; + if (u == NULL) { + explicit_bzero(buf, size); + return (0); + } + if ((len = strlcpy(buf, u, size)) >= (size_t)size) + return (0); + return (len); +} +#endif + +static int +ssl_password_cb(char *buf, int size, int rwflag, void *u) +{ + int ret = 0; + size_t len; + char *pass; + + pass = getpass((const char *)u); + if (pass == NULL) + return 0; + len = strlen(pass); + if (strlcpy(buf, pass, size) >= (size_t)size) + goto end; + ret = len; +end: + if (len) + explicit_bzero(pass, len); + return ret; +} + +char * +ssl_load_key(const char *name, off_t *len, char *pass, mode_t perm, const char *pkiname) +{ + FILE *fp = NULL; + EVP_PKEY *key = NULL; + BIO *bio = NULL; + long size; + char *data, *buf, *filebuf; + struct stat st; + char mode[12]; + char prompt[2048]; + + /* Initialize SSL library once */ + ssl_init(); + + /* + * Read (possibly) encrypted key from file + */ + if ((fp = fopen(name, "r")) == NULL) + return (NULL); + if ((filebuf = malloc_conceal(BUFSIZ)) == NULL) + goto fail; + setvbuf(fp, filebuf, _IOFBF, BUFSIZ); + + if (fstat(fileno(fp), &st) != 0) + goto fail; + if (st.st_uid != 0) { + log_warnx("warn: %s: not owned by uid 0", name); + errno = EACCES; + goto fail; + } + if (st.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO) & ~perm) { + strmode(perm, mode); + log_warnx("warn: %s: insecure permissions: must be at most %s", + name, &mode[1]); + errno = EACCES; + goto fail; + } + + (void)snprintf(prompt, sizeof prompt, "passphrase for %s: ", pkiname); + key = PEM_read_PrivateKey(fp, NULL, ssl_password_cb, prompt); + fclose(fp); + fp = NULL; + freezero(filebuf, BUFSIZ); + filebuf = NULL; + if (key == NULL) + goto fail; + /* + * Write unencrypted key to memory buffer + */ + if ((bio = BIO_new(BIO_s_mem())) == NULL) + goto fail; + if (!PEM_write_bio_PrivateKey(bio, key, NULL, NULL, 0, NULL, NULL)) + goto fail; + if ((size = BIO_get_mem_data(bio, &data)) <= 0) + goto fail; + if ((buf = calloc_conceal(1, size + 1)) == NULL) + goto fail; + memcpy(buf, data, size); + + BIO_free_all(bio); + EVP_PKEY_free(key); + + *len = (off_t)size + 1; + return (buf); + +fail: + ssl_error("ssl_load_key"); + BIO_free_all(bio); + EVP_PKEY_free(key); + if (fp) + fclose(fp); + freezero(filebuf, BUFSIZ); + return (NULL); +} + +SSL_CTX * +ssl_ctx_create(const char *pkiname, char *cert, off_t cert_len, const char *ciphers) +{ + SSL_CTX *ctx; + size_t pkinamelen = 0; + + 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, SSL_SESSION_TIMEOUT); + SSL_CTX_set_options(ctx, + SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TICKET); + SSL_CTX_set_options(ctx, + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + SSL_CTX_set_options(ctx, SSL_OP_NO_CLIENT_RENEGOTIATION); + SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); + + if (ciphers == NULL) + ciphers = SSL_CIPHERS; + if (!SSL_CTX_set_cipher_list(ctx, ciphers)) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: could not set cipher list"); + } + + if (cert != NULL) { + if (pkiname != NULL) + pkinamelen = strlen(pkiname) + 1; + if (!SSL_CTX_use_certificate_chain_mem(ctx, cert, cert_len)) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: invalid certificate chain"); + } else if (!ssl_ctx_fake_private_key(ctx, + pkiname, pkinamelen, cert, cert_len, NULL, NULL)) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: could not fake private key"); + } else if (!SSL_CTX_check_private_key(ctx)) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: invalid private key"); + } + } + + return (ctx); +} + +int +ssl_load_certificate(struct pki *p, const char *pathname) +{ + p->pki_cert = ssl_load_file(pathname, &p->pki_cert_len, 0755); + if (p->pki_cert == NULL) + return 0; + return 1; +} + +int +ssl_load_keyfile(struct pki *p, const char *pathname, const char *pkiname) +{ + char pass[1024]; + + p->pki_key = ssl_load_key(pathname, &p->pki_key_len, pass, 0740, pkiname); + if (p->pki_key == NULL) + return 0; + return 1; +} + +int +ssl_load_cafile(struct ca *c, const char *pathname) +{ + c->ca_cert = ssl_load_file(pathname, &c->ca_cert_len, 0755); + if (c->ca_cert == NULL) + return 0; + return 1; +} + +const char * +ssl_to_text(const SSL *ssl) +{ + static char buf[256]; + + (void)snprintf(buf, sizeof buf, "%s:%s:%d", + SSL_get_version(ssl), + SSL_get_cipher_name(ssl), + SSL_get_cipher_bits(ssl, NULL)); + + return (buf); +} + +void +ssl_error(const char *where) +{ + unsigned long code; + char errbuf[128]; + + for (; (code = ERR_get_error()) != 0 ;) { + ERR_error_string_n(code, errbuf, sizeof(errbuf)); + log_debug("debug: SSL library error: %s: %s", where, errbuf); + } +} + +int +ssl_load_pkey(const void *data, size_t datalen, char *buf, off_t len, + X509 **x509ptr, EVP_PKEY **pkeyptr) +{ + BIO *in; + X509 *x509 = NULL; + EVP_PKEY *pkey = NULL; + RSA *rsa = NULL; + EC_KEY *eckey = NULL; + void *exdata = NULL; + + if ((in = BIO_new_mem_buf(buf, len)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY, ERR_R_BUF_LIB); + return (0); + } + + if ((x509 = PEM_read_bio_X509(in, NULL, + ssl_password_cb, NULL)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY, ERR_R_PEM_LIB); + goto fail; + } + + if ((pkey = X509_get_pubkey(x509)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY, ERR_R_X509_LIB); + goto fail; + } + + BIO_free(in); + in = NULL; + + if (data != NULL && datalen) { + if (((rsa = EVP_PKEY_get1_RSA(pkey)) == NULL && + (eckey = EVP_PKEY_get1_EC_KEY(pkey)) == NULL) || + (exdata = malloc(datalen)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY, ERR_R_EVP_LIB); + goto fail; + } + + memcpy(exdata, data, datalen); + if (rsa) + RSA_set_ex_data(rsa, 0, exdata); +#if defined(SUPPORT_ECDSA) + if (eckey) + ECDSA_set_ex_data(eckey, 0, exdata); +#endif + RSA_free(rsa); /* dereference, will be cleaned up with pkey */ +#if defined(SUPPORT_ECDSA) + EC_KEY_free(eckey); /* dereference, will be cleaned up with pkey */ +#endif + } + + *x509ptr = x509; + *pkeyptr = pkey; + + return (1); + + fail: + RSA_free(rsa); + EC_KEY_free(eckey); + BIO_free(in); + EVP_PKEY_free(pkey); + X509_free(x509); + free(exdata); + + return (0); +} + +int +ssl_ctx_fake_private_key(SSL_CTX *ctx, const void *data, size_t datalen, + char *buf, off_t len, X509 **x509ptr, EVP_PKEY **pkeyptr) +{ + int ret = 0; + EVP_PKEY *pkey = NULL; + X509 *x509 = NULL; + + if (!ssl_load_pkey(data, datalen, buf, len, &x509, &pkey)) + return (0); + + /* + * Use the public key as the "private" key - the secret key + * parameters are hidden in an extra process that will be + * contacted by the RSA engine. The SSL/TLS library needs at + * least the public key parameters in the current process. + */ + ret = SSL_CTX_use_PrivateKey(ctx, pkey); + if (!ret) + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY, ERR_LIB_SSL); + + if (pkeyptr != NULL) + *pkeyptr = pkey; + else + EVP_PKEY_free(pkey); + + if (x509ptr != NULL) + *x509ptr = x509; + else + X509_free(x509); + + return (ret); +} diff --git a/smtpd/ssl.h b/smtpd/ssl.h new file mode 100644 index 00000000..11c80c68 --- /dev/null +++ b/smtpd/ssl.h @@ -0,0 +1,71 @@ +/* $OpenBSD: ssl.h,v 1.21 2019/09/18 11:26:30 eric Exp $ */ +/* + * Copyright (c) 2013 Gilles Chehade <gilles@poolp.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 SSL_CIPHERS "HIGH:!aNULL:!MD5" +#define SSL_SESSION_TIMEOUT 300 + +struct pki { + char pki_name[HOST_NAME_MAX+1]; + + char *pki_cert_file; + char *pki_cert; + off_t pki_cert_len; + + char *pki_key_file; + char *pki_key; + off_t pki_key_len; + + EVP_PKEY *pki_pkey; + + int pki_dhe; +}; + +struct ca { + char ca_name[HOST_NAME_MAX+1]; + + char *ca_cert_file; + char *ca_cert; + off_t ca_cert_len; +}; + + +/* ssl.c */ +void ssl_init(void); +int ssl_setup(SSL_CTX **, struct pki *, + int (*)(SSL *, int *, void *), const char *); +SSL_CTX *ssl_ctx_create(const char *, char *, off_t, const char *); +int ssl_cmp(struct pki *, struct pki *); +char *ssl_load_file(const char *, off_t *, mode_t); +char *ssl_load_key(const char *, off_t *, char *, mode_t, const char *); + +const char *ssl_to_text(const SSL *); +void ssl_error(const char *); + +int ssl_load_certificate(struct pki *, const char *); +int ssl_load_keyfile(struct pki *, const char *, const char *); +int ssl_load_cafile(struct ca *, const char *); +int ssl_load_pkey(const void *, size_t, char *, off_t, + X509 **, EVP_PKEY **); +int ssl_ctx_fake_private_key(SSL_CTX *, const void *, size_t, + char *, off_t, X509 **, EVP_PKEY **); + +/* ssl_privsep.c */ +int ssl_by_mem_ctrl(X509_LOOKUP *, int, const char *, long, char **); +int SSL_CTX_use_certificate_chain_mem(SSL_CTX *, void *, int); + +/* ssl_verify.c */ +int ssl_check_name(X509 *, const char *, int *); diff --git a/smtpd/ssl_smtpd.c b/smtpd/ssl_smtpd.c new file mode 100644 index 00000000..4e5b7e75 --- /dev/null +++ b/smtpd/ssl_smtpd.c @@ -0,0 +1,105 @@ +/* $OpenBSD: ssl_smtpd.c,v 1.13 2015/12/30 16:02:08 benno Exp $ */ + +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Reyk Floeter <reyk@openbsd.org> + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <event.h> +#include <fcntl.h> +#include <limits.h> +#include <imsg.h> +#include <pwd.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" +#include "log.h" +#include "ssl.h" + + +void * +ssl_mta_init(void *pkiname, char *cert, off_t cert_len, const char *ciphers) +{ + SSL_CTX *ctx = NULL; + SSL *ssl = NULL; + + ctx = ssl_ctx_create(pkiname, cert, cert_len, ciphers); + + if ((ssl = SSL_new(ctx)) == NULL) + goto err; + if (!SSL_set_ssl_method(ssl, SSLv23_client_method())) + goto err; + + SSL_CTX_free(ctx); + return (void *)(ssl); + +err: + SSL_free(ssl); + SSL_CTX_free(ctx); + ssl_error("ssl_mta_init"); + return (NULL); +} + +/* dummy_verify */ +static int +dummy_verify(int ok, X509_STORE_CTX *store) +{ + /* + * We *want* SMTP to request an optional client certificate, however we don't want the + * verification to take place in the SMTP process. This dummy verify will allow us to + * asynchronously verify in the lookup process. + */ + return 1; +} + +void * +ssl_smtp_init(void *ssl_ctx, int verify) +{ + SSL *ssl = NULL; + + log_debug("debug: session_start_ssl: switching to SSL"); + + if (verify) + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, dummy_verify); + + if ((ssl = SSL_new(ssl_ctx)) == NULL) + goto err; + if (!SSL_set_ssl_method(ssl, SSLv23_server_method())) + goto err; + + return (void *)(ssl); + +err: + SSL_free(ssl); + ssl_error("ssl_smtp_init"); + return (NULL); +} diff --git a/smtpd/ssl_verify.c b/smtpd/ssl_verify.c new file mode 100644 index 00000000..2e784b97 --- /dev/null +++ b/smtpd/ssl_verify.c @@ -0,0 +1,297 @@ +/* $OpenBSD: ssl_verify.c,v 1.2 2019/11/02 03:16:45 gilles Exp $ */ +/* + * Copyright (c) 2014 Jeremie Courreges-Anglas <jca@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. + */ + +/* Adapted from lib/libtls/tls_verify.c */ + +#include "includes.h" + +#include <sys/socket.h> + +#include <arpa/inet.h> +#include <netinet/in.h> + +#include <limits.h> +#include <string.h> + +#include <openssl/x509v3.h> + +#if 0 +#include <tls.h> +#include "tls_internal.h" +#endif + +#include "ssl.h" +#include "log.h" + +struct tls; +#define tls_set_errorx(ctx, ...) log_warnx(__VA_ARGS__) +union tls_addr { + struct in_addr in; + struct in6_addr in6; +}; + +static int +tls_match_name(const char *cert_name, const char *name) +{ + const char *cert_domain, *domain, *next_dot; + + if (strcasecmp(cert_name, name) == 0) + return 0; + + /* Wildcard match? */ + if (cert_name[0] == '*') { + /* + * Valid wildcards: + * - "*.domain.tld" + * - "*.sub.domain.tld" + * - etc. + * Reject "*.tld". + * No attempt to prevent the use of eg. "*.co.uk". + */ + cert_domain = &cert_name[1]; + /* Disallow "*" */ + if (cert_domain[0] == '\0') + return -1; + /* Disallow "*foo" */ + if (cert_domain[0] != '.') + return -1; + /* Disallow "*.." */ + if (cert_domain[1] == '.') + return -1; + next_dot = strchr(&cert_domain[1], '.'); + /* Disallow "*.bar" */ + if (next_dot == NULL) + return -1; + /* Disallow "*.bar.." */ + if (next_dot[1] == '.') + return -1; + + domain = strchr(name, '.'); + + /* No wildcard match against a name with no host part. */ + if (name[0] == '.') + return -1; + /* No wildcard match against a name with no domain part. */ + if (domain == NULL || strlen(domain) == 1) + return -1; + + if (strcasecmp(cert_domain, domain) == 0) + return 0; + } + + return -1; +} + +/* + * See RFC 5280 section 4.2.1.6 for SubjectAltName details. + * alt_match is set to 1 if a matching alternate name is found. + * alt_exists is set to 1 if any known alternate name exists in the certificate. + */ +static int +tls_check_subject_altname(struct tls *ctx, X509 *cert, const char *name, + int *alt_match, int *alt_exists) +{ + STACK_OF(GENERAL_NAME) *altname_stack = NULL; + union tls_addr addrbuf; + int addrlen, type; + int count, i; + int rv = 0; + + *alt_match = 0; + *alt_exists = 0; + + altname_stack = X509_get_ext_d2i(cert, NID_subject_alt_name, + NULL, NULL); + if (altname_stack == NULL) + return 0; + + if (inet_pton(AF_INET, name, &addrbuf) == 1) { + type = GEN_IPADD; + addrlen = 4; + } else if (inet_pton(AF_INET6, name, &addrbuf) == 1) { + type = GEN_IPADD; + addrlen = 16; + } else { + type = GEN_DNS; + addrlen = 0; + } + + count = sk_GENERAL_NAME_num(altname_stack); + for (i = 0; i < count; i++) { + GENERAL_NAME *altname; + + altname = sk_GENERAL_NAME_value(altname_stack, i); + + if (altname->type == GEN_DNS || altname->type == GEN_IPADD) + *alt_exists = 1; + + if (altname->type != type) + continue; + + if (type == GEN_DNS) { + const unsigned char *data; + int format, len; + + format = ASN1_STRING_type(altname->d.dNSName); + if (format == V_ASN1_IA5STRING) { + data = ASN1_STRING_get0_data(altname->d.dNSName); + len = ASN1_STRING_length(altname->d.dNSName); + + if (len < 0 || (size_t)len != strlen(data)) { + tls_set_errorx(ctx, + "error verifying name '%s': " + "NUL byte in subjectAltName, " + "probably a malicious certificate", + name); + rv = -1; + break; + } + + /* + * Per RFC 5280 section 4.2.1.6: + * " " is a legal domain name, but that + * dNSName must be rejected. + */ + if (strcmp(data, " ") == 0) { + tls_set_errorx(ctx, + "error verifying name '%s': " + "a dNSName of \" \" must not be " + "used", name); + rv = -1; + break; + } + + if (tls_match_name(data, name) == 0) { + *alt_match = 1; + break; + } + } else { +#ifdef DEBUG + fprintf(stdout, "%s: unhandled subjectAltName " + "dNSName encoding (%d)\n", getprogname(), + format); +#endif + } + + } else if (type == GEN_IPADD) { + const unsigned char *data; + int datalen; + + datalen = ASN1_STRING_length(altname->d.iPAddress); + data = ASN1_STRING_get0_data(altname->d.iPAddress); + + if (datalen < 0) { + tls_set_errorx(ctx, + "Unexpected negative length for an " + "IP address: %d", datalen); + rv = -1; + break; + } + + /* + * Per RFC 5280 section 4.2.1.6: + * IPv4 must use 4 octets and IPv6 must use 16 octets. + */ + if (datalen == addrlen && + memcmp(data, &addrbuf, addrlen) == 0) { + *alt_match = 1; + break; + } + } + } + + sk_GENERAL_NAME_pop_free(altname_stack, GENERAL_NAME_free); + return rv; +} + +static int +tls_check_common_name(struct tls *ctx, X509 *cert, const char *name, + int *cn_match) +{ + X509_NAME *subject_name; + char *common_name = NULL; + union tls_addr addrbuf; + int common_name_len; + int rv = 0; + + *cn_match = 0; + + subject_name = X509_get_subject_name(cert); + if (subject_name == NULL) + goto done; + + common_name_len = X509_NAME_get_text_by_NID(subject_name, + NID_commonName, NULL, 0); + if (common_name_len < 0) + goto done; + + common_name = calloc(common_name_len + 1, 1); + if (common_name == NULL) + goto done; + + X509_NAME_get_text_by_NID(subject_name, NID_commonName, common_name, + common_name_len + 1); + + /* NUL bytes in CN? */ + if (common_name_len < 0 || + (size_t)common_name_len != strlen(common_name)) { + tls_set_errorx(ctx, "error verifying name '%s': " + "NUL byte in Common Name field, " + "probably a malicious certificate", name); + rv = -1; + goto done; + } + + /* + * We don't want to attempt wildcard matching against IP addresses, + * so perform a simple comparison here. + */ + if (inet_pton(AF_INET, name, &addrbuf) == 1 || + inet_pton(AF_INET6, name, &addrbuf) == 1) { + if (strcmp(common_name, name) == 0) + *cn_match = 1; + goto done; + } + + if (tls_match_name(common_name, name) == 0) + *cn_match = 1; + + done: + free(common_name); + return rv; +} + +int +ssl_check_name(X509 *cert, const char *name, int *match) +{ + int alt_exists; + + *match = 0; + + if (tls_check_subject_altname(NULL, cert, name, match, + &alt_exists) == -1) + return -1; + + /* + * As per RFC 6125 section 6.4.4, if any known alternate name existed + * in the certificate, we do not attempt to match on the CN. + */ + if (*match || alt_exists) + return 0; + + return tls_check_common_name(NULL, cert, name, match); +} diff --git a/smtpd/stat_backend.c b/smtpd/stat_backend.c new file mode 100644 index 00000000..30cb299b --- /dev/null +++ b/smtpd/stat_backend.c @@ -0,0 +1,124 @@ +/* $OpenBSD: stat_backend.c,v 1.11 2018/12/27 10:35:26 gilles Exp $ */ + +/* + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/queue.h> +#include <sys/tree.h> + +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <string.h> +#include <limits.h> + +#include "log.h" +#include "smtpd.h" + +extern struct stat_backend stat_backend_ramstat; + +struct stat_backend * +stat_backend_lookup(const char *name) +{ + return &stat_backend_ramstat; +} + +void +stat_increment(const char *key, size_t count) +{ + struct stat_value *value; + + if (count == 0) + return; + + value = stat_counter(count); + + m_create(p_control, IMSG_STAT_INCREMENT, 0, 0, -1); + m_add_string(p_control, key); + m_add_data(p_control, value, sizeof(*value)); + m_close(p_control); +} + +void +stat_decrement(const char *key, size_t count) +{ + struct stat_value *value; + + if (count == 0) + return; + + value = stat_counter(count); + + m_create(p_control, IMSG_STAT_DECREMENT, 0, 0, -1); + m_add_string(p_control, key); + m_add_data(p_control, value, sizeof(*value)); + m_close(p_control); +} + +void +stat_set(const char *key, const struct stat_value *value) +{ + m_create(p_control, IMSG_STAT_SET, 0, 0, -1); + m_add_string(p_control, key); + m_add_data(p_control, value, sizeof(*value)); + m_close(p_control); +} + +/* helpers */ + +struct stat_value * +stat_counter(size_t counter) +{ + static struct stat_value value; + + value.type = STAT_COUNTER; + value.u.counter = counter; + return &value; +} + +struct stat_value * +stat_timestamp(time_t timestamp) +{ + static struct stat_value value; + + value.type = STAT_TIMESTAMP; + value.u.timestamp = timestamp; + return &value; +} + +struct stat_value * +stat_timeval(struct timeval *tv) +{ + static struct stat_value value; + + value.type = STAT_TIMEVAL; + value.u.tv = *tv; + return &value; +} + +struct stat_value * +stat_timespec(struct timespec *ts) +{ + static struct stat_value value; + + value.type = STAT_TIMESPEC; + value.u.ts = *ts; + return &value; +} diff --git a/smtpd/stat_ramstat.c b/smtpd/stat_ramstat.c new file mode 100644 index 00000000..bbf1541a --- /dev/null +++ b/smtpd/stat_ramstat.c @@ -0,0 +1,162 @@ +/* $OpenBSD: stat_ramstat.c,v 1.11 2018/05/31 21:06:12 gilles Exp $ */ + +/* + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/queue.h> +#include <sys/tree.h> + +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> + +#include "smtpd.h" +#include "log.h" + + +static void ramstat_init(void); +static void ramstat_close(void); +static void ramstat_increment(const char *, size_t); +static void ramstat_decrement(const char *, size_t); +static void ramstat_set(const char *, const struct stat_value *); +static int ramstat_iter(void **, char **, struct stat_value *); + +struct ramstat_entry { + RB_ENTRY(ramstat_entry) entry; + char key[STAT_KEY_SIZE]; + struct stat_value value; +}; +RB_HEAD(stats_tree, ramstat_entry) stats; +RB_PROTOTYPE(stats_tree, ramstat_entry, entry, ramstat_entry_cmp); + +struct stat_backend stat_backend_ramstat = { + ramstat_init, + ramstat_close, + ramstat_increment, + ramstat_decrement, + ramstat_set, + ramstat_iter +}; + +static void +ramstat_init(void) +{ + log_trace(TRACE_STAT, "ramstat: init"); + + RB_INIT(&stats); + + /* ramstat_set() should be called for each key we want + * to have displayed by smtpctl show stats at startup. + */ + ramstat_set("uptime", stat_timestamp(env->sc_uptime)); +} + +static void +ramstat_close(void) +{ + log_trace(TRACE_STAT, "ramstat: close"); +} + +static void +ramstat_increment(const char *name, size_t val) +{ + struct ramstat_entry *np, lk; + + log_trace(TRACE_STAT, "ramstat: increment: %s", name); + (void)strlcpy(lk.key, name, sizeof (lk.key)); + np = RB_FIND(stats_tree, &stats, &lk); + if (np == NULL) { + np = xcalloc(1, sizeof *np); + (void)strlcpy(np->key, name, sizeof (np->key)); + RB_INSERT(stats_tree, &stats, np); + } + log_trace(TRACE_STAT, "ramstat: %s (%p): %zd -> %zd", + name, name, np->value.u.counter, np->value.u.counter + val); + np->value.u.counter += val; +} + +static void +ramstat_decrement(const char *name, size_t val) +{ + struct ramstat_entry *np, lk; + + log_trace(TRACE_STAT, "ramstat: decrement: %s", name); + (void)strlcpy(lk.key, name, sizeof (lk.key)); + np = RB_FIND(stats_tree, &stats, &lk); + if (np == NULL) { + np = xcalloc(1, sizeof *np); + (void)strlcpy(np->key, name, sizeof (np->key)); + RB_INSERT(stats_tree, &stats, np); + } + log_trace(TRACE_STAT, "ramstat: %s (%p): %zd -> %zd", + name, name, np->value.u.counter, np->value.u.counter - val); + np->value.u.counter -= val; +} + +static void +ramstat_set(const char *name, const struct stat_value *val) +{ + struct ramstat_entry *np, lk; + + log_trace(TRACE_STAT, "ramstat: set: %s", name); + (void)strlcpy(lk.key, name, sizeof (lk.key)); + np = RB_FIND(stats_tree, &stats, &lk); + if (np == NULL) { + np = xcalloc(1, sizeof *np); + (void)strlcpy(np->key, name, sizeof (np->key)); + RB_INSERT(stats_tree, &stats, np); + } + log_trace(TRACE_STAT, "ramstat: %s: n/a -> n/a", name); + np->value = *val; +} + +static int +ramstat_iter(void **iter, char **name, struct stat_value *val) +{ + struct ramstat_entry *np; + + log_trace(TRACE_STAT, "ramstat: iter"); + if (RB_EMPTY(&stats)) + return 0; + + if (*iter == NULL) + np = RB_MIN(stats_tree, &stats); + else + np = RB_NEXT(stats_tree, &stats, *iter); + + *iter = np; + if (np == NULL) + return 0; + + *name = np->key; + *val = np->value; + return 1; +} + + +static int +ramstat_entry_cmp(struct ramstat_entry *e1, struct ramstat_entry *e2) +{ + return strcmp(e1->key, e2->key); +} + +RB_GENERATE(stats_tree, ramstat_entry, entry, ramstat_entry_cmp); diff --git a/smtpd/table.5 b/smtpd/table.5 new file mode 100644 index 00000000..e9d4fa4b --- /dev/null +++ b/smtpd/table.5 @@ -0,0 +1,258 @@ +.\" $OpenBSD: table.5,v 1.11 2019/08/11 13:00:57 gilles Exp $ +.\" +.\" Copyright (c) 2013 Eric Faurot <eric@openbsd.org> +.\" Copyright (c) 2013 Gilles Chehade <gilles@poolp.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: August 11 2019 $ +.Dt TABLE 5 +.Os +.Sh NAME +.Nm table +.Nd format description for smtpd tables +.Sh DESCRIPTION +This manual page documents the file format for the various tables used in the +.Xr smtpd 8 +mail daemon. +.Pp +The format described here applies to tables as defined in +.Xr smtpd.conf 5 . +.Sh TABLE TYPES +There are two types of tables: lists and mappings. +A list consists of a series of values, +while a mapping consists of a series of keys and their associated values. +The following illustrates how to declare them as static tables: +.Bd -literal -offset indent +table mylist { value1, value2, value3 } +table mymapping { key1 = value1, key2 = value2, key3 = value3 } +.Ed +.Pp +When using a +.Ql file +table, a list will be written with each value on a line by itself. +Comments can be put anywhere in the file using a hash mark +.Pq Sq # , +and extend to the end of the current line. +.Bd -literal -offset indent +value1 +value2 +value3 +.Ed +.Pp +A mapping will be written with each key and value on a line, +whitespaces separating both columns: +.Bd -literal -offset indent +key1 value1 +key2 value2 +key3 value3 +.Ed +.Pp +A file table can be converted to a Berkeley database using the +.Xr makemap 8 +utility with no syntax change. +.Pp +Tables using a +.Ql file +or Berkeley DB backend will be referenced as follows: +.Bd -unfilled -offset indent +.Ic table Ar name Cm file : Ns Pa /path/to/file +.Ic table Ar name Cm db : Ns Pa /path/to/file.db +.Ed +.Ss Aliasing tables +Aliasing tables are mappings that associate a recipient to one or many +destinations. +They can be used in two contexts: primary domain aliases and virtual domain +mapping. +.Bd -unfilled -offset indent +.Ic action Ar name method Cm alias Pf < table Ns > +.Ic action Ar name method Cm virtual Pf < table Ns > +.Ed +.Pp +In a primary domain context, the key is the user part of the recipient address, +whilst the value is one or many recipients as described in +.Xr aliases 5 : +.Bd -literal -offset indent +user1 otheruser +user2 otheruser1,otheruser2 +user3 otheruser@example.com +.Ed +.Pp +In a virtual domain context, the key is either a user part, a full email +address or a catch all, following selection rules described in +.Xr smtpd.conf 5 , +and the value is one or many recipients as described in +.Xr aliases 5 : +.Bd -literal -offset indent +user1 otheruser +user2@example.org otheruser1,otheruser2 +@example.org otheruser@example.com +@ catchall@example.com +.Ed +.Pp +The following directive shares the same table format, +but with a different meaning. +Here, the user is allowed to send mail from the listed addresses: +.Bd -unfilled -offset indent +.Ic listen on Ar interface Cm auth Oo Ar ... Oc Cm senders Pf < Ar table Ns > +.Ed +.Ss Domain tables +Domain tables are simple lists of domains or hosts. +.Bd -unfilled -offset indent +.Ic match Cm for domain Pf < table Ns > Cm action Ar name +.Ic match Cm helo Pf < table Ns > Oo Ar ... Oc Cm action Ar name +.Ed +.Pp +In that context, the list of domains will be matched against the recipient +domain or against the HELO name advertised by the sending host, +respectively. +For +.Ql static , +.Ql file +and +.Xr dbopen 3 +backends, a wildcard may be used so the domain table may contain: +.Bd -literal -offset indent +example.org +*.example.org +.Ed +.Ss Credentials tables +Credentials tables are mappings of credentials. +They can be used in two contexts: +.Bd -unfilled -offset indent +.Ic listen on Ar interface Cm tls Oo Ar ... Oc Cm auth Pf < Ar table Ns > +.Ic action Ar name Cm relay host Ar relay-url Cm auth Pf < Ar table Ns > +.Ed +.Pp +In a listener context, the credentials are a mapping of username and encrypted +passwords: +.Bd -literal -offset indent +user1 $2b$10$hIJ4QfMcp.90nJwKqGbKM.MybArjHOTpEtoTV.DgLYAiThuoYmTSe +user2 $2b$10$bwSmUOBGcZGamIfRuXGTvuTo3VLbPG9k5yeKNMBtULBhksV5KdGsK +.Ed +.Pp +The passwords are to be encrypted using the +.Xr smtpctl 8 +encrypt subcommand. +.Pp +In a relay context, the credentials are a mapping of labels and +username:password pairs: +.Bd -literal -offset indent +label1 user:password +.Ed +.Pp +The label must be unique and is used as a selector for the proper credentials +when multiple credentials are valid for a single destination. +The password is not encrypted as it must be provided to the remote host. +.Ss Netaddr tables +Netaddr tables are lists of IPv4 and IPv6 network addresses. +They can only be used in the following context: +.Pp +.D1 Ic match Cm from src Pf < Ar table Ns > Cm action Ar name +.Pp +When used as a "from source", the address of a client is compared to the list +of addresses in the table until a match is found. +.Pp +A netaddr table can contain exact addresses or netmasks, and looks as follow: +.Bd -literal -offset indent +192.168.1.1 +::1 +ipv6:::1 +192.168.1.0/24 +.Ed +.Ss Userinfo tables +User info tables are used in rule context to specify an alternate user base, +mapping virtual users to local system users by UID, GID and home directory. +.Pp +.D1 Ic action Ar name method Cm userbase Pf < Ar table Ns > +.Pp +A userinfo table looks as follows: +.Bd -literal -offset indent +joe 1000:100:/home/virtual/joe +jack 1000:100:/home/virtual/jack +.Ed +.Pp +In this example, both joe and jack are virtual users mapped to the local +system user with UID 1000 and GID 100, but different home directories. +These directories may contain a +.Xr forward 5 +file. +This can be used in conjunction with an alias table +that maps an email address or the domain part to the desired virtual +username. +For example: +.Bd -literal -offset indent +joe@example.org joe +jack@example.com jack +.Ed +.Ss Source tables +Source tables are lists of IPv4 and IPv6 addresses. +They can only be used in the following context: +.Pp +.D1 Ic action Ar name Cm relay src Pf < Ar table Ns > +.Pp +Successive queries to the source table will return the elements one by one. +.Pp +A source table looks as follow: +.Bd -literal -offset indent +192.168.1.2 +192.168.1.3 +::1 +::2 +ipv6:::3 +ipv6:::4 +.Ed +.Ss Mailaddr tables +Mailaddr tables are lists of email addresses. +They can be used in the following contexts: +.Bd -unfilled -offset indent +.Ic match Cm mail\-from Pf < Ar table Ns > Cm action Ar name +.Ic match Cm rcpt\-to Pf < Ar table Ns > Cm action Ar name +.Ed +.Pp +A mailaddr entry is used to match an email address against a username, +a domain or a full email address. +A "*" wildcard may be used in part of the domain name. +.Pp +A mailaddr table looks as follow: +.Bd -literal -offset indent +user +@domain +user@domain +user@*.domain +.Ed +.Ss Addrname tables +Addrname tables are used to map IP addresses to hostnames. +They can be used in both listen context and relay context: +.Bd -unfilled -offset indent +.Ic listen on Ar interface Cm hostnames Pf < Ar table Ns > +.Ic action Ar name Cm relay helo\-src Pf < Ar table Ns > +.Ed +.Pp +In listen context, the table is used to look up the server name to advertise +depending on the local address of the socket on which a connection is accepted. +In relay context, the table is used to determine the hostname for the HELO +sequence of the SMTP protocol, depending on the local address used for the +outgoing connection. +.Pp +The format is a mapping from inet4 or inet6 addresses to hostnames: +.Bd -literal -offset indent +::1 localhost +127.0.0.1 localhost +88.190.23.165 www.opensmtpd.org +.Ed +.Sh SEE ALSO +.Xr smtpd.conf 5 , +.Xr makemap 8 , +.Xr smtpd 8 diff --git a/smtpd/table.c b/smtpd/table.c new file mode 100644 index 00000000..469eeee1 --- /dev/null +++ b/smtpd/table.c @@ -0,0 +1,709 @@ +/* $OpenBSD: table.c,v 1.48 2019/01/10 07:40:52 eric Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <net/if.h> + +#include <errno.h> +#include <event.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <regex.h> +#include <limits.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +struct table_backend *table_backend_lookup(const char *); + +extern struct table_backend table_backend_static; +#ifdef HAVE_DB_API +extern struct table_backend table_backend_db; +#endif +extern struct table_backend table_backend_getpwnam; +extern struct table_backend table_backend_proc; + +static const char * table_service_name(enum table_service); +static int table_parse_lookup(enum table_service, const char *, const char *, + union lookup *); +static int parse_sockaddr(struct sockaddr *, int, const char *); + +static unsigned int last_table_id = 0; + +static struct table_backend *backends[] = { + &table_backend_static, +#ifdef HAVE_DB_API + &table_backend_db, +#endif + &table_backend_getpwnam, + &table_backend_proc, + NULL +}; + +struct table_backend * +table_backend_lookup(const char *backend) +{ + int i; + + if (!strcmp(backend, "file")) + backend = "static"; + + for (i = 0; backends[i]; i++) + if (!strcmp(backends[i]->name, backend)) + return (backends[i]); + + return NULL; +} + +static const char * +table_service_name(enum table_service s) +{ + switch (s) { + case K_NONE: return "NONE"; + case K_ALIAS: return "ALIAS"; + case K_DOMAIN: return "DOMAIN"; + case K_CREDENTIALS: return "CREDENTIALS"; + case K_NETADDR: return "NETADDR"; + case K_USERINFO: return "USERINFO"; + case K_SOURCE: return "SOURCE"; + case K_MAILADDR: return "MAILADDR"; + case K_ADDRNAME: return "ADDRNAME"; + case K_MAILADDRMAP: return "MAILADDRMAP"; + case K_RELAYHOST: return "RELAYHOST"; + case K_STRING: return "STRING"; + case K_REGEX: return "REGEX"; + } + return "???"; +} + +struct table * +table_find(struct smtpd *conf, const char *name) +{ + return dict_get(conf->sc_tables_dict, name); +} + +int +table_match(struct table *table, enum table_service kind, const char *key) +{ + return table_lookup(table, kind, key, NULL); +} + +int +table_lookup(struct table *table, enum table_service kind, const char *key, + union lookup *lk) +{ + char lkey[1024], *buf = NULL; + int r; + + r = -1; + if (table->t_backend->lookup == NULL) + errno = ENOTSUP; + else if (!lowercase(lkey, key, sizeof lkey)) { + log_warnx("warn: lookup key too long: %s", key); + errno = EINVAL; + } + else + r = table->t_backend->lookup(table, kind, lkey, lk ? &buf : NULL); + + if (r == 1) { + log_trace(TRACE_LOOKUP, "lookup: %s \"%s\" as %s in table %s:%s -> %s%s%s", + lk ? "lookup" : "match", + key, + table_service_name(kind), + table->t_backend->name, + table->t_name, + lk ? "\"" : "", + lk ? buf : "true", + lk ? "\"" : ""); + if (buf) + r = table_parse_lookup(kind, lkey, buf, lk); + } + else + log_trace(TRACE_LOOKUP, "lookup: %s \"%s\" as %s in table %s:%s -> %s%s", + lk ? "lookup" : "match", + key, + table_service_name(kind), + table->t_backend->name, + table->t_name, + (r == -1) ? "error: " : (lk ? "none" : "false"), + (r == -1) ? strerror(errno) : ""); + + free(buf); + + return (r); +} + +int +table_fetch(struct table *table, enum table_service kind, union lookup *lk) +{ + char *buf = NULL; + int r; + + r = -1; + if (table->t_backend->fetch == NULL) + errno = ENOTSUP; + else + r = table->t_backend->fetch(table, kind, &buf); + + if (r == 1) { + log_trace(TRACE_LOOKUP, "lookup: fetch %s from table %s:%s -> \"%s\"", + table_service_name(kind), + table->t_backend->name, + table->t_name, + buf); + r = table_parse_lookup(kind, NULL, buf, lk); + } + else + log_trace(TRACE_LOOKUP, "lookup: fetch %s from table %s:%s -> %s%s", + table_service_name(kind), + table->t_backend->name, + table->t_name, + (r == -1) ? "error: " : "none", + (r == -1) ? strerror(errno) : ""); + + free(buf); + + return (r); +} + +struct table * +table_create(struct smtpd *conf, const char *backend, const char *name, + const char *config) +{ + struct table *t; + struct table_backend *tb; + char path[LINE_MAX]; + size_t n; + struct stat sb; + + if (name && table_find(conf, name)) + fatalx("table_create: table \"%s\" already defined", name); + + if ((tb = table_backend_lookup(backend)) == NULL) { + if ((size_t)snprintf(path, sizeof(path), PATH_LIBEXEC"/table-%s", + backend) >= sizeof(path)) { + fatalx("table_create: path too long \"" + PATH_LIBEXEC"/table-%s\"", backend); + } + if (stat(path, &sb) == 0) { + tb = table_backend_lookup("proc"); + (void)strlcpy(path, backend, sizeof(path)); + if (config) { + (void)strlcat(path, ":", sizeof(path)); + if (strlcat(path, config, sizeof(path)) + >= sizeof(path)) + fatalx("table_create: config file path too long"); + } + config = path; + } + } + + if (tb == NULL) + fatalx("table_create: backend \"%s\" does not exist", backend); + + t = xcalloc(1, sizeof(*t)); + t->t_backend = tb; + + if (config) { + if (strlcpy(t->t_config, config, sizeof t->t_config) + >= sizeof t->t_config) + fatalx("table_create: table config \"%s\" too large", + t->t_config); + } + + if (strcmp(tb->name, "static") != 0) + t->t_type = T_DYNAMIC; + + if (name == NULL) + (void)snprintf(t->t_name, sizeof(t->t_name), "<dynamic:%u>", + last_table_id++); + else { + n = strlcpy(t->t_name, name, sizeof(t->t_name)); + if (n >= sizeof(t->t_name)) + fatalx("table_create: table name too long"); + } + + dict_set(conf->sc_tables_dict, t->t_name, t); + + return (t); +} + +void +table_destroy(struct smtpd *conf, struct table *t) +{ + dict_xpop(conf->sc_tables_dict, t->t_name); + free(t); +} + +int +table_config(struct table *t) +{ + if (t->t_backend->config == NULL) + return (1); + return (t->t_backend->config(t)); +} + +void +table_add(struct table *t, const char *key, const char *val) +{ + if (t->t_backend->add == NULL) + fatalx("table_add: cannot add to table"); + + if (t->t_backend->add(t, key, val) == 0) + log_warnx("warn: failed to add \"%s\" in table \"%s\"", key, t->t_name); +} + +void +table_dump(struct table *t) +{ + const char *type; + char buf[LINE_MAX]; + + switch(t->t_type) { + case T_NONE: + type = "NONE"; + break; + case T_DYNAMIC: + type = "DYNAMIC"; + break; + case T_LIST: + type = "LIST"; + break; + case T_HASH: + type = "HASH"; + break; + default: + type = "???"; + break; + } + + if (t->t_config[0]) + snprintf(buf, sizeof(buf), " config=\"%s\"", t->t_config); + else + buf[0] = '\0'; + + log_debug("TABLE \"%s\" backend=%s type=%s%s", t->t_name, + t->t_backend->name, type, buf); + + if (t->t_backend->dump) + t->t_backend->dump(t); +} + +int +table_check_type(struct table *t, uint32_t mask) +{ + return t->t_type & mask; +} + +int +table_check_service(struct table *t, uint32_t mask) +{ + return t->t_backend->services & mask; +} + +int +table_check_use(struct table *t, uint32_t tmask, uint32_t smask) +{ + return table_check_type(t, tmask) && table_check_service(t, smask); +} + +int +table_open(struct table *t) +{ + if (t->t_backend->open == NULL) + return (1); + return (t->t_backend->open(t)); +} + +void +table_close(struct table *t) +{ + if (t->t_backend->close) + t->t_backend->close(t); +} + +int +table_update(struct table *t) +{ + if (t->t_backend->update == NULL) + return (1); + return (t->t_backend->update(t)); +} + + +/* + * quick reminder: + * in *_match() s1 comes from session, s2 comes from table + */ + +int +table_domain_match(const char *s1, const char *s2) +{ + return hostname_match(s1, s2); +} + +int +table_mailaddr_match(const char *s1, const char *s2) +{ + struct mailaddr m1; + struct mailaddr m2; + + if (!text_to_mailaddr(&m1, s1)) + return 0; + if (!text_to_mailaddr(&m2, s2)) + return 0; + return mailaddr_match(&m1, &m2); +} + +static int table_match_mask(struct sockaddr_storage *, struct netaddr *); +static int table_inet4_match(struct sockaddr_in *, struct netaddr *); +static int table_inet6_match(struct sockaddr_in6 *, struct netaddr *); + +int +table_netaddr_match(const char *s1, const char *s2) +{ + struct netaddr n1; + struct netaddr n2; + + if (strcasecmp(s1, s2) == 0) + return 1; + if (!text_to_netaddr(&n1, s1)) + return 0; + if (!text_to_netaddr(&n2, s2)) + return 0; + if (n1.ss.ss_family != n2.ss.ss_family) + return 0; + if (SS_LEN(&n1.ss) != SS_LEN(&n2.ss)) + return 0; + return table_match_mask(&n1.ss, &n2); +} + +static int +table_match_mask(struct sockaddr_storage *ss, struct netaddr *ssmask) +{ + if (ss->ss_family == AF_INET) + return table_inet4_match((struct sockaddr_in *)ss, ssmask); + + if (ss->ss_family == AF_INET6) + return table_inet6_match((struct sockaddr_in6 *)ss, ssmask); + + return (0); +} + +static int +table_inet4_match(struct sockaddr_in *ss, struct netaddr *ssmask) +{ + in_addr_t mask; + int i; + + /* a.b.c.d/8 -> htonl(0xff000000) */ + mask = 0; + for (i = 0; i < ssmask->bits; ++i) + mask = (mask >> 1) | 0x80000000; + mask = htonl(mask); + + /* (addr & mask) == (net & mask) */ + if ((ss->sin_addr.s_addr & mask) == + (((struct sockaddr_in *)ssmask)->sin_addr.s_addr & mask)) + return 1; + + return 0; +} + +static int +table_inet6_match(struct sockaddr_in6 *ss, struct netaddr *ssmask) +{ + struct in6_addr *in; + struct in6_addr *inmask; + struct in6_addr mask; + int i; + + memset(&mask, 0, sizeof(mask)); + for (i = 0; i < ssmask->bits / 8; i++) + mask.s6_addr[i] = 0xff; + i = ssmask->bits % 8; + if (i) + mask.s6_addr[ssmask->bits / 8] = 0xff00 >> i; + + in = &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] & mask.s6_addr[i])) + return (0); + } + + return (1); +} + +int +table_regex_match(const char *string, const char *pattern) +{ + regex_t preg; + int cflags = REG_EXTENDED|REG_NOSUB; + + if (strncmp(pattern, "(?i)", 4) == 0) { + cflags |= REG_ICASE; + pattern += 4; + } + + if (regcomp(&preg, pattern, cflags) != 0) + return (0); + + if (regexec(&preg, string, 0, NULL, 0) != 0) + return (0); + + return (1); +} + +void +table_dump_all(struct smtpd *conf) +{ + struct table *t; + void *iter; + + iter = NULL; + while (dict_iter(conf->sc_tables_dict, &iter, NULL, (void **)&t)) + table_dump(t); +} + +void +table_open_all(struct smtpd *conf) +{ + struct table *t; + void *iter; + + iter = NULL; + while (dict_iter(conf->sc_tables_dict, &iter, NULL, (void **)&t)) + if (!table_open(t)) + fatalx("failed to open table %s", t->t_name); +} + +void +table_close_all(struct smtpd *conf) +{ + struct table *t; + void *iter; + + iter = NULL; + while (dict_iter(conf->sc_tables_dict, &iter, NULL, (void **)&t)) + table_close(t); +} + +static int +table_parse_lookup(enum table_service service, const char *key, + const char *line, union lookup *lk) +{ + char buffer[LINE_MAX], *p; + size_t len; + + len = strlen(line); + + switch (service) { + case K_ALIAS: + lk->expand = calloc(1, sizeof(*lk->expand)); + if (lk->expand == NULL) + return (-1); + if (!expand_line(lk->expand, line, 1)) { + expand_free(lk->expand); + return (-1); + } + return (1); + + case K_DOMAIN: + if (strlcpy(lk->domain.name, line, sizeof(lk->domain.name)) + >= sizeof(lk->domain.name)) + return (-1); + return (1); + + case K_CREDENTIALS: + + /* credentials are stored as user:password */ + if (len < 3) + return (-1); + + /* too big to fit in a smtp session line */ + if (len >= LINE_MAX) + return (-1); + + p = strchr(line, ':'); + if (p == NULL) { + if (strlcpy(lk->creds.username, key, sizeof (lk->creds.username)) + >= sizeof (lk->creds.username)) + return (-1); + if (strlcpy(lk->creds.password, line, sizeof(lk->creds.password)) + >= sizeof(lk->creds.password)) + return (-1); + return (1); + } + + if (p == line || p == line + len - 1) + return (-1); + + memmove(lk->creds.username, line, p - line); + lk->creds.username[p - line] = '\0'; + + if (strlcpy(lk->creds.password, p+1, sizeof(lk->creds.password)) + >= sizeof(lk->creds.password)) + return (-1); + + return (1); + + case K_NETADDR: + if (!text_to_netaddr(&lk->netaddr, line)) + return (-1); + return (1); + + case K_USERINFO: + if (!bsnprintf(buffer, sizeof(buffer), "%s:%s", key, line)) + return (-1); + if (!text_to_userinfo(&lk->userinfo, buffer)) + return (-1); + return (1); + + case K_SOURCE: + if (parse_sockaddr((struct sockaddr *)&lk->source.addr, + PF_UNSPEC, line) == -1) + return (-1); + return (1); + + case K_MAILADDR: + if (!text_to_mailaddr(&lk->mailaddr, line)) + return (-1); + return (1); + + case K_MAILADDRMAP: + lk->maddrmap = calloc(1, sizeof(*lk->maddrmap)); + if (lk->maddrmap == NULL) + return (-1); + maddrmap_init(lk->maddrmap); + if (!mailaddr_line(lk->maddrmap, line)) { + maddrmap_free(lk->maddrmap); + return (-1); + } + return (1); + + case K_ADDRNAME: + if (parse_sockaddr((struct sockaddr *)&lk->addrname.addr, + PF_UNSPEC, key) == -1) + return (-1); + if (strlcpy(lk->addrname.name, line, sizeof(lk->addrname.name)) + >= sizeof(lk->addrname.name)) + return (-1); + return (1); + + case K_RELAYHOST: + if (strlcpy(lk->relayhost, line, sizeof(lk->relayhost)) + >= sizeof(lk->relayhost)) + return (-1); + return (1); + + default: + return (-1); + } +} + +static int +parse_sockaddr(struct sockaddr *sa, int family, const char *str) +{ + struct in_addr ina; + struct in6_addr in6a; + struct sockaddr_in *sin; + struct sockaddr_in6 *sin6; + char *cp, *str2; + const char *errstr; + + switch (family) { + case PF_UNSPEC: + if (parse_sockaddr(sa, PF_INET, str) == 0) + return (0); + return parse_sockaddr(sa, PF_INET6, str); + + case PF_INET: + if (inet_pton(PF_INET, str, &ina) != 1) + return (-1); + + sin = (struct sockaddr_in *)sa; + memset(sin, 0, sizeof *sin); +#ifdef HAVE_STRUCT_SOCKADDR_IN_SIN_LEN + sin->sin_len = sizeof(struct sockaddr_in); +#endif + sin->sin_family = PF_INET; + sin->sin_addr.s_addr = ina.s_addr; + return (0); + + case PF_INET6: + if (strncasecmp("ipv6:", str, 5) == 0) + str += 5; + cp = strchr(str, SCOPE_DELIMITER); + if (cp) { + str2 = strdup(str); + if (str2 == NULL) + return (-1); + str2[cp - str] = '\0'; + if (inet_pton(PF_INET6, str2, &in6a) != 1) { + free(str2); + return (-1); + } + cp++; + free(str2); + } else if (inet_pton(PF_INET6, str, &in6a) != 1) + return (-1); + + sin6 = (struct sockaddr_in6 *)sa; + memset(sin6, 0, sizeof *sin6); +#ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_LEN + sin6->sin6_len = sizeof(struct sockaddr_in6); +#endif + sin6->sin6_family = PF_INET6; + sin6->sin6_addr = in6a; + + if (cp == NULL) + return (0); + + if (IN6_IS_ADDR_LINKLOCAL(&in6a) || + IN6_IS_ADDR_MC_LINKLOCAL(&in6a) || + IN6_IS_ADDR_MC_NODELOCAL(&in6a)) + if ((sin6->sin6_scope_id = if_nametoindex(cp))) + return (0); + + sin6->sin6_scope_id = strtonum(cp, 0, UINT32_MAX, &errstr); + if (errstr) + return (-1); + return (0); + + default: + break; + } + + return (-1); +} diff --git a/smtpd/table_db.c b/smtpd/table_db.c new file mode 100644 index 00000000..f7d766dd --- /dev/null +++ b/smtpd/table_db.c @@ -0,0 +1,282 @@ +/* $OpenBSD: table_db.c,v 1.21 2019/06/28 13:32:51 deraadt Exp $ */ + +/* + * Copyright (c) 2011 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <netinet/in.h> +#include <arpa/inet.h> +#ifdef HAVE_DB_H +#include <db.h> +#elif defined(HAVE_DB1_DB_H) +#include <db1/db.h> +#elif defined(HAVE_DB_185_H) +#include <db_185.h> +#endif +#include <ctype.h> +#include <err.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "smtpd.h" +#include "log.h" + + +/* db(3) backend */ +static int table_db_config(struct table *); +static int table_db_update(struct table *); +static int table_db_open(struct table *); +static void *table_db_open2(struct table *); +static int table_db_lookup(struct table *, enum table_service, const char *, char **); +static int table_db_fetch(struct table *, enum table_service, char **); +static void table_db_close(struct table *); +static void table_db_close2(void *); + +static char *table_db_get_entry(void *, const char *, size_t *); +static char *table_db_get_entry_match(void *, const char *, size_t *, + int(*)(const char *, const char *)); + +struct table_backend table_backend_db = { + "db", + K_ALIAS|K_CREDENTIALS|K_DOMAIN|K_NETADDR|K_USERINFO|K_SOURCE|K_MAILADDR|K_ADDRNAME|K_MAILADDRMAP, + table_db_config, + NULL, + NULL, + table_db_open, + table_db_update, + table_db_close, + table_db_lookup, + table_db_fetch, +}; + +static struct keycmp { + enum table_service service; + int (*func)(const char *, const char *); +} keycmp[] = { + { K_DOMAIN, table_domain_match }, + { K_NETADDR, table_netaddr_match }, + { K_MAILADDR, table_mailaddr_match } +}; + +struct dbhandle { + DB *db; + char pathname[PATH_MAX]; + time_t mtime; + int iter; +}; + +static int +table_db_config(struct table *table) +{ + struct dbhandle *handle; + + handle = table_db_open2(table); + if (handle == NULL) + return 0; + + table_db_close2(handle); + return 1; +} + +static int +table_db_update(struct table *table) +{ + struct dbhandle *handle; + + handle = table_db_open2(table); + if (handle == NULL) + return 0; + + table_db_close2(table->t_handle); + table->t_handle = handle; + return 1; +} + +static int +table_db_open(struct table *table) +{ + table->t_handle = table_db_open2(table); + if (table->t_handle == NULL) + return 0; + return 1; +} + +static void +table_db_close(struct table *table) +{ + table_db_close2(table->t_handle); + table->t_handle = NULL; +} + +static void * +table_db_open2(struct table *table) +{ + struct dbhandle *handle; + struct stat sb; + + handle = xcalloc(1, sizeof *handle); + if (strlcpy(handle->pathname, table->t_config, sizeof handle->pathname) + >= sizeof handle->pathname) + goto error; + + if (stat(handle->pathname, &sb) == -1) + goto error; + + handle->mtime = sb.st_mtime; + handle->db = dbopen(table->t_config, O_RDONLY, 0600, DB_HASH, NULL); + if (handle->db == NULL) + goto error; + + return handle; + +error: + if (handle->db) + handle->db->close(handle->db); + free(handle); + return NULL; +} + +static void +table_db_close2(void *hdl) +{ + struct dbhandle *handle = hdl; + handle->db->close(handle->db); + free(handle); +} + +static int +table_db_lookup(struct table *table, enum table_service service, const char *key, + char **dst) +{ + struct dbhandle *handle = table->t_handle; + char *line; + size_t len = 0; + int ret; + int (*match)(const char *, const char *) = NULL; + size_t i; + struct stat sb; + + if (stat(handle->pathname, &sb) == -1) + return -1; + + /* DB has changed, close and reopen */ + if (sb.st_mtime != handle->mtime) { + table_db_update(table); + handle = table->t_handle; + } + + for (i = 0; i < nitems(keycmp); ++i) + if (keycmp[i].service == service) + match = keycmp[i].func; + + if (match == NULL) + line = table_db_get_entry(handle, key, &len); + else + line = table_db_get_entry_match(handle, key, &len, match); + if (line == NULL) + return 0; + + ret = 1; + if (dst) + *dst = line; + else + free(line); + + return ret; +} + +static int +table_db_fetch(struct table *table, enum table_service service, char **dst) +{ + struct dbhandle *handle = table->t_handle; + DBT dbk; + DBT dbd; + int r; + + if (handle->iter == 0) + r = handle->db->seq(handle->db, &dbk, &dbd, R_FIRST); + else + r = handle->db->seq(handle->db, &dbk, &dbd, R_NEXT); + handle->iter = 1; + if (!r) { + r = handle->db->seq(handle->db, &dbk, &dbd, R_FIRST); + if (!r) + return 0; + } + + *dst = strdup(dbk.data); + if (*dst == NULL) + return -1; + + return 1; +} + + +static char * +table_db_get_entry_match(void *hdl, const char *key, size_t *len, + int(*func)(const char *, const char *)) +{ + struct dbhandle *handle = hdl; + DBT dbk; + DBT dbd; + int r; + char *buf = NULL; + + for (r = handle->db->seq(handle->db, &dbk, &dbd, R_FIRST); !r; + r = handle->db->seq(handle->db, &dbk, &dbd, R_NEXT)) { + buf = xmemdup(dbk.data, dbk.size); + if (func(key, buf)) { + *len = dbk.size; + return buf; + } + free(buf); + } + return NULL; +} + +static char * +table_db_get_entry(void *hdl, const char *key, size_t *len) +{ + struct dbhandle *handle = hdl; + int ret; + DBT dbk; + DBT dbv; + char pkey[LINE_MAX]; + + /* workaround the stupidity of the DB interface */ + if (strlcpy(pkey, key, sizeof pkey) >= sizeof pkey) + errx(1, "table_db_get_entry: key too long"); + dbk.data = pkey; + dbk.size = strlen(pkey) + 1; + + if ((ret = handle->db->get(handle->db, &dbk, &dbv, 0)) != 0) + return NULL; + + *len = dbv.size; + + return xmemdup(dbv.data, dbv.size); +} diff --git a/smtpd/table_getpwnam.c b/smtpd/table_getpwnam.c new file mode 100644 index 00000000..ccf889be --- /dev/null +++ b/smtpd/table_getpwnam.c @@ -0,0 +1,120 @@ +/* $OpenBSD: table_getpwnam.c,v 1.12 2018/12/27 14:23:41 eric Exp $ */ + +/* + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <limits.h> +#include <string.h> + +#include "smtpd.h" +#include "log.h" + + +/* getpwnam(3) backend */ +static int table_getpwnam_config(struct table *); +static int table_getpwnam_update(struct table *); +static int table_getpwnam_open(struct table *); +static int table_getpwnam_lookup(struct table *, enum table_service, const char *, + char **); +static void table_getpwnam_close(struct table *); + +struct table_backend table_backend_getpwnam = { + "getpwnam", + K_USERINFO, + table_getpwnam_config, + NULL, + NULL, + table_getpwnam_open, + table_getpwnam_update, + table_getpwnam_close, + table_getpwnam_lookup, +}; + + +static int +table_getpwnam_config(struct table *table) +{ + if (table->t_config[0]) + return 0; + return 1; +} + +static int +table_getpwnam_update(struct table *table) +{ + return 1; +} + +static int +table_getpwnam_open(struct table *table) +{ + return 1; +} + +static void +table_getpwnam_close(struct table *table) +{ + return; +} + +static int +table_getpwnam_lookup(struct table *table, enum table_service kind, const char *key, + char **dst) +{ + struct passwd *pw; + + if (kind != K_USERINFO) + return -1; + + errno = 0; + do { + pw = getpwnam(key); + } while (pw == NULL && errno == EINTR); + + if (pw == NULL) { + if (errno) + return -1; + return 0; + } + if (dst == NULL) + return 1; + + if (asprintf(dst, "%d:%d:%s", + pw->pw_uid, + pw->pw_gid, + pw->pw_dir) == -1) { + *dst = NULL; + return -1; + } + + return (1); +} diff --git a/smtpd/table_proc.c b/smtpd/table_proc.c new file mode 100644 index 00000000..44589bd7 --- /dev/null +++ b/smtpd/table_proc.c @@ -0,0 +1,283 @@ +/* $OpenBSD: table_proc.c,v 1.16 2019/10/03 04:51:15 gilles Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#ifdef HAVE_PATHS_H +#include <paths.h> +#endif +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <limits.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +struct table_proc_priv { + pid_t pid; + struct imsgbuf ibuf; +}; + +static struct imsg imsg; +static size_t rlen; +static char *rdata; + +extern char **environ; + +static void +table_proc_call(struct table_proc_priv *p) +{ + ssize_t n; + + if (imsg_flush(&p->ibuf) == -1) { + log_warn("warn: table-proc: imsg_flush"); + fatalx("table-proc: exiting"); + } + + while (1) { + if ((n = imsg_get(&p->ibuf, &imsg)) == -1) { + log_warn("warn: table-proc: imsg_get"); + break; + } + if (n) { + rlen = imsg.hdr.len - IMSG_HEADER_SIZE; + rdata = imsg.data; + + if (imsg.hdr.type != PROC_TABLE_OK) { + log_warnx("warn: table-proc: bad response"); + break; + } + return; + } + + if ((n = imsg_read(&p->ibuf)) == -1 && errno != EAGAIN) { + log_warn("warn: table-proc: imsg_read"); + break; + } + + if (n == 0) { + log_warnx("warn: table-proc: pipe closed"); + break; + } + } + + fatalx("table-proc: exiting"); +} + +static void +table_proc_read(void *dst, size_t len) +{ + if (len > rlen) { + log_warnx("warn: table-proc: bad msg len"); + fatalx("table-proc: exiting"); + } + + if (dst) + memmove(dst, rdata, len); + + rlen -= len; + rdata += len; +} + +static void +table_proc_end(void) +{ + if (rlen) { + log_warnx("warn: table-proc: bogus data"); + fatalx("table-proc: exiting"); + } + imsg_free(&imsg); +} + +/* + * API + */ + +static int +table_proc_open(struct table *table) +{ + struct table_proc_priv *priv; + struct table_open_params op; + int fd; + + priv = xcalloc(1, sizeof(*priv)); + + fd = fork_proc_backend("table", table->t_config, table->t_name); + if (fd == -1) + fatalx("table-proc: exiting"); + + imsg_init(&priv->ibuf, fd); + + memset(&op, 0, sizeof op); + op.version = PROC_TABLE_API_VERSION; + (void)strlcpy(op.name, table->t_name, sizeof op.name); + imsg_compose(&priv->ibuf, PROC_TABLE_OPEN, 0, 0, -1, &op, sizeof op); + + table_proc_call(priv); + table_proc_end(); + + table->t_handle = priv; + + return (1); +} + +static int +table_proc_update(struct table *table) +{ + struct table_proc_priv *priv = table->t_handle; + int r; + + imsg_compose(&priv->ibuf, PROC_TABLE_UPDATE, 0, 0, -1, NULL, 0); + + table_proc_call(priv); + table_proc_read(&r, sizeof(r)); + table_proc_end(); + + return (r); +} + +static void +table_proc_close(struct table *table) +{ + struct table_proc_priv *priv = table->t_handle; + + imsg_compose(&priv->ibuf, PROC_TABLE_CLOSE, 0, 0, -1, NULL, 0); + if (imsg_flush(&priv->ibuf) == -1) + fatal("imsg_flush"); + + table->t_handle = NULL; +} + +static int +imsg_add_params(struct ibuf *buf) +{ + size_t count = 0; + + if (imsg_add(buf, &count, sizeof(count)) == -1) + return (-1); + + return (0); +} + +static int +table_proc_lookup(struct table *table, enum table_service s, const char *k, char **dst) +{ + struct table_proc_priv *priv = table->t_handle; + struct ibuf *buf; + int r; + + buf = imsg_create(&priv->ibuf, + dst ? PROC_TABLE_LOOKUP : PROC_TABLE_CHECK, 0, 0, + sizeof(s) + strlen(k) + 1); + + if (buf == NULL) + return (-1); + if (imsg_add(buf, &s, sizeof(s)) == -1) + return (-1); + if (imsg_add_params(buf) == -1) + return (-1); + if (imsg_add(buf, k, strlen(k) + 1) == -1) + return (-1); + imsg_close(&priv->ibuf, buf); + + table_proc_call(priv); + table_proc_read(&r, sizeof(r)); + + if (r == 1 && dst) { + if (rlen == 0) { + log_warnx("warn: table-proc: empty response"); + fatalx("table-proc: exiting"); + } + if (rdata[rlen - 1] != '\0') { + log_warnx("warn: table-proc: not NUL-terminated"); + fatalx("table-proc: exiting"); + } + *dst = strdup(rdata); + if (*dst == NULL) + r = -1; + table_proc_read(NULL, rlen); + } + + table_proc_end(); + + return (r); +} + +static int +table_proc_fetch(struct table *table, enum table_service s, char **dst) +{ + struct table_proc_priv *priv = table->t_handle; + struct ibuf *buf; + int r; + + buf = imsg_create(&priv->ibuf, PROC_TABLE_FETCH, 0, 0, sizeof(s)); + if (buf == NULL) + return (-1); + if (imsg_add(buf, &s, sizeof(s)) == -1) + return (-1); + if (imsg_add_params(buf) == -1) + return (-1); + imsg_close(&priv->ibuf, buf); + + table_proc_call(priv); + table_proc_read(&r, sizeof(r)); + + if (r == 1) { + if (rlen == 0) { + log_warnx("warn: table-proc: empty response"); + fatalx("table-proc: exiting"); + } + if (rdata[rlen - 1] != '\0') { + log_warnx("warn: table-proc: not NUL-terminated"); + fatalx("table-proc: exiting"); + } + *dst = strdup(rdata); + if (*dst == NULL) + r = -1; + table_proc_read(NULL, rlen); + } + + table_proc_end(); + + return (r); +} + +struct table_backend table_backend_proc = { + "proc", + K_ANY, + NULL, + NULL, + NULL, + table_proc_open, + table_proc_update, + table_proc_close, + table_proc_lookup, + table_proc_fetch, +}; diff --git a/smtpd/table_static.c b/smtpd/table_static.c new file mode 100644 index 00000000..8f78ae11 --- /dev/null +++ b/smtpd/table_static.c @@ -0,0 +1,398 @@ +/* $OpenBSD: table_static.c,v 1.32 2018/12/28 14:21:02 eric Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <ctype.h> +#include <errno.h> + +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <limits.h> +#include <string.h> + +#include "smtpd.h" +#include "log.h" + +struct table_static_priv { + int type; + struct dict dict; + void *iter; +}; + +/* static backend */ +static int table_static_config(struct table *); +static int table_static_add(struct table *, const char *, const char *); +static void table_static_dump(struct table *); +static int table_static_update(struct table *); +static int table_static_open(struct table *); +static int table_static_lookup(struct table *, enum table_service, const char *, + char **); +static int table_static_fetch(struct table *, enum table_service, char **); +static void table_static_close(struct table *); + +struct table_backend table_backend_static = { + "static", + K_ALIAS|K_CREDENTIALS|K_DOMAIN|K_NETADDR|K_USERINFO| + K_SOURCE|K_MAILADDR|K_ADDRNAME|K_MAILADDRMAP|K_RELAYHOST| + K_STRING|K_REGEX, + table_static_config, + table_static_add, + table_static_dump, + table_static_open, + table_static_update, + table_static_close, + table_static_lookup, + table_static_fetch +}; + +static struct keycmp { + enum table_service service; + int (*func)(const char *, const char *); +} keycmp[] = { + { K_DOMAIN, table_domain_match }, + { K_NETADDR, table_netaddr_match }, + { K_MAILADDR, table_mailaddr_match }, + { K_REGEX, table_regex_match }, +}; + + +static void +table_static_priv_free(struct table_static_priv *priv) +{ + void *p; + + while (dict_poproot(&priv->dict, (void **)&p)) + if (p != priv) + free(p); + free(priv); +} + +static int +table_static_priv_add(struct table_static_priv *priv, const char *key, const char *val) +{ + char lkey[1024]; + void *old, *new = NULL; + + if (!lowercase(lkey, key, sizeof lkey)) { + errno = ENAMETOOLONG; + return (-1); + } + + if (val) { + new = strdup(val); + if (new == NULL) + return (-1); + } + + /* use priv if value is null, so we can detect duplicate entries */ + old = dict_set(&priv->dict, lkey, new ? new : priv); + if (old) { + if (old != priv) + free(old); + return (1); + } + + return (0); +} + +static int +table_static_priv_load(struct table_static_priv *priv, const char *path) +{ + FILE *fp; + char *buf = NULL, *p; + int lineno = 0; + size_t sz = 0; + ssize_t flen; + char *keyp; + char *valp; + int ret = 0; + + if ((fp = fopen(path, "r")) == NULL) { + log_warn("%s: fopen", path); + return 0; + } + + while ((flen = getline(&buf, &sz, fp)) != -1) { + lineno++; + if (buf[flen - 1] == '\n') + buf[--flen] = '\0'; + + keyp = buf; + while (isspace((unsigned char)*keyp)) { + ++keyp; + --flen; + } + if (*keyp == '\0') + continue; + while (isspace((unsigned char)keyp[flen - 1])) + keyp[--flen] = '\0'; + if (*keyp == '#') { + if (priv->type == T_NONE) { + keyp++; + while (isspace((unsigned char)*keyp)) + ++keyp; + if (!strcmp(keyp, "@list")) + priv->type = T_LIST; + } + continue; + } + + if (priv->type == T_NONE) { + for (p = keyp; *p; p++) { + if (*p == ' ' || *p == '\t' || *p == ':') { + priv->type = T_HASH; + break; + } + } + if (priv->type == T_NONE) + priv->type = T_LIST; + } + + if (priv->type == T_LIST) { + table_static_priv_add(priv, keyp, NULL); + continue; + } + + /* T_HASH */ + valp = keyp; + strsep(&valp, " \t:"); + if (valp) { + while (*valp) { + if (!isspace((unsigned char)*valp) && + !(*valp == ':' && + isspace((unsigned char)*(valp + 1)))) + break; + ++valp; + } + if (*valp == '\0') + valp = NULL; + } + if (valp == NULL) { + log_warnx("%s: invalid map entry line %d", + path, lineno); + goto end; + } + + table_static_priv_add(priv, keyp, valp); + } + + if (ferror(fp)) { + log_warn("%s: getline", path); + goto end; + } + + /* Accept empty alias files; treat them as hashes */ + if (priv->type == T_NONE) + priv->type = T_HASH; + + ret = 1; +end: + free(buf); + fclose(fp); + return ret; +} + +static int +table_static_config(struct table *t) +{ + struct table_static_priv *priv, *old; + + /* already up, and no config file? ok */ + if (t->t_handle && *t->t_config == '\0') + return 1; + + /* new config */ + priv = calloc(1, sizeof(*priv)); + if (priv == NULL) + return 0; + priv->type = t->t_type; + dict_init(&priv->dict); + + if (*t->t_config) { + /* load the config file */ + if (table_static_priv_load(priv, t->t_config) == 0) { + table_static_priv_free(priv); + return 0; + } + } + + if ((old = t->t_handle)) + table_static_priv_free(old); + t->t_handle = priv; + t->t_type = priv->type; + + return 1; +} + +static int +table_static_add(struct table *table, const char *key, const char *val) +{ + struct table_static_priv *priv = table->t_handle; + int r; + + /* cannot add to a table read from a file */ + if (*table->t_config) + return 0; + + if (table->t_type == T_NONE) + table->t_type = val ? T_HASH : T_LIST; + else if (table->t_type == T_LIST && val) + return 0; + else if (table->t_type == T_HASH && val == NULL) + return 0; + + if (priv == NULL) { + if (table_static_config(table) == 0) + return 0; + priv = table->t_handle; + } + + r = table_static_priv_add(priv, key, val); + if (r == -1) + return 0; + return 1; +} + +static void +table_static_dump(struct table *table) +{ + struct table_static_priv *priv = table->t_handle; + const char *key; + char *value; + void *iter; + + iter = NULL; + while (dict_iter(&priv->dict, &iter, &key, (void**)&value)) { + if (value && (void*)value != (void*)priv) + log_debug(" \"%s\" -> \"%s\"", key, value); + else + log_debug(" \"%s\"", key); + } +} + +static int +table_static_update(struct table *table) +{ + if (table_static_config(table) == 1) { + log_info("info: Table \"%s\" successfully updated", table->t_name); + return 1; + } + + log_info("info: Failed to update table \"%s\"", table->t_name); + return 0; +} + +static int +table_static_open(struct table *table) +{ + if (table->t_handle == NULL) + return table_static_config(table); + return 1; +} + +static void +table_static_close(struct table *table) +{ + struct table_static_priv *priv = table->t_handle; + + if (priv) + table_static_priv_free(priv); + table->t_handle = NULL; +} + +static int +table_static_lookup(struct table *table, enum table_service service, const char *key, + char **dst) +{ + struct table_static_priv *priv = table->t_handle; + char *line; + int ret; + int (*match)(const char *, const char *) = NULL; + size_t i; + void *iter; + const char *k; + char *v; + + for (i = 0; i < nitems(keycmp); ++i) + if (keycmp[i].service == service) + match = keycmp[i].func; + + line = NULL; + iter = NULL; + ret = 0; + while (dict_iter(&priv->dict, &iter, &k, (void **)&v)) { + if (match) { + if (match(key, k)) { + line = v; + ret = 1; + } + } + else { + if (strcmp(key, k) == 0) { + line = v; + ret = 1; + } + } + if (ret) + break; + } + + if (dst == NULL) + return ret ? 1 : 0; + + if (ret == 0) + return 0; + + *dst = strdup(line); + if (*dst == NULL) + return -1; + + return 1; +} + +static int +table_static_fetch(struct table *t, enum table_service service, char **dst) +{ + struct table_static_priv *priv = t->t_handle; + const char *k; + + if (!dict_iter(&priv->dict, &priv->iter, &k, (void **)NULL)) { + priv->iter = NULL; + if (!dict_iter(&priv->dict, &priv->iter, &k, (void **)NULL)) + return 0; + } + + *dst = strdup(k); + if (*dst == NULL) + return -1; + + return 1; +} diff --git a/smtpd/to.c b/smtpd/to.c new file mode 100644 index 00000000..81a1bb54 --- /dev/null +++ b/smtpd/to.c @@ -0,0 +1,880 @@ +/* $OpenBSD: to.c,v 1.44 2019/11/12 20:21:46 gilles Exp $ */ + +/* + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2012 Gilles Chehade <gilles@poolp.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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/resource.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <imsg.h> +#include <limits.h> +#include <inttypes.h> +#include <netdb.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +static const char *in6addr_to_text(const struct in6_addr *); +static int alias_is_filter(struct expandnode *, const char *, size_t); +static int alias_is_username(struct expandnode *, const char *, size_t); +static int alias_is_address(struct expandnode *, const char *, size_t); +static int alias_is_filename(struct expandnode *, const char *, size_t); +static int alias_is_include(struct expandnode *, const char *, size_t); +static int alias_is_error(struct expandnode *, const char *, size_t); + +static int broken_inet_net_pton_ipv6(const char *, void *, size_t); + +const char * +sockaddr_to_text(struct sockaddr *sa) +{ + static char buf[NI_MAXHOST]; + + if (getnameinfo(sa, SA_LEN(sa), buf, sizeof(buf), NULL, 0, + NI_NUMERICHOST)) + return ("(unknown)"); + else + return (buf); +} + +static const char * +in6addr_to_text(const struct in6_addr *addr) +{ + struct sockaddr_in6 sa_in6; + uint16_t tmp16; + + memset(&sa_in6, 0, sizeof(sa_in6)); +#ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_LEN + sa_in6.sin6_len = sizeof(sa_in6); +#endif + sa_in6.sin6_family = AF_INET6; + memcpy(&sa_in6.sin6_addr, addr, sizeof(sa_in6.sin6_addr)); + + /* XXX thanks, KAME, for this ugliness... adopted from route/show.c */ + if (IN6_IS_ADDR_LINKLOCAL(&sa_in6.sin6_addr) || + IN6_IS_ADDR_MC_LINKLOCAL(&sa_in6.sin6_addr)) { + memcpy(&tmp16, &sa_in6.sin6_addr.s6_addr[2], sizeof(tmp16)); + sa_in6.sin6_scope_id = ntohs(tmp16); + sa_in6.sin6_addr.s6_addr[2] = 0; + sa_in6.sin6_addr.s6_addr[3] = 0; + } + + return (sockaddr_to_text((struct sockaddr *)&sa_in6)); +} + +int +text_to_mailaddr(struct mailaddr *maddr, const char *email) +{ + char *username; + char *hostname; + char buffer[LINE_MAX]; + + if (strlcpy(buffer, email, sizeof buffer) >= sizeof buffer) + return 0; + + memset(maddr, 0, sizeof *maddr); + + username = buffer; + hostname = strrchr(username, '@'); + + if (hostname == NULL) { + if (strlcpy(maddr->user, username, sizeof maddr->user) + >= sizeof maddr->user) + return 0; + } + else if (username == hostname) { + *hostname++ = '\0'; + if (strlcpy(maddr->domain, hostname, sizeof maddr->domain) + >= sizeof maddr->domain) + return 0; + } + else { + *hostname++ = '\0'; + if (strlcpy(maddr->user, username, sizeof maddr->user) + >= sizeof maddr->user) + return 0; + if (strlcpy(maddr->domain, hostname, sizeof maddr->domain) + >= sizeof maddr->domain) + return 0; + } + + return 1; +} + +const char * +mailaddr_to_text(const struct mailaddr *maddr) +{ + static char buffer[LINE_MAX]; + + (void)strlcpy(buffer, maddr->user, sizeof buffer); + (void)strlcat(buffer, "@", sizeof buffer); + if (strlcat(buffer, maddr->domain, sizeof buffer) >= sizeof buffer) + return NULL; + + return buffer; +} + + +const char * +sa_to_text(const struct sockaddr *sa) +{ + static char buf[NI_MAXHOST + 5]; + char *p; + + buf[0] = '\0'; + p = buf; + + if (sa->sa_family == AF_LOCAL) + (void)strlcpy(buf, "local", sizeof buf); + else if (sa->sa_family == AF_INET) { + in_addr_t addr; + + addr = ((const struct sockaddr_in *)sa)->sin_addr.s_addr; + addr = ntohl(addr); + (void)bsnprintf(p, NI_MAXHOST, "%d.%d.%d.%d", + (addr >> 24) & 0xff, (addr >> 16) & 0xff, + (addr >> 8) & 0xff, addr & 0xff); + } + else if (sa->sa_family == AF_INET6) { + const struct sockaddr_in6 *in6; + const struct in6_addr *in6_addr; + + in6 = (const struct sockaddr_in6 *)sa; + p = buf; + in6_addr = &in6->sin6_addr; + (void)bsnprintf(p, NI_MAXHOST, "[%s]", in6addr_to_text(in6_addr)); + } + + return (buf); +} + +const char * +ss_to_text(const struct sockaddr_storage *ss) +{ + return (sa_to_text((const struct sockaddr*)ss)); +} + +const char * +time_to_text(time_t when) +{ + struct tm *lt; + static char buf[40]; + char *day[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + char *month[] = {"Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec"}; + const char *tz; + long offset; + + lt = localtime(&when); + if (lt == NULL || when == 0) + fatalx("time_to_text: localtime"); + +#if HAVE_STRUCT_TM_TM_GMTOFF + offset = lt->tm_gmtoff; + tz = lt->tm_zone; +#elif defined HAVE_DECL_ALTZONE && defined HAVE_DECL_TIMEZONE + offset = lt->tm_isdst > 0 ? altzone : timezone; + tz = lt->tm_isdst > 0 ? tzname[1] : tzname[0]; +#endif + + /* We do not use strftime because it is subject to locale substitution*/ + if (!bsnprintf(buf, sizeof(buf), + "%s, %d %s %d %02d:%02d:%02d %c%02d%02d (%s)", + day[lt->tm_wday], lt->tm_mday, month[lt->tm_mon], + lt->tm_year + 1900, + lt->tm_hour, lt->tm_min, lt->tm_sec, + offset >= 0 ? '+' : '-', + abs((int)offset / 3600), + abs((int)offset % 3600) / 60, + tz)) + fatalx("time_to_text: bsnprintf"); + + return buf; +} + +const char * +duration_to_text(time_t t) +{ + static char dst[64]; + char buf[64]; + int h, m, s; + long long d; + + if (t == 0) { + (void)strlcpy(dst, "0s", sizeof dst); + return (dst); + } + + dst[0] = '\0'; + if (t < 0) { + (void)strlcpy(dst, "-", sizeof dst); + t = -t; + } + + s = t % 60; + t /= 60; + m = t % 60; + t /= 60; + h = t % 24; + d = t / 24; + + if (d) { + (void)snprintf(buf, sizeof buf, "%lldd", d); + (void)strlcat(dst, buf, sizeof dst); + } + if (h) { + (void)snprintf(buf, sizeof buf, "%dh", h); + (void)strlcat(dst, buf, sizeof dst); + } + if (m) { + (void)snprintf(buf, sizeof buf, "%dm", m); + (void)strlcat(dst, buf, sizeof dst); + } + if (s) { + (void)snprintf(buf, sizeof buf, "%ds", s); + (void)strlcat(dst, buf, sizeof dst); + } + + return (dst); +} + +int +text_to_netaddr(struct netaddr *netaddr, const char *s) +{ + struct sockaddr_storage ss; + struct sockaddr_in ssin; + struct sockaddr_in6 ssin6; + int bits; + char buf[NI_MAXHOST]; + size_t len; + + memset(&ssin, 0, sizeof(struct sockaddr_in)); + memset(&ssin6, 0, sizeof(struct sockaddr_in6)); + + if (strncasecmp("IPv6:", s, 5) == 0) + s += 5; + + bits = inet_net_pton(AF_INET, s, &ssin.sin_addr, + sizeof(struct in_addr)); + if (bits != -1) { + ssin.sin_family = AF_INET; + memcpy(&ss, &ssin, sizeof(ssin)); +#ifdef HAVE_STRUCT_SOCKADDR_STORAGE_SS_LEN + ss.ss_len = sizeof(struct sockaddr_in); +#endif + } else { + if (s[0] != '[') { + if ((len = strlcpy(buf, s, sizeof buf)) >= sizeof buf) + return 0; + } + else { + s++; + if (strncasecmp("IPv6:", s, 5) == 0) + s += 5; + if ((len = strlcpy(buf, s, sizeof buf)) >= sizeof buf) + return 0; + if (buf[len-1] != ']') + return 0; + buf[len-1] = 0; + } + bits = inet_net_pton(AF_INET6, buf, &ssin6.sin6_addr, + sizeof(struct in6_addr)); + if (bits == -1) { + if (errno != EAFNOSUPPORT) + return 0; + bits = broken_inet_net_pton_ipv6(buf, &ssin6.sin6_addr, + sizeof(struct in6_addr)); + if (bits == -1) + return 0; + } + ssin6.sin6_family = AF_INET6; + memcpy(&ss, &ssin6, sizeof(ssin6)); +#ifdef HAVE_STRUCT_SOCKADDR_STORAGE_SS_LEN + ss.ss_len = sizeof(struct sockaddr_in6); +#endif + } + + netaddr->ss = ss; + netaddr->bits = bits; + return 1; +} + +int +text_to_relayhost(struct relayhost *relay, const char *s) +{ + static const struct schema { + const char *name; + int tls; + uint16_t flags; + uint16_t port; + } schemas [] = { + /* + * new schemas should be *appended* otherwise the default + * schema index needs to be updated later in this function. + */ + { "smtp://", RELAY_TLS_OPPORTUNISTIC, 0, 25 }, + { "smtp+tls://", RELAY_TLS_STARTTLS, 0, 25 }, + { "smtp+notls://", RELAY_TLS_NO, 0, 25 }, + /* need to specify an explicit port for LMTP */ + { "lmtp://", RELAY_TLS_NO, RELAY_LMTP, 0 }, + { "smtps://", RELAY_TLS_SMTPS, 0, 465 } + }; + const char *errstr = NULL; + char *p, *q; + char buffer[1024]; + char *beg, *end; + size_t i; + size_t len; + + memset(buffer, 0, sizeof buffer); + if (strlcpy(buffer, s, sizeof buffer) >= sizeof buffer) + return 0; + + for (i = 0; i < nitems(schemas); ++i) + if (strncasecmp(schemas[i].name, s, + strlen(schemas[i].name)) == 0) + break; + + if (i == nitems(schemas)) { + /* there is a schema, but it's not recognized */ + if (strstr(buffer, "://")) + return 0; + + /* no schema, default to smtp:// */ + i = 0; + p = buffer; + } + else + p = buffer + strlen(schemas[i].name); + + relay->tls = schemas[i].tls; + relay->flags = schemas[i].flags; + relay->port = schemas[i].port; + + /* first, we extract the label if any */ + if ((q = strchr(p, '@')) != NULL) { + *q = 0; + if (strlcpy(relay->authlabel, p, sizeof (relay->authlabel)) + >= sizeof (relay->authlabel)) + return 0; + p = q + 1; + } + + /* then, we extract the mail exchanger */ + beg = end = p; + if (*beg == '[') { + if ((end = strchr(beg, ']')) == NULL) + return 0; + /* skip ']', it has to be included in the relay hostname */ + ++end; + len = end - beg; + } + else { + for (end = beg; *end; ++end) + if (!isalnum((unsigned char)*end) && + *end != '_' && *end != '.' && *end != '-') + break; + len = end - beg; + } + if (len >= sizeof relay->hostname) + return 0; + for (i = 0; i < len; ++i) + relay->hostname[i] = beg[i]; + relay->hostname[i] = 0; + + /* finally, we extract the port */ + p = beg + len; + if (*p == ':') { + relay->port = strtonum(p+1, 1, IPPORT_HILASTAUTO, &errstr); + if (errstr) + return 0; + } + + if (!valid_domainpart(relay->hostname)) + return 0; + if ((relay->flags & RELAY_LMTP) && (relay->port == 0)) + return 0; + if (relay->authlabel[0]) { + /* disallow auth on non-tls scheme. */ + if (relay->tls != RELAY_TLS_STARTTLS && + relay->tls != RELAY_TLS_SMTPS) + return 0; + relay->flags |= RELAY_AUTH; + } + + return 1; +} + +uint64_t +text_to_evpid(const char *s) +{ + uint64_t ulval; + char *ep; + + errno = 0; + ulval = strtoull(s, &ep, 16); + if (s[0] == '\0' || *ep != '\0') + return 0; + if (errno == ERANGE && ulval == ULLONG_MAX) + return 0; + if (ulval == 0) + return 0; + return (ulval); +} + +uint32_t +text_to_msgid(const char *s) +{ + uint64_t ulval; + char *ep; + + errno = 0; + ulval = strtoull(s, &ep, 16); + if (s[0] == '\0' || *ep != '\0') + return 0; + if (errno == ERANGE && ulval == ULLONG_MAX) + return 0; + if (ulval == 0) + return 0; + if (ulval > 0xffffffff) + return 0; + return (ulval & 0xffffffff); +} + +const char * +rule_to_text(struct rule *r) +{ + static char buf[4096]; + + memset(buf, 0, sizeof buf); + (void)strlcpy(buf, "match", sizeof buf); + if (r->flag_tag) { + if (r->flag_tag < 0) + (void)strlcat(buf, " !", sizeof buf); + (void)strlcat(buf, " tag ", sizeof buf); + (void)strlcat(buf, r->table_tag, sizeof buf); + } + + if (r->flag_from) { + if (r->flag_from < 0) + (void)strlcat(buf, " !", sizeof buf); + if (r->flag_from_socket) + (void)strlcat(buf, " from socket", sizeof buf); + else if (r->flag_from_rdns) { + (void)strlcat(buf, " from rdns", sizeof buf); + if (r->table_from) { + (void)strlcat(buf, " ", sizeof buf); + (void)strlcat(buf, r->table_from, sizeof buf); + } + } + else if (strcmp(r->table_from, "<anyhost>") == 0) + (void)strlcat(buf, " from any", sizeof buf); + else if (strcmp(r->table_from, "<localhost>") == 0) + (void)strlcat(buf, " from local", sizeof buf); + else { + (void)strlcat(buf, " from src ", sizeof buf); + (void)strlcat(buf, r->table_from, sizeof buf); + } + } + + if (r->flag_for) { + if (r->flag_for < 0) + (void)strlcat(buf, " !", sizeof buf); + if (strcmp(r->table_for, "<anydestination>") == 0) + (void)strlcat(buf, " for any", sizeof buf); + else if (strcmp(r->table_for, "<localnames>") == 0) + (void)strlcat(buf, " for local", sizeof buf); + else { + (void)strlcat(buf, " for domain ", sizeof buf); + (void)strlcat(buf, r->table_for, sizeof buf); + } + } + + if (r->flag_smtp_helo) { + if (r->flag_smtp_helo < 0) + (void)strlcat(buf, " !", sizeof buf); + (void)strlcat(buf, " helo ", sizeof buf); + (void)strlcat(buf, r->table_smtp_helo, sizeof buf); + } + + if (r->flag_smtp_auth) { + if (r->flag_smtp_auth < 0) + (void)strlcat(buf, " !", sizeof buf); + (void)strlcat(buf, " auth", sizeof buf); + if (r->table_smtp_auth) { + (void)strlcat(buf, " ", sizeof buf); + (void)strlcat(buf, r->table_smtp_auth, sizeof buf); + } + } + + if (r->flag_smtp_starttls) { + if (r->flag_smtp_starttls < 0) + (void)strlcat(buf, " !", sizeof buf); + (void)strlcat(buf, " tls", sizeof buf); + } + + if (r->flag_smtp_mail_from) { + if (r->flag_smtp_mail_from < 0) + (void)strlcat(buf, " !", sizeof buf); + (void)strlcat(buf, " mail-from ", sizeof buf); + (void)strlcat(buf, r->table_smtp_mail_from, sizeof buf); + } + + if (r->flag_smtp_rcpt_to) { + if (r->flag_smtp_rcpt_to < 0) + (void)strlcat(buf, " !", sizeof buf); + (void)strlcat(buf, " rcpt-to ", sizeof buf); + (void)strlcat(buf, r->table_smtp_rcpt_to, sizeof buf); + } + (void)strlcat(buf, " action ", sizeof buf); + if (r->reject) + (void)strlcat(buf, "reject", sizeof buf); + else + (void)strlcat(buf, r->dispatcher, sizeof buf); + return buf; +} + + +int +text_to_userinfo(struct userinfo *userinfo, const char *s) +{ + char buf[PATH_MAX]; + char *p; + const char *errstr; + + memset(buf, 0, sizeof buf); + p = buf; + while (*s && *s != ':') + *p++ = *s++; + if (*s++ != ':') + goto error; + + if (strlcpy(userinfo->username, buf, + sizeof userinfo->username) >= sizeof userinfo->username) + goto error; + + memset(buf, 0, sizeof buf); + p = buf; + while (*s && *s != ':') + *p++ = *s++; + if (*s++ != ':') + goto error; + userinfo->uid = strtonum(buf, 0, UID_MAX, &errstr); + if (errstr) + goto error; + + memset(buf, 0, sizeof buf); + p = buf; + while (*s && *s != ':') + *p++ = *s++; + if (*s++ != ':') + goto error; + userinfo->gid = strtonum(buf, 0, GID_MAX, &errstr); + if (errstr) + goto error; + + if (strlcpy(userinfo->directory, s, + sizeof userinfo->directory) >= sizeof userinfo->directory) + goto error; + + return 1; + +error: + return 0; +} + +int +text_to_credentials(struct credentials *creds, const char *s) +{ + char *p; + char buffer[LINE_MAX]; + size_t offset; + + p = strchr(s, ':'); + if (p == NULL) { + creds->username[0] = '\0'; + if (strlcpy(creds->password, s, sizeof creds->password) + >= sizeof creds->password) + return 0; + return 1; + } + + offset = p - s; + + memset(buffer, 0, sizeof buffer); + if (strlcpy(buffer, s, sizeof buffer) >= sizeof buffer) + return 0; + p = buffer + offset; + *p = '\0'; + + if (strlcpy(creds->username, buffer, sizeof creds->username) + >= sizeof creds->username) + return 0; + if (strlcpy(creds->password, p+1, sizeof creds->password) + >= sizeof creds->password) + return 0; + + return 1; +} + +int +text_to_expandnode(struct expandnode *expandnode, const char *s) +{ + size_t l; + + l = strlen(s); + if (alias_is_error(expandnode, s, l) || + alias_is_include(expandnode, s, l) || + alias_is_filter(expandnode, s, l) || + alias_is_filename(expandnode, s, l) || + alias_is_address(expandnode, s, l) || + alias_is_username(expandnode, s, l)) + return (1); + + return (0); +} + +const char * +expandnode_to_text(struct expandnode *expandnode) +{ + switch (expandnode->type) { + case EXPAND_FILTER: + case EXPAND_FILENAME: + case EXPAND_INCLUDE: + case EXPAND_ERROR: + case EXPAND_USERNAME: + return expandnode->u.user; + case EXPAND_ADDRESS: + return mailaddr_to_text(&expandnode->u.mailaddr); + case EXPAND_INVALID: + break; + } + + return NULL; +} + +/******/ +static int +alias_is_filter(struct expandnode *alias, const char *line, size_t len) +{ + int v = 0; + + if (*line == '"') + v = 1; + if (*(line+v) == '|') { + if (strlcpy(alias->u.buffer, line + v + 1, + sizeof(alias->u.buffer)) >= sizeof(alias->u.buffer)) + return 0; + if (v) { + v = strlen(alias->u.buffer); + if (v == 0) + return (0); + if (alias->u.buffer[v-1] != '"') + return (0); + alias->u.buffer[v-1] = '\0'; + } + alias->type = EXPAND_FILTER; + return (1); + } + return (0); +} + +static int +alias_is_username(struct expandnode *alias, const char *line, size_t len) +{ + memset(alias, 0, sizeof *alias); + + if (strlcpy(alias->u.user, line, + sizeof(alias->u.user)) >= sizeof(alias->u.user)) + return 0; + + while (*line) { + if (!isalnum((unsigned char)*line) && + *line != '_' && *line != '.' && *line != '-' && *line != '+') + return 0; + ++line; + } + + alias->type = EXPAND_USERNAME; + return 1; +} + +static int +alias_is_address(struct expandnode *alias, const char *line, size_t len) +{ + char *domain; + + memset(alias, 0, sizeof *alias); + + 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 - 1) + return 0; + + /* scan pre @ for disallowed chars */ + *domain++ = '\0'; + (void)strlcpy(alias->u.mailaddr.user, line, sizeof(alias->u.mailaddr.user)); + (void)strlcpy(alias->u.mailaddr.domain, domain, + sizeof(alias->u.mailaddr.domain)); + + while (*line) { + char allowedset[] = "!#$%*/?|^{}`~&'+-=_."; + if (!isalnum((unsigned char)*line) && + strchr(allowedset, *line) == NULL) + return 0; + ++line; + } + + while (*domain) { + char allowedset[] = "-."; + if (!isalnum((unsigned char)*domain) && + strchr(allowedset, *domain) == NULL) + return 0; + ++domain; + } + + alias->type = EXPAND_ADDRESS; + return 1; +} + +static int +alias_is_filename(struct expandnode *alias, const char *line, size_t len) +{ + memset(alias, 0, sizeof *alias); + + if (*line != '/') + return 0; + + if (strlcpy(alias->u.buffer, line, + sizeof(alias->u.buffer)) >= sizeof(alias->u.buffer)) + return 0; + alias->type = EXPAND_FILENAME; + return 1; +} + +static int +alias_is_include(struct expandnode *alias, const char *line, size_t len) +{ + size_t skip; + + memset(alias, 0, sizeof *alias); + + if (strncasecmp(":include:", line, 9) == 0) + skip = 9; + else if (strncasecmp("include:", line, 8) == 0) + skip = 8; + else + return 0; + + if (!alias_is_filename(alias, line + skip, len - skip)) + return 0; + + alias->type = EXPAND_INCLUDE; + return 1; +} + +static int +alias_is_error(struct expandnode *alias, const char *line, size_t len) +{ + size_t skip; + + memset(alias, 0, sizeof *alias); + + if (strncasecmp(":error:", line, 7) == 0) + skip = 7; + else if (strncasecmp("error:", line, 6) == 0) + skip = 6; + else + return 0; + + if (strlcpy(alias->u.buffer, line + skip, + sizeof(alias->u.buffer)) >= sizeof(alias->u.buffer)) + return 0; + + if (strlen(alias->u.buffer) < 5) + return 0; + + /* [45][0-9]{2} [a-zA-Z0-9].* */ + if (alias->u.buffer[3] != ' ' || + !isalnum((unsigned char)alias->u.buffer[4]) || + (alias->u.buffer[0] != '4' && alias->u.buffer[0] != '5') || + !isdigit((unsigned char)alias->u.buffer[1]) || + !isdigit((unsigned char)alias->u.buffer[2])) + return 0; + + alias->type = EXPAND_ERROR; + return 1; +} + +static int +broken_inet_net_pton_ipv6(const char *src, void *dst, size_t size) +{ + int ret; + int bits; + char buf[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255:255:255:255/128")]; + char *sep; + const char *errstr; + + if (strlcpy(buf, src, sizeof buf) >= sizeof buf) { + errno = EMSGSIZE; + return (-1); + } + + sep = strchr(buf, '/'); + if (sep != NULL) + *sep++ = '\0'; + + ret = inet_pton(AF_INET6, buf, dst); + if (ret != 1) + return (-1); + + if (sep == NULL) + return 128; + + bits = strtonum(sep, 0, 128, &errstr); + if (errstr) + return (-1); + + return bits; +} diff --git a/smtpd/tree.c b/smtpd/tree.c new file mode 100644 index 00000000..1d720a59 --- /dev/null +++ b/smtpd/tree.c @@ -0,0 +1,259 @@ +/* $OpenBSD: tree.c,v 1.6 2018/12/23 16:06:24 gilles Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/tree.h> + +#include <err.h> +#include <inttypes.h> +#include <stdlib.h> +#include <limits.h> + +#include "tree.h" + +struct treeentry { + SPLAY_ENTRY(treeentry) entry; + uint64_t id; + void *data; +}; + +static int treeentry_cmp(struct treeentry *, struct treeentry *); + +SPLAY_PROTOTYPE(_tree, treeentry, entry, treeentry_cmp); + +int +tree_check(struct tree *t, uint64_t id) +{ + struct treeentry key; + + key.id = id; + return (SPLAY_FIND(_tree, &t->tree, &key) != NULL); +} + +void * +tree_set(struct tree *t, uint64_t id, void *data) +{ + struct treeentry *entry, key; + char *old; + + key.id = id; + if ((entry = SPLAY_FIND(_tree, &t->tree, &key)) == NULL) { + if ((entry = malloc(sizeof *entry)) == NULL) + err(1, "tree_set: malloc"); + entry->id = id; + SPLAY_INSERT(_tree, &t->tree, entry); + old = NULL; + t->count += 1; + } else + old = entry->data; + + entry->data = data; + + return (old); +} + +void +tree_xset(struct tree *t, uint64_t id, void *data) +{ + struct treeentry *entry; + + if ((entry = malloc(sizeof *entry)) == NULL) + err(1, "tree_xset: malloc"); + entry->id = id; + entry->data = data; + if (SPLAY_INSERT(_tree, &t->tree, entry)) + errx(1, "tree_xset(%p, 0x%016"PRIx64 ")", t, id); + t->count += 1; +} + +void * +tree_get(struct tree *t, uint64_t id) +{ + struct treeentry key, *entry; + + key.id = id; + if ((entry = SPLAY_FIND(_tree, &t->tree, &key)) == NULL) + return (NULL); + + return (entry->data); +} + +void * +tree_xget(struct tree *t, uint64_t id) +{ + struct treeentry key, *entry; + + key.id = id; + if ((entry = SPLAY_FIND(_tree, &t->tree, &key)) == NULL) + errx(1, "tree_get(%p, 0x%016"PRIx64 ")", t, id); + + return (entry->data); +} + +void * +tree_pop(struct tree *t, uint64_t id) +{ + struct treeentry key, *entry; + void *data; + + key.id = id; + if ((entry = SPLAY_FIND(_tree, &t->tree, &key)) == NULL) + return (NULL); + + data = entry->data; + SPLAY_REMOVE(_tree, &t->tree, entry); + free(entry); + t->count -= 1; + + return (data); +} + +void * +tree_xpop(struct tree *t, uint64_t id) +{ + struct treeentry key, *entry; + void *data; + + key.id = id; + if ((entry = SPLAY_FIND(_tree, &t->tree, &key)) == NULL) + errx(1, "tree_xpop(%p, 0x%016" PRIx64 ")", t, id); + + data = entry->data; + SPLAY_REMOVE(_tree, &t->tree, entry); + free(entry); + t->count -= 1; + + return (data); +} + +int +tree_poproot(struct tree *t, uint64_t *id, void **data) +{ + struct treeentry *entry; + + entry = SPLAY_ROOT(&t->tree); + if (entry == NULL) + return (0); + if (id) + *id = entry->id; + if (data) + *data = entry->data; + SPLAY_REMOVE(_tree, &t->tree, entry); + free(entry); + t->count -= 1; + + return (1); +} + +int +tree_root(struct tree *t, uint64_t *id, void **data) +{ + struct treeentry *entry; + + entry = SPLAY_ROOT(&t->tree); + if (entry == NULL) + return (0); + if (id) + *id = entry->id; + if (data) + *data = entry->data; + return (1); +} + +int +tree_iter(struct tree *t, void **hdl, uint64_t *id, void **data) +{ + struct treeentry *curr = *hdl; + + if (curr == NULL) + curr = SPLAY_MIN(_tree, &t->tree); + else + curr = SPLAY_NEXT(_tree, &t->tree, curr); + + if (curr) { + *hdl = curr; + if (id) + *id = curr->id; + if (data) + *data = curr->data; + return (1); + } + + return (0); +} + +int +tree_iterfrom(struct tree *t, void **hdl, uint64_t k, uint64_t *id, void **data) +{ + struct treeentry *curr = *hdl, key; + + if (curr == NULL) { + if (k == 0) + curr = SPLAY_MIN(_tree, &t->tree); + else { + key.id = k; + curr = SPLAY_FIND(_tree, &t->tree, &key); + if (curr == NULL) { + SPLAY_INSERT(_tree, &t->tree, &key); + curr = SPLAY_NEXT(_tree, &t->tree, &key); + SPLAY_REMOVE(_tree, &t->tree, &key); + } + } + } else + curr = SPLAY_NEXT(_tree, &t->tree, curr); + + if (curr) { + *hdl = curr; + if (id) + *id = curr->id; + if (data) + *data = curr->data; + return (1); + } + + return (0); +} + +void +tree_merge(struct tree *dst, struct tree *src) +{ + struct treeentry *entry; + + while (!SPLAY_EMPTY(&src->tree)) { + entry = SPLAY_ROOT(&src->tree); + SPLAY_REMOVE(_tree, &src->tree, entry); + if (SPLAY_INSERT(_tree, &dst->tree, entry)) + errx(1, "tree_merge: duplicate"); + } + dst->count += src->count; + src->count = 0; +} + +static int +treeentry_cmp(struct treeentry *a, struct treeentry *b) +{ + if (a->id < b->id) + return (-1); + if (a->id > b->id) + return (1); + return (0); +} + +SPLAY_GENERATE(_tree, treeentry, entry, treeentry_cmp); diff --git a/smtpd/tree.h b/smtpd/tree.h new file mode 100644 index 00000000..3d719f09 --- /dev/null +++ b/smtpd/tree.h @@ -0,0 +1,48 @@ +/* $OpenBSD: tree.h,v 1.1 2018/12/23 16:06:24 gilles Exp $ */ + +/* + * Copyright (c) 2013 Eric Faurot <eric@openbsd.org> + * Copyright (c) 2011 Gilles Chehade <gilles@poolp.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. + */ + +#ifndef _TREE_H_ +#define _TREE_H_ + +SPLAY_HEAD(_tree, treeentry); + +struct tree { + struct _tree tree; + size_t count; +}; + + +/* tree.c */ +#define tree_init(t) do { SPLAY_INIT(&((t)->tree)); (t)->count = 0; } while(0) +#define tree_empty(t) SPLAY_EMPTY(&((t)->tree)) +#define tree_count(t) ((t)->count) +int tree_check(struct tree *, uint64_t); +void *tree_set(struct tree *, uint64_t, void *); +void tree_xset(struct tree *, uint64_t, void *); +void *tree_get(struct tree *, uint64_t); +void *tree_xget(struct tree *, uint64_t); +void *tree_pop(struct tree *, uint64_t); +void *tree_xpop(struct tree *, uint64_t); +int tree_poproot(struct tree *, uint64_t *, void **); +int tree_root(struct tree *, uint64_t *, void **); +int tree_iter(struct tree *, void **, uint64_t *, void **); +int tree_iterfrom(struct tree *, void **, uint64_t, uint64_t *, void **); +void tree_merge(struct tree *, struct tree *); + +#endif diff --git a/smtpd/unpack_dns.c b/smtpd/unpack_dns.c new file mode 100644 index 00000000..974d5727 --- /dev/null +++ b/smtpd/unpack_dns.c @@ -0,0 +1,300 @@ +/* $OpenBSD: unpack_dns.c,v 1.1 2018/01/06 07:57:53 sunil Exp $ */ + +/* + * Copyright (c) 2011-2014 Eric Faurot <eric@faurot.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "includes.h" + +#ifdef HAVE_ARPA_NAMESER_COMPAT_H +#include <arpa/nameser_compat.h> +#endif +#include <arpa/inet.h> + +#include <string.h> + +#include "unpack_dns.h" + +static int unpack_data(struct unpack *, void *, size_t); +static int unpack_u16(struct unpack *, uint16_t *); +static int unpack_u32(struct unpack *, uint32_t *); +static int unpack_inaddr(struct unpack *, struct in_addr *); +static int unpack_in6addr(struct unpack *, struct in6_addr *); +static int unpack_dname(struct unpack *, char *, size_t); + +void +unpack_init(struct unpack *unpack, const char *buf, size_t len) +{ + unpack->buf = buf; + unpack->len = len; + unpack->offset = 0; + unpack->err = NULL; +} + +int +unpack_header(struct unpack *p, struct dns_header *h) +{ + if (unpack_data(p, h, HFIXEDSZ) == -1) + return (-1); + + h->flags = ntohs(h->flags); + h->qdcount = ntohs(h->qdcount); + h->ancount = ntohs(h->ancount); + h->nscount = ntohs(h->nscount); + h->arcount = ntohs(h->arcount); + + return (0); +} + +int +unpack_query(struct unpack *p, struct dns_query *q) +{ + unpack_dname(p, q->q_dname, sizeof(q->q_dname)); + unpack_u16(p, &q->q_type); + unpack_u16(p, &q->q_class); + + return (p->err) ? (-1) : (0); +} + +int +unpack_rr(struct unpack *p, struct dns_rr *rr) +{ + uint16_t rdlen; + size_t save_offset; + + unpack_dname(p, rr->rr_dname, sizeof(rr->rr_dname)); + unpack_u16(p, &rr->rr_type); + unpack_u16(p, &rr->rr_class); + unpack_u32(p, &rr->rr_ttl); + unpack_u16(p, &rdlen); + + if (p->err) + return (-1); + + if (p->len - p->offset < rdlen) { + p->err = "too short"; + return (-1); + } + + save_offset = p->offset; + + switch (rr->rr_type) { + + case T_CNAME: + unpack_dname(p, rr->rr.cname.cname, sizeof(rr->rr.cname.cname)); + break; + + case T_MX: + unpack_u16(p, &rr->rr.mx.preference); + unpack_dname(p, rr->rr.mx.exchange, sizeof(rr->rr.mx.exchange)); + break; + + case T_NS: + unpack_dname(p, rr->rr.ns.nsname, sizeof(rr->rr.ns.nsname)); + break; + + case T_PTR: + unpack_dname(p, rr->rr.ptr.ptrname, sizeof(rr->rr.ptr.ptrname)); + break; + + case T_SOA: + unpack_dname(p, rr->rr.soa.mname, sizeof(rr->rr.soa.mname)); + unpack_dname(p, rr->rr.soa.rname, sizeof(rr->rr.soa.rname)); + unpack_u32(p, &rr->rr.soa.serial); + unpack_u32(p, &rr->rr.soa.refresh); + unpack_u32(p, &rr->rr.soa.retry); + unpack_u32(p, &rr->rr.soa.expire); + unpack_u32(p, &rr->rr.soa.minimum); + break; + + case T_A: + if (rr->rr_class != C_IN) + goto other; + unpack_inaddr(p, &rr->rr.in_a.addr); + break; + + case T_AAAA: + if (rr->rr_class != C_IN) + goto other; + unpack_in6addr(p, &rr->rr.in_aaaa.addr6); + break; + default: + other: + rr->rr.other.rdata = p->buf + p->offset; + rr->rr.other.rdlen = rdlen; + p->offset += rdlen; + } + + if (p->err) + return (-1); + + /* make sure that the advertised rdlen is really ok */ + if (p->offset - save_offset != rdlen) + p->err = "bad dlen"; + + return (p->err) ? (-1) : (0); +} + +ssize_t +dname_expand(const unsigned char *data, size_t len, size_t offset, + size_t *newoffset, char *dst, size_t max) +{ + size_t n, count, end, ptr, start; + ssize_t res; + + if (offset >= len) + return (-1); + + res = 0; + end = start = offset; + + for (; (n = data[offset]); ) { + if ((n & 0xc0) == 0xc0) { + if (offset + 2 > len) + return (-1); + ptr = 256 * (n & ~0xc0) + data[offset + 1]; + if (ptr >= start) + return (-1); + if (end < offset + 2) + end = offset + 2; + offset = start = ptr; + continue; + } + if (offset + n + 1 > len) + return (-1); + + /* copy n + at offset+1 */ + if (dst != NULL && max != 0) { + count = (max < n + 1) ? (max) : (n + 1); + memmove(dst, data + offset, count); + dst += count; + max -= count; + } + res += n + 1; + offset += n + 1; + if (end < offset) + end = offset; + } + if (end < offset + 1) + end = offset + 1; + + if (dst != NULL && max != 0) + dst[0] = 0; + if (newoffset) + *newoffset = end; + return (res + 1); +} + +char * +print_dname(const char *_dname, char *buf, size_t max) +{ + const unsigned char *dname = _dname; + char *res; + size_t left, n, count; + + if (_dname[0] == 0) { + (void)strlcpy(buf, ".", max); + return buf; + } + + res = buf; + left = max - 1; + for (n = 0; dname[0] && left; n += dname[0]) { + count = (dname[0] < (left - 1)) ? dname[0] : (left - 1); + memmove(buf, dname + 1, count); + dname += dname[0] + 1; + left -= count; + buf += count; + if (left) { + left -= 1; + *buf++ = '.'; + } + } + buf[0] = 0; + + return (res); +} + +static int +unpack_data(struct unpack *p, void *data, size_t len) +{ + if (p->err) + return (-1); + + if (p->len - p->offset < len) { + p->err = "too short"; + return (-1); + } + + memmove(data, p->buf + p->offset, len); + p->offset += len; + + return (0); +} + +static int +unpack_u16(struct unpack *p, uint16_t *u16) +{ + if (unpack_data(p, u16, 2) == -1) + return (-1); + + *u16 = ntohs(*u16); + + return (0); +} + +static int +unpack_u32(struct unpack *p, uint32_t *u32) +{ + if (unpack_data(p, u32, 4) == -1) + return (-1); + + *u32 = ntohl(*u32); + + return (0); +} + +static int +unpack_inaddr(struct unpack *p, struct in_addr *a) +{ + return (unpack_data(p, a, 4)); +} + +static int +unpack_in6addr(struct unpack *p, struct in6_addr *a6) +{ + return (unpack_data(p, a6, 16)); +} + +static int +unpack_dname(struct unpack *p, char *dst, size_t max) +{ + ssize_t e; + + if (p->err) + return (-1); + + e = dname_expand(p->buf, p->len, p->offset, &p->offset, dst, max); + if (e == -1) { + p->err = "bad domain name"; + return (-1); + } + if (e < 0 || e > MAXDNAME) { + p->err = "domain name too long"; + return (-1); + } + + return (0); +} diff --git a/smtpd/unpack_dns.h b/smtpd/unpack_dns.h new file mode 100644 index 00000000..2318a0c5 --- /dev/null +++ b/smtpd/unpack_dns.h @@ -0,0 +1,96 @@ +/* $OpenBSD: unpack_dns.h,v 1.1 2018/01/06 07:57:53 sunil Exp $ */ + +/* + * Copyright (c) 2011-2014 Eric Faurot <eric@faurot.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> + +#include <netinet/in.h> + +#include <arpa/inet.h> +#include <arpa/nameser.h> + +struct unpack { + const char *buf; + size_t len; + size_t offset; + const char *err; +}; + +struct dns_header { + uint16_t id; + uint16_t flags; + uint16_t qdcount; + uint16_t ancount; + uint16_t nscount; + uint16_t arcount; +}; + +struct dns_query { + char q_dname[MAXDNAME]; + uint16_t q_type; + uint16_t q_class; +}; + +struct dns_rr { + char rr_dname[MAXDNAME]; + uint16_t rr_type; + uint16_t rr_class; + uint32_t rr_ttl; + union { + struct { + char cname[MAXDNAME]; + } cname; + struct { + uint16_t preference; + char exchange[MAXDNAME]; + } mx; + struct { + char nsname[MAXDNAME]; + } ns; + struct { + char ptrname[MAXDNAME]; + } ptr; + struct { + char mname[MAXDNAME]; + char rname[MAXDNAME]; + uint32_t serial; + uint32_t refresh; + uint32_t retry; + uint32_t expire; + uint32_t minimum; + } soa; + struct { + struct in_addr addr; + } in_a; + struct { + struct in6_addr addr6; + } in_aaaa; + struct { + uint16_t rdlen; + const void *rdata; + } other; + } rr; +}; + +void unpack_init(struct unpack *, const char *, size_t); +int unpack_header(struct unpack *, struct dns_header *); +int unpack_rr(struct unpack *, struct dns_rr *); +int unpack_query(struct unpack *, struct dns_query *); +char *print_dname(const char *, char *, size_t); +ssize_t dname_expand(const unsigned char *, size_t, size_t, size_t *, + char *, size_t); + diff --git a/smtpd/util.c b/smtpd/util.c new file mode 100644 index 00000000..b2b1458c --- /dev/null +++ b/smtpd/util.c @@ -0,0 +1,870 @@ +/* $OpenBSD: util.c,v 1.151 2020/02/24 23:54:28 millert Exp $ */ + +/* + * Copyright (c) 2000,2001 Markus Friedl. All rights reserved. + * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org> + * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net> + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/resource.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <ctype.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <fts.h> +#include <imsg.h> +#include <inttypes.h> +#include <libgen.h> +#include <netdb.h> +#include <pwd.h> +#include <limits.h> +#include <resolv.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> +#include <time.h> +#include <unistd.h> + +#include "smtpd.h" +#include "log.h" + +const char *log_in6addr(const struct in6_addr *); +const char *log_sockaddr(struct sockaddr *); +static int parse_mailname_file(char *, size_t); + +int tracing = 0; +int foreground_log = 0; + +void * +xmalloc(size_t size) +{ + void *r; + + if ((r = malloc(size)) == NULL) + fatal("malloc"); + + return (r); +} + +void * +xcalloc(size_t nmemb, size_t size) +{ + void *r; + + if ((r = calloc(nmemb, size)) == NULL) + fatal("calloc"); + + return (r); +} + +char * +xstrdup(const char *str) +{ + char *r; + + if ((r = strdup(str)) == NULL) + fatal("strdup"); + + return (r); +} + +void * +xmemdup(const void *ptr, size_t size) +{ + void *r; + + if ((r = malloc(size)) == NULL) + fatal("malloc"); + + memmove(r, ptr, size); + + return (r); +} + +int +xasprintf(char **ret, const char *format, ...) +{ + int r; + va_list ap; + + va_start(ap, format); + r = vasprintf(ret, format, ap); + va_end(ap); + if (r == -1) + fatal("vasprintf"); + + return (r); +} + + +#if !defined(NO_IO) +int +io_xprintf(struct io *io, const char *fmt, ...) +{ + va_list ap; + int len; + + va_start(ap, fmt); + len = io_vprintf(io, fmt, ap); + va_end(ap); + if (len == -1) + fatal("io_xprintf(%p, %s, ...)", io, fmt); + + return len; +} + +int +io_xprint(struct io *io, const char *str) +{ + int len; + + len = io_print(io, str); + if (len == -1) + fatal("io_xprint(%p, %s, ...)", io, str); + + return len; +} +#endif + +char * +strip(char *s) +{ + size_t l; + + while (isspace((unsigned char)*s)) + s++; + + for (l = strlen(s); l; l--) { + if (!isspace((unsigned char)s[l-1])) + break; + s[l-1] = '\0'; + } + + return (s); +} + +int +bsnprintf(char *str, size_t size, const char *format, ...) +{ + int ret; + va_list ap; + + va_start(ap, format); + ret = vsnprintf(str, size, format, ap); + va_end(ap); + if (ret < 0 || ret >= (int)size) + return 0; + + return 1; +} + + +int +ckdir(const char *path, mode_t mode, uid_t owner, gid_t group, int create) +{ + char mode_str[12]; + int ret; + struct stat sb; + + if (stat(path, &sb) == -1) { + if (errno != ENOENT || create == 0) { + log_warn("stat: %s", path); + return (0); + } + + /* chmod is deferred to avoid umask effect */ + if (mkdir(path, 0) == -1) { + log_warn("mkdir: %s", path); + return (0); + } + + if (chown(path, owner, group) == -1) { + log_warn("chown: %s", path); + return (0); + } + + if (chmod(path, mode) == -1) { + log_warn("chmod: %s", path); + return (0); + } + + if (stat(path, &sb) == -1) { + log_warn("stat: %s", path); + return (0); + } + } + + ret = 1; + + /* check if it's a directory */ + if (!S_ISDIR(sb.st_mode)) { + ret = 0; + log_warnx("%s is not a directory", path); + } + + /* check that it is owned by owner/group */ + if (sb.st_uid != owner) { + ret = 0; + log_warnx("%s is not owned by uid %d", path, owner); + } + if (sb.st_gid != group) { + ret = 0; + log_warnx("%s is not owned by gid %d", path, group); + } + + /* check permission */ + if ((sb.st_mode & 07777) != mode) { + ret = 0; + strmode(mode, mode_str); + mode_str[10] = '\0'; + log_warnx("%s must be %s (%o)", path, mode_str + 1, mode); + } + + return ret; +} + +int +rmtree(char *path, int keepdir) +{ + char *path_argv[2]; + FTS *fts; + FTSENT *e; + int ret, depth; + + path_argv[0] = path; + path_argv[1] = NULL; + ret = 0; + depth = 0; + + fts = fts_open(path_argv, FTS_PHYSICAL | FTS_NOCHDIR, NULL); + if (fts == NULL) { + log_warn("fts_open: %s", path); + return (-1); + } + + while ((e = fts_read(fts)) != NULL) { + switch (e->fts_info) { + case FTS_D: + depth++; + break; + case FTS_DP: + case FTS_DNR: + depth--; + if (keepdir && depth == 0) + continue; + if (rmdir(e->fts_path) == -1) { + log_warn("rmdir: %s", e->fts_path); + ret = -1; + } + break; + + case FTS_F: + if (unlink(e->fts_path) == -1) { + log_warn("unlink: %s", e->fts_path); + ret = -1; + } + } + } + + fts_close(fts); + + return (ret); +} + +int +mvpurge(char *from, char *to) +{ + size_t n; + int retry; + const char *sep; + char buf[PATH_MAX]; + + if ((n = strlen(to)) == 0) + fatalx("to is empty"); + + sep = (to[n - 1] == '/') ? "" : "/"; + retry = 0; + +again: + (void)snprintf(buf, sizeof buf, "%s%s%u", to, sep, arc4random()); + if (rename(from, buf) == -1) { + /* ENOTDIR has actually 2 meanings, and incorrect input + * could lead to an infinite loop. Consider that after + * 20 tries something is hopelessly wrong. + */ + if (errno == ENOTEMPTY || errno == EISDIR || errno == ENOTDIR) { + if ((retry++) >= 20) + return (-1); + goto again; + } + return -1; + } + + return 0; +} + + +int +mktmpfile(void) +{ + char path[PATH_MAX]; + int fd; + + if (!bsnprintf(path, sizeof(path), "%s/smtpd.XXXXXXXXXX", + PATH_TEMPORARY)) { + log_warn("snprintf"); + fatal("exiting"); + } + + if ((fd = mkstemp(path)) == -1) { + log_warn("cannot create temporary file %s", path); + fatal("exiting"); + } + unlink(path); + return (fd); +} + + +/* Close file, signifying temporary error condition (if any) to the caller. */ +int +safe_fclose(FILE *fp) +{ + if (ferror(fp)) { + fclose(fp); + return 0; + } + if (fflush(fp)) { + fclose(fp); + if (errno == ENOSPC) + return 0; + fatal("safe_fclose: fflush"); + } + if (fsync(fileno(fp))) + fatal("safe_fclose: fsync"); + if (fclose(fp)) + fatal("safe_fclose: fclose"); + + return 1; +} + +int +hostname_match(const char *hostname, const char *pattern) +{ + while (*pattern != '\0' && *hostname != '\0') { + if (*pattern == '*') { + while (*pattern == '*') + pattern++; + while (*hostname != '\0' && + tolower((unsigned char)*hostname) != + tolower((unsigned char)*pattern)) + hostname++; + continue; + } + + if (tolower((unsigned char)*pattern) != + tolower((unsigned char)*hostname)) + return 0; + pattern++; + hostname++; + } + + return (*hostname == '\0' && *pattern == '\0'); +} + +int +mailaddr_match(const struct mailaddr *maddr1, const struct mailaddr *maddr2) +{ + struct mailaddr m1 = *maddr1; + struct mailaddr m2 = *maddr2; + char *p; + + /* catchall */ + if (m2.user[0] == '\0' && m2.domain[0] == '\0') + return 1; + + if (m2.domain[0] && !hostname_match(m1.domain, m2.domain)) + return 0; + + if (m2.user[0]) { + /* if address from table has a tag, we must respect it */ + if (strchr(m2.user, *env->sc_subaddressing_delim) == NULL) { + /* otherwise, strip tag from session address if any */ + p = strchr(m1.user, *env->sc_subaddressing_delim); + if (p) + *p = '\0'; + } + if (strcasecmp(m1.user, m2.user)) + return 0; + } + return 1; +} + +int +valid_localpart(const char *s) +{ +#define IS_ATEXT(c) (isalnum((unsigned char)(c)) || strchr(MAILADDR_ALLOWED, (c))) +nextatom: + if (!IS_ATEXT(*s) || *s == '\0') + return 0; + while (*(++s) != '\0') { + if (*s == '.') + break; + if (IS_ATEXT(*s)) + continue; + return 0; + } + if (*s == '.') { + s++; + goto nextatom; + } + return 1; +} + +int +valid_domainpart(const char *s) +{ + struct in_addr ina; + struct in6_addr ina6; + char *c, domain[SMTPD_MAXDOMAINPARTSIZE]; + const char *p; + size_t dlen; + + if (*s == '[') { + if (strncasecmp("[IPv6:", s, 6) == 0) + p = s + 6; + else + p = s + 1; + + if (strlcpy(domain, p, sizeof domain) >= sizeof domain) + return 0; + + c = strchr(domain, ']'); + if (!c || c[1] != '\0') + return 0; + + *c = '\0'; + + if (inet_pton(AF_INET6, domain, &ina6) == 1) + return 1; + if (inet_pton(AF_INET, domain, &ina) == 1) + return 1; + + return 0; + } + + if (*s == '\0') + return 0; + + dlen = strlen(s); + if (dlen >= sizeof domain) + return 0; + + if (s[dlen - 1] == '.') + return 0; + + return res_hnok(s); +} + +#define LABELCHR(c) ((c) == '-' || (c) == '_' || isalpha((unsigned char)(c)) || isdigit((unsigned char)(c))) +#define LABELMAX 63 +#define DNAMEMAX 253 + +int +valid_domainname(const char *str) +{ + const char *label, *s; + + /* + * Expect a sequence of dot-separated labels, possibly with a trailing + * dot. The empty string is rejected, as well a single dot. + */ + for (s = str; *s; s++) { + + /* Start of a new label. */ + label = s; + while (LABELCHR(*s)) + s++; + + /* Must have at least one char and at most LABELMAX. */ + if (s == label || s - label > LABELMAX) + return 0; + + /* If last label, stop here. */ + if (*s == '\0') + break; + + /* Expect a dot as label separator or last char. */ + if (*s != '.') + return 0; + } + + /* Must have at leat one label and no more than DNAMEMAX chars. */ + if (s == str || s - str > DNAMEMAX) + return 0; + + return 1; +} + +int +valid_smtp_response(const char *s) +{ + if (strlen(s) < 5) + return 0; + + if ((s[0] < '2' || s[0] > '5') || + (s[1] < '0' || s[1] > '9') || + (s[2] < '0' || s[2] > '9') || + (s[3] != ' ')) + return 0; + + return 1; +} + +int +secure_file(int fd, char *path, char *userdir, uid_t uid, int mayread) +{ + char buf[PATH_MAX]; + char homedir[PATH_MAX]; + struct stat st; + char *cp; + + if (realpath(path, buf) == NULL) + return 0; + + if (realpath(userdir, homedir) == NULL) + homedir[0] = '\0'; + + /* Check the open file to avoid races. */ + if (fstat(fd, &st) == -1 || + !S_ISREG(st.st_mode) || + st.st_uid != uid || + (st.st_mode & (mayread ? 022 : 066)) != 0) + return 0; + + /* For each component of the canonical path, walking upwards. */ + for (;;) { + if ((cp = dirname(buf)) == NULL) + return 0; + (void)strlcpy(buf, cp, sizeof(buf)); + + if (stat(buf, &st) == -1 || + (st.st_uid != 0 && st.st_uid != uid) || + (st.st_mode & 022) != 0) + return 0; + + /* We can stop checking after reaching homedir level. */ + if (strcmp(homedir, buf) == 0) + break; + + /* + * dirname should always complete with a "/" path, + * but we can be paranoid and check for "." too + */ + if ((strcmp("/", buf) == 0) || (strcmp(".", buf) == 0)) + break; + } + + return 1; +} + +void +addargs(arglist *args, char *fmt, ...) +{ + va_list ap; + char *cp; + uint nalloc; + int r; + char **tmp; + + va_start(ap, fmt); + r = vasprintf(&cp, fmt, ap); + va_end(ap); + if (r == -1) + fatal("addargs: argument too long"); + + nalloc = args->nalloc; + if (args->list == NULL) { + nalloc = 32; + args->num = 0; + } else if (args->num+2 >= nalloc) + nalloc *= 2; + + tmp = reallocarray(args->list, nalloc, sizeof(char *)); + if (tmp == NULL) + fatal("addargs: reallocarray"); + args->list = tmp; + args->nalloc = nalloc; + args->list[args->num++] = cp; + args->list[args->num] = NULL; +} + +int +lowercase(char *buf, const char *s, size_t len) +{ + if (len == 0) + return 0; + + if (strlcpy(buf, s, len) >= len) + return 0; + + while (*buf != '\0') { + *buf = tolower((unsigned char)*buf); + buf++; + } + + return 1; +} + +int +uppercase(char *buf, const char *s, size_t len) +{ + if (len == 0) + return 0; + + if (strlcpy(buf, s, len) >= len) + return 0; + + while (*buf != '\0') { + *buf = toupper((unsigned char)*buf); + buf++; + } + + return 1; +} + +void +xlowercase(char *buf, const char *s, size_t len) +{ + if (len == 0) + fatalx("lowercase: len == 0"); + + if (!lowercase(buf, s, len)) + fatalx("lowercase: truncation"); +} + +uint64_t +generate_uid(void) +{ + static uint32_t id; + static uint8_t inited; + uint64_t uid; + + if (!inited) { + id = arc4random(); + inited = 1; + } + while ((uid = ((uint64_t)(id++) << 32 | arc4random())) == 0) + ; + + return (uid); +} + +int +session_socket_error(int fd) +{ + int error; + socklen_t len; + + len = sizeof(error); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len) == -1) + fatal("session_socket_error: getsockopt"); + + return (error); +} + +const char * +parse_smtp_response(char *line, size_t len, char **msg, int *cont) +{ + if (len >= LINE_MAX) + return "line too long"; + + if (len > 3) { + if (msg) + *msg = line + 4; + if (cont) + *cont = (line[3] == '-'); + } else if (len == 3) { + if (msg) + *msg = line + 3; + if (cont) + *cont = 0; + } else + return "line too short"; + + /* validate reply code */ + if (line[0] < '2' || line[0] > '5' || !isdigit((unsigned char)line[1]) || + !isdigit((unsigned char)line[2])) + return "reply code out of range"; + + return NULL; +} + +static int +parse_mailname_file(char *hostname, size_t len) +{ + FILE *fp; + char *buf = NULL; + size_t bufsz = 0; + ssize_t buflen; + + if ((fp = fopen(MAILNAME_FILE, "r")) == NULL) + return 1; + + if ((buflen = getline(&buf, &bufsz, fp)) == -1) + goto error; + + if (buf[buflen - 1] == '\n') + buf[buflen - 1] = '\0'; + + if (strlcpy(hostname, buf, len) >= len) { + fprintf(stderr, MAILNAME_FILE " entry too long"); + goto error; + } + + return 0; +error: + fclose(fp); + free(buf); + return 1; +} + +int +getmailname(char *hostname, size_t len) +{ + struct addrinfo hints, *res = NULL; + int error; + + /* Try MAILNAME_FILE first */ + if (parse_mailname_file(hostname, len) == 0) + return 0; + + /* Next, gethostname(3) */ + if (gethostname(hostname, len) == -1) { + fprintf(stderr, "getmailname: gethostname() failed\n"); + return -1; + } + + if (strchr(hostname, '.') != NULL) + return 0; + + /* Canonicalize if domain part is missing */ + memset(&hints, 0, sizeof hints); + hints.ai_family = PF_UNSPEC; + hints.ai_flags = AI_CANONNAME; + error = getaddrinfo(hostname, NULL, &hints, &res); + if (error) + return 0; /* Continue with non-canon hostname */ + + if (strlcpy(hostname, res->ai_canonname, len) >= len) { + fprintf(stderr, "hostname too long"); + freeaddrinfo(res); + return -1; + } + + freeaddrinfo(res); + return 0; +} + +int +base64_encode(unsigned char const *src, size_t srclen, + char *dest, size_t destsize) +{ + return __b64_ntop(src, srclen, dest, destsize); +} + +int +base64_decode(char const *src, unsigned char *dest, size_t destsize) +{ + return __b64_pton(src, dest, destsize); +} + +int +base64_encode_rfc3548(unsigned char const *src, size_t srclen, + char *dest, size_t destsize) +{ + size_t i; + int ret; + + if ((ret = base64_encode(src, srclen, dest, destsize)) == -1) + return -1; + + for (i = 0; i < destsize; ++i) { + if (dest[i] == '/') + dest[i] = '_'; + else if (dest[i] == '+') + dest[i] = '-'; + } + + return ret; +} + +void +log_trace(int mask, const char *emsg, ...) +{ + va_list ap; + + if (tracing & mask) { + va_start(ap, emsg); + vlog(LOG_DEBUG, emsg, ap); + va_end(ap); + } +} + +void +log_trace_verbose(int v) +{ + tracing = v; + + /* Set debug logging in log.c */ + log_setverbose(v & TRACE_DEBUG ? 2 : foreground_log); +} + +void +xclosefrom(int lowfd) +{ +#if defined HAVE_CLOSEFROM_INT + if (closefrom(lowfd) == -1) + err(1, "closefrom"); +#else + closefrom(lowfd); +#endif +} + +void +portable_freeaddrinfo(struct addrinfo *ai) +{ + struct addrinfo *p; + + do { + p = ai; + ai = ai->ai_next; + free(p->ai_canonname); + free(p); + } while (ai); +} diff --git a/smtpd/waitq.c b/smtpd/waitq.c new file mode 100644 index 00000000..082a1e51 --- /dev/null +++ b/smtpd/waitq.c @@ -0,0 +1,104 @@ +/* $OpenBSD: waitq.c,v 1.6 2018/05/31 21:06:12 gilles Exp $ */ + +/* + * Copyright (c) 2012 Eric Faurot <eric@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 "includes.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/uio.h> + +#include <imsg.h> +#include <stdio.h> +#include <stdlib.h> +#include <limits.h> + +#include "smtpd.h" + +struct waiter { + TAILQ_ENTRY(waiter) entry; + void (*cb)(void *, void *, void *); + void *arg; +}; + +struct waitq { + SPLAY_ENTRY(waitq) entry; + void *tag; + TAILQ_HEAD(, waiter) waiters; +}; + +static int waitq_cmp(struct waitq *, struct waitq *); + +SPLAY_HEAD(waitqtree, waitq); +SPLAY_PROTOTYPE(waitqtree, waitq, entry, waitq_cmp); + +static struct waitqtree waitqs = SPLAY_INITIALIZER(&waitqs); + +static int +waitq_cmp(struct waitq *a, struct waitq *b) +{ + if (a->tag < b->tag) + return (-1); + if (a->tag > b->tag) + return (1); + return (0); +} + +SPLAY_GENERATE(waitqtree, waitq, entry, waitq_cmp); + +int +waitq_wait(void *tag, void (*cb)(void *, void *, void *), void *arg) +{ + struct waitq *wq, key; + struct waiter *w; + + key.tag = tag; + wq = SPLAY_FIND(waitqtree, &waitqs, &key); + if (wq == NULL) { + wq = xmalloc(sizeof *wq); + wq->tag = tag; + TAILQ_INIT(&wq->waiters); + SPLAY_INSERT(waitqtree, &waitqs, wq); + } + + w = xmalloc(sizeof *w); + w->cb = cb; + w->arg = arg; + TAILQ_INSERT_TAIL(&wq->waiters, w, entry); + + return (w == TAILQ_FIRST(&wq->waiters)); +} + +void +waitq_run(void *tag, void *result) +{ + struct waitq *wq, key; + struct waiter *w; + + key.tag = tag; + wq = SPLAY_FIND(waitqtree, &waitqs, &key); + SPLAY_REMOVE(waitqtree, &waitqs, wq); + + while ((w = TAILQ_FIRST(&wq->waiters))) { + TAILQ_REMOVE(&wq->waiters, w, entry); + w->cb(tag, w->arg, result); + free(w); + } + free(wq); +} |