/*
 * FCRON - periodic command scheduler
 *
 *  Copyright 2000-2025 Thibault Godouet <fcron@free.fr>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *  The GNU General Public License can also be found in the file
 *  `LICENSE' that comes with the fcron source distribution.
 */


#include "fcron.h"

#include "job.h"
#include "mail.h"
#include "temp_file.h"


void sig_dfl(void);
void end_job(cl_t * line, int status, FILE * mailf, short mailpos,
             char **sendmailenv);
void end_mailer(cl_t * line, int status);
#ifdef HAVE_LIBPAM
void die_mail_pame(cl_t * cl, int pamerrno, struct passwd *pas, char *str,
                   env_list_t * env);
#endif
#define PIPE_READ 0
#define PIPE_WRITE 1
int read_write_pipe(int fd, void *buf, size_t size, int action);
int read_pipe(int fd, void *to, size_t size);
int write_pipe(int fd, void *buf, size_t size);
void become_user(struct cl_t *cl, struct passwd *pas, char *home);

#ifdef HAVE_LIBPAM
void
die_mail_pame(cl_t *cl, int pamerrno, struct passwd *pas, char *str,
              env_list_t *env)
/* log an error in syslog, mail user if necessary, and die */
{
    char buf[MAX_MSG];

    snprintf(buf, sizeof(buf), "%s for user '%s'", str, pas->pw_name);

    if (is_mail(cl->cl_option)) {
        char **envp = env_list_export_envp(env);
        FILE *mailf =
            create_mail(cl, "Could not run fcron job", NULL, NULL, envp);

        /* print the error in both syslog and a file, in order to mail it to user */
        if (dup2(fileno(mailf), 1) != 1 || dup2(1, 2) != 2)
            die_e("dup2() error");      /* dup2 also clears close-on-exec flag */

        foreground = 1;
        error_pame(pamh, pamerrno, buf, cl->cl_shell);
        error("Job '%s' has *not* run.", cl->cl_shell);
        foreground = 0;

        pam_end(pamh, pamerrno);

        become_user(cl, pas, "/");

        launch_mailer(cl, mailf, envp);
        /* launch_mailer() does not return : we never get here */
    }
    else
        die_pame(pamh, pamerrno, buf, cl->cl_shell);
}
#endif

void
become_user(struct cl_t *cl, struct passwd *pas, char *home)
/* Become the user who owns the job: change privileges, check PAM authorization,
 * and change dir to HOME. */
{

#ifndef RUN_NON_PRIVILEGED
    if (pas == NULL)
        die("become_user() called with a NULL struct passwd");

    /* Change running state to the user in question */
    if (initgroups(pas->pw_name, pas->pw_gid) < 0)
        die_e("initgroups failed: %s", pas->pw_name);

    if (setgid(pas->pw_gid) < 0)
        die("setgid failed: %s %d", pas->pw_name, pas->pw_gid);

    if (setuid(pas->pw_uid) < 0)
        die("setuid failed: %s %d", pas->pw_name, pas->pw_uid);
#endif                          /* not RUN_NON_PRIVILEGED */

    /* make sure HOME is defined and change dir to it */
    if (chdir(home) != 0) {
        error_e("Could not chdir to HOME dir '%s'. Trying to chdir to '/'.",
                home);
        if (chdir("/") < 0)
            die_e("Could not chdir to HOME dir /");
    }

}

void
setup_user_and_env(struct cl_t *cl, struct passwd *pas,
                   char ***sendmailenv, char ***jobenv, char **curshell,
                   char **curhome, char **content_type, char **encoding)
/* Check PAM authorization, and setup the environment variables
 * to run sendmail and to run the job itself. Change dir to HOME and check if SHELL is ok */
/* (*curshell) and (*curhome) will be allocated and should thus be freed
 * if curshell and curhome are not NULL. */
