root/usr/src/cmd/sendmail/src/alias.c
/*
 * Copyright (c) 1998-2003 Sendmail, Inc. and its suppliers.
 *      All rights reserved.
 * Copyright (c) 1983, 1995-1997 Eric P. Allman.  All rights reserved.
 * Copyright (c) 1988, 1993
 *      The Regents of the University of California.  All rights reserved.
 *
 * By using this file, you agree to the terms and conditions set
 * forth in the LICENSE file which can be found at the top level of
 * the sendmail distribution.
 *
 */

#include <sendmail.h>

SM_RCSID("@(#)$Id: alias.c,v 8.219 2006/10/24 18:04:09 ca Exp $")

#define SEPARATOR ':'
# define ALIAS_SPEC_SEPARATORS  " ,/:"

static MAP      *AliasFileMap = NULL;   /* the actual aliases.files map */
static int      NAliasFileMaps; /* the number of entries in AliasFileMap */

static char     *aliaslookup __P((char *, int *, char *));

/*
**  ALIAS -- Compute aliases.
**
**      Scans the alias file for an alias for the given address.
**      If found, it arranges to deliver to the alias list instead.
**      Uses libdbm database if -DDBM.
**
**      Parameters:
**              a -- address to alias.
**              sendq -- a pointer to the head of the send queue
**                      to put the aliases in.
**              aliaslevel -- the current alias nesting depth.
**              e -- the current envelope.
**
**      Returns:
**              none
**
**      Side Effects:
**              Aliases found are expanded.
**
**      Deficiencies:
**              It should complain about names that are aliased to
**                      nothing.
*/

void
alias(a, sendq, aliaslevel, e)
        register ADDRESS *a;
        ADDRESS **sendq;
        int aliaslevel;
        register ENVELOPE *e;
{
        register char *p;
        char *owner;
        auto int status = EX_OK;
        char obuf[MAXNAME + 7];

        if (tTd(27, 1))
                sm_dprintf("alias(%s)\n", a->q_user);

        /* don't realias already aliased names */
        if (!QS_IS_OK(a->q_state))
                return;

        if (NoAlias)
                return;

        e->e_to = a->q_paddr;

        /*
        **  Look up this name.
        **
        **      If the map was unavailable, we will queue this message
        **      until the map becomes available; otherwise, we could
        **      bounce messages inappropriately.
        */

#if _FFR_REDIRECTEMPTY
        /*
        **  envelope <> can't be sent to mailing lists, only owner-
        **  send spam of this type to owner- of the list
        **  ----  to stop spam from going to mailing lists!
        */

        if (e->e_sender != NULL && *e->e_sender == '\0')
        {
                /* Look for owner of alias */
                (void) sm_strlcpyn(obuf, sizeof(obuf), 2, "owner-", a->q_user);
                if (aliaslookup(obuf, &status, a->q_host) != NULL)
                {
                        if (LogLevel > 8)
                                sm_syslog(LOG_WARNING, e->e_id,
                                       "possible spam from <> to list: %s, redirected to %s\n",
                                       a->q_user, obuf);
                        a->q_user = sm_rpool_strdup_x(e->e_rpool, obuf);
                }
        }
#endif /* _FFR_REDIRECTEMPTY */

        p = aliaslookup(a->q_user, &status, a->q_host);
        if (status == EX_TEMPFAIL || status == EX_UNAVAILABLE)
        {
                a->q_state = QS_QUEUEUP;
                if (e->e_message == NULL)
                        e->e_message = sm_rpool_strdup_x(e->e_rpool,
                                                "alias database unavailable");

                /* XXX msg only per recipient? */
                if (a->q_message == NULL)
                        a->q_message = "alias database unavailable";
                return;
        }
        if (p == NULL)
                return;

        /*
        **  Match on Alias.
        **      Deliver to the target list.
        */

        if (tTd(27, 1))
                sm_dprintf("%s (%s, %s) aliased to %s\n",
                           a->q_paddr, a->q_host, a->q_user, p);
        if (bitset(EF_VRFYONLY, e->e_flags))
        {
                a->q_state = QS_VERIFIED;
                return;
        }
        message("aliased to %s", shortenstring(p, MAXSHORTSTR));
        if (LogLevel > 10)
                sm_syslog(LOG_INFO, e->e_id,
                          "alias %.100s => %s",
                          a->q_paddr, shortenstring(p, MAXSHORTSTR));
        a->q_flags &= ~QSELFREF;
        if (tTd(27, 5))
        {
                sm_dprintf("alias: QS_EXPANDED ");
                printaddr(sm_debug_file(), a, false);
        }
        a->q_state = QS_EXPANDED;

        /*
        **  Always deliver aliased items as the default user.
        **  Setting q_gid to 0 forces deliver() to use DefUser
        **  instead of the alias name for the call to initgroups().
        */

        a->q_uid = DefUid;
        a->q_gid = 0;
        a->q_fullname = NULL;
        a->q_flags |= QGOODUID|QALIAS;

        (void) sendtolist(p, a, sendq, aliaslevel + 1, e);

        if (bitset(QSELFREF, a->q_flags) && QS_IS_EXPANDED(a->q_state))
                a->q_state = QS_OK;

        /*
        **  Look for owner of alias
        */

        if (strncmp(a->q_user, "owner-", 6) == 0 ||
            strlen(a->q_user) > sizeof(obuf) - 7)
                (void) sm_strlcpy(obuf, "owner-owner", sizeof(obuf));
        else
                (void) sm_strlcpyn(obuf, sizeof(obuf), 2, "owner-", a->q_user);
        owner = aliaslookup(obuf, &status, a->q_host);
        if (owner == NULL)
                return;

        /* reflect owner into envelope sender */
        if (strpbrk(owner, ",:/|\"") != NULL)
                owner = obuf;
        a->q_owner = sm_rpool_strdup_x(e->e_rpool, owner);

        /* announce delivery to this alias; NORECEIPT bit set later */
        if (e->e_xfp != NULL)
                (void) sm_io_fprintf(e->e_xfp, SM_TIME_DEFAULT,
                                "Message delivered to mailing list %s\n",
                                a->q_paddr);
        e->e_flags |= EF_SENDRECEIPT;
        a->q_flags |= QDELIVERED|QEXPANDED;
}
/*
**  ALIASLOOKUP -- look up a name in the alias file.
**
**      Parameters:
**              name -- the name to look up.
**              pstat -- a pointer to a place to put the status.
**              av -- argument for %1 expansion.
**
**      Returns:
**              the value of name.
**              NULL if unknown.
**
**      Side Effects:
**              none.
**
**      Warnings:
**              The return value will be trashed across calls.
*/

