text: /*++
  /* NAME
  /*	pipe_command 3
  /* SUMMARY
  /*	deliver message to external command
  /* SYNOPSIS
  /*	#include <pipe_command.h>
  /*
  /*	int	pipe_command(src, why, key, value, ...)
  /*	VSTREAM	*src;
  /*	DSN_BUF	*why;
  /*	int	key;
  /* DESCRIPTION
  /*	pipe_command() runs a command with a message as standard
  /*	input.  A limited amount of standard output and standard error
  /*	output is captured for diagnostics purposes.
  /*
  /*	If the command invokes exit() with a non-zero status,
  /*	the delivery status is taken from an RFC 3463-style code
  /*	at the beginning of command output. If that information is
  /*	unavailable, the delivery status is taken from the command
  /*	exit status as per <sysexits.h>.
  /*
  /*	Arguments:
  /* .IP src
  /*	An open message queue file, positioned at the start of the actual
  /*	message content.
  /* .IP why
  /*	Delivery status information. The reason attribute may contain
  /*	a limited portion of command output, among other free text.
  /* .IP key
  /*	Specifies what value will follow. pipe_command() takes a list
  /*	of macros with arguments, terminated by CA_PIPE_CMD_END which
  /*	has no argument. The following is a listing of macros and
  /*	expected argument types.
  /* .RS
  /* .IP "CA_PIPE_CMD_COMMAND(const char *)"
  /*	Specifies the command to execute as a string. The string is
  /*	passed to the shell when it contains shell meta characters
  /*	or when it appears to be a shell built-in command, otherwise
  /*	the command is executed without invoking a shell.
  /*	One of CA_PIPE_CMD_COMMAND or CA_PIPE_CMD_ARGV must be specified.
  /*	See also the CA_PIPE_CMD_SHELL attribute below.
  /* .IP "CA_PIPE_CMD_ARGV(char **)"
  /*	The command is specified as an argument vector. This vector is
  /*	passed without further inspection to the \fIexecvp\fR() routine.
  /*	One of CA_PIPE_CMD_COMMAND or CA_PIPE_CMD_ARGV must be specified.
  /* .IP "CA_PIPE_CMD_CHROOT(const char *)"
  /*	Root and working directory for command execution. This takes
  /*	effect before CA_PIPE_CMD_CWD. A null pointer means don't
  /*	change root and working directory anyway. Failure to change
  /*	directory causes mail delivery to be deferred.
  /* .IP "CA_PIPE_CMD_CWD(const char *)"
  /*	Working directory for command execution, after changing process
  /*	privileges to CA_PIPE_CMD_UID and CA_PIPE_CMD_GID. A null pointer means
  /*	don't change directory anyway. Failure to change directory
  /*	causes mail delivery to be deferred.
  /* .IP "CA_PIPE_CMD_ENV(char **)"
  /*	Additional environment information, in the form of a null-terminated
  /*	list of name, value, name, value, ... elements. By default only the
  /*	command search path is initialized to _PATH_DEFPATH.
  /* .IP "CA_PIPE_CMD_EXPORT(char **)"
  /*	Null-terminated array with names of environment parameters
  /*	that can be exported. By default, everything is exported.
  /* .IP "CA_PIPE_CMD_COPY_FLAGS(int)"
  /*	Flags that are passed on to the \fImail_copy\fR() routine.
  /*	The default flags value is 0 (zero).
  /* .IP "CA_PIPE_CMD_SENDER(const char *)"
  /*	The envelope sender address, which is passed on to the
  /*	\fImail_copy\fR() routine.
  /* .IP "CA_PIPE_CMD_ORIG_RCPT(const char *)"
  /*	The original recipient envelope address, which is passed on
  /*	to the \fImail_copy\fR() routine.
  /* .IP "CA_PIPE_CMD_DELIVERED(const char *)"
  /*	The recipient envelope address, which is passed on to the
  /*	\fImail_copy\fR() routine.
  /* .IP "CA_PIPE_CMD_EOL(const char *)"
  /*	End-of-line delimiter. The default is to use the newline character.
  /* .IP "CA_PIPE_CMD_UID(uid_t)"
  /*	The user ID to execute the command as. The default is
  /*	the user ID corresponding to the \fIdefault_privs\fR
  /*	configuration parameter. The user ID must be non-zero.
  /* .IP "CA_PIPE_CMD_GID(gid_t)"
  /*	The group ID to execute the command as. The default is
  /*	the group ID corresponding to the \fIdefault_privs\fR
  /*	configuration parameter. The group ID must be non-zero.
  /* .IP "CA_PIPE_CMD_TIME_LIMIT(int)"
  /*	The amount of time the command is allowed to run before it
  /*	is terminated with SIGKILL. A non-negative CA_PIPE_CMD_TIME_LIMIT
  /*	value must be specified.
  /* .IP "CA_PIPE_CMD_SHELL(const char *)"
  /*	The shell to use when executing the command specified with
  /*	CA_PIPE_CMD_COMMAND. This shell is invoked regardless of the
  /*	command content.
  /* .RE
  /* DIAGNOSTICS
  /*	Panic: interface violations (for example, a zero-valued
  /*	user ID or group ID, or a missing command).
  /*
  /*	pipe_command() returns one of the following status codes:
  /* .IP PIPE_STAT_OK
  /*	The command has taken responsibility for further delivery of
  /*	the message.
  /* .IP PIPE_STAT_DEFER
  /*	The command failed with a "try again" type error.
  /*	The reason is given via the \fIwhy\fR argument.
  /* .IP PIPE_STAT_BOUNCE
  /*	The command indicated that the message was not acceptable,
  /*	or the command did not finish within the time limit.
  /*	The reason is given via the \fIwhy\fR argument.
  /* .IP PIPE_STAT_CORRUPT
  /*	The queue file is corrupted.
  /* SEE ALSO
  /*	mail_copy(3) deliver to any.
  /*	mark_corrupt(3) mark queue file as corrupt.
  /*	sys_exits(3) sendmail-compatible exit status codes.
  /* LICENSE
  /* .ad
  /* .fi
  /*	The Secure Mailer license must be distributed with this software.
  /* AUTHOR(S)
  /*	Wietse Venema
  /*	IBM T.J. Watson Research
  /*	P.O. Box 704
  /*	Yorktown Heights, NY 10598, USA
  /*--*/
  
  /* System library. */
  
  #include <sys_defs.h>
  #include <sys/wait.h>
  #include <signal.h>
  #include <unistd.h>
  #include <errno.h>
  #include <stdarg.h>
  #include <fcntl.h>
  #include <stdlib.h>
  #ifdef USE_PATHS_H
  #include <paths.h>
  #endif
  #include <syslog.h>
  
  /* Utility library. */
  
  #include <msg.h>
  #include <vstream.h>
  #include <msg_vstream.h>
  #include <vstring.h>
  #include <stringops.h>
  #include <iostuff.h>
  #include <timed_wait.h>
  #include <set_ugid.h>
  #include <set_eugid.h>
  #include <argv.h>
  #include <chroot_uid.h>
  
  /* Global library. */
  
  #include <mail_params.h>
  #include <mail_copy.h>
  #include <clean_env.h>
  #include <pipe_command.h>
  #include <exec_command.h>
  #include <sys_exits.h>
  #include <dsn_util.h>
  #include <dsn_buf.h>
  
  /* Application-specific. */
  
  struct pipe_args {
      int     flags;			/* see mail_copy.h */
      char   *sender;			/* envelope sender */
      char   *orig_rcpt;			/* original recipient */
      char   *delivered;			/* envelope recipient */
      char   *eol;			/* carriagecontrol */
      char  **argv;			/* either an array */
      char   *command;			/* or a plain string */
      uid_t   uid;			/* privileges */
      gid_t   gid;			/* privileges */
      char  **env;			/* extra environment */
      char  **export;			/* exportable environment */
      char   *shell;			/* command shell */
      char   *cwd;			/* preferred working directory */
      char   *chroot;			/* root directory */
  };
  
  static int pipe_command_timeout;	/* command has timed out */
  static int pipe_command_maxtime;	/* available time to complete */
  
  /* get_pipe_args - capture the variadic argument list */
  
  static void get_pipe_args(struct pipe_args * args, va_list ap)
  {
      const char *myname = "get_pipe_args";
      int     key;
  
      /*
       * First, set the default values.
       */
      args->flags = 0;
      args->sender = 0;
      args->orig_rcpt = 0;
      args->delivered = 0;
      args->eol = "\n";
      args->argv = 0;
      args->command = 0;
      args->uid = var_default_uid;
      args->gid = var_default_gid;
      args->env = 0;
      args->export = 0;
      args->shell = 0;
      args->cwd = 0;
      args->chroot = 0;
  
      pipe_command_maxtime = -1;
  
      /*
       * Then, override the defaults with user-supplied inputs.
       */
      while ((key = va_arg(ap, int)) != PIPE_CMD_END) {
  	switch (key) {
  	case PIPE_CMD_COPY_FLAGS:
  	    args->flags |= va_arg(ap, int);
  	    break;
  	case PIPE_CMD_SENDER:
  	    args->sender = va_arg(ap, char *);
  	    break;
  	case PIPE_CMD_ORIG_RCPT:
  	    args->orig_rcpt = va_arg(ap, char *);
  	    break;
  	case PIPE_CMD_DELIVERED:
  	    args->delivered = va_arg(ap, char *);
  	    break;
  	case PIPE_CMD_EOL:
  	    args->eol = va_arg(ap, char *);
  	    break;
  	case PIPE_CMD_ARGV:
  	    if (args->command)
  		msg_panic("%s: got PIPE_CMD_ARGV and PIPE_CMD_COMMAND", myname);
  	    args->argv = va_arg(ap, char **);
  	    break;
  	case PIPE_CMD_COMMAND:
  	    if (args->argv)
  		msg_panic("%s: got PIPE_CMD_ARGV and PIPE_CMD_COMMAND", myname);
  	    args->command = va_arg(ap, char *);
  	    break;
  	case PIPE_CMD_UID:
  	    args->uid = va_arg(ap, uid_t);	/* in case uid_t is short */
  	    break;
  	case PIPE_CMD_GID:
  	    args->gid = va_arg(ap, gid_t);	/* in case gid_t is short */
  	    break;
  	case PIPE_CMD_TIME_LIMIT:
  	    pipe_command_maxtime = va_arg(ap, int);
  	    break;
  	case PIPE_CMD_ENV:
  	    args->env = va_arg(ap, char **);
  	    break;
  	case PIPE_CMD_EXPORT:
  	    args->export = va_arg(ap, char **);
  	    break;
  	case PIPE_CMD_SHELL:
  	    args->shell = va_arg(ap, char *);
  	    break;
  	case PIPE_CMD_CWD:
  	    args->cwd = va_arg(ap, char *);
  	    break;
  	case PIPE_CMD_CHROOT:
  	    args->chroot = va_arg(ap, char *);
  	    break;
  	default:
  	    msg_panic("%s: unknown key: %d", myname, key);
  	}
      }
      if (args->command == 0 && args->argv == 0)
  	msg_panic("%s: missing PIPE_CMD_ARGV or PIPE_CMD_COMMAND", myname);
      if (args->uid == 0)
  	msg_panic("%s: privileged uid", myname);
      if (args->gid == 0)
  	msg_panic("%s: privileged gid", myname);
      if (pipe_command_maxtime < 0)
  	msg_panic("%s: missing or invalid PIPE_CMD_TIME_LIMIT", myname);
  }
  
  /* pipe_command_write - write to command with time limit */
  
  static ssize_t pipe_command_write(int fd, void *buf, size_t len,
  				          int unused_timeout,
  				          void *unused_context)
  {
      int     maxtime = (pipe_command_timeout == 0) ? pipe_command_maxtime : 0;
      const char *myname = "pipe_command_write";
  
      /*
       * Don't wait when all available time was already used up.
       */
      if (write_wait(fd, maxtime) < 0) {
  	if (pipe_command_timeout == 0) {
  	    msg_warn("%s: write time limit exceeded", myname);
  	    pipe_command_timeout = 1;
  	}
  	return (0);
      } else {
  	return (write(fd, buf, len));
      }
  }
  
  /* pipe_command_read - read from command with time limit */
  
  static ssize_t pipe_command_read(int fd, void *buf, size_t len,
  				         int unused_timeout,
  				         void *unused_context)
  {
      int     maxtime = (pipe_command_timeout == 0) ? pipe_command_maxtime : 0;
      const char *myname = "pipe_command_read";
  
      /*
       * Don't wait when all available time was already used up.
       */
      if (read_wait(fd, maxtime) < 0) {
  	if (pipe_command_timeout == 0) {
  	    msg_warn("%s: read time limit exceeded", myname);
  	    pipe_command_timeout = 1;
  	}
  	return (0);
      } else {
  	return (read(fd, buf, len));
      }
  }
  
  /* kill_command - terminate command forcibly */
  
  static void kill_command(pid_t pid, int sig, uid_t kill_uid, gid_t kill_gid)
  {
      uid_t   saved_euid = geteuid();
      gid_t   saved_egid = getegid();
  
      /*
       * Switch privileges to that of the child process. Terminate the child
       * and its offspring.
       */
      set_eugid(kill_uid, kill_gid);
      if (kill(-pid, sig) < 0 && kill(pid, sig) < 0)
  	msg_warn("cannot kill process (group) %lu: %m",
  		 (unsigned long) pid);
      set_eugid(saved_euid, saved_egid);
  }
  
  /* pipe_command_wait_or_kill - wait for command with time limit, or kill it */
  
  static int pipe_command_wait_or_kill(pid_t pid, WAIT_STATUS_T *statusp, int sig,
  				             uid_t kill_uid, gid_t kill_gid)
  {
      int     maxtime = (pipe_command_timeout == 0) ? pipe_command_maxtime : 1;
      const char *myname = "pipe_command_wait_or_kill";
      int     n;
  
      /*
       * Don't wait when all available time was already used up.
       */
      if ((n = timed_waitpid(pid, statusp, 0, maxtime)) < 0 && errno == ETIMEDOUT) {
  	if (pipe_command_timeout == 0) {
  	    msg_warn("%s: child wait time limit exceeded", myname);
  	    pipe_command_timeout = 1;
  	}
  	kill_command(pid, sig, kill_uid, kill_gid);
  	n = waitpid(pid, statusp, 0);
      }
      return (n);
  }
  
  /* pipe_child_cleanup - child fatal error handler */
  
  static void pipe_child_cleanup(void)
  {
  
      /*
       * WARNING: don't place code here. This code may run as mail_owner, as
       * root, or as the user/group specified with the "user" attribute. The
       * only safe action is to terminate.
       * 
       * Future proofing. If you need exit() here then you broke Postfix.
       */
      _exit(EX_TEMPFAIL);
  }
  
  /* pipe_command - execute command with extreme prejudice */
  
  int     pipe_command(VSTREAM *src, DSN_BUF *why,...)
  {
      const char *myname = "pipe_command";
      va_list ap;
      VSTREAM *cmd_in_stream;
      VSTREAM *cmd_out_stream;
      char    log_buf[VSTREAM_BUFSIZE + 1];
      ssize_t log_len;
      pid_t   pid;
      int     write_status;
      int     write_errno;
      WAIT_STATUS_T wait_status;
      int     cmd_in_pipe[2];
      int     cmd_out_pipe[2];
      struct pipe_args args;
      char  **cpp;
      ARGV   *argv;
      DSN_SPLIT dp;
      const SYS_EXITS_DETAIL *sp;
  
      /*
       * Process the variadic argument list. This also does sanity checks on
       * what data the caller is passing to us.
       */
      va_start(ap, why);
      get_pipe_args(&args, ap);
      va_end(ap);
  
      /*
       * For convenience...
       */
      if (args.command == 0)
  	args.command = args.argv[0];
  
      /*
       * Set up pipes that connect us to the command input and output streams.
       * We're using a rather disgusting hack to capture command output: set
       * the output to non-blocking mode, and don't attempt to read the output
       * until AFTER the process has terminated. The rationale for this is: 1)
       * the command output will be used only when delivery fails; 2) the
       * amount of output is expected to be small; 3) the output can be
       * truncated without too much loss. I could even argue that truncating
       * the amount of diagnostic output is a good thing to do, but I won't go
       * that far.
       * 
       * Turn on non-blocking writes to the child process so that we can enforce
       * timeouts after partial writes.
       * 
       * XXX Too much trouble with different systems returning weird write()
       * results when a pipe is writable.
       */
      if (pipe(cmd_in_pipe) < 0 || pipe(cmd_out_pipe) < 0)
  	msg_fatal("%s: pipe: %m", myname);
      non_blocking(cmd_out_pipe[1], NON_BLOCKING);
  #if 0
      non_blocking(cmd_in_pipe[1], NON_BLOCKING);
  #endif
  
      /*
       * Spawn off a child process and irrevocably change privilege to the
       * user. This includes revoking all rights on open files (via the close
       * on exec flag). If we cannot run the command now, try again some time
       * later.
       */
      switch (pid = fork()) {
  
  	/*
  	 * Error. Instead of trying again right now, back off, give the
  	 * system a chance to recover, and try again later.
  	 */
      case -1:
  	msg_warn("fork: %m");
  	dsb_unix(why, "4.3.0", sys_exits_detail(EX_OSERR)->text,
  		 "Delivery failed: %m");
  	return (PIPE_STAT_DEFER);
  
  	/*
  	 * Child. Run the child in a separate process group so that the
  	 * parent can kill not just the child but also its offspring.
  	 * 
  	 * Redirect fatal exits to our own fatal exit handler (never leave the
  	 * parent's handler enabled :-) so we can replace random exit status
  	 * codes by EX_TEMPFAIL.
  	 */
      case 0:
  	(void) msg_cleanup(pipe_child_cleanup);
  
  	/*
  	 * In order to chroot it is necessary to switch euid back to root.
  	 * Right after chroot we call set_ugid() so all privileges will be
  	 * dropped again.
  	 * 
  	 * XXX For consistency we use chroot_uid() to change root+current
  	 * directory. However, we must not use chroot_uid() to change process
  	 * privileges (assuming a version that accepts numeric privileges).
  	 * That would create a maintenance problem, because we would have two
  	 * different code paths to set the external command's privileges.
  	 */
  	if (args.chroot) {
  	    seteuid(0);
  	    chroot_uid(args.chroot, (char *) 0);
  	}
  
  	/*
  	 * XXX If we put code before the set_ugid() call, then the code that
  	 * changes root directory must switch back to the mail_owner UID,
  	 * otherwise we'd be running with root privileges.
  	 */
  	set_ugid(args.uid, args.gid);
  	if (setsid() < 0)
  	    msg_warn("setsid failed: %m");
  
  	/*
  	 * Pipe plumbing.
  	 */
  	close(cmd_in_pipe[1]);
  	close(cmd_out_pipe[0]);
  	if (DUP2(cmd_in_pipe[0], STDIN_FILENO) < 0
  	    || DUP2(cmd_out_pipe[1], STDOUT_FILENO) < 0
  	    || DUP2(cmd_out_pipe[1], STDERR_FILENO) < 0)
  	    msg_fatal("%s: dup2: %m", myname);
  	close(cmd_in_pipe[0]);
  	close(cmd_out_pipe[1]);
  
  	/*
  	 * Working directory plumbing.
  	 */
  	if (args.cwd && chdir(args.cwd) < 0)
  	    msg_fatal("cannot change directory to \"%s\" for uid=%lu gid=%lu: %m",
  		      args.cwd, (unsigned long) args.uid,
  		      (unsigned long) args.gid);
  
  	/*
  	 * Environment plumbing. Always reset the command search path. XXX
  	 * That should probably be done by clean_env().
  	 */
  	if (args.export)
  	    clean_env(args.export);
  	if (setenv("PATH", _PATH_DEFPATH, 1))
  	    msg_fatal("%s: setenv: %m", myname);
  	if (args.env)
  	    for (cpp = args.env; *cpp; cpp += 2)
  		if (setenv(cpp[0], cpp[1], 1))
  		    msg_fatal("setenv: %m");
  
  	/*
  	 * Process plumbing. If possible, avoid running a shell.
  	 * 
  	 * As a safety for buggy libraries, we close the syslog socket.
  	 * Otherwise we could leak a file descriptor that was created by a
  	 * privileged process.
  	 * 
  	 * XXX To avoid losing fatal error messages we open a VSTREAM and
  	 * capture the output in the parent process.
  	 */
  	closelog();
  	msg_vstream_init(var_procname, VSTREAM_ERR);
  	if (args.argv) {
  	    execvp(args.argv[0], args.argv);
  	    msg_fatal("%s: execvp %s: %m", myname, args.argv[0]);
  	} else if (args.shell && *args.shell) {
  	    argv = argv_split(args.shell, CHARS_SPACE);
  	    argv_add(argv, args.command, (char *) 0);
  	    argv_terminate(argv);
  	    execvp(argv->argv[0], argv->argv);
  	    msg_fatal("%s: execvp %s: %m", myname, argv->argv[0]);
  	} else {
  	    exec_command(args.command);
  	}
  	/* NOTREACHED */
  
  	/*
  	 * Parent.
  	 */
      default:
  	close(cmd_in_pipe[0]);
  	close(cmd_out_pipe[1]);
  
  	cmd_in_stream = vstream_fdopen(cmd_in_pipe[1], O_WRONLY);
  	cmd_out_stream = vstream_fdopen(cmd_out_pipe[0], O_RDONLY);
  
  	/*
  	 * Give the command a limited amount of time to run, by enforcing
  	 * timeouts on all I/O from and to it.
  	 */
  	vstream_control(cmd_in_stream,
  			CA_VSTREAM_CTL_WRITE_FN(pipe_command_write),
  			CA_VSTREAM_CTL_END);
  	vstream_control(cmd_out_stream,
  			CA_VSTREAM_CTL_READ_FN(pipe_command_read),
  			CA_VSTREAM_CTL_END);
  	pipe_command_timeout = 0;
  
  	/*
  	 * Pipe the message into the command. Examine the error report only
  	 * if we can't recognize a more specific error from the command exit
  	 * status or from the command output.
  	 */
  	write_status = mail_copy(args.sender, args.orig_rcpt,
  				 args.delivered, src,
  				 cmd_in_stream, args.flags,
  				 args.eol, why);
  	write_errno = errno;
  
  	/*
  	 * Capture a limited amount of command output, for inclusion in a
  	 * bounce message. Turn tabs and newlines into whitespace, and
  	 * replace other non-printable characters by underscore.
  	 */
  	log_len = vstream_fread(cmd_out_stream, log_buf, sizeof(log_buf) - 1);
  	(void) vstream_fclose(cmd_out_stream);
  	log_buf[log_len] = 0;
  	translit(log_buf, "\t\n", "  ");
  	printable(log_buf, '_');
  
  	/*
  	 * Just because the child closes its output streams, don't assume
  	 * that it will terminate. Instead, be prepared for the situation
  	 * that the child does not terminate, even when the parent
  	 * experiences no read/write timeout. Make sure that the child
  	 * terminates before the parent attempts to retrieve its exit status,
  	 * otherwise the parent could become stuck, and the mail system would
  	 * eventually run out of delivery agents. Do a thorough job, and kill
  	 * not just the child process but also its offspring.
  	 */
  	if (pipe_command_timeout)
  	    kill_command(pid, SIGKILL, args.uid, args.gid);
  	if (pipe_command_wait_or_kill(pid, &wait_status, SIGKILL,
  				      args.uid, args.gid) < 0)
  	    msg_fatal("wait: %m");
  	if (pipe_command_timeout) {
  	    dsb_unix(why, "5.3.0", log_len ?
  		     log_buf : sys_exits_detail(EX_SOFTWARE)->text,
  		     "Command time limit exceeded: \"%s\"%s%s",
  		     args.command,
  		     log_len ? ". Command output: " : "", log_buf);
  	    return (PIPE_STAT_BOUNCE);
  	}
  
  	/*
  	 * Command exits. Give special treatment to sendmail style exit
  	 * status codes.
  	 */
  	if (!NORMAL_EXIT_STATUS(wait_status)) {
  	    if (WIFSIGNALED(wait_status)) {
  		dsb_unix(why, "4.3.0", log_len ?
  			 log_buf : sys_exits_detail(EX_SOFTWARE)->text,
  			 "Command died with signal %d: \"%s\"%s%s",
  			 WTERMSIG(wait_status), args.command,
  			 log_len ? ". Command output: " : "", log_buf);
  		return (PIPE_STAT_DEFER);
  	    }
  	    /* Use "D.S.N text" command output. XXX What diagnostic code? */
  	    else if (dsn_valid(log_buf) > 0) {
  		dsn_split(&dp, "5.3.0", log_buf);
  		dsb_unix(why, DSN_STATUS(dp.dsn), dp.text, "%s", dp.text);
  		return (DSN_CLASS(dp.dsn) == '4' ?
  			PIPE_STAT_DEFER : PIPE_STAT_BOUNCE);
  	    }
  	    /* Use <sysexits.h> compatible exit status. */
  	    else if (SYS_EXITS_CODE(WEXITSTATUS(wait_status))) {
  		sp = sys_exits_detail(WEXITSTATUS(wait_status));
  		dsb_unix(why, sp->dsn,
  			 log_len ? log_buf : sp->text, "%s%s%s", sp->text,
  			 log_len ? ". Command output: " : "", log_buf);
  		return (sp->dsn[0] == '4' ?
  			PIPE_STAT_DEFER : PIPE_STAT_BOUNCE);
  	    }
  
  	    /*
  	     * No "D.S.N text" or <sysexits.h> compatible status. Fake it.
  	     */
  	    else {
  		sp = sys_exits_detail(WEXITSTATUS(wait_status));
  		dsb_unix(why, sp->dsn,
  			 log_len ? log_buf : sp->text,
  			 "Command died with status %d: \"%s\"%s%s",
  			 WEXITSTATUS(wait_status), args.command,
  			 log_len ? ". Command output: " : "", log_buf);
  		return (PIPE_STAT_BOUNCE);
  	    }
  	} else if (write_status &
  		   MAIL_COPY_STAT_CORRUPT) {
  	    return (PIPE_STAT_CORRUPT);
  	} else if (write_status && write_errno != EPIPE) {
  	    vstring_prepend(why->reason, "Command failed: ",
  			    sizeof("Command failed: ") - 1);
  	    vstring_sprintf_append(why->reason, ": \"%s\"", args.command);
  	    return (PIPE_STAT_BOUNCE);
  	} else {
  	    vstring_strcpy(why->reason, log_buf);
  	    return (PIPE_STAT_OK);
  	}
      }
  }
  