/*
 * QScan virus filter for Qmail/Sophos antivirus
 * Jedi/Sector One <j@4u.net> 
 */

#include <config.h>
#include "qscan.h"
#include "qscan_p.h"
#ifdef WITH_DMALLOC
# include <dmalloc.h>
#endif

static void write_log(const int priority, const char * const format, ...)
{
    va_list args;
    
    va_start(args, format);    
    openlog("qscan", LOG_NDELAY | LOG_PID, LOG_MAIL);
    vsyslog(priority, format, args);
    closelog();
    va_end(args);
}

static void perror_log(const char * const msg)
{
    write_log(LOG_ERR, "%s : %s", msg, strerror(errno));
}

static int spawn_ripmime(void)
{
    if (pipe(ripmime_pipe) != 0) {
        perror_log("pipe");
        return -1;
    }
    if ((ripmime_pid = fork()) == (pid_t) -1) {
        perror_log("fork");
        (void) close(ripmime_pipe[0]);        
        (void) close(ripmime_pipe[1]);
        return -1;
    }
    if (ripmime_pid == (pid_t) 0) {
        if (close(ripmime_pipe[1]) != 0) {
            perror_log("close");
            exit(EXIT_FAILURE);
        }
        if (ripmime_pipe[0] != 0) {
            if (dup2(ripmime_pipe[0], 0) == -1) {
                perror_log("dup2");
                exit(EXIT_FAILURE);
            }
            if (close(ripmime_pipe[0]) != 0) {
                perror_log("close");
                exit(EXIT_FAILURE);
            }
        }
        (void) execlp(RIPMIME, RIPMIME, RIPMIME_ARGS, ripmime_destdir,
                      (char *) NULL);
        perror_log("execlp");
        exit(EXIT_FAILURE);
    }
    if (close(ripmime_pipe[0]) != 0) {
        perror_log("close");
        return -1;
    }
    return 0;
}

static int spawn_qq(void)
{
    if (ripmime_pid <= (pid_t) 0) {
        perror_log("need ripmime before qq");
        return -1;
    }
    if (pipe(qq_pipe) != 0) {
        perror_log("pipe");
        return -1;
    }
    if (pipe(qq_pipe_envelope) != 0) {
        perror_log("pipe");
        return -1;
    }
    if ((qq_pid = fork()) == (pid_t) -1) {
        perror_log("fork");
        (void) close(qq_pipe[0]);        
        (void) close(qq_pipe[1]);
        (void) close(qq_pipe_envelope[0]);        
        (void) close(qq_pipe_envelope[1]);
        return -1;
    }
    if (qq_pid == (pid_t) 0) {
        if (close(ripmime_pipe[1]) != 0) {
            perror_log("close");
            exit(EXIT_FAILURE);
        }
        if (close(qq_pipe[1]) != 0) {
            perror_log("close");
            exit(EXIT_FAILURE);
        }
        if (qq_pipe[0] != 0) {
            if (dup2(qq_pipe[0], 0) == -1) {
                perror_log("dup2");
                exit(EXIT_FAILURE);
            }
            if (close(qq_pipe[0]) != 0) {
                perror_log("close");
                exit(EXIT_FAILURE);
            }
        }
        if (close(qq_pipe_envelope[1]) != 0) {
            perror_log("close");
            exit(EXIT_FAILURE);
        }
        if (qq_pipe_envelope[0] != 1) {
            if (dup2(qq_pipe_envelope[0], 1) == -1) {
                perror_log("dup2");
                exit(EXIT_FAILURE);
            }
            (void) close(qq_pipe_envelope[1]);
        }
        (void) execlp(QMAIL_QUEUE, QMAIL_QUEUE, (char *) NULL);
        perror_log("execlp");
        exit(EXIT_FAILURE);
    }
    if (close(qq_pipe[0]) != 0) {
        perror_log("close");
        return -1;
    }
    return 0;
}

static int find_concurrency(void)
{
    DIR *dir;
    const struct dirent *entry;
    int concurrency = 0;
    
    if ((dir = opendir(RIPMIME_DESTDIR_BASE)) == NULL) {
        perror_log("unable to opendir() the ripmime base directory");	
        return -1;
    }
    while ((entry = readdir(dir)) != NULL) {
	if (entry->d_name == NULL || *(entry->d_name) == '.') {
	    continue;
	}
	concurrency++;
	if (concurrency >= INT_MAX) {
	    break;
	}
    }
    if (closedir(dir) != 0) {
        perror_log("unable to closedir() the ripmime base directory");	
        return -1;
    }    
    return concurrency;
}