static char *
aliaslookup(name, pstat, av)
        char *name;
        int *pstat;
        char *av;
{
        static MAP *map = NULL;
#if _FFR_ALIAS_DETAIL
        int i;
        char *argv[4];
#endif /* _FFR_ALIAS_DETAIL */

        if (map == NULL)
        {
                STAB *s = stab("aliases", ST_MAP, ST_FIND);

                if (s == NULL)
                        return NULL;
                map = &s->s_map;
        }
        DYNOPENMAP(map);

        /* special case POstMastER -- always use lower case */
        if (sm_strcasecmp(name, "postmaster") == 0)
                name = "postmaster";

#if _FFR_ALIAS_DETAIL
        i = 0;
        argv[i++] = name;
        argv[i++] = av;

        /* XXX '+' is hardwired here as delimiter! */
        if (av != NULL && *av == '+')
                argv[i++] = av + 1;
        argv[i++] = NULL;
        return (*map->map_class->map_lookup)(map, name, argv, pstat);
#else /* _FFR_ALIAS_DETAIL */
        return (*map->map_class->map_lookup)(map, name, NULL, pstat);
#endif /* _FFR_ALIAS_DETAIL */
}
/*
**  SETALIAS -- set up an alias map
**
**      Called when reading configuration file.
**
**      Parameters:
**              spec -- the alias specification
**
**      Returns:
**              none.
*/