/* Return the the two env var sets, the shell to use to execle() commands and the home dir */
{
    env_list_t *env_list = env_list_init();
    env_t *e = NULL;
    char *path = NULL;
    char *myshell = NULL;
#ifdef HAVE_LIBPAM
    int retcode = 0;
    char **env;
#endif

    if (pas == NULL)
        die("setup_user_and_env() called with a NULL struct passwd");

    env_list_setenv(env_list, "USER", pas->pw_name, 1);
    env_list_setenv(env_list, "LOGNAME", pas->pw_name, 1);
    env_list_setenv(env_list, "HOME", pas->pw_dir, 1);
    /* inherit fcron's PATH for sendmail. We will later change it to DEFAULT_JOB_PATH
     * or a user defined PATH for the job itself */
    path = getenv("PATH");
    env_list_setenv(env_list, "PATH", (path != NULL) ? path : DEFAULT_JOB_PATH,
                    1);

    if (cl->cl_tz != NULL)
        env_list_setenv(env_list, "TZ", cl->cl_tz, 1);
    /* To ensure compatibility with Vixie cron, we don't use the shell defined
     * in /etc/passwd by default, but the default value from fcron.conf instead: */
    if (shell != NULL && shell[0] != '\0')
        /* default: use value from fcron.conf */
        env_list_setenv(env_list, "SHELL", shell, 1);
    else
        /* shell is empty, ie. not defined: fail back to /etc/passwd's value */
        env_list_setenv(env_list, "SHELL", pas->pw_shell, 1);

#if ( ! defined(RUN_NON_PRIVILEGED)) && defined(HAVE_LIBPAM)
    /* Open PAM session for the user and obtain any security
     * credentials we might need */

    retcode = pam_start("fcron", pas->pw_name, &apamconv, &pamh);
    if (retcode != PAM_SUCCESS)
        die_pame(pamh, retcode, "Could not start PAM for '%s'", cl->cl_shell);
    /* Some system seem to need that pam_authenticate() call.
     * Anyway, we have no way to authentificate the user :
     * we must set auth to pam_permit. */
    retcode = pam_authenticate(pamh, PAM_SILENT);
    if (retcode != PAM_SUCCESS)
        die_mail_pame(cl, retcode, pas,
                      "Could not authenticate PAM user", env_list);
    retcode = pam_acct_mgmt(pamh, PAM_SILENT);  /* permitted access? */
    if (retcode != PAM_SUCCESS)
        die_mail_pame(cl, retcode, pas,
                      "Could not init PAM account management", env_list);
    retcode = pam_setcred(pamh, PAM_ESTABLISH_CRED | PAM_SILENT);
    if (retcode != PAM_SUCCESS)
        die_mail_pame(cl, retcode, pas, "Could not set PAM credentials",
                      env_list);
    retcode = pam_open_session(pamh, PAM_SILENT);
    if (retcode != PAM_SUCCESS)
        die_mail_pame(cl, retcode, pas, "Could not open PAM session", env_list);

    for (env = pam_getenvlist(pamh); env && *env; env++) {
        env_list_putenv(env_list, *env, 1);
    }

    /* Close the log here, because PAM calls openlog(3) and
     * our log messages could go to the wrong facility */
    xcloselog();
#endif                          /* ( ! defined(RUN_NON_PRIVILEGED)) && defined(HAVE_LIBPAM) */

    /* export the environment for sendmail before we apply user customization */
    if (sendmailenv != NULL)
        *sendmailenv = env_list_export_envp(env_list);

    /* Now add user customizations to the environment to form jobenv */

    if (jobenv != NULL) {

        /* Make sure we don't keep fcron daemon's PATH (which we used for sendmail) */
        env_list_setenv(env_list, "PATH", DEFAULT_JOB_PATH, 1);

        for (e = env_list_first(cl->cl_file->cf_env_list); e != NULL;
             e = env_list_next(cl->cl_file->cf_env_list)) {
            env_list_putenv(env_list, e->e_envvar, 1);
        }

        /* make sure HOME is defined */
        env_list_putenv(env_list, "HOME=/", 0); /* don't overwrite if already defined */
        if (curhome != NULL) {
            (*curhome) = strdup2(env_list_getenv(env_list, "HOME"));
        }

        /* check that SHELL is valid */
        myshell = env_list_getenv(env_list, "SHELL");
        if (myshell == NULL || myshell[0] == '\0') {
            myshell = shell;
        }
        else if (access(myshell, X_OK) != 0) {
            if (errno == ENOENT)
                error("shell \"%s\" : no file or directory. SHELL set to %s",
                      myshell, shell);
            else
                error_e("shell \"%s\" not valid : SHELL set to %s", myshell,
                        shell);

            myshell = shell;
        }
        env_list_setenv(env_list, "SHELL", myshell, 1);
        if (curshell != NULL)
            *curshell = strdup2(myshell);

        *jobenv = env_list_export_envp(env_list);

    }

    if (content_type != NULL) {
        (*content_type) = strdup2(env_list_getenv(env_list, "CONTENT_TYPE"));
    }
    if (encoding != NULL) {
        (*encoding) =
            strdup2(env_list_getenv(env_list, "CONTENT_TRANSFER_ENCODING"));
    }

    env_list_destroy(env_list);

}