static int cleanup_destdir(void)
{
    DIR *dir;
    const struct dirent *entry;
    const char *found;
    int ret = 0;
    
    if (chdir(ripmime_destdir) != 0) {
        perror_log("unable to chdir to ripmime_destdir");
        return -1;
    }
    if ((dir = opendir(".")) == NULL) {
        perror_log("unable to opendir() ripmime_destdir");
        return -1;
    }
    while ((entry = readdir(dir)) != NULL) {
        if ((found = entry->d_name) == NULL ||
            (found[0] == '.' &&
             (found[1] == 0 ||
              (found[1] == '.' && found[2] == 0)))) {
            continue;
        }
        if (unlink(found) != 0) {
            ret = -1;
        }
    }
    if (closedir(dir) != 0) {
        perror_log("unable to closedir() ripmime_destdir");
        return -1;
    }
    return ret;
}

static int has_attachments(void)
{
    DIR *dir;
    const struct dirent *entry;
    const char *found;
    struct stat st;
    int ret = 0;

    if (chdir(ripmime_destdir) != 0) {
        perror_log("unable to chdir() to ripmime_destdir");
        return -1;
    }
    if ((dir = opendir(".")) == NULL) {
        perror_log("unable to opendir() ripmime_destdir");
        return -1;
    }
    while ((entry = readdir(dir)) != NULL) {
        if ((found = entry->d_name) == NULL ||
            (found[0] == '.' &&
             (found[1] == 0 ||
              (found[1] == '.' && found[2] == 0)))) {
            continue;
        }
        if (lstat(found, &st) != 0) {
            write_log(LOG_ERR, "unable to lstat [%s]", found);
            ret = -1;
        } else if (!S_ISREG(st.st_mode) || (st.st_mode & S_IRUSR) ==
                   (mode_t) 0) {
            write_log(LOG_ERR, "bad file [%s] to scan", found);
            if (unlink(found) != 0) {
                perror_log("unable to unlink a bad file");
            }
            ret = -1;            
        } else if (ret >= 0 && ret < INT_MAX) {
            ret++;
        }
    }
    if (closedir(dir) != 0) {
        perror_log("unable to closedir() ripmime_destdir");
        return -1;
    }
    return ret;
}

static void wait_everybody_and_die(const int ret)
{
    int status;
    pid_t pid;
    
    if (*ripmime_destdir != 0) {
        if (cleanup_destdir() != 0) {
            perror_log("error while cleaning up destdir before dying");
        }
        if (rmdir(ripmime_destdir) != 0) {
            perror_log("error while removing destdir before dying");
        }
    }
    if (ripmime_pipe[1] > 1) {
        (void) close(ripmime_pipe[1]);
    }
    if (qq_pipe[1] > 1) {
        (void) close(qq_pipe[1]);
    }
    if (qq_pipe_envelope[1] > 1) {
        (void) close(qq_pipe_envelope[1]);
    }
    if (av_pipe[0] > 1) {
        (void) close(av_pipe[0]);
    }
    (void) close(0);
    (void) close(1);
    for(;;) {            
        if ((pid = wait(&status)) == (pid_t) -1 && errno != EINTR) {
            exit(ret);
        }
    }
}