void
setalias(spec)
        char *spec;
{
        register char *p;
        register MAP *map;
        char *class;
        STAB *s;

        if (tTd(27, 8))
                sm_dprintf("setalias(%s)\n", spec);

        for (p = spec; p != NULL; )
        {
                char buf[50];

                while (isascii(*p) && isspace(*p))
                        p++;
                if (*p == '\0')
                        break;
                spec = p;

                if (NAliasFileMaps >= MAXMAPSTACK)
                {
                        syserr("Too many alias databases defined, %d max",
                                MAXMAPSTACK);
                        return;
                }
                if (AliasFileMap == NULL)
                {
                        (void) sm_strlcpy(buf, "aliases.files sequence",
                                          sizeof(buf));
                        AliasFileMap = makemapentry(buf);
                        if (AliasFileMap == NULL)
                        {
                                syserr("setalias: cannot create aliases.files map");
                                return;
                        }
                }
                (void) sm_snprintf(buf, sizeof(buf), "Alias%d", NAliasFileMaps);
                s = stab(buf, ST_MAP, ST_ENTER);
                map = &s->s_map;
                memset(map, '\0', sizeof(*map));
                map->map_mname = s->s_name;
                p = strpbrk(p, ALIAS_SPEC_SEPARATORS);
                if (p != NULL && *p == SEPARATOR)
                {
                        /* map name */
                        *p++ = '\0';
                        class = spec;
                        spec = p;
                }
                else
                {
                        class = "implicit";
                        map->map_mflags = MF_INCLNULL;
                }

                /* find end of spec */
                if (p != NULL)
                {
                        bool quoted = false;

                        for (; *p != '\0'; p++)
                        {
                                /*
                                **  Don't break into a quoted string.
                                **  Needed for ldap maps which use
                                **  commas in their specifications.
                                */

                                if (*p == '"')
                                        quoted = !quoted;
                                else if (*p == ',' && !quoted)
                                        break;
                        }

                        /* No more alias specifications follow */
                        if (*p == '\0')
                                p = NULL;
                }
                if (p != NULL)
                        *p++ = '\0';

                if (tTd(27, 20))
                        sm_dprintf("  map %s:%s %s\n", class, s->s_name, spec);

                /* look up class */
                s = stab(class, ST_MAPCLASS, ST_FIND);
                if (s == NULL)
                {
                        syserr("setalias: unknown alias class %s", class);
                }
                else if (!bitset(MCF_ALIASOK, s->s_mapclass.map_cflags))
                {
                        syserr("setalias: map class %s can't handle aliases",
                                class);
                }
                else
                {
                        map->map_class = &s->s_mapclass;
                        map->map_mflags |= MF_ALIAS;
                        if (map->map_class->map_parse(map, spec))
                        {
                                map->map_mflags |= MF_VALID;
                                AliasFileMap->map_stack[NAliasFileMaps++] = map;
                        }
                }
        }
}
/*
**  ALIASWAIT -- wait for distinguished @:@ token to appear.
**
**      This can decide to reopen or rebuild the alias file
**
**      Parameters:
**              map -- a pointer to the map descriptor for this alias file.
**              ext -- the filename extension (e.g., ".db") for the
**                      database file.
**              isopen -- if set, the database is already open, and we
**                      should check for validity; otherwise, we are
**                      just checking to see if it should be created.
**
**      Returns:
**              true -- if the database is open when we return.
**              false -- if the database is closed when we return.
*/