void
change_user_setup_env(struct cl_t *cl,
                      char ***sendmailenv, char ***jobenv, char **curshell,
                      char **curhome, char **content_type, char **encoding)
/* call setup_user_and_env() and become_user().
 * As a result, *curshell and *curhome will be allocated and should thus be freed
 * if curshell and curhome are not NULL. */
{
    struct passwd *pas;

    errno = 0;
    pas = getpwnam(cl->cl_runas);
    if (pas == NULL)
        die_e("failed to get passwd fields for user \"%s\"", cl->cl_runas);

    setup_user_and_env(cl, pas, sendmailenv, jobenv, curshell, curhome,
                       content_type, encoding);

    become_user(cl, pas, (curhome != NULL) ? *curhome : "/");
}

void
sig_dfl(void)
    /* set signals handling to its default */
{
    signal(SIGTERM, SIG_DFL);
    signal(SIGCHLD, SIG_DFL);
    signal(SIGHUP, SIG_DFL);
    signal(SIGUSR1, SIG_DFL);
    signal(SIGUSR2, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
}

FILE *
create_mail(cl_t *line, char *subject, char *content_type, char *encoding,
            char **env)
    /* create a temp file and write in it a mail header */
{
    /* create temporary file for stdout and stderr of the job */
    int mailfd = temp_file(NULL);
    FILE *mailf = fdopen(mailfd, "r+");
    char hostname[USER_NAME_LEN];
    char *mailfrom = line->cl_runas;    /* default value if not explicitely defined for that cl */
    /* hostname to add to email addresses (depending on if they have a '@') */
    char *hostname_from = "";
    char *hostname_to = "";
    char *mailbox_addr = NULL;
    int i = 0;

    if (mailf == NULL)
        die_e("Could not fdopen() mailfd");

    if (line->cl_mailfrom != NULL) {
        mailfrom = line->cl_mailfrom;
    }

#ifdef HAVE_GETHOSTNAME
    /* first letter will be '@' if hostname is defined */
    if (gethostname((hostname + 1), sizeof(hostname) - 1) != 0) {
        error_e("Could not get hostname");
        hostname[0] = '\0';
    }
    else {
        /* it is unspecified whether a truncated hostname is NUL-terminated */
        hostname[0] = '@';
        hostname[sizeof(hostname) - 1] = '\0';

        /* check if mailfrom/mailto are complete email addresses */
        hostname_from = (strchr(mailfrom, '@') == NULL) ? hostname : "";
        hostname_to = (strchr(line->cl_mailto, '@') == NULL) ? hostname : "";
    }
#else                           /* HAVE_GETHOSTNAME */
    hostname[0] = '\0';
#endif                          /* HAVE_GETHOSTNAME */

    /* write mail header. Global 'maildisplayname' comes from fcronconf.h */
    if (maildisplayname[0] != '\0') {
        /* New behavior -- RFC-compliant */
        mailbox_addr =
            make_mailbox_addr(maildisplayname, mailfrom, hostname_from);
        if (!mailbox_addr) {
            warn("could not make the mailbox address");
        }
    }
    if (mailbox_addr) {
        fprintf(mailf, FROM_HEADER_KEY "%s\n", mailbox_addr);
        Free_safe(mailbox_addr);
    }
    else {
        /* Old behavior */
        fprintf(mailf, FROM_HEADER_KEY "%s%s (fcron)\n", mailfrom,
                hostname_from);
    }

    fprintf(mailf, "To: %s%s\n", line->cl_mailto, hostname_to);

    if (subject)
        fprintf(mailf, "Subject: fcron <%s%s> %s: %s\n",
                line->cl_file->cf_user, (hostname[0] != '\0') ? hostname : "@?",
                subject, line->cl_shell);
    else
        fprintf(mailf, "Subject: fcron <%s%s> %s\n", line->cl_file->cf_user,
                (hostname[0] != '\0') ? hostname : "@?", line->cl_shell);

    if (content_type == NULL) {
        fprintf(mailf, "Content-Type: text/plain; charset=%s\n",
                default_mail_charset);
    }
    else {
        /* user specified Content-Type header. */
        char *c = NULL;

        /* Remove new-lines or users could specify arbitrary mail headers!
         * (fcrontab should already prevent that, but better safe than sorry) */
        for (c = content_type; *c != '\0'; c++) {
            if (*c == '\n')
                *c = ' ';
        }
        fprintf(mailf, "Content-Type: %s\n", content_type);
    }

    if (encoding != NULL) {
        char *c = NULL;

        /* Remove new-lines or users could specify arbitrary mail headers!
         * (fcrontab should already prevent that, but better safe than sorry) */
        for (c = encoding; *c != '\0'; c++) {
            if (*c == '\n')
                *c = ' ';
        }
        fprintf(mailf, "Content-Transfer-Encoding: %s\n", encoding);
    }

    /* Add headers so as automated systems can identify that this message
     * is an automated one sent by fcron.
     * That's useful for example for vacation auto-reply systems: no need
     * to send such an automated response to fcron! */

    /* The Auto-Submitted header is
     * defined (and suggested by) RFC3834. */
    fprintf(mailf, "Auto-Submitted: auto-generated\n");

    /* See environ(7) and execle(3) to get documentation on environ:
     * it is an array of NULL-terminated strings, whose last entry is NULL */
    if (env != NULL) {
        for (i = 0; env[i] != NULL; i++) {
            fprintf(mailf, "X-Cron-Env: <%s>\n", env[i]);
        }
    }

    /* Final line return to end the header section: */
    fprintf(mailf, "\n");

    return mailf;
}


int
read_write_pipe(int fd, void *buf, size_t size, int action)
    /* Read/write data from/to pipe.
     * action can either be PIPE_WRITE or PIPE_READ.
     * Handles signal interruptions, and read in several passes.
     * Returns ERR in case of a closed pipe, the errno from errno
     * for other errors, and OK if everything was read successfully */
{
    int size_processed = 0;
    int ret;
    int num_retry = 0;

    while (size_processed < size) {
        errno = 0;
        if (action == PIPE_READ) {
            ret = read(fd, (char *)buf + size_processed, size);
        }
        else if (action == PIPE_WRITE) {
            ret = write(fd, (char *)buf + size_processed, size);
        }
        else {
            error("Invalid action parameter for function read_write_pipe():"
                  " %d", action);
            return ERR;
        }
        if (ret > 0) {
            /* some data read correctly -- we still may need
             * one or several calls of read() to read the rest */
            size_processed += ret;
        }
        else if (ret < 0 && errno == EINTR) {
            /* interrupted by a signal : let's try again */
            continue;
        }
        else if (ret == 0) {
            /* Notes:
             * - is it really an error when writing ? should we simply 'continue'
             *   in this case ?
             * - read() on a pipe returning 0 means the pipe was closed at the other end.
             *   We shouldn't need to retry multiple times (but it should never happen) */
            if (num_retry < 3) {
                num_retry++;
                error_e
                    ("read_write_pipe(): read/write returned 0: retrying... "
                     "(size: %d, size_processed: %d, num_retry: %d)",
                     size, size_processed, num_retry);
                sleep(1);
                continue;
            }
            else {
                return ERR;
            }
        }
        else {
            /* in particular this includes write() returning -1 with errno=EPIPE */
            return errno;
        }
    }

    return OK;
}

int
read_pipe(int fd, void *buf, size_t size)
    /* Read data from pipe.
     * Handles signal interruptions, and read in several passes.
     * Returns ERR in case of a closed pipe, the errno from read
     * for other errors, and OK if everything was read successfully */
{
    return read_write_pipe(fd, buf, size, PIPE_READ);
}

int
write_pipe(int fd, void *buf, size_t size)
    /* Read data from pipe.
     * Handles signal interruptions, and read in several passes.
     * Returns ERR in case of a closed pipe, the errno from write
     * for other errors, and OK if everything was read successfully */
{
    return read_write_pipe(fd, buf, size, PIPE_WRITE);
}

void
run_job_grand_child_setup_stderr_stdout(cl_t *line, int *pipe_fd)
    /* setup stderr and stdout correctly so as the mail containing
     * the output of the job can be send at the end of the job.
     * Close the pipe (both ways). */
{

    if (is_mail(line->cl_option)) {
        /* we can't dup2 directly to mailfd, since a "cmd > /dev/stderr" in
         * a script would erase all previously collected message */
        if (dup2(pipe_fd[1], 1) != 1 || dup2(1, 2) != 2)
            die_e("dup2() error");      /* dup2 also clears close-on-exec flag */
        /* we close the pipe_fd[]s : the resources remain, and the pipe will
         * be effectively close when the job stops */
        xclose_check(&(pipe_fd[0]), "pipe_fd[0] in setup_stderr_stdout");
        xclose_check(&(pipe_fd[1]), "pipe_fd[1] in setup_stderr_stdout");
        /* Standard buffering results in unwanted behavior (some messages,
         * at least error from fcron process itself, are lost) */
#ifdef HAVE_SETLINEBUF
        setlinebuf(stdout);
        setlinebuf(stderr);
#else
        setvbuf(stdout, NULL, _IONBF, 0);
        setvbuf(stderr, NULL, _IONBF, 0);
#endif
    }
    else if (foreground) {
        if (freopen("/dev/null", "w", stdout) == NULL)
            error_e("could not freopen /dev/null as stdout");
        if (freopen("/dev/null", "w", stderr) == NULL)
            error_e("could not freopen /dev/null as stderr");
    }

}

void
run_job_grand_child_setup_nice(cl_t *line)
    /* set the nice value for the job */
{
    if (line->cl_nice != 0) {
        errno = 0;              /* so that it works with any libc and kernel */
        if (nice(line->cl_nice) == -1 && errno != 0)
            error_e("could not set nice value");
    }
}

int
run_job(struct exe_t *exeent)
    /* fork(), redirect outputs to a temp file, and execl() the task.
     * Return ERR if it could not fork() the first time, OK otherwise. */
{

    pid_t pid;
    cl_t *line = exeent->e_line;
    int pipe_pid_fd[2];
    int ret = 0;

    /* prepare the job execution */
    if (pipe(pipe_pid_fd) != 0) {
        error_e("pipe(pipe_pid_fd) : setting job_pid to -1");
        exeent->e_job_pid = -1;
        pipe_pid_fd[0] = pipe_pid_fd[1] = -1;
    }

#ifdef CHECKRUNJOB
    debug
        ("run_job(): first pipe created successfully : about to do first fork()");
#endif                          /* CHECKRUNJOB */

    switch (pid = fork()) {
    case -1:
        error_e("Fork error : could not exec '%s'", line->cl_shell);
        return ERR;
        break;

    case 0:
        /* child */
        {
            struct passwd *pas = NULL;
            char **jobenv = NULL;
            char **sendmailenv = NULL;
            char *curshell = NULL;
            char *curhome = NULL;
            char *content_type = NULL;
            char *encoding = NULL;
            FILE *mailf = NULL;
            int status = 0;
            int to_stdout = foreground && is_stdout(line->cl_option);
            int pipe_fd[2];
            short int mailpos = 0;      /* 'empty mail file' size */
#ifdef WITH_SELINUX
            int flask_enabled = is_selinux_enabled();
#endif

            /* // */
            debug("run_job(): child: %s, output to %s, %s, %s\n",
                  is_mail(line->cl_option) ? "mail" : "no mail",
                  to_stdout ? "stdout" : "file",
                  foreground ? "running in foreground" :
                  "running in background",
                  is_stdout(line->cl_option) ? "stdout" : "normal");
            /* // */

            errno = 0;
            pas = getpwnam(line->cl_runas);
            if (pas == NULL)
                die_e("failed to get passwd fields for user \"%s\"",
                      line->cl_runas);

            setup_user_and_env(line, pas, &sendmailenv, &jobenv, &curshell,
                               &curhome, &content_type, &encoding);

            /* close unneeded READ fd */
            xclose_check(&(pipe_pid_fd[0]), "child's pipe_pid_fd[0]");

            pipe_fd[0] = pipe_fd[1] = -1;
            if (!to_stdout && is_mail(line->cl_option)) {
                /* we create the temp file (if needed) before change_user(),
                 * as temp_file() needs root privileges */
                /* if we run in foreground, stdout and stderr point to the console.
                 * Otherwise, stdout and stderr point to /dev/null . */
                mailf = create_mail(line, NULL, content_type, encoding, jobenv);
                mailpos = ftell(mailf);
                if (pipe(pipe_fd) != 0)
                    die_e("could not pipe() (job not executed)");
            }

            become_user(line, pas, curhome);
            Free_safe(curhome);

            /* restore umask to default */
            umask(saved_umask);

            sig_dfl();

#ifdef CHECKRUNJOB
            debug
                ("run_job(): child: change_user() done -- about to do 2nd fork()");
#endif                          /* CHECKRUNJOB */

            /* now, run the job */
            switch (pid = fork()) {
            case -1:
                error_e("Fork error : could not exec '%s'", line->cl_shell);
                if (write(pipe_pid_fd[1], &pid, sizeof(pid)) < 0)
                    error_e("could not write child pid to pipe_pid_fd[1]");
                xclose_check(&(pipe_fd[0]), "child's pipe_fd[0]");
                xclose_check(&(pipe_fd[1]), "child's pipe_fd[1]");
                xclose_check(&(pipe_pid_fd[1]), "child's pipe_pid_fd[1]");
                exit(EXIT_ERR);
                break;

            case 0:
                /* grand child (child of the 2nd fork) */

                /* the grand child does not use this pipe: close remaining fd */
                xclose_check(&(pipe_pid_fd[1]), "grand child's pipe_pid_fd[1]");

                if (!to_stdout)
                    /* note : the following closes the pipe */
                    run_job_grand_child_setup_stderr_stdout(line, pipe_fd);

                foreground = 1;
                /* now, errors will be mailed to the user (or to /dev/null) */

                run_job_grand_child_setup_nice(line);

                xcloselog();

#if defined(CHECKJOBS) || defined(CHECKRUNJOB)
                /* this will force to mail a message containing at least the exact
                 * and complete command executed for each execution of all jobs */
                debug("run_job(): grand-child: Executing \"%s -c %s\"",
                      curshell, line->cl_shell);
#endif                          /* CHECKJOBS OR CHECKRUNJOB */

#ifdef WITH_SELINUX
                if (flask_enabled
                    && setexeccon(line->cl_file->cf_user_context) < 0)
                    die_e("Can't set execute context '%s' for user '%s'.",
                          line->cl_file->cf_user_context, line->cl_runas);
#else
                if (setsid() == -1) {
                    die_e("setsid(): errno %d", errno);
                }
#endif
                execle(curshell, curshell, "-c", line->cl_shell, NULL, jobenv);
                /* execle returns only on error */
                die_e("Couldn't exec shell '%s'", curshell);

                /* execution never gets here */

            default:
                /* child (parent of the 2nd fork) */

                /* close unneeded WRITE pipe and READ pipe */
                xclose_check(&(pipe_fd[1]), "child's pipe_fd[1]");

#ifdef CHECKRUNJOB
                debug("run_job(): child: pipe_fd[1] and pipe_pid_fd[0] closed"
                      " -- about to write grand-child pid to pipe");
#endif                          /* CHECKRUNJOB */

                /* give the pid of the child to the parent (main) fcron process */
                ret = write_pipe(pipe_pid_fd[1], &pid, sizeof(pid));
                if (ret != OK) {
                    if (ret == ERR)
                        error
                            ("run_job(): child: Could not write job pid to pipe");
                    else {
                        errno = ret;
                        error_e
                            ("run_job(): child: Could not write job pid to pipe");
                    }
                }

#ifdef CHECKRUNJOB
                debug("run_job(): child: grand-child pid written to pipe");
#endif                          /* CHECKRUNJOB */

                if (!is_nolog(line->cl_option))
                    explain("Job '%s' started for user %s (pid %d)",
                            line->cl_shell, line->cl_file->cf_user, pid);

                if (!to_stdout && is_mail(line->cl_option)) {
                    /* user wants a mail : we use the pipe */
                    char mailbuf[TERM_LEN];
                    FILE *pipef = fdopen(pipe_fd[0], "r");

                    if (pipef == NULL)
                        die_e("Could not fdopen() pipe_fd[0]");

                    mailbuf[sizeof(mailbuf) - 1] = '\0';
                    while (fgets(mailbuf, sizeof(mailbuf), pipef) != NULL)
                        if (fputs(mailbuf, mailf) < 0)
                            warn("fputs() failed to write to mail file for job '%s' (pid %d)", line->cl_shell, pid);
                    /* (closes also pipe_fd[0]): */
                    xfclose_check(&pipef, "child's pipef");
                }

#ifdef CHECKRUNJOB
                debug("run_job(): child: closing pipe with parent");
#endif                          /* CHECKRUNJOB */
                xclose_check(&(pipe_pid_fd[1]), "child's pipe_pid_fd[1]");

                /* we use a while because of a possible interruption by a signal */
                while ((pid = waitpid(-1, &status, 0)) > 0) {
#ifdef CHECKRUNJOB
                    debug("run_job(): child: ending job pid %d", pid);
#endif                          /* CHECKRUNJOB */
                    end_job(line, status, mailf, mailpos, sendmailenv);
                }

                /* execution never gets here */

            }

            /* execution should never gets here, but if it happened we exit with an error */
            exit(EXIT_ERR);
        }

    default:
        /* parent */

        /* close unneeded WRITE fd */
        xclose_check(&(pipe_pid_fd[1]), "parent's pipe_pid_fd[1]");

        exeent->e_ctrl_pid = pid;

#ifdef CHECKRUNJOB
        debug("run_job(): parent: about to read grand-child pid...");
#endif                          /* CHECKRUNJOB */

        /* read the pid of the job */
        ret = read_pipe(pipe_pid_fd[0], &(exeent->e_job_pid), sizeof(pid_t));
        if (ret != OK) {
            if (ret == ERR) {
                error("Could not read job pid because of closed pipe:"
                      " setting it to -1");
            }
            else {
                errno = ret;
                error_e("Could not read job pid : setting it to -1");
            }

            exeent->e_job_pid = -1;
        }
        xclose_check(&(pipe_pid_fd[0]), "parent's pipe_pid_fd[0]");

#ifdef CHECKRUNJOB
        debug
            ("run_job(): parent: finished reading pid of the job -- end of run_job().");
#endif                          /* CHECKRUNJOB */

    }

    return OK;

}

void
end_job(cl_t *line, int status, FILE *mailf, short mailpos, char **sendmailenv)
    /* if task have made some output, mail it to user */
{

    char mail_output = 0;
    char *m = NULL;

#ifdef USE_SENDMAIL
    if (mailf != NULL && (is_mailzerolength(line->cl_option)
                          || (is_mail(line->cl_option)
                              && (
                                     /* job wrote some output and we wan't it in any case: */
                                     ((fseek(mailf, 0, SEEK_END) == 0
                                       && ftell(mailf) > mailpos)
                                      && !is_erroronlymail(line->cl_option))
                                     ||
                                     /* or we want an email only if the job returned an error: */
                                     !(WIFEXITED(status)
                                       && WEXITSTATUS(status) == 0)
                              )
                          )
        )
        ) {
        /* an output exit : we will mail it */
        mail_output = 1;
    }
    /* or else there is no output to email -- mail_output is already set to 0 */
#endif                          /* USE_SENDMAIL */

    m = (mail_output == 1) ? " (mailing output)" : "";
    if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
        if (!is_nolog(line->cl_option))
            explain("Job '%s' completed%s", line->cl_shell, m);
    }
    else if (WIFEXITED(status)) {
        warn("Job '%s' terminated (exit status: %d)%s",
             line->cl_shell, WEXITSTATUS(status), m);
        /* there was an error : in order to inform the user by mail, we need
         * to add some data to mailf */
        if (mailf != NULL)
            fprintf(mailf, "Job '%s' terminated (exit status: %d)%s",
                    line->cl_shell, WEXITSTATUS(status), m);
    }
    else if (WIFSIGNALED(status)) {
        error("Job '%s' terminated due to signal %d%s",
              line->cl_shell, WTERMSIG(status), m);
        if (mailf != NULL)
            fprintf(mailf, "Job '%s' terminated due to signal %d%s",
                    line->cl_shell, WTERMSIG(status), m);
    }
    else {                      /* is this possible? */
        error("Job '%s' terminated abnormally %s", line->cl_shell, m);
        if (mailf != NULL)
            fprintf(mailf, "Job '%s' terminated abnormally %s", line->cl_shell,
                    m);
    }

#ifdef HAVE_LIBPAM
    /* we close the PAM session before running the mailer command :
     * it avoids a fork(), and we use PAM anyway to control whether a user command
     * should be run or not.
     * We consider that the administrator can use a PAM compliant mailer to control
     * whether a mail can be sent or not.
     * It should be ok like that, otherwise contact me ... -tg */

    /* Aiee! we may need to be root to do this properly under Linux.  Let's
     * hope we're more l33t than PAM and try it as non-root. If someone
     * complains, I'll fix this :P -hmh */
    pam_setcred(pamh, PAM_DELETE_CRED | PAM_SILENT);
    pam_end(pamh, pam_close_session(pamh, PAM_SILENT));
#endif

    if (mail_output == 1) {
        launch_mailer(line, mailf, sendmailenv);
        /* never reached */
        die_e("Internal error: launch_mailer returned");
    }

    /* if mail is sent, execution doesn't get here : close /dev/null */
    xfclose_check(&mailf, "Can't close file mailf");

    exit(0);

}