static int spawn_antivirus(char ** const foundvirus)
{
    char line[AV_LINE_MAX];
    FILE *avfp;
    int status;
    pid_t pid;
    int ret = 0;

    if (foundvirus == NULL) {
        perror_log("foundvirus is a NULL pointer");
        return -1;
    }
    *foundvirus = NULL;
    if (pipe(av_pipe) != 0) {
        perror_log("pipe");
        return -1;
    }
    if ((av_pid = fork()) == (pid_t) -1) {
        perror_log("fork");
        (void) close(av_pipe[0]);
        (void) close(av_pipe[1]);
        return -1;
    }
    if (av_pid == (pid_t) 0) {
        if (av_pipe[1] != 1) {
            if (dup2(av_pipe[1], 1) == -1) {
                perror_log("dup2");
                exit(EXIT_FAILURE);
            }
            if (close(av_pipe[1]) != 0) {
                perror_log("close");
                exit(EXIT_FAILURE);
            }
        }
        if (close(av_pipe[0]) != 0 ||
            close(qq_pipe[1]) != 0 ||
            close(qq_pipe_envelope[1]) != 0) {
            perror_log("close");
            exit(EXIT_FAILURE);
        }
        if (chdir(ripmime_destdir) != 0) {
            perror_log("ripmime_destdir vanished, won't scan");
            exit(EXIT_FAILURE);
        }
        (void) nice(SWEEP_NICE);
        (void) alarm(SWEEP_TIMEOUT);
        (void) execlp(SWEEP, SWEEP, SWEEP_ARGS, ripmime_destdir,
                      (char *) NULL);
        perror_log("execlp");
        exit(EXIT_FAILURE);
    }
    if (close(av_pipe[1]) != 0) {
        perror_log("close");
        wait_everybody_and_die(99);
    }
    if ((avfp = fdopen(av_pipe[0], "r")) == NULL) {
        perror_log("fdopen");
        wait_everybody_and_die(99);
    }
    while (fgets(line, (int) sizeof line, avfp) != NULL) {
        register char *virus;
        if ((virus = strstr(line, SWEEP_VIRUS_PREFIX)) != NULL) {
            register char *crlf;

            if (ret < INT_MAX) {
                ret++;
            }
            if (*foundvirus != NULL) {
                continue;
            }
            while ((crlf = strrchr(line, '\n')) != NULL ||
                   (crlf = strrchr(line, '\r')) != NULL) {
                *crlf = 0;
            }
            *virus = 0;
            virus = strrchr(line, ' ');
            *foundvirus = strdup(++virus);
        }        
    }
    if (fclose(avfp) != 0) {
        perror_log("close");
    }
    (void) close(av_pipe[0]);
    while ((pid = wait(&status)) == (pid_t) -1 && errno == EINTR);
    if (pid != av_pid) {
        if (close(qq_pipe[1]) != 0) {
            perror_log("close qq pipe");
        }
        if (close(qq_pipe_envelope[1]) != 0) {
            perror_log("close envelope pipe");
        }        
        if (pid == qq_pid) {
            perror_log("we were waiting for the scanner to die, qq died first");
        } else {
            perror_log("unexpected pid died");
        }
        while ((pid = wait(&status)) == (pid_t) -1 && errno == EINTR);
        if (pid == av_pid) {
            perror_log("we were waiting for qq to die, but scanner died");
        } else {
            perror_log("unexpected pid died");
        }
        wait_everybody_and_die(99);        
    }
    
    return ret;
}

static void delay_seconds(const time_t seconds)
{
#ifdef HAVE_NANOSLEEP
    struct timespec ts;
    
    ts.tv_sec = seconds;
    ts.tv_nsec = 0L;
    (void) nanosleep(&ts, NULL);
#elif defined(HAVE_USLEEP)
# ifdef USLEEP_RETURNS_INT
    if (usleep((unsigned long) seconds * 1000000UL) != 0) {
        if (errno == EINVAL) 
# endif
        {
            unsigned long wr = (unsigned long) seconds;
            
            do {
                (void) usleep(999999UL);
                wr--;
            } while (wr > 0UL);
        }
# ifdef USLEEP_RETURNS_INT
    }    
# endif
#else
    (void) sleep((unsigned int) seconds);
#endif
}