bool
aliaswait(map, ext, isopen)
        MAP *map;
        char *ext;
        bool isopen;
{
        bool attimeout = false;
        time_t mtime;
        struct stat stb;
        char buf[MAXPATHLEN];

        if (tTd(27, 3))
                sm_dprintf("aliaswait(%s:%s)\n",
                           map->map_class->map_cname, map->map_file);
        if (bitset(MF_ALIASWAIT, map->map_mflags))
                return isopen;
        map->map_mflags |= MF_ALIASWAIT;

        if (SafeAlias > 0)
        {
                auto int st;
                unsigned int sleeptime = 2;
                unsigned int loopcount = 0;     /* only used for debugging */
                time_t toolong = curtime() + SafeAlias;

                while (isopen &&
                       map->map_class->map_lookup(map, "@", NULL, &st) == NULL)
                {
                        if (curtime() > toolong)
                        {
                                /* we timed out */
                                attimeout = true;
                                break;
                        }

                        /*
                        **  Close and re-open the alias database in case
                        **  the one is mv'ed instead of cp'ed in.
                        */

                        if (tTd(27, 2))
                        {
                                loopcount++;
                                sm_dprintf("aliaswait: sleeping for %u seconds (loopcount = %u)\n",
                                           sleeptime, loopcount);
                        }

                        map->map_mflags |= MF_CLOSING;
                        map->map_class->map_close(map);
                        map->map_mflags &= ~(MF_OPEN|MF_WRITABLE|MF_CLOSING);
                        (void) sleep(sleeptime);
                        sleeptime *= 2;
                        if (sleeptime > 60)
                                sleeptime = 60;
                        isopen = map->map_class->map_open(map, O_RDONLY);
                }
        }

        /* see if we need to go into auto-rebuild mode */
        if (!bitset(MCF_REBUILDABLE, map->map_class->map_cflags))
        {
                if (tTd(27, 3))
                        sm_dprintf("aliaswait: not rebuildable\n");
                map->map_mflags &= ~MF_ALIASWAIT;
                return isopen;
        }
        if (stat(map->map_file, &stb) < 0)
        {
                if (tTd(27, 3))
                        sm_dprintf("aliaswait: no source file\n");
                map->map_mflags &= ~MF_ALIASWAIT;
                return isopen;
        }
        mtime = stb.st_mtime;
        if (sm_strlcpyn(buf, sizeof(buf), 2,
                        map->map_file, ext == NULL ? "" : ext) >= sizeof(buf))
        {
                if (LogLevel > 3)
                        sm_syslog(LOG_INFO, NOQID,
                                  "alias database %s%s name too long",
                                  map->map_file, ext == NULL ? "" : ext);
                message("alias database %s%s name too long",
                        map->map_file, ext == NULL ? "" : ext);
        }

        if (stat(buf, &stb) < 0 || stb.st_mtime < mtime || attimeout)
        {
                if (LogLevel > 3)
                        sm_syslog(LOG_INFO, NOQID,
                                  "alias database %s out of date", buf);
                message("Warning: alias database %s out of date", buf);
        }
        map->map_mflags &= ~MF_ALIASWAIT;
        return isopen;
}
/*
**  REBUILDALIASES -- rebuild the alias database.
**
**      Parameters:
**              map -- the database to rebuild.
**              automatic -- set if this was automatically generated.
**
**      Returns:
**              true if successful; false otherwise.
**
**      Side Effects:
**              Reads the text version of the database, builds the
**              DBM or DB version.
*/

bool
rebuildaliases(map, automatic)
        register MAP *map;
        bool automatic;
{
        SM_FILE_T *af;
        bool nolock = false;
        bool success = false;
        long sff = SFF_OPENASROOT|SFF_REGONLY|SFF_NOLOCK;
        sigfunc_t oldsigint, oldsigquit;
#ifdef SIGTSTP
        sigfunc_t oldsigtstp;
#endif /* SIGTSTP */

        if (!bitset(MCF_REBUILDABLE, map->map_class->map_cflags))
                return false;

        if (!bitnset(DBS_LINKEDALIASFILEINWRITABLEDIR, DontBlameSendmail))
                sff |= SFF_NOWLINK;
        if (!bitnset(DBS_GROUPWRITABLEALIASFILE, DontBlameSendmail))
                sff |= SFF_NOGWFILES;
        if (!bitnset(DBS_WORLDWRITABLEALIASFILE, DontBlameSendmail))
                sff |= SFF_NOWWFILES;

        /* try to lock the source file */
        if ((af = safefopen(map->map_file, O_RDWR, 0, sff)) == NULL)
        {
                struct stat stb;

                if ((errno != EACCES && errno != EROFS) || automatic ||
                    (af = safefopen(map->map_file, O_RDONLY, 0, sff)) == NULL)
                {
                        int saveerr = errno;

                        if (tTd(27, 1))
                                sm_dprintf("Can't open %s: %s\n",
                                        map->map_file, sm_errstring(saveerr));
                        if (!automatic && !bitset(MF_OPTIONAL, map->map_mflags))
                                message("newaliases: cannot open %s: %s",
                                        map->map_file, sm_errstring(saveerr));
                        errno = 0;
                        return false;
                }
                nolock = true;
                if (tTd(27, 1) ||
                    fstat(sm_io_getinfo(af, SM_IO_WHAT_FD, NULL), &stb) < 0 ||
                    bitset(S_IWUSR|S_IWGRP|S_IWOTH, stb.st_mode))
                        message("warning: cannot lock %s: %s",
                                map->map_file, sm_errstring(errno));
        }

        /* see if someone else is rebuilding the alias file */
        if (!nolock &&
            !lockfile(sm_io_getinfo(af, SM_IO_WHAT_FD, NULL), map->map_file,
                      NULL, LOCK_EX|LOCK_NB))
        {
                /* yes, they are -- wait until done */
                message("Alias file %s is locked (maybe being rebuilt)",
                        map->map_file);
                if (OpMode != MD_INITALIAS)
                {
                        /* wait for other rebuild to complete */
                        (void) lockfile(sm_io_getinfo(af, SM_IO_WHAT_FD, NULL),
                                        map->map_file, NULL, LOCK_EX);
                }
                (void) sm_io_close(af, SM_TIME_DEFAULT);
                errno = 0;
                return false;
        }

        oldsigint = sm_signal(SIGINT, SIG_IGN);
        oldsigquit = sm_signal(SIGQUIT, SIG_IGN);
#ifdef SIGTSTP
        oldsigtstp = sm_signal(SIGTSTP, SIG_IGN);
#endif /* SIGTSTP */

        if (map->map_class->map_open(map, O_RDWR))
        {
                if (LogLevel > 7)
                {
                        sm_syslog(LOG_NOTICE, NOQID,
                                "alias database %s %srebuilt by %s",
                                map->map_file, automatic ? "auto" : "",
                                username());
                }
                map->map_mflags |= MF_OPEN|MF_WRITABLE;
                map->map_pid = CurrentPid;
                readaliases(map, af, !automatic, true);
                success = true;
        }
        else
        {
                if (tTd(27, 1))
                        sm_dprintf("Can't create database for %s: %s\n",
                                map->map_file, sm_errstring(errno));
                if (!automatic)
                        syserr("Cannot create database for alias file %s",
                                map->map_file);
        }

        /* close the file, thus releasing locks */
        (void) sm_io_close(af, SM_TIME_DEFAULT);

        /* add distinguished entries and close the database */
        if (bitset(MF_OPEN, map->map_mflags))
        {
                map->map_mflags |= MF_CLOSING;
                map->map_class->map_close(map);
                map->map_mflags &= ~(MF_OPEN|MF_WRITABLE|MF_CLOSING);
        }

        /* restore the old signals */
        (void) sm_signal(SIGINT, oldsigint);
        (void) sm_signal(SIGQUIT, oldsigquit);
#ifdef SIGTSTP
        (void) sm_signal(SIGTSTP, oldsigtstp);
#endif /* SIGTSTP */
        return success;
}
/*
**  READALIASES -- read and process the alias file.
**
**      This routine implements the part of initaliases that occurs
**      when we are not going to use the DBM stuff.
**
**      Parameters:
**              map -- the alias database descriptor.
**              af -- file to read the aliases from.
**              announcestats -- announce statistics regarding number of
**                      aliases, longest alias, etc.
**              logstats -- lot the same info.
**
**      Returns:
**              none.
**
**      Side Effects:
**              Reads aliasfile into the symbol table.
**              Optionally, builds the .dir & .pag files.
*/

