root/usr.bin/mail/send.c
/*      $NetBSD: send.c,v 1.40 2025/07/09 16:59:54 rillig Exp $ */

/*
 * Copyright (c) 1980, 1993
 *      The Regents of the University of California.  All rights reserved.
 *
 * 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 above 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. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``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 REGENTS 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.
 */

#include <sys/cdefs.h>
#ifndef lint
#if 0
static char sccsid[] = "@(#)send.c      8.1 (Berkeley) 6/6/93";
#else
__RCSID("$NetBSD: send.c,v 1.40 2025/07/09 16:59:54 rillig Exp $");
#endif
#endif /* not lint */

#include <assert.h>

#include "rcv.h"
#include "extern.h"
#ifdef MIME_SUPPORT
#include "mime.h"
#endif
#include "sig.h"

/*
 * Mail -- a mail program
 *
 * Mail to others.
 */

/*
 * compare twwo name structures.  Support for get_smopts.
 */
static int
namecmp(struct name *np1, struct name *np2)
{
        for (/*EMPTY*/; np1 && np2; np1 = np1->n_flink, np2 = np2->n_flink) {
                if (strcmp(np1->n_name, np2->n_name) != 0)
                        return 1;
        }
        return np1 || np2;
}

/*
 * Get the sendmail options from the smopts list.
 */
static struct name *
get_smopts(struct name *to)
{
        struct smopts_s *sp;
        struct name *smargs, *np;
        smargs = NULL;
        for (np = to; np; np = np->n_flink) {
                if ((sp = findsmopts(np->n_name, 0)) == NULL)
                        continue;
                if (smargs == NULL)
                        smargs = sp->s_smopts;
                else if (namecmp(smargs, sp->s_smopts) != 0)
                        return NULL;
        }
        if (smargs &&
            smargs->n_flink == NULL &&
            (smargs->n_name == NULL || smargs->n_name[0] == '\0'))
                return NULL;
        return smargs;
}

/*
 * Output a reasonable looking status field.
 */
static void
statusput(struct message *mp, FILE *obuf, const char *prefix)
{
        char statout[3];
        char *cp = statout;

        if (mp->m_flag & MREAD)
                *cp++ = 'R';
        if ((mp->m_flag & MNEW) == 0)
                *cp++ = 'O';
        *cp = 0;
        if (statout[0])
                (void)fprintf(obuf, "%sStatus: %s\n",
                        prefix == NULL ? "" : prefix, statout);
}

/*
 * Send message described by the passed pointer to the
 * passed output buffer.  Return -1 on error.
 * Adjust the status: field if need be.
 * If doign is given, suppress ignored header fields.
 * prefix is a string to prepend to each output line.
 */