static int viruscan_destdir(void)
{
    char *foundvirus;
    int nb_attachments;
    int avret;
    int ret = 0;
    int tries = SCAN_MAX_RETRIES;
    
    if ((nb_attachments = has_attachments()) < 0) {
        perror_log("error while checking for attachments");
        return -1;
    }
    if (nb_attachments == 0) {
        if (rmdir(ripmime_destdir) != 0) {
            perror_log("unable to remove ripmime_destdir, supposed to be empty");
            if (cleanup_destdir() != 0) {
                perror_log("tried to cleanup destdir, but still got troubles");
            }
            if (rmdir(ripmime_destdir) != 0) {
                perror_log("last chance to unlink ripmime_destdir failed");
            }
        }
        *ripmime_destdir = 0;
        
        return 0;
    }
    while (find_concurrency() >= SCAN_CONCURRENCY) {
	if (tries <= 0) {
	    write_log(LOG_INFO, "max scanning concurrency reached, "
		      "intentionnaly delaying");
	    ret = -1;
	    goto clean;
	}
	tries--;
        delay_seconds((time_t) SCAN_DELAY_BETWEEN_RETRIES);
    }
    if ((avret = spawn_antivirus(&foundvirus)) < 0) {
        perror_log("antivirus failure");
        ret = -1;
	goto clean;
    } else if (avret > 0) {
        write_log(LOG_INFO, "%s in a mail from [%s]", foundvirus, 
                  getenv("TCPREMOTEIP"));
        if (avret > 1) {
            write_log(LOG_INFO, 
                      "%d other virus %s found in the same mail from [%s]",
                      avret - 1, avret > 2 ? "were" : "was",
                      getenv("TCPREMOTEIP"));
        }
        ret = 1;
    }
    clean:
    if (cleanup_destdir() != 0) {
        perror_log("destdir cleanup after virus checking failed");
    }
    if (rmdir(ripmime_destdir) != 0) {
        perror_log("unlinking ripmime_destdir failed after virus checking");
    }
    *ripmime_destdir = 0;    
    
    return ret;
}

static int create_destdir_name(void)
{
    (void) snprintf(ripmime_destdir, sizeof ripmime_destdir,
                    RIPMIME_DESTDIR_BASE "/%lu_%lu",
                    (unsigned long) now, (unsigned long) getpid());
    return 0;
}

static int create_destdir(void)
{
    if (create_destdir_name() != 0) {
        perror_log("unable to create the destdir name");
        return -1;
    }        
    if (mkdir(ripmime_destdir, (mode_t) 0750) != 0) {
        if (chdir(ripmime_destdir) != 0) {
            perror_log("unable to create/access ripmime_destdir");
            return -1;
        }
        if (cleanup_destdir() != 0) {
            perror_log("unable to clean up destdir");
            if (rmdir(ripmime_destdir) != 0) {
                perror_log("unable to remote destdir");
            }
            *ripmime_destdir = 0;
            
            return -1;
        }
    }
    return 0;
}

static int wait_ripmime(void)
{
    int status;
    pid_t pid;
    
    while ((pid = wait(&status)) == (pid_t) -1 && errno == EINTR);
    if (pid != ripmime_pid) {
        if (close(qq_pipe[1]) != 0) {
            perror_log("close qq pipe");
        }
        if (close(qq_pipe_envelope[1]) != 0) {
            perror_log("close envelope pipe");
        }        
        if (pid == qq_pid) {
            perror_log("we were waiting for ripmime to die, but qq died first");
        } else {
            perror_log("unexpected pid died");
        }
        while ((pid = wait(&status)) == (pid_t) -1 && errno == EINTR);
        if (pid == ripmime_pid) {
            perror_log("we were waiting for qq to die, but ripmime died");
        } else {
            perror_log("unexpected pid died");
        }
        wait_everybody_and_die(99);
    }    
    return 0;
}

static int safe_write(const int fd, const void *buf_, size_t count)
{
    ssize_t written;    
    register const char *buf = (const char *) buf_;

    while (count > (size_t) 0U) {
        for (;;) {
            if ((written = write(fd, buf, count)) <= (ssize_t) 0) {
                if (errno == EAGAIN) {
                    sleep(1);
                } else if (errno != EINTR) {
                    return -1;
                }
                continue;
            }
            break;
        }
        buf += written;
        count -= written;
    }
    return 0;
}

static int add_received(void)
{
    static const char *months[] = {
        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    };
    char date[256];
    struct tm *tm;
    
    if ((tm = gmtime(&now)) == NULL ||
        tm->tm_mon < 0 || tm->tm_mon > 11) {
        return -1;
    }
    (void) snprintf(date, sizeof date, 
                    "Received: from qmail by qscan (mail filter); "
                    "%d %s %d %02d:%02d:%02d +0000\n",
                    tm->tm_mday, months[tm->tm_mon], 
                    tm->tm_year + 1900,
                    tm->tm_hour, tm->tm_min, tm->tm_sec);
    return safe_write(qq_pipe[1], date, strlen(date));
}