void
readaliases(map, af, announcestats, logstats)
        register MAP *map;
        SM_FILE_T *af;
        bool announcestats;
        bool logstats;
{
        register char *p;
        char *rhs;
        bool skipping;
        long naliases, bytes, longest;
        ADDRESS al, bl;
        char line[BUFSIZ];

        /*
        **  Read and interpret lines
        */

        FileName = map->map_file;
        LineNumber = 0;
        naliases = bytes = longest = 0;
        skipping = false;
        while (sm_io_fgets(af, SM_TIME_DEFAULT, line, sizeof(line)) != NULL)
        {
                int lhssize, rhssize;
                int c;

                LineNumber++;
                p = strchr(line, '\n');

                /* XXX what if line="a\\" ? */
                while (p != NULL && p > line && p[-1] == '\\')
                {
                        p--;
                        if (sm_io_fgets(af, SM_TIME_DEFAULT, p,
                                        SPACELEFT(line, p)) == NULL)
                                break;
                        LineNumber++;
                        p = strchr(p, '\n');
                }
                if (p != NULL)
                        *p = '\0';
                else if (!sm_io_eof(af))
                {
                        errno = 0;
                        syserr("554 5.3.0 alias line too long");

                        /* flush to end of line */
                        while ((c = sm_io_getc(af, SM_TIME_DEFAULT)) !=
                                SM_IO_EOF && c != '\n')
                                continue;

                        /* skip any continuation lines */
                        skipping = true;
                        continue;
                }
                switch (line[0])
                {
                  case '#':
                  case '\0':
                        skipping = false;
                        continue;

                  case ' ':
                  case '\t':
                        if (!skipping)
                                syserr("554 5.3.5 Non-continuation line starts with space");
                        skipping = true;
                        continue;
                }
                skipping = false;

                /*
                **  Process the LHS
                **      Find the colon separator, and parse the address.
                **      It should resolve to a local name -- this will
                **      be checked later (we want to optionally do
                **      parsing of the RHS first to maximize error
                **      detection).
                */

                for (p = line; *p != '\0' && *p != ':' && *p != '\n'; p++)
                        continue;
                if (*p++ != ':')
                {
                        syserr("554 5.3.5 missing colon");
                        continue;
                }
                if (parseaddr(line, &al, RF_COPYALL, ':', NULL, CurEnv, true)
                    == NULL)
                {
                        syserr("554 5.3.5 %.40s... illegal alias name", line);
                        continue;
                }

                /*
                **  Process the RHS.
                **      'al' is the internal form of the LHS address.
                **      'p' points to the text of the RHS.
                */

                while (isascii(*p) && isspace(*p))
                        p++;
                rhs = p;
                for (;;)
                {
                        register char *nlp;

                        nlp = &p[strlen(p)];
                        if (nlp > p && nlp[-1] == '\n')
                                *--nlp = '\0';

                        if (CheckAliases)
                        {
                                /* do parsing & compression of addresses */
                                while (*p != '\0')
                                {
                                        auto char *delimptr;

                                        while ((isascii(*p) && isspace(*p)) ||
                                                                *p == ',')
                                                p++;
                                        if (*p == '\0')
                                                break;
                                        if (parseaddr(p, &bl, RF_COPYNONE, ',',
                                                      &delimptr, CurEnv, true)
                                            == NULL)
                                                usrerr("553 5.3.5 %s... bad address", p);
                                        p = delimptr;
                                }
                        }
                        else
                        {
                                p = nlp;
                        }

                        /* see if there should be a continuation line */
                        c = sm_io_getc(af, SM_TIME_DEFAULT);
                        if (!sm_io_eof(af))
                                (void) sm_io_ungetc(af, SM_TIME_DEFAULT, c);
                        if (c != ' ' && c != '\t')
                                break;

                        /* read continuation line */
                        if (sm_io_fgets(af, SM_TIME_DEFAULT, p,
                                        sizeof(line) - (p-line)) == NULL)
                                break;
                        LineNumber++;

                        /* check for line overflow */
                        if (strchr(p, '\n') == NULL && !sm_io_eof(af))
                        {
                                usrerr("554 5.3.5 alias too long");
                                while ((c = sm_io_getc(af, SM_TIME_DEFAULT))
                                       != SM_IO_EOF && c != '\n')
                                        continue;
                                skipping = true;
                                break;
                        }
                }

                if (skipping)
                        continue;

                if (!bitnset(M_ALIASABLE, al.q_mailer->m_flags))
                {
                        syserr("554 5.3.5 %s... cannot alias non-local names",
                                al.q_paddr);
                        continue;
                }

                /*
                **  Insert alias into symbol table or database file.
                **
                **      Special case pOStmaStER -- always make it lower case.
                */

                if (sm_strcasecmp(al.q_user, "postmaster") == 0)
                        makelower(al.q_user);

                lhssize = strlen(al.q_user);
                rhssize = strlen(rhs);
                if (rhssize > 0)
                {
                        /* is RHS empty (just spaces)? */
                        p = rhs;
                        while (isascii(*p) && isspace(*p))
                                p++;
                }
                if (rhssize == 0 || *p == '\0')
                {
                        syserr("554 5.3.5 %.40s... missing value for alias",
                               line);

                }
                else
                {
                        map->map_class->map_store(map, al.q_user, rhs);

                        /* statistics */
                        naliases++;
                        bytes += lhssize + rhssize;
                        if (rhssize > longest)
                                longest = rhssize;
                }

#if 0
        /*
        **  address strings are now stored in the envelope rpool,
        **  and therefore cannot be freed.
        */
                if (al.q_paddr != NULL)
                        sm_free(al.q_paddr);  /* disabled */
                if (al.q_host != NULL)
                        sm_free(al.q_host);  /* disabled */
                if (al.q_user != NULL)
                        sm_free(al.q_user);  /* disabled */
#endif /* 0 */
        }

        CurEnv->e_to = NULL;
        FileName = NULL;
        if (Verbose || announcestats)
                message("%s: %ld aliases, longest %ld bytes, %ld bytes total",
                        map->map_file, naliases, longest, bytes);
        if (LogLevel > 7 && logstats)
                sm_syslog(LOG_INFO, NOQID,
                        "%s: %ld aliases, longest %ld bytes, %ld bytes total",
                        map->map_file, naliases, longest, bytes);
}
/*
**  FORWARD -- Try to forward mail
**
**      This is similar but not identical to aliasing.
**
**      Parameters:
**              user -- the name of the user who's mail we would like
**                      to forward to.  It must have been verified --
**                      i.e., the q_home field must have been filled
**                      in.
**              sendq -- a pointer to the head of the send queue to
**                      put this user's aliases in.
**              aliaslevel -- the current alias nesting depth.
**              e -- the current envelope.
**
**      Returns:
**              none.
**
**      Side Effects:
**              New names are added to send queues.
*/