void
launch_mailer(cl_t *line, FILE *mailf, char **sendmailenv)
    /* mail the output of a job to user */
{
#ifdef USE_SENDMAIL
    foreground = 0;

    /* set stdin to the job's output */

    /* fseek() should work, but it seems that it is not always the case
     * (users have reported problems on gentoo and LFS).
     * For those users, lseek() works, so I have decided to use both,
     * as I am not sure that lseek(fileno(...)...) will work as expected
     * on non linux systems. */
    if (fseek(mailf, 0, SEEK_SET) != 0)
        die_e("Can't fseek()");
    if (lseek(fileno(mailf), 0, SEEK_SET) != 0)
        die_e("Can't lseek()");
    if (dup2(fileno(mailf), 0) != 0)
        die_e("Can't dup2(fileno(mailf))");

    xcloselog();

    if (chdir("/") < 0)
        die_e("Could not chdir to /");

    /* run sendmail with mail file as standard input */
    /* // */
    debug("execle(%s, %s, %s, %s, NULL, sendmailenv)", sendmail, sendmail,
          SENDMAIL_ARGS, line->cl_mailto);
    /* // */
    execle(sendmail, sendmail, SENDMAIL_ARGS, line->cl_mailto, NULL,
           sendmailenv);
    die_e("Couldn't exec '%s'", sendmail);
#else                           /* defined(USE_SENDMAIL) */
    exit(EXIT_OK);
#endif
}