static void timeout(int sig)
{
    (void) sig;
    
    wait_everybody_and_die(52);
}

static void check_bypass(void)
{
    if (getenv("QS_BYPASS") == NULL && getenv("TCPREMOTEIP") != NULL) {
        return;
    }
    (void) execlp(QMAIL_QUEUE, QMAIL_QUEUE, (char *) NULL);
    exit(99);
}

static int inject_message(void)
{
    char buf[MSG_BUFSIZE];
    ssize_t len;
    ssize_t ripmime_written;
    ssize_t qq_written;
    ssize_t written;    
    
    for (;;) {
        if ((len = read(0, buf, sizeof buf)) < (ssize_t) 0) {
            if (errno == EINTR) {
                continue;
            }
            perror_log("read() troubles when reading the message feed");
            return -1;
        }
        if (len == (ssize_t) 0) {
            return 0;
        }
        ripmime_written = qq_written = (ssize_t) 0;
        do {
            if ((written = write(ripmime_pipe[1], buf + ripmime_written,
                                 (size_t) (len - ripmime_written))) <
                (ssize_t) 0) {
                if (errno == EINTR) {
                    continue;
                }
                perror_log("write() troubles when feeding ripmime");
                return -1;                
            }
            ripmime_written += written;
            if ((written = write(qq_pipe[1], buf + qq_written,
                                 (size_t) (len - qq_written))) <
                (ssize_t) 0) {
                if (errno == EINTR) {
                    continue;
                }
                perror_log("write() troubles when feeding qq");
                return -1;                
            }
            qq_written += written;
        } while (ripmime_written < len || qq_written < len);
    }        
}
    
static int inject_envelope(void)
{
    char buf[ENVELOPE_BUFSIZE];
    ssize_t len;
    
    for (;;) {
	if ((len = read(1, buf, sizeof buf)) < (ssize_t) 0) {
	    if (errno == EINTR) {
		continue;
	    }
	    perror_log("read() troubles when reading the envelope");
	    return -1;
	}
	if (len == (ssize_t) 0) {
	    return 0;
	}
	if (safe_write(qq_pipe_envelope[1], buf, (size_t) len) != 0) {
	    perror_log("write() troubles when feeding the envelope");
	    return -1;
	}
    }
}    

int main(void)
{
    int nbviruses;
    
    (void) signal(SIGALRM, timeout);
    (void) alarm(QSCAN_TIMEOUT);
    now = time(NULL);
    check_bypass();
    if (create_destdir() != 0) {
        perror_log("unable to create the destination directory");
        return 1;
    }
    if (spawn_ripmime() != 0) {
        perror_log("unable to spawn ripmime");
        return 1;
    }
    if (spawn_qq() != 0) {
        perror_log("unable to spawn qq");
        return 1;
    }
    if (add_received() != 0) {
        perror_log("unable to add the received line");
        wait_everybody_and_die(99);
    }
    if (inject_message() != 0) {
        perror_log("unable to inject message");
        wait_everybody_and_die(99);        
    }
    if (close(ripmime_pipe[1]) != 0) {
        perror_log("close ripmime pipe");
        wait_everybody_and_die(99);
    }
    if (wait_ripmime() != 0) {
        perror_log("troubles when waiting for ripmime");
        wait_everybody_and_die(99);
    }
    if ((nbviruses = viruscan_destdir()) < 0) {
        write_log(LOG_INFO, "virus scanning delayed");
        wait_everybody_and_die(99);
    }
    if (nbviruses != 0) {
        write_log(LOG_INFO, "delivery from [%s] refused", 
                  getenv("TCPREMOTEIP"));
        wait_everybody_and_die(39);
    }
    if (inject_envelope() != 0) {
	perror_log("unable to inject the envelope to qq");
	wait_everybody_and_die(99);
    }
    if (close(qq_pipe[1]) != 0) {
        perror_log("close qq pipe");
        wait_everybody_and_die(99);
    }
    if (close(qq_pipe_envelope[1]) != 0) {
        perror_log("close envelope pipe");
        wait_everybody_and_die(99);
    }
    wait_everybody_and_die(0);
    
    return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1