void
forward(user, sendq, aliaslevel, e)
        ADDRESS *user;
        ADDRESS **sendq;
        int aliaslevel;
        register ENVELOPE *e;
{
        char *pp;
        char *ep;
        bool got_transient;

        if (tTd(27, 1))
                sm_dprintf("forward(%s)\n", user->q_paddr);

        if (!bitnset(M_HASPWENT, user->q_mailer->m_flags) ||
            !QS_IS_OK(user->q_state))
                return;
        if (ForwardPath != NULL && *ForwardPath == '\0')
                return;
        if (user->q_home == NULL)
        {
                syserr("554 5.3.0 forward: no home");
                user->q_home = "/no/such/directory";
        }

        /* good address -- look for .forward file in home */
        macdefine(&e->e_macro, A_PERM, 'z', user->q_home);
        macdefine(&e->e_macro, A_PERM, 'u', user->q_user);
        macdefine(&e->e_macro, A_PERM, 'h', user->q_host);
        if (ForwardPath == NULL)
                ForwardPath = newstr("\201z/.forward");

        got_transient = false;
        for (pp = ForwardPath; pp != NULL; pp = ep)
        {
                int err;
                char buf[MAXPATHLEN];
                struct stat st;

                ep = strchr(pp, SEPARATOR);
                if (ep != NULL)
                        *ep = '\0';
                expand(pp, buf, sizeof(buf), e);
                if (ep != NULL)
                        *ep++ = SEPARATOR;
                if (buf[0] == '\0')
                        continue;
                if (tTd(27, 3))
                        sm_dprintf("forward: trying %s\n", buf);

                err = include(buf, true, user, sendq, aliaslevel, e);
                if (err == 0)
                        break;
                else if (transienterror(err))
                {
                        /* we may have to suspend this message */
                        got_transient = true;
                        if (tTd(27, 2))
                                sm_dprintf("forward: transient error on %s\n",
                                           buf);
                        if (LogLevel > 2)
                        {
                                char *curhost = CurHostName;

                                CurHostName = NULL;
                                sm_syslog(LOG_ERR, e->e_id,
                                          "forward %s: transient error: %s",
                                          buf, sm_errstring(err));
                                CurHostName = curhost;
                        }

                }
                else
                {
                        switch (err)
                        {
                          case ENOENT:
                                break;

                          case E_SM_WWDIR:
                          case E_SM_GWDIR:
                                /* check if it even exists */
                                if (stat(buf, &st) < 0 && errno == ENOENT)
                                {
                                        if (bitnset(DBS_DONTWARNFORWARDFILEINUNSAFEDIRPATH,
                                                    DontBlameSendmail))
                                                break;
                                }
                                /* FALLTHROUGH */

#if _FFR_FORWARD_SYSERR
                          case E_SM_NOSLINK:
                          case E_SM_NOHLINK:
                          case E_SM_REGONLY:
                          case E_SM_ISEXEC:
                          case E_SM_WWFILE:
                          case E_SM_GWFILE:
                                syserr("forward: %s: %s", buf, sm_errstring(err));
                                break;
#endif /* _FFR_FORWARD_SYSERR */

                                /* FALLTHROUGH */
                          default:
                                if (LogLevel > (RunAsUid == 0 ? 2 : 10))
                                        sm_syslog(LOG_WARNING, e->e_id,
                                                  "forward %s: %s", buf,
                                                  sm_errstring(err));
                                if (Verbose)
                                        message("forward: %s: %s",
                                                buf, sm_errstring(err));
                                break;
                        }
                }
        }
        if (pp == NULL && got_transient)
        {
                /*
                **  There was no successful .forward open and at least one
                **  transient open.  We have to defer this address for
                **  further delivery.
                */

                message("transient .forward open error: message queued");
                user->q_state = QS_QUEUEUP;
                return;
        }
}