PUBLIC int
sendmessage(struct message *mp, FILE *obuf, struct ignoretab *doign,
    const char *prefix, struct mime_info *mip)
{
        off_t len;
        FILE *ibuf;
        char line[LINESIZE];
        int isheadflag, infld, ignoring, dostat, firstline;
        char *cp;
        size_t prefixlen;
        size_t linelen;
        int rval;

        ignoring = 0;
        prefixlen = 0;
        rval = -1;

        /*
         * Compute the prefix string, without trailing whitespace
         */
        if (prefix != NULL) {
                const char *dp, *dp2 = NULL;
                for (dp = prefix; *dp; dp++)
                        if (!is_WSP(*dp))
                                dp2 = dp;
                prefixlen = dp2 == 0 ? 0 : dp2 - prefix + 1;
        }
        ibuf = setinput(mp);
        len = mp->m_size;
        isheadflag = 1;
        dostat = doign == 0 || !isign("status", doign);
        infld = 0;
        firstline = 1;

        sig_check();
        /*
         * Process headers first
         */
#ifdef MIME_SUPPORT
        if (mip)
                obuf = mime_decode_header(mip);
#endif
        while (len > 0 && isheadflag) {
                sig_check();
                if (fgets(line, (int)sizeof(line), ibuf) == NULL)
                        break;
                len -= linelen = strlen(line);
                if (firstline) {
                        /*
                         * First line is the From line, so no headers
                         * there to worry about
                         */
                        firstline = 0;
                        ignoring = doign == ignoreall || doign == bouncetab;
#ifdef MIME_SUPPORT
                        /* XXX - ignore multipart boundary lines! */
                        if (line[0] == '-' && line[1] == '-')
                                ignoring = 1;
#endif
                } else if (line[0] == '\n') {
                        /*
                         * If line is blank, we've reached end of
                         * headers, so force out status: field
                         * and note that we are no longer in header
                         * fields
                         */
                        if (dostat) {
                                statusput(mp, obuf, prefix);
                                dostat = 0;
                        }
                        isheadflag = 0;
                        ignoring = doign == ignoreall;
                } else if (infld && is_WSP(line[0])) {
                        /*
                         * If this line is a continuation (via space or tab)
                         * of a previous header field, just echo it
                         * (unless the field should be ignored).
                         * In other words, nothing to do.
                         */
                } else {
                        /*
                         * Pick up the header field if we have one.
                         */
                        char *save_cp;
                        for (cp = line; *cp && *cp != ':' && !is_WSP(*cp); cp++)
                                continue;
                        save_cp = cp;
                        cp = skip_WSP(cp);
                        if (*cp++ != ':') {
                                /*
                                 * Not a header line, force out status:
                                 * This happens in uucp style mail where
                                 * there are no headers at all.
                                 */
                                if (dostat) {
                                        statusput(mp, obuf, prefix);
                                        dostat = 0;
                                }
                                if (doign != ignoreall)
                                        /* add blank line */
                                        (void)putc('\n', obuf);
                                isheadflag = 0;
                                ignoring = 0;
                        } else {
                                /*
                                 * If it is an ignored field and
                                 * we care about such things, skip it.
                                 */
                                int save_c;
                                save_c = *save_cp;
                                *save_cp = 0;   /* temporarily null terminate */
                                if (doign && isign(line, doign))
                                        ignoring = 1;
                                else if ((line[0] == 's' || line[0] == 'S') &&
                                         strcasecmp(line, "status") == 0) {
                                        /*
                                         * If the field is "status," go compute
                                         * and print the real Status: field
                                         */
                                        if (dostat) {
                                                statusput(mp, obuf, prefix);
                                                dostat = 0;
                                        }
                                        ignoring = 1;
                                } else {
                                        ignoring = 0;
                                }
                                infld = 1;
                                *save_cp = save_c;      /* restore the line */
                        }
                }
                if (!ignoring) {
                        /*
                         * Strip trailing whitespace from prefix
                         * if line is blank.
                         */
                        if (prefix != NULL) {
                                if (linelen > 1)
                                        (void)fputs(prefix, obuf);
                                else
                                        (void)fwrite(prefix, sizeof(*prefix),
                                                        prefixlen, obuf);
                        }
                        (void)fwrite(line, sizeof(*line), linelen, obuf);
                        if (ferror(obuf))
                                goto out;
                }
        }
        sig_check();

        /*
         * Copy out message body
         */
#ifdef MIME_SUPPORT
        if (mip) {
                obuf = mime_decode_body(mip);
                sig_check();
                if (obuf == NULL) { /* XXX - early out */
                        rval = 0;
                        goto out;
                }
        }
#endif
        if (doign == ignoreall)
                len--;          /* skip final blank line */

        linelen = 0;            /* needed for in case len == 0 */
        if (prefix != NULL) {
                while (len > 0) {
                        sig_check();
                        if (fgets(line, (int)sizeof(line), ibuf) == NULL) {
                                linelen = 0;
                                break;
                        }
                        len -= linelen = strlen(line);
                        /*
                         * Strip trailing whitespace from prefix
                         * if line is blank.
                         */
                        if (linelen > 1)
                                (void)fputs(prefix, obuf);
                        else
                                (void)fwrite(prefix, sizeof(*prefix),
                                                prefixlen, obuf);
                        (void)fwrite(line, sizeof(*line), linelen, obuf);
                        if (ferror(obuf))
                                goto out;
                }
        }
        else {
                while (len > 0) {
                        sig_check();
                        linelen = (size_t)len < sizeof(line) ? (size_t)len : sizeof(line);
                        if ((linelen = fread(line, sizeof(*line), linelen, ibuf)) == 0) {
                                break;
                        }
                        len -= linelen;
                        if (fwrite(line, sizeof(*line), linelen, obuf) != linelen)
                                goto out;
                }
        }
        sig_check();
        if (doign == ignoreall && linelen > 0 && line[linelen - 1] != '\n') {
                int c;

                /* no final blank line */
                if ((c = getc(ibuf)) != EOF && putc(c, obuf) == EOF)
                        goto out;
        }
        rval = 0;
 out:
        sig_check();
        return rval;
}

/*
 * Fix the header by glopping all of the expanded names from
 * the distribution list into the appropriate fields.
 */
