root/usr.sbin/smtpd/mda_variables.c
/*      $OpenBSD: mda_variables.c,v 1.11 2026/04/03 10:18:58 jtt 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 <stdlib.h>
#include <string.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 = NULL;
        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 */
                if ((mods = strchr(rbracket + 1, ':')) != NULL)
                        *mods++ = '\0';
        } 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 (string == NULL)
                        return -1;
                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, toklen;

        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 ((ebuf = strchr(pbuf+2, '}')) == NULL)
                        return 0;

                /* extract token from %{token} */
                toklen = ebuf - (pbuf+2);
                if (toklen >= sizeof token)
                        return 0;

                memcpy(token, pbuf+2, toklen);
                token[toklen] = '\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;
}