/* * Copyright (c) 1998-2002 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 SM_RCSID("@(#)$Id: alias.c,v 8.214.2.1 2003/07/28 17:47:55 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 = "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(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 */ 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; } }