/*

Copyright (c) 2007 Carl Byington - 510 Software Group, released under
the GPL version 3 or any later version at your choice available at
http://www.gnu.org/licenses/gpl-3.0.txt

Based on spamass-milter by Georg C. F. Greve <greve@gnu.org>

*/

#include "config.h"
#include "dnsbl.h"
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>


static const char Id[] = "$Id$";

char *spamc = "@SPAMC@";
char *spamc_empty = "";
bool  warnedmacro = false;   /* have we logged that we couldn't fetch a macro? */
const int maxlen = 1000;    // used for snprintf buffers


SpamAssassin::SpamAssassin(mlfiPriv *priv_, int ip, char *helo_, char *from, char *qid)
{
    error           = false;
    running         = false;
    first_recipient = true;
    priv            = priv_;
    ip4             = ip;
    helo            = helo_;
    envfrom         = from;
    queueid         = qid;
    pid             = 0;
    pipe_io[0][0] = -1;
    pipe_io[0][1] = -1;
    pipe_io[1][0] = -1;
    pipe_io[1][1] = -1;
}


SpamAssassin::~SpamAssassin()
{
    // close all pipes that are still open
    if (pipe_io[0][0] > -1) close(pipe_io[0][0]);
    if (pipe_io[0][1] > -1) close(pipe_io[0][1]);
    if (pipe_io[1][0] > -1) close(pipe_io[1][0]);
    if (pipe_io[1][1] > -1) close(pipe_io[1][1]);
    // child still running?
    if (running) {
        // make sure the pid is valid
        if (pid > 0) {
            // slaughter child
            kill(pid, SIGKILL);
            // wait for child to terminate
            int status;
            waitpid(pid, &status, 0);
        }
    }
}


void SpamAssassin::mlfi_envrcpt(SMFICTX *ctx, char *envrcpt)
{
    if (first_recipient) {
        /* Send the envelope headers as X-Envelope-From: and
           X-Envelope-To: so that SpamAssassin can use them in its
           whitelist checks.  Also forge as complete a dummy
           Received: header as possible because SA gets a lot of
           info from it.

            HReceived: $?sfrom $s $.$?_($?s$|from $.$_)
                $.$?{auth_type}(authenticated$?{auth_ssf} bits=${auth_ssf}$.)
                $.by $j ($v/$Z)$?r with $r$. id $i$?{tls_version}
                (version=${tls_version} cipher=${cipher} bits=${cipher_bits} verify=${verify})$.$?u
                for $u; $|;
                $.$b$?g
                (envelope-from $g)$.

        */
        const char *macro_b, *macro_i, *macro_j, *macro_r,
                   *macro_s, *macro_v, *macro_Z, *macro__;
        char date[32];
        time_t tval;
        time(&tval);
        strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S %z", localtime(&tval));
        macro_b = date;

        /* queue ID */
        macro_i = queueid;

        /* FQDN of this site */
        macro_j = smfi_getsymval(ctx, "j");
        if (!macro_j) {
            macro_j = "localhost";
            warnmacro("j", "ENVRCPT");
        }

        /* Protocol used to receive the message */
        macro_r = smfi_getsymval(ctx, "r");
        if (!macro_r) {
            macro_r = "SMTP";
            warnmacro("r", "ENVRCPT");
        }

        macro_s = helo;

        /* Sendmail binary version */
        macro_v = smfi_getsymval(ctx, "v");
        if (!macro_v) {
            macro_v = "8.13.0";
            warnmacro("v", "ENVRCPT");
        }

        /* Sendmail .cf version */
        macro_Z = smfi_getsymval(ctx, "Z");
        if (!macro_Z) {
            macro_Z = "8.13.0";
            warnmacro("Z", "ENVRCPT");
        }

        /* Validated sending site's address */
        macro__ = smfi_getsymval(ctx, "_");
        if (!macro__) {
            macro__ = "unknown";
            warnmacro("_", "ENVRCPT");
        }

        output(string("Received: from ") + macro_s + " (" + macro__+ ")\r\n\t" +
               "by " + macro_j + " (" + macro_v + "/" + macro_Z + ") with " + macro_r + " id " + macro_i + "\r\n\t" +
               "for " + envfrom + ";\r\n\t" +
               macro_b + "\r\n");

        output(string("X-Envelope-From: ") + envfrom + "\r\n");
    }
    output(string("X-Envelope-To: ") + envrcpt + "\r\n");
    first_recipient = false;
}


void SpamAssassin::mlfi_header(char* headerf, char* headerv)
{
    if (!running) Connect();
    output(spamc_input);
    output(headerf);
    output(": ");
    output(headerv);
    output("\r\n");
    spamc_input.empty();
}


void SpamAssassin::mlfi_eoh()
{
    output("\r\n");
}


void SpamAssassin::mlfi_body(u_char *bodyp, size_t bodylen)
{
    output((char *)bodyp, bodylen);
}


int SpamAssassin::mlfi_eom()
{
    close_output(); // signal EOF to SpamAssassin
    input();        // read what the Assassin is telling us
    my_syslog(priv, "spamc returned " + spamc_output);
    return atoi(spamc_output.c_str());
}