static void
fixhead(struct header *hp, struct name *tolist)
{
        struct name *np;

        hp->h_to = NULL;
        hp->h_cc = NULL;
        hp->h_bcc = NULL;
        for (np = tolist; np != NULL; np = np->n_flink) {
                if (np->n_type & GDEL)
                        continue;       /* Don't copy deleted addresses to the header */
                if ((np->n_type & GMASK) == GTO)
                        hp->h_to =
                                cat(hp->h_to, nalloc(np->n_name, np->n_type));
                else if ((np->n_type & GMASK) == GCC)
                        hp->h_cc =
                                cat(hp->h_cc, nalloc(np->n_name, np->n_type));
                else if ((np->n_type & GMASK) == GBCC)
                        hp->h_bcc =
                                cat(hp->h_bcc, nalloc(np->n_name, np->n_type));
        }
}

/*
 * Format the given header line to not exceed 72 characters.
 */
static void
fmt(const char *str, struct name *np, FILE *fo, size_t comma)
{
        size_t col;
        size_t len;

        comma = comma ? 1 : 0;
        col = strlen(str);
        if (col)
                (void)fputs(str, fo);
        for (/*EMPTY*/; np != NULL; np = np->n_flink) {
                if (np->n_flink == NULL)
                        comma = 0;
                len = strlen(np->n_name);
                col++;          /* for the space */
                if (col + len + comma > 72 && col > 4) {
                        (void)fputs("\n    ", fo);
                        col = 4;
                } else
                        (void)putc(' ', fo);
                (void)fputs(np->n_name, fo);
                if (comma)
                        (void)putc(',', fo);
                col += len + comma;
        }
        (void)putc('\n', fo);
}

/*
 * Formatting for extra_header lines: try to wrap them before 72
 * characters.
 *
 * XXX - should this allow for quoting?
 */
static void
fmt2(const char *str, FILE *fo)
{
        const char *p;
        size_t col;

        col = 0;
        p = strchr(str, ':');
        assert(p != NULL);      /* this is a coding error */

        while (str <= p) {      /* output the header name */
                (void)putc(*str, fo);
                str++;
                col++;
        }
        assert(is_WSP(*str));   /* this is a coding error */

        (void)putc(' ', fo);    /* output a single space after the ':' */
        str = skip_WSP(str);

        while (*str != '\0') {
                if (p <= str) {
                        p = strpbrk(str, " \t");
                        if (p == NULL)
                                p = str + strlen(str);
                        if (col + (p - str) > 72 && col > 4) {
                                (void)fputs("\n    ", fo);
                                col = 4;
                                str = skip_WSP(str);
                                continue;
                        }
                }
                (void)putc(*str, fo);
                str++;
                col++;
        }
        (void)putc('\n', fo);
}

/*
 * Dump the to, subject, cc header on the
 * passed file buffer.
 */
PUBLIC int
puthead(struct header *hp, FILE *fo, int w)
{
        struct name *np;
        int gotcha;

        gotcha = 0;
        if (hp->h_to != NULL && w & GTO)
                fmt("To:", hp->h_to, fo, w & GCOMMA), gotcha++;
        if (hp->h_subject != NULL && w & GSUBJECT)
                (void)fprintf(fo, "Subject: %s\n", hp->h_subject), gotcha++;
        if (hp->h_cc != NULL && w & GCC)
                fmt("Cc:", hp->h_cc, fo, w & GCOMMA), gotcha++;
        if (hp->h_bcc != NULL && w & GBCC)
                fmt("Bcc:", hp->h_bcc, fo, w & GCOMMA), gotcha++;
        if (hp->h_in_reply_to != NULL && w & GMISC)
                (void)fprintf(fo, "In-Reply-To: %s\n", hp->h_in_reply_to), gotcha++;
        if (hp->h_references != NULL && w & GMISC)
                fmt("References:", hp->h_references, fo, w & GCOMMA), gotcha++;
        if (hp->h_extra != NULL && w & GMISC) {
                for (np = hp->h_extra; np != NULL; np = np->n_flink)
                        fmt2(np->n_name, fo);
                gotcha++;
        }
        if (hp->h_smopts != NULL && w & GSMOPTS)
                (void)fprintf(fo, "(sendmail options: %s)\n", detract(hp->h_smopts, GSMOPTS)), gotcha++;
#ifdef MIME_SUPPORT
        if (w & GMIME && (hp->h_attach || value(ENAME_MIME_ENCODE_MSG)))
                mime_putheader(fo, hp), gotcha++;
#endif
        if (gotcha && w & GNL)
                (void)putc('\n', fo);
        return 0;
}

/*
 * Prepend a header in front of the collected stuff
 * and return the new file.
 */
