aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGilles Chehade <gilles@poolp.org>2020-05-22 14:35:02 +0200
committerGilles Chehade <gilles@poolp.org>2020-05-22 14:35:02 +0200
commit90087f07a7cfffe10b4958e13558a0052f85691d (patch)
treebf0aac44374cfd5cd0da91cc67a6744decd74665
parentmoving smtpd to usr.sbin/smtpd to ease cherry-picking of upstream (diff)
downloadOpenSMTPD-90087f07a7cfffe10b4958e13558a0052f85691d.tar.xz
OpenSMTPD-90087f07a7cfffe10b4958e13558a0052f85691d.zip
Revert "moving smtpd to usr.sbin/smtpd to ease cherry-picking of upstream"
This reverts commit 90620a574d8824e5b2aa18709f2d5b5b6bb3cb38.
-rw-r--r--contrib/libexec/encrypt/Makefile.am2
-rw-r--r--contrib/libexec/lockspool/Makefile.am2
-rw-r--r--contrib/libexec/mail.local/Makefile.am2
-rw-r--r--mk/pathnames4
-rw-r--r--openbsd-compat/Makefile.am2
-rw-r--r--smtpd/Makefile10
-rw-r--r--smtpd/aliases.5102
-rw-r--r--smtpd/aliases.c234
-rw-r--r--smtpd/bounce.c820
-rw-r--r--smtpd/ca.c777
-rw-r--r--smtpd/cert.c416
-rw-r--r--smtpd/compress_backend.c72
-rw-r--r--smtpd/compress_gzip.c186
-rw-r--r--smtpd/config.c350
-rw-r--r--smtpd/control.c817
-rw-r--r--smtpd/crypto.c400
-rw-r--r--smtpd/dict.c269
-rw-r--r--smtpd/dict.h48
-rw-r--r--smtpd/dns.c379
-rw-r--r--smtpd/enqueue.c932
-rw-r--r--smtpd/envelope.c786
-rw-r--r--smtpd/esc.c116
-rw-r--r--smtpd/expand.c332
-rw-r--r--smtpd/filter.c868
-rw-r--r--smtpd/forward.583
-rw-r--r--smtpd/forward.c104
-rw-r--r--smtpd/iobuf.c462
-rw-r--r--smtpd/iobuf.h67
-rw-r--r--smtpd/ioev.c1064
-rw-r--r--smtpd/ioev.h70
-rw-r--r--smtpd/libressl.c213
-rw-r--r--smtpd/limit.c124
-rw-r--r--smtpd/lka.c914
-rw-r--r--smtpd/lka_filter.c1746
-rw-r--r--smtpd/lka_session.c556
-rw-r--r--smtpd/log.c220
-rw-r--r--smtpd/log.h52
-rw-r--r--smtpd/mail.lmtp.855
-rw-r--r--smtpd/mail.lmtp.c332
-rw-r--r--smtpd/mail.maildir.845
-rw-r--r--smtpd/mail.maildir.c284
-rw-r--r--smtpd/mail.mboxfile.834
-rw-r--r--smtpd/mail.mboxfile.c109
-rw-r--r--smtpd/mail.mda.835
-rw-r--r--smtpd/mail.mda.c70
-rw-r--r--smtpd/mail/Makefile20
-rw-r--r--smtpd/mailaddr.c135
-rw-r--r--smtpd/makemap.8174
-rw-r--r--smtpd/makemap.c521
-rw-r--r--smtpd/mda.c919
-rw-r--r--smtpd/mda_mbox.c94
-rw-r--r--smtpd/mda_unpriv.c110
-rw-r--r--smtpd/mda_variables.c374
-rw-r--r--smtpd/mproc.c676
-rw-r--r--smtpd/mta.c2647
-rw-r--r--smtpd/mta_session.c2008
-rw-r--r--smtpd/newaliases.886
-rw-r--r--smtpd/parse.y3598
-rw-r--r--smtpd/parser.c341
-rw-r--r--smtpd/parser.h43
-rw-r--r--smtpd/pony.c212
-rw-r--r--smtpd/proxy.c387
-rw-r--r--smtpd/queue.c750
-rw-r--r--smtpd/queue_backend.c806
-rw-r--r--smtpd/queue_fs.c695
-rw-r--r--smtpd/queue_null.c120
-rw-r--r--smtpd/queue_proc.c337
-rw-r--r--smtpd/queue_ram.c336
-rw-r--r--smtpd/report_smtp.c335
-rw-r--r--smtpd/resolver.c462
-rw-r--r--smtpd/rfc5322.c266
-rw-r--r--smtpd/rfc5322.h41
-rw-r--r--smtpd/ruleset.c265
-rw-r--r--smtpd/runq.c183
-rw-r--r--smtpd/scheduler.c618
-rw-r--r--smtpd/scheduler_backend.c82
-rw-r--r--smtpd/scheduler_null.c164
-rw-r--r--smtpd/scheduler_proc.c446
-rw-r--r--smtpd/scheduler_ramqueue.c1204
-rw-r--r--smtpd/sendmail.886
-rw-r--r--smtpd/smtp.196
-rw-r--r--smtpd/smtp.c387
-rw-r--r--smtpd/smtp.h95
-rw-r--r--smtpd/smtp/Makefile24
-rw-r--r--smtpd/smtp_client.c923
-rw-r--r--smtpd/smtp_session.c3223
-rw-r--r--smtpd/smtpc.c465
-rw-r--r--smtpd/smtpctl.8336
-rw-r--r--smtpd/smtpctl.c1469
-rw-r--r--smtpd/smtpctl/Makefile56
-rw-r--r--smtpd/smtpd-api.h290
-rw-r--r--smtpd/smtpd-defines.h68
-rw-r--r--smtpd/smtpd-filters.7653
-rw-r--r--smtpd/smtpd.8167
-rw-r--r--smtpd/smtpd.c2328
-rw-r--r--smtpd/smtpd.conf19
-rw-r--r--smtpd/smtpd.conf.51240
-rw-r--r--smtpd/smtpd.h1784
-rw-r--r--smtpd/smtpd/Makefile102
-rw-r--r--smtpd/spfwalk.c391
-rw-r--r--smtpd/srs.c379
-rw-r--r--smtpd/ssl.c458
-rw-r--r--smtpd/ssl.h71
-rw-r--r--smtpd/ssl_smtpd.c105
-rw-r--r--smtpd/ssl_verify.c297
-rw-r--r--smtpd/stat_backend.c124
-rw-r--r--smtpd/stat_ramstat.c162
-rw-r--r--smtpd/table.5258
-rw-r--r--smtpd/table.c709
-rw-r--r--smtpd/table_db.c282
-rw-r--r--smtpd/table_getpwnam.c120
-rw-r--r--smtpd/table_proc.c283
-rw-r--r--smtpd/table_static.c398
-rw-r--r--smtpd/to.c880
-rw-r--r--smtpd/tree.c259
-rw-r--r--smtpd/tree.h48
-rw-r--r--smtpd/unpack_dns.c300
-rw-r--r--smtpd/unpack_dns.h96
-rw-r--r--smtpd/util.c870
-rw-r--r--smtpd/waitq.c104
120 files changed, 54419 insertions, 6 deletions
diff --git a/contrib/libexec/encrypt/Makefile.am b/contrib/libexec/encrypt/Makefile.am
index 2f96e60d..6ad7b82d 100644
--- a/contrib/libexec/encrypt/Makefile.am
+++ b/contrib/libexec/encrypt/Makefile.am
@@ -1,7 +1,7 @@
pkglibexec_PROGRAMS = encrypt
encrypt_SOURCES = encrypt.c
-encrypt_SOURCES += $(top_srcdir)/usr.sbin/smtpd/log.c
+encrypt_SOURCES += $(top_srcdir)/smtpd/log.c
AM_CPPFLAGS = -I$(top_srcdir)/openbsd-compat
diff --git a/contrib/libexec/lockspool/Makefile.am b/contrib/libexec/lockspool/Makefile.am
index 2801c101..dacf5386 100644
--- a/contrib/libexec/lockspool/Makefile.am
+++ b/contrib/libexec/lockspool/Makefile.am
@@ -2,7 +2,7 @@ pkglibexec_PROGRAMS = lockspool
lockspool_SOURCES = lockspool.c
lockspool_SOURCES += locking.c
-lockspool_SOURCES += $(top_srcdir)/usr.sbin/smtpd/log.c
+lockspool_SOURCES += $(top_srcdir)/smtpd/log.c
EXTRA_DIST = mail.local.h pathnames.h
diff --git a/contrib/libexec/mail.local/Makefile.am b/contrib/libexec/mail.local/Makefile.am
index 217659c1..bd5211a2 100644
--- a/contrib/libexec/mail.local/Makefile.am
+++ b/contrib/libexec/mail.local/Makefile.am
@@ -2,7 +2,7 @@ pkglibexec_PROGRAMS = mail.local
mail_local_SOURCES = mail.local.c
mail_local_SOURCES += locking.c
-mail_local_SOURCES += $(top_srcdir)/usr.sbin/smtpd/log.c
+mail_local_SOURCES += $(top_srcdir)/smtpd/log.c
EXTRA_DIST = mail.local.h pathnames.h
diff --git a/mk/pathnames b/mk/pathnames
index efa785ef..b233ec33 100644
--- a/mk/pathnames
+++ b/mk/pathnames
@@ -1,8 +1,8 @@
-smtpd_srcdir = $(top_srcdir)/usr.sbin/smtpd
+smtpd_srcdir = $(top_srcdir)/smtpd
compat_srcdir = $(top_srcdir)/openbsd-compat
regress_srcdir = $(top_srcdir)/regress/bin
-PATHS= -DSMTPD_CONFDIR=\"$(sysconfdir)\" \
+PATHS= -DSMTPD_CONFDIR=\"$(sysconfdir)\" \
-DPATH_CHROOT=\"$(PRIVSEP_PATH)\" \
-DPATH_SMTPCTL=\"$(sbindir)/smtpctl\" \
-DPATH_MAILLOCAL=\"$(pkglibexecdir)/mail.local\" \
diff --git a/openbsd-compat/Makefile.am b/openbsd-compat/Makefile.am
index 0e704cfd..01a3efe7 100644
--- a/openbsd-compat/Makefile.am
+++ b/openbsd-compat/Makefile.am
@@ -1,6 +1,6 @@
noinst_LIBRARIES = libopenbsd.a
-AM_CPPFLAGS = -I$(top_srcdir) -I$(top_srcdir)/usr.sbin/smtpd -I$(top_srcdir)/openbsd-compat
+AM_CPPFLAGS = -I$(top_srcdir) -I$(top_srcdir)/smtpd -I$(top_srcdir)/openbsd-compat
libopenbsd_a_SOURCES = empty.c
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(&timestamp);
+
+ 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, &param[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, &copy,
+ 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, &copy,
+ 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, &copy,
+ 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, &copy,
+ 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(&copy, " "))) {
+ 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, &copy,
+ 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(&copy, " "))) {
+ 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(&params, 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(&params, 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, &ltgt);
+ ltgt.dispatch = dispatch_aaaa;
+ lookup_record(T_AAAA, buf2, &ltgt);
+ continue;
+ }
+ if (strncasecmp("a:", *ap, 2) == 0) {
+ record = *(ap) + 2;
+ if (parse_target(record, &ltgt) < 0)
+ continue;
+ ltgt.dispatch = dispatch_a;
+ lookup_record(T_A, record, &ltgt);
+ ltgt.dispatch = dispatch_aaaa;
+ lookup_record(T_AAAA, record, &ltgt);
+ continue;
+ }
+ if (strncasecmp("exists:", *ap, 7) == 0) {
+ ltgt.dispatch = dispatch_a;
+ lookup_record(T_A, *(ap) + 7, &ltgt);
+ continue;
+ }
+ if (strncasecmp("include:", *ap, 8) == 0) {
+ ltgt.dispatch = dispatch_txt;
+ lookup_record(T_TXT, *(ap) + 8, &ltgt);
+ continue;
+ }
+ if (strncasecmp("redirect=", *ap, 9) == 0) {
+ ltgt.dispatch = dispatch_txt;
+ lookup_record(T_TXT, *(ap) + 9, &ltgt);
+ 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, &ltgt);
+ continue;
+ }
+ if (strncasecmp("mx:", *ap, 3) == 0) {
+ record = *(ap) + 3;
+ if (parse_target(record, &ltgt) < 0)
+ continue;
+ ltgt.dispatch = dispatch_mx;
+ lookup_record(T_MX, record, &ltgt);
+ 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, &ltgt);
+ ltgt.dispatch = dispatch_aaaa;
+ lookup_record(T_AAAA, buf, &ltgt);
+}
+
+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);
+}