root/usr.sbin/smtpd/mda.c
/*      $OpenBSD: mda.c,v 1.147 2024/01/20 09:01:03 claudio 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 <ctype.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <time.h>
#include <unistd.h>
#include <vis.h>

#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, fd;
        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;

                fd = imsg_get_fd(imsg);
                if (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,
                    fd, s->id, e->id);

                if ((s->datafp = fdopen(fd, "r")) == NULL) {
                        log_warn("warn: mda: fdopen");
                        close(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;
                fd = imsg_get_fd(imsg);
                if (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,
                    fd, s->id, s->evp->id);

                io_set_nonblocking(fd);
                io_set_fd(s->io, 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';
                fd = imsg_get_fd(imsg);
                if (fd != -1)
                        mda_getlastline(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;
        }

        fatalx("mda_imsg: unexpected %s imsg", imsg_to_str(imsg->hdr.type));
}

void
mda_postfork(void)
{
}

void
mda_postprivdrop(void)
{
        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) == -1) {
                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;
}