static FILE *
infix(struct header *hp, FILE *fi)
{
        FILE *nfo, *nfi;
        int c, fd;
        char tempname[PATHSIZE];

        (void)snprintf(tempname, sizeof(tempname),
            "%s/mail.RsXXXXXXXXXX", tmpdir);
        if ((fd = mkstemp(tempname)) == -1 ||
            (nfo = Fdopen(fd, "wef")) == NULL) {
                if (fd != -1)
                        (void)close(fd);
                warn("%s", tempname);
                return fi;
        }
        if ((nfi = Fopen(tempname, "ref")) == NULL) {
                warn("%s", tempname);
                (void)Fclose(nfo);
                (void)rm(tempname);
                return fi;
        }
        (void)rm(tempname);
        (void)puthead(hp, nfo,
            GTO | GSUBJECT | GCC | GBCC | GMISC | GNL | GCOMMA
#ifdef MIME_SUPPORT
            | GMIME
#endif
                );

        c = getc(fi);
        while (c != EOF) {
                (void)putc(c, nfo);
                c = getc(fi);
        }
        if (ferror(fi)) {
                warn("read");
                rewind(fi);
                return fi;
        }
        (void)fflush(nfo);
        if (ferror(nfo)) {
                warn("%s", tempname);
                (void)Fclose(nfo);
                (void)Fclose(nfi);
                rewind(fi);
                return fi;
        }
        (void)Fclose(nfo);
        (void)Fclose(fi);
        rewind(nfi);
        return nfi;
}

/*
 * Save the outgoing mail on the passed file.
 *
 * Take care not to save a valid headline or the mbox file will be
 * broken.  Prefix valid headlines with '>'.
 *
 * Note: most servers prefix any line starting with "From " in the
 * body of the message.  We are more restrictive to avoid header/body
 * issues.
 */
/*ARGSUSED*/
static int
savemail(const char name[], FILE *fi)
{
        FILE *fo;
        time_t now;
        mode_t m;
        char *line;
        size_t linelen;
        int afterblank;

        m = umask(077);
        fo = Fopen(name, "aef");
        (void)umask(m);
        if (fo == NULL) {
                warn("%s", name);
                return -1;
        }
        (void)time(&now);
        (void)fprintf(fo, "From %s %s", myname, ctime(&now));
        afterblank = 0;
        while ((line = fgetln(fi, &linelen)) != NULL) {
                char c, *cp;
                cp = line + linelen - 1;
                if (afterblank &&
                    linelen > sizeof("From . Aaa Aaa O0 00:00 0000") &&
                    line[0] == 'F' &&
                    line[1] == 'r' &&
                    line[2] == 'o' &&
                    line[3] == 'm' &&
                    line[4] == ' ' &&
                    *cp == '\n') {
                        c = *cp;
                        *cp = '\0';
                        if (ishead(line))
                                (void)fputc('>', fo);
                        *cp = c;
                }
                (void)fwrite(line, 1, linelen, fo);
                afterblank = linelen == 1 && line[0] == '\n';
        }
        (void)putc('\n', fo);
        (void)fflush(fo);
        if (ferror(fo))
                warn("%s", name);
        (void)Fclose(fo);
        rewind(fi);
        return 0;
}

/*
 * Mail a message that is already prepared in a file.
 */
PUBLIC void
mail2(FILE *mtf, const char **namelist)
{
        int pid;
        const char *cp;

        if (debug) {
                const char **t;

                (void)printf("Sendmail arguments:");
                for (t = namelist; *t != NULL; t++)
                        (void)printf(" \"%s\"", *t);
                (void)printf("\n");
                return;
        }
        if ((cp = value(ENAME_RECORD)) != NULL)
                (void)savemail(expand(cp), mtf);
        /*
         * Fork, set up the temporary mail file as standard
         * input for "mail", and exec with the user list we generated
         * far above.
         */
        pid = fork();
        switch (pid) {
        case -1:
                warn("fork");
                savedeadletter(mtf);
                return;
        case 0:
        {
                sigset_t nset;
                (void)sigemptyset(&nset);
                (void)sigaddset(&nset, SIGHUP);
                (void)sigaddset(&nset, SIGINT);
                (void)sigaddset(&nset, SIGQUIT);
                (void)sigaddset(&nset, SIGTSTP);
                (void)sigaddset(&nset, SIGTTIN);
                (void)sigaddset(&nset, SIGTTOU);
                prepare_child(&nset, fileno(mtf), -1);
                if ((cp = value(ENAME_SENDMAIL)) != NULL)
                        cp = expand(cp);
                else
                        cp = _PATH_SENDMAIL;
                (void)execv(cp, (char *const *)__UNCONST(namelist));
                warn("%s", cp);
                _exit(1);
        }
        default:
                if (value(ENAME_VERBOSE) != NULL)
                        (void)wait_child(pid);
                else
                        free_child(pid);
                break;
        }
}