void SpamAssassin::Connect()
{
    if (error) return;
    // set up pipes for in- and output
    error |= (pipe(pipe_io[0]));
    error |= (pipe(pipe_io[1]));
    if (error) return;

    // now execute SpamAssassin client for contact with SpamAssassin spamd
    // start child process
    pid = fork();
    switch (pid) {
        case -1:
            // forking trouble.
            my_syslog(priv, "unable to fork for spamc");
            error = true;
            close(pipe_io[0][0]);
            close(pipe_io[0][1]);
            close(pipe_io[1][0]);
            close(pipe_io[1][1]);
            pipe_io[0][0] = -1;
            pipe_io[0][1] = -1;
            pipe_io[1][0] = -1;
            pipe_io[1][1] = -1;
            return;
        case 0:
            // +++ CHILD +++

            // close unused pipes
            close(pipe_io[1][0]);
            close(pipe_io[0][1]);

            // redirect stdin(0), stdout(1) and stderr(2)
            dup2(pipe_io[0][0],0);
            dup2(pipe_io[1][1],1);
            dup2(pipe_io[1][1],2);

            closeall(3);

            // execute spamc
            char* argv[3];
            argv[0] = spamc;
            argv[1] = "-c";
            argv[2] = NULL;
            execvp(argv[0] , argv); // does not return!
            _exit(1);               // exec failed
            break;
    }

    // +++ PARENT +++

    // close unused pipes
    close(pipe_io[0][0]);
    close(pipe_io[1][1]);
    pipe_io[0][0] = -1;
    pipe_io[1][1] = -1;

    // mark the pipes non-blocking
    if (fcntl(pipe_io[0][1], F_SETFL, O_NONBLOCK) == -1)
        error = true;
    #if 0  /* don't really need to make the sink pipe nonblocking */
        if (fcntl(pipe_io[1][0], F_SETFL, O_NONBLOCK) == -1)
            error = true;
    #endif

    // we have to assume the client is running now.
    running = true;
}


void SpamAssassin::output(const char* buffer, size_t size)
{
    // if there are problems, fail.
    if (error) return;

    if (!running) {
        // buffer it
        spamc_input.append(buffer, size);
        return;
    }

    // send to SpamAssassin
    long total = 0;
    long wsize = 0;
    string reason;
    int status;
    do {
        struct pollfd fds[2];
        int nfds = 2, nready;
        fds[0].fd      = pipe_io[0][1];
        fds[0].events  = POLLOUT;
        fds[0].revents = 0;
        fds[1].fd      = pipe_io[1][0];
        fds[1].events  = POLLIN;
        fds[1].revents = 0;

        nready = poll(fds, nfds, 1000);
        if (nready == -1) {
            my_syslog(priv, "poll failed");
            error = true;
            return;
        }

        if (fds[1].revents & (POLLERR|POLLNVAL|POLLHUP)) {
            my_syslog(priv, "poll says my read pipe is busted");
            error = true;
            return;
        }

        if (fds[0].revents & (POLLERR|POLLNVAL|POLLHUP)) {
            my_syslog(priv, "poll says my write pipe is busted");
            error = true;
            return;
        }

        if (fds[1].revents & POLLIN) {
            read_pipe();
        }

        if (fds[0].revents & POLLOUT) {
            switch(wsize = write(pipe_io[0][1], (char *)buffer + total, size - total)) {
                case -1:
                    if (errno == EAGAIN) continue;
                    reason = string(strerror(errno));
                    // close the pipes
                    close(pipe_io[0][1]);
                    close(pipe_io[1][0]);
                    pipe_io[0][1] = -1;
                    pipe_io[1][0] = -1;
                    // Slaughter child
                    kill(pid, SIGKILL);
                    // wait for child to terminate
                    waitpid(pid, &status, 0);
                    my_syslog(priv, "write error: " + reason);
                    error   = true;
                    running = false;
                    return;
                default:
                    total += wsize;
                    break;
            }
        }
    } while ( total < size );
}


void SpamAssassin::output(const char* buffer)
{
    output(buffer, strlen(buffer));
}


void SpamAssassin::output(string buffer)
{
    output(buffer.c_str(), buffer.size());
}


void SpamAssassin::close_output()
{
    if (close(pipe_io[0][1]))
        my_syslog(priv, "close error: " + string(strerror(errno)));
    pipe_io[0][1] = -1;
}


void SpamAssassin::input()
{
    if (!running || error) return;
    empty_and_close_pipe();
    if (running) {
        // wait until child is dead
        int status;
        if (waitpid(pid, &status, 0) < 0) {
            error = true;
        };
    }
    running = false;
}


int  SpamAssassin::read_pipe()
{
    long size;
    int  status;
    char iobuff[1024];
    string reason;

    if (pipe_io[1][0] == -1) return 0;

    size = read(pipe_io[1][0], iobuff, 1024);

    if (size < 0) {
        reason = string(strerror(errno));
        // Close remaining pipe.
        close(pipe_io[1][0]);
        pipe_io[1][0] = -1;
        // Slaughter child
        kill(pid, SIGKILL);
        // wait for child to terminate
        waitpid(pid, &status, 0);
        my_syslog(priv, "read error: " + reason);
        size    = 0;
        error   = true;
        running = false;
    } else if (size == 0) {
        // EOF. Close the pipe
        if (close(pipe_io[1][0])) {
            error = true;
            my_syslog(priv, "close error: " + string(strerror(errno)));
        }
        pipe_io[1][0] = -1;
    } else {
        // append to mail buffer
        spamc_output.append(iobuff, size);
    }
    return size;
}


void SpamAssassin::empty_and_close_pipe()
{
    while (read_pipe())
        ;
}


void SpamAssassin::closeall(int fd)
{
    int fdlimit = sysconf(_SC_OPEN_MAX);
    while (fd < fdlimit)
        close(fd++);
}


void SpamAssassin::warnmacro(char *macro, char *scope)
{
    if (warnedmacro) return;
    char buf[maxlen];
    snprintf(buf, sizeof(buf), "Could not retrieve sendmail macro %s. Add it to confMILTER_MACROS_%s for better results.", macro, scope);
    my_syslog(priv, buf);
    warnedmacro = true;
}