static struct name *
ncopy(struct name *np)
{
        struct name *rv;
        struct name *lp;
        struct name *tp;

        lp = NULL; /* XXX gcc -Wuninitialized sh3 */
        rv = NULL;
        for (/*EMPTY*/; np; np = np->n_flink) {
                tp = nalloc(np->n_name, np->n_type);
                if (rv == NULL)
                        rv = tp;
                else {
                        lp->n_flink = tp;
                        tp->n_blink = lp;
                }
                lp = tp;
        }
        return rv;
}

/*
 * Mail a message on standard input to the people indicated
 * in the passed header.  (Internal interface).
 */
PUBLIC void
mail1(struct header *hp, int printheaders)
{
        const char **namelist;
        struct name *to;
        FILE *mtf;

        /*
         * Collect user's mail from standard input.
         * Get the result as mtf.
         */
        if ((mtf = collect(hp, printheaders)) == NULL)
                return;

        /*
         * Grab any extra header lines.  Do this after collect() so
         * that we can add header lines while collecting.
         */
        hp->h_extra = ncopy(extra_headers);

        if (value(ENAME_INTERACTIVE) != NULL) {
                if (value(ENAME_ASKCC) != NULL || value(ENAME_ASKBCC) != NULL) {
                        if (value(ENAME_ASKCC) != NULL)
                                (void)grabh(hp, GCC);
                        if (value(ENAME_ASKBCC) != NULL)
                                (void)grabh(hp, GBCC);
                } else {
                        (void)printf("EOT\n");
                        (void)fflush(stdout);
                }
        }
        if (fsize(mtf) == 0) {
                if (value(ENAME_DONTSENDEMPTY) != NULL)
                        goto out;
                if (hp->h_subject == NULL)
                        (void)printf("No message, no subject; hope that's ok\n");
                else
                        (void)printf("Null message body; hope that's ok\n");
        }
        /*
         * Now, take the user names from the combined
         * to and cc lists and do all the alias
         * processing.
         */
        senderr = 0;
        to = usermap(cat(hp->h_bcc, cat(hp->h_to, hp->h_cc)));
        if (to == NULL) {
                (void)printf("No recipients specified\n");
                senderr++;
        }
#ifdef MIME_SUPPORT
        /*
         * If there are attachments, repackage the mail as a
         * multi-part MIME message.
         */
        if (hp->h_attach || value(ENAME_MIME_ENCODE_MSG))
                mtf = mime_encode(mtf, hp);
#endif
        /*
         * Look through the recipient list for names with /'s
         * in them which we write to as files directly.
         */
        to = outof(to, mtf, hp);
        if (senderr)
                savedeadletter(mtf);
        to = elide(to);
        if (count(to) == 0)
                goto out;
        fixhead(hp, to);
        if ((mtf = infix(hp, mtf)) == NULL) {
                (void)fprintf(stderr, ". . . message lost, sorry.\n");
                return;
        }
        if (hp->h_smopts == NULL) {
                hp->h_smopts = get_smopts(to);
                if (hp->h_smopts != NULL &&
                    hp->h_smopts->n_name[0] != '\0' &&
                    value(ENAME_SMOPTS_VERIFY) != NULL)
                        if (grabh(hp, GSMOPTS)) {
                                (void)printf("mail aborted!\n");
                                savedeadletter(mtf);
                                goto out;
                        }
        }
        namelist = unpack(hp->h_smopts, to);
        mail2(mtf, namelist);
 out:
        (void)Fclose(mtf);
}

/*
 * Interface between the argument list and the mail1 routine
 * which does all the dirty work.
 */
PUBLIC int
mail(struct name *to, struct name *cc, struct name *bcc,
     struct name *smopts, char *subject, struct attachment *attach)
{
        struct header head;

        /* ensure that all header fields are initially NULL */
        (void)memset(&head, 0, sizeof(head));

        head.h_to = to;
        head.h_subject = subject;
        head.h_cc = cc;
        head.h_bcc = bcc;
        head.h_smopts = smopts;
#ifdef MIME_SUPPORT
        head.h_attach = attach;
#endif
        mail1(&head, 0);
        return 0;
}

/*
 * Send mail to a bunch of user names.  The interface is through
 * the mail1 routine above.
 */
PUBLIC int
sendmail(void *v)
{
        char *str = v;
        struct header head;

        /* ensure that all header fields are initially NULL */
        (void)memset(&head, 0, sizeof(head));

        head.h_to = extract(str, GTO);

        mail1(&head, 0);
        return 0;
}