/*
**    spasm - The spasm anti-spam milter
**    Copyright (C) 2002-2004 Sean Hanson <aradia@theasylum.org>
**
**    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
*/

#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pwd.h>
#include <regex.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/signal.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sysexits.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <libmilter/mfapi.h>

#include "spasm.h"
#include "dbfunc.h"

#define SPASM_DAEMON
#include "filters.h"

/* Filter status structure */
struct filterStatus {
    char *filter;
    int status;
    struct filterStatus *next;
};

/* Recipient structure */
struct spasmRecipient {
    char *rcpt;
    char *username;
    char *domain;
    char *quarantinefile;
    FILE *quarantinefp;
    char *rcptfile;
    int status;
    int av;
    int sa;
    int logging;
    int subject_tagging;
    int use_domain_log;
    struct filterStatus *fstatus;
    struct spasmRecipient *next;
};

/* Private data structure */
struct mlfiPriv {
    char *hostname;
    char *hostdomain;
    unsigned long int ipaddress;
    char *helohost;
    char *helodomain;
    char *mailfrom;
    char *mailfromdomain;
    char *ipstring;
    struct spasmRecipient *rcpts;
    int unknownusers;
};

#define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx))

/* Various random thingies */
#define DOMAIN_NAME_REGEX "([^.]*[.][^.]*)[.]*$"
#define CIDR_REGEX "^[[:digit:]]{1,3}[.][[:digit:]]{1,3}[.][[:digit:]]{1,3}[.][[:digit:]]{1,3}[/][[:digit:]]{2}$"
static regex_t re_sld;
static regex_t re_cidr;
static char *progname;

static const SPASM_FILTER *pwl_only_fltr;

void spasm_error(int syslevel, char *errstr, ...)
{
    va_list arglist;
    char spasm_errstr[1024];

    va_start(arglist, errstr);
    vsnprintf(spasm_errstr, sizeof(spasm_errstr), errstr, arglist);
    va_end(arglist);

    syslog(syslevel, "%s", spasm_errstr);
}

/* Zombie prevention */
#ifdef WANT_SOME_SIGNALS_SOMEDAY
static void
sigCHLD()
{
/*
    int status;
    while(wait3(&status, WNOHANG, (struct rusage *) 0) >= 0);
*/
	/* Sit around and twiddle our thumbs */
}
#endif

/* SIGINT, SIGTERM */
#ifdef WANT_SOME_SIGNALS_SOMEDAY
static void
cleanup()
{
    int status;
    regfree(&re_sld);
    regfree(&re_cidr);
    syslog(LOG_INFO, "(cleanup) Shutting down");
    closelog();
    while(wait3(&status,WNOHANG,(struct rusage*) 0) >= 0);
    exit(0);
}
#endif

/* Try to catch SEGVs */
#ifdef WANT_SOME_SIGNALS_SOMEDAY
static void
sigSEGV()
{
    regfree(&re_sld);
    regfree(&re_cidr);
    syslog(LOG_ERR, "(sigSEGV) Caught SIGSEGV");
    closelog();
    exit(0); /* Never gets here */
}
#endif

/* lowercase() - set a string to lowercase letters for case-insensitive matching */
static void
lowercase(char *buf)
{
    if (buf) {
        while (*buf) {
            if (isupper(*buf))
                *buf = tolower(*buf);
            buf++;
        }
    }
}

/*
 * create global pointer(s) to filter data structures that require O(1)
 * access time.
 */
static void
cache_filter_ptrs(void)
{
    const SPASM_FILTER *p;

    p = spasm_filters;
    while (p->db_name) {
	if (p->adhoc_id == SPASM_WL_ONLY) {
	    pwl_only_fltr = p;
	    break;
	}
	p++;
    }
}

/* isCIDR() - determine whether a string is a CIDR entry */
int isCIDR(char *entry)
{
    regmatch_t pmatch;
    int rv = 0;

    if(regexec(&re_cidr, entry, 1, &pmatch, 0) == 0) {
	rv = 1;
    }
    return rv;
}

/* inCIDR() - check a host address against a CIDR block */
int inCIDR(char *ipstring, unsigned long int ip, char *cidrc)
{
    char *cidr;
    char *chr;
    struct in_addr in;	/* cidr address */
    struct in_addr in2; /* host address */
    unsigned long hosts, ip1, ip2, ip3;
    
    if((ipstring == NULL && ip == 0) || cidr == NULL) {
	return 1;
    }
    if((cidr = strdup(cidrc)) == NULL) {
	syslog(LOG_ERR, "(inCIDR) Error allocating memory for cidr: %m");
	return 1;
    }
    if((chr = strchr(cidr, '/')) == NULL) {
	free(cidr);
	return 1;
    }
    *chr = '\0';
    chr++;
    hosts = 1<<(32-(atoi(chr)));
    if(inet_aton(cidr, &in) == 0) {
	free(cidr);
	return 1;
    } else {
	ip1 = ntohl(in.s_addr);
    }
    free(cidr);
    if(ipstring != NULL) {
	if(inet_aton(ipstring, &in2) == 0) {
	    return 1;
	} else {
	    ip2 = ntohl(in2.s_addr);
	}
    } else {
	ip2 = ntohl(ip);
    }
    ip3 = ip1 + hosts - 1;
    if(ip2 >= ip1 && ip2 <= ip3) return 1;
    return 0;
}

/* add_header() - add header[s] to a quarantine file */
void add_header(char *qfile, char *header)
{
    int fd;
    FILE *ifp, *ofp;
    char *fname;
    int i;
    char fbuf[1024];

    if(header == NULL || qfile == NULL) return;
    if((fname = strdup(qfile)) == NULL) {
	syslog(LOG_ERR, "(add_header) Error duplicating qfile: %m");
	return;
    }
    for(i=1;i<7;i++) *(fname+strlen(qfile)-i) = 'X';
    if((ifp = fopen(qfile, "r")) < 0) {
	syslog(LOG_ERR, "(add_header) Error opening qfile: %m");
	free(fname);
	return;
    }
    if((fd = mkstemp(fname)) < 0 ) {
	syslog(LOG_ERR, "(add_header) Error opening temp file: %m");
	free(fname);
	fclose(ifp);
	return;
    }
    chmod(fname, 0644);
    if((ofp = fdopen(fd, "a")) == NULL) {
	syslog(LOG_ERR, "(add_header) Error opening temp file stream: %m");
	free(fname);
	fclose(ifp);
	close(fd);
    }
    while(fgets(fbuf, sizeof(fbuf), ifp)) {
	if(strcmp(fbuf, "\r\n") == 0) break;
	fprintf(ofp, "%s", fbuf);
    }
    fprintf(ofp, "%s\r\n", header);
    while(fgets(fbuf, sizeof(fbuf), ifp)) {
	fprintf(ofp, "%s", fbuf);
    }
    fclose(ifp);
    fclose(ofp);
    if(rename(fname, qfile) != 0) syslog(LOG_ERR, "(add_header) Error replacing qfile: %m");
    free(fname);
    return;
}

/* add_filter_status() - add a filter status entry */
void add_filter_status(struct spasmRecipient *srecpt, char *filter, int status) {
    struct filterStatus *fstatus;

    if(srecpt == NULL || filter == NULL) return;

    if(srecpt->fstatus == NULL) {
	if((srecpt->fstatus = calloc(sizeof(struct filterStatus), 1)) == NULL) {
	    syslog(LOG_ERR, "(add_filter_status) Error allocating memory for priv->fstatus: %m");
	} else {
	    fstatus = srecpt->fstatus;
	    fstatus->filter = strdup(filter);
	    fstatus->status = status;
	    fstatus->next = NULL;
	}
    } else {
	fstatus = srecpt->fstatus;
	while(fstatus->next != NULL) fstatus = fstatus->next;
	if((fstatus->next = calloc(sizeof(struct filterStatus), 1)) == NULL) {
	    syslog(LOG_ERR, "(add_filter_status) Error allocating memory for fstatus->next: %m");
	} else {
	    fstatus = fstatus->next;
	    fstatus->filter = strdup(filter);
	    fstatus->status = status;
	    fstatus->next = NULL;
	}
    }
    if(srecpt->status < status) srecpt->status = status;
}

/* filter_lookup() - lookup single filter entry */
int filter_lookup(char *username, char *domain, char *entry)
{
    int rv = 0;
    char *email;

    if(DEBUGGING) syslog(LOG_DEBUG, "(filter_lookup) Starting (%s@%s -> %s)...", username, domain, entry);
    if((email = calloc(strlen(username) + strlen(domain) + 2, 1)) == NULL) {
        syslog(LOG_ERR, "(filter_lookup) Error allocating memory for email: %m");
        return 0;
    }
    sprintf(email, "%s@%s", username, domain);
    rv = spasm_lookup_filter(email, entry);
    free(email);
    if(DEBUGGING) syslog(LOG_DEBUG, "(filter_lookup) Finished (rv=%d)", rv);
    return rv;
}

/* wl_lookup() - lookup single whitelist entry */
int wl_lookup(char *username, char *domain, char *entry)
{
    int rv = 0;
    char *email;

    if(DEBUGGING) syslog(LOG_DEBUG, "(wl_lookup) Starting (%s@%s -> %s)...", username, domain, entry);
    if((email = calloc(strlen(username) + strlen(domain) + 2, 1)) == NULL) {
	syslog(LOG_ERR, "(wl_lookup) Error allocating memory for email: %m");
	return 0;
    }
    sprintf(email, "%s@%s", username, domain);
    rv = spasm_lookup_wl(email, entry);
    free(email);
    if(DEBUGGING) syslog(LOG_DEBUG, "(wl_lookup) Finished (rv=%d)", rv);
    return rv;
}

/* add_to_blacklist() - add an entry to a blacklist textfile */
void add_to_blacklist(char *blfile, char *key)
{
    syslog(LOG_INFO, "(add_to_blacklist) Adding %s to %s", key, blfile);
    spasm_insert_bl(blfile, key, "1");
    return;
}

/* log_rejection() - Log rejected spam in per-user logfiles */
void log_rejection(int level, char *rcptfile, char *rcpt, char *mailfrom, char *hostname, char *ipstring, char *helo, struct filterStatus *fstatus)
{
    int fd;
    time_t timep;
    char times[35];
    char *buf;
    char *reason;
    struct flock lockinfo;
    size_t strsize;
    struct filterStatus *cur;
    size_t fsize = 0;
    struct timeval tv;
    FILE *devnull;
    int tvl;

    lowercase(rcptfile);
    lockinfo.l_type = F_WRLCK;
    lockinfo.l_start = 0;
    lockinfo.l_whence = SEEK_SET;
    lockinfo.l_len = 0;

    if(rcptfile == NULL) {
	syslog(LOG_ERR, "(log_rejection) Error: rcptfile is NULL");
	return;
    }

    if((buf = sp_strcat(LOG_DIRECTORY, rcptfile)) == NULL) return;

    cur = fstatus;
    while(cur != NULL) {
	if(level && (cur->status >= level)) fsize += (strlen(cur->filter)+1);
	cur = cur->next;
    }

    if(fsize == 0) return;

    fd = open(buf, O_WRONLY|O_CREAT|O_APPEND, 0644);
    if(fd < 0) {
	syslog(LOG_ERR, "(log_rejection) Error opening file %s: %m", buf);
	free(buf);
	return;
    }
    if(fcntl(fd, F_SETLKW, &lockinfo) == -1) {
	syslog(LOG_ERR, "(log_rejection) Error locking file %s: %m", buf);
	free(buf);
	close(fd);
	return;
    }
    free(buf);
    if((reason = calloc(fsize+1, 1)) == NULL) {
        syslog(LOG_ERR, "(log_rejection) Error allocating memory for reason: %m");
	close(fd);
        return;
    }
    cur = fstatus;
    while(cur != NULL) {
        if(level && (cur->status >= level)) {
            strcat(reason, cur->filter);
            strcat(reason, "|");
        }
        cur = cur->next;
    }

    gettimeofday(&tv, NULL);
    devnull = fopen("/dev/null", "wb");
    tvl = fprintf(devnull, "%ld%ld", tv.tv_sec, tv.tv_usec);
    fclose(devnull);
    timep = time(NULL);
    memset(times, '\0', sizeof(times));
    sprintf(times, "%s", (char *)ctime((time_t*)&timep));
    /* Get rid of ctime()'s newline */
    times[strlen(times)-1] = '\0';
    strsize = tvl + strlen(rcpt) + strlen(mailfrom) + strlen(hostname) + strlen(ipstring) + strlen(reason) + strlen(times) + 11;
    if(helo != NULL) strsize += strlen(helo);
    else strsize += 6;
    if((buf = calloc(strsize, 1)) != NULL) {
	sprintf(buf, "%ld%ld:%s:%s:%s:%s:%s:%s:%s\n", tv.tv_sec, tv.tv_usec, times, rcpt, mailfrom, hostname, ipstring, helo, reason);
	write(fd, buf, strlen(buf));
	free(buf);
    }
    close(fd);
    free(reason);
    return;
}

/* userwl_lookup() - Lookup a whitelist entry */
int userwl_lookup(char *username, char *domain, char *mailfrom, char *hostname, char *ipstring)
{
    char *ipstring2;
    char *buf;
    int rv = 0;

    if(DEBUGGING) syslog(LOG_DEBUG, "(userwl_lookup) Starting");
    ipstring2 = strdup(ipstring);
    if(ipstring2 == NULL) {
	syslog(LOG_ERR, "(userwl_lookup) Error allocating memory for ipstring2: %m");
	return 0;
    }

    /* Lookup mailfrom */
    if(wl_lookup(username, domain, mailfrom)) rv = 1;
    /* Lookup hostname */
    else if(wl_lookup(username, domain, hostname)) rv = 1;
    /* Lookup ip address */
    else if(wl_lookup(username, domain, ipstring2)) rv = 1;
    else {
	/* Check all subdomains in hostname */
	buf = hostname;
        while((buf = strchr(buf, '.')) && rv == 0) {
	    buf++;
	    if(wl_lookup(username, domain, buf)) rv = 1;
	}
	/* Check all pseudo-classful network blocks in ip address */
	while((buf = strrchr(ipstring2, '.')) && rv == 0) {
	    *buf = '\0';
	    if(wl_lookup(username, domain, ipstring2)) rv = 1;
	}
    }
    /* Check CIDR blocks */
    if(rv == 0) {
        char *email;
        struct spasm_list *first, *cur;

        if((email = calloc(strlen(username) + strlen(domain) + 2, 1)) == NULL) {
            syslog(LOG_ERR, "(cidr_lookup) Error allocating memory for email: %m");
        } else {
            sprintf(email, "%s@%s", username, domain);
            first = spasm_get_wl(email);
            cur = first;
            while(cur != NULL) {
                if(rv != 1 && isCIDR(cur->entry) && inCIDR(ipstring, 0, cur->entry)) rv = 1;
                first = cur->next;
                free(cur->entry);
                free(cur);
                cur = first;
            }
            if(rv == 0) {
                first = spasm_get_wl(domain);
                cur = first;
                while(cur != NULL) {
                    if(rv != 1 && isCIDR(cur->entry) && inCIDR(ipstring, 0, cur->entry)) rv = 1;
                    first = cur->next;
                    free(cur->entry);
                    free(cur);
                    cur = first;
                }
            }
            free(email);
	}
    }

    free(ipstring2);
    if(DEBUGGING) syslog(LOG_DEBUG, "(userwl_lookup) Finished");
    return rv;
}

/* user_lookup() - verifies a user for unknownusers count */
int user_lookup(char *username, char *domain)
{

#ifndef VERIFY_USERS
    return 1;
#endif

    /* First, check local users */
    if(strcmp(domain, DEFAULT_DOMAIN) == 0) {
#ifdef VERIFY_USERS_IN_PASSWD
	if(getpwnam(username) != NULL) return 1;
#endif

#ifdef VERIFY_USERS_IN_ALIASES
	{
            FILE *fp;
            char buf[256];

            if((fp = fopen("/etc/mail/aliases", "r")) == NULL) {
		syslog(LOG_ERR, "(user_lookup) Error opening /etc/mail/aliases: %m");
		return 1;
	    }
            while(fgets(buf, sizeof(buf), fp) != NULL) {
                if(strncasecmp(buf, username, strlen(username)) == 0) {
	            fclose(fp);
	            return 1;
                }
            }
            fclose(fp);
        }
#endif
	return 0;
    }

    /* Add custom user lookups here */

    return 1;
}

/* dnsbl_lookup() - Lookup a hostname, return 1 if found, 0 if !found */
int dnsbl_lookup(char *hostname)
{
    struct hostent hostbuf, *hp;
    size_t hstbuflen;
    char *tmphstbuf;
    int res;
    int herr;

    if(DEBUGGING) syslog(LOG_DEBUG, "(dnsbl_lookup) Starting");
    if(hostname == NULL) return 0;
    hstbuflen = 1024;
    tmphstbuf = (char *)malloc(hstbuflen);
    while((res = gethostbyname_r(hostname, &hostbuf, tmphstbuf, hstbuflen, &hp, &herr)) == ERANGE)
    {
        hstbuflen *= 2;
        tmphstbuf = (char *)realloc(tmphstbuf, hstbuflen);
    }
    free(tmphstbuf);
    if(DEBUGGING) syslog(LOG_DEBUG, "(dnsbl_lookup) Finished");
    if(res || hp == NULL) {
        if(hp != NULL) free(hp);
        return 0;
    }
    return 1;
}

/* search_regex() - Perform an individual regex */
int search_regex(char *pattern, char *key)
{
    regmatch_t pmatch;
    regex_t re;

    if(pattern == NULL || key == NULL) return 0;
    if(regcomp(&re, pattern, REG_EXTENDED|REG_ICASE) != 0) {
	syslog(LOG_ERR, "(search_regex) Error compiling regex \"%s\": %m", pattern);
	return 0;
    }
    if(regexec(&re, key, 1, &pmatch, 0) == 0) {
	regfree(&re);
	return 1;
    }
    regfree(&re);
    return 0;
}

/* search_regex_spamtraps() = Run each entry in spamtrap database against arguments */
int search_regex_spamtraps(char *key)
{
    struct spasm_list *entries = NULL;
    struct spasm_list *cur = NULL;
    int rv = 0;

    entries = spasm_get_re(SPAMTRAPS_DB);
    cur = entries;
    while(cur != NULL) {
	if(search_regex(cur->entry, key) == 1) {
	    rv = 1;
	    break;
	}
	cur = cur->next;
    }
    cur = entries;
    while(cur != NULL) {
	entries = cur->next;
	if(cur->entry != NULL) free(cur->entry);
	free(cur);
	cur = entries;
    }
    return rv;
}

/* search_regex_envfrom() - Run each entry in envfrom database against argument */
int search_regex_envfrom(char *key)
{
    struct spasm_list *entries = NULL;
    struct spasm_list *cur = NULL;
    int rv = 0;

    entries = spasm_get_re(ENVFROM_DB);
    cur = entries;
    while(cur != NULL) {
	if(search_regex(cur->entry, key) == 1) {
	    rv = 1;
	    break;
	}
	cur = cur->next;
    }
    cur = entries;
    while(cur != NULL) {
	entries = cur->next;
	if(cur->entry != NULL) free(cur->entry);
	free(cur);
	cur = entries;
    }
    return rv;
}

/* filter_local() - Lookup entries in local db4 hash */
int filter_local(char *dbname, char *mailfrom, char *mailfromdomain, char *hostname, char *ipstring)
{
    char *buf;
    char *ipstring2;
    int rv = 0;

    if(DEBUGGING) syslog(LOG_DEBUG, "(filter_local) Starting");
    if((ipstring2 = strdup(ipstring)) == NULL) {
	syslog(LOG_ERR, "(filter_local) Error allocating memory for ipstring2: %m");
	return -1;
    }

    /* Lookup mailfrom address */
    if(spasm_lookup_bl(dbname, mailfrom)) rv = 1;

    /* Lookup mailfrom address's domain */
    else if(spasm_lookup_bl(dbname, mailfromdomain)) rv = 1;

    /* Lookup server hostname */
    else if(spasm_lookup_bl(dbname, hostname)) rv = 1;

    /* Lookup server ip address */
    else if(spasm_lookup_bl(dbname, ipstring2)) rv = 1;
    
    /* Lookup all subdomains of server hostname */
    if(hostname != NULL) {
        buf = hostname;
        while((buf = strchr(buf, '.')) && rv != 1) {
	    buf++;
	    if(spasm_lookup_bl(dbname, buf)) rv = 1;
        }
    }

    /* Lookup pseudo-classful network blocks in ip address */
    while((buf = strrchr(ipstring2, '.')) && rv != 1) {
	*buf = '\0';
	if(spasm_lookup_bl(dbname, ipstring2)) rv = 1;
    }

    /* Check CIDR blocks */
    if(rv == 0) {
        struct spasm_list *first, *cur;

        first = spasm_get_bl(dbname);
        cur = first;
        while(cur != NULL) {
            if(rv != 1 && isCIDR(cur->entry) && inCIDR(ipstring, 0, cur->entry)) rv = 1;
            first = cur->next;
            free(cur->entry);
            free(cur);
            cur = first;
        }
    }

    free(ipstring2);
    if(DEBUGGING) syslog(LOG_DEBUG, "(filter_local) Finished");
    return rv;
}

/* filter_rDNS() - Determine if ip address was resolved */
int filter_rDNS(char *hostname)
{
    if(hostname == NULL) return 0;
    if(strncmp(hostname, "[", 1) == 0) {
	return 1;
    }
    return 0;
}

/* filter_HELO() - Reject if no HELO/EHLO given, or argument != resolved hostname */
int filter_HELO(char *helodomain, char *hostdomain)
{
    if(hostdomain == NULL) return 0;
    if(helodomain == NULL) return 1;
    if(strcmp(helodomain, hostdomain) != 0) return 1;
    return 0;
}

/* filter_MAIL_HOST() - Determine if domain of mailfrom == domain of hostname */
int filter_MAIL_HOST(char *mailfromdomain, char *hostdomain)
{
    if(mailfromdomain == NULL || hostdomain == NULL) return 0;
    if(strcmp(mailfromdomain, hostdomain) == 0) return 0;
    else return 1;
}

/* filter_AV() - Scan message for viruses */
int filter_AV(char *qfile)
{
    int pfd1[2], pfd2[2];
    int pid;
    char fbuf[1024];
    int status;

    if(qfile == NULL) {
        return 0;
    }
    if(pipe(pfd1) < 0) {
        syslog(LOG_ERR, "(filter_AV) Error creating AV pipe pfd1: %m");
        return 0;
    }
    if(pipe(pfd2) < 0) {
	syslog(LOG_ERR, "(filter_AV) Error creating AV pipe pfd2: %m");
	close(pfd1[0]);
	close(pfd1[1]);
	return 0;
    }
    if((pid = fork()) < 0) {
        syslog(LOG_ERR, "(filter_AV) Error forking AV process: %m");
        close(pfd1[0]);
        close(pfd1[1]);
	close(pfd2[0]);
	close(pfd2[1]);
        return 0;
    }
    if(pid == 0) { /* Child process */
        extern int errno;
        dup2(pfd1[0], 0);
	dup2(pfd2[1], 1);
        close(pfd1[1]);
	close(pfd2[0]);
        execl(VIRUS_BIN, VIRUS_NAME, VIRUS_ARG1, qfile, 0);
        /* Shouldn't get here */
        syslog(LOG_ERR, "(filter_av) Error executing %s (%d): %m", VIRUS_NAME, errno);
        exit(128);
    }
    close(pfd1[0]);
    close(pfd2[1]);
    close(pfd1[1]);
    memset(fbuf, 0, sizeof(fbuf));
    read(pfd2[0], fbuf, sizeof(fbuf));
    close(pfd2[0]);
    if(waitpid(pid, &status, 0) == -1) {
	syslog(LOG_ERR, "(filter_AV) Error waiting for child: %m");
    }
    else if(WIFEXITED(status) == 0) {
        syslog(LOG_ERR, "(filter_AV) Exited non-normally: (%d)!", status);
    }
    else if(WEXITSTATUS(status) == VIRUS_FOUND) {
	char *chr;
	char virus[512];
	memset(virus, 0, sizeof(virus));
	if((chr = strstr(fbuf, "*** Found virus ")) != NULL) {
	    sscanf(chr, "*** Found virus %511s", virus);
	    snprintf(fbuf, sizeof(fbuf), "X-spasm-Virus: %s\r\n", virus);
	    add_header(qfile, fbuf);
	} else {
	    add_header(qfile, "X-spasm-Virus: Found\r\n");
	}
	return 1;
    }
    return 0;
}

/* filter_SA() - Check message against SpamAssassin */
int filter_SA(char *qfile, int required_hits)
{
    int rv = 0;
    int fd, sock;
    char fbuf[1024];
    char *response = NULL;
    size_t size, len;
    char *sa_report;
    char *chr;
    char *header = NULL;
    struct sockaddr_in sin;
    float sa_score;
    struct stat qfstat;

    if(qfile == NULL || required_hits == 0) {
        return 0;
    }
    if((fd = open(qfile, O_RDONLY)) < 0) {
        syslog(LOG_ERR, "(filter_SA) Error opening qfile: %m");
        return 0;
    }
    fstat(fd, &qfstat);
    if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
	syslog(LOG_ERR, "(filter_SA) Error creating socket: %m");
	close(fd);
	return 0;
    }
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SA_PORT);
    if(inet_aton(SA_HOST, &sin.sin_addr) == 0) {
	close(fd);
	close(sock);
	return 0;
    }
    if(connect(sock, (struct sockaddr *)&sin, sizeof(struct sockaddr)) == -1) {
	syslog(LOG_ERR, "(filter_SA) Error connecting to spamd: %m");
	close(fd);
	close(sock);
	return 0;
    }
    size = snprintf(fbuf, sizeof(fbuf), "SYMBOLS SPAMC/1.3\r\nContent-length: %ld\r\nUser: %s\r\n\r\n", qfstat.st_size, SPASM_USER);
    send(sock, fbuf, size, 0);
    while((size = read(fd, fbuf, sizeof(fbuf))) > 0) {
        send(sock, fbuf, size, 0);
    }
    close(fd);
    shutdown(sock, 1);
    memset(fbuf, 0, sizeof(fbuf));
    len=1; /* initialise with length of required \0 terminator */
    if((response = calloc(1, 1)) == NULL) {
	syslog(LOG_ERR, "(filter_SA) Error allocating memory for response: %m");
    }
    while((size = read(sock, fbuf, sizeof(fbuf))) > 0) {
	len += size;
	if((response = realloc(response, len)) == NULL) {
	    syslog(LOG_ERR, "(filter_SA) Error reallocating memory for response: %m");
	} else {
	    strncat(response, fbuf, len);
	}
    }
    close(sock);
    sa_report = strstr(response, "Spam:");
    sscanf(sa_report, "Spam: %*s ; %f / %*f", &sa_score);
    sa_report = strstr(response, "\r\n\r\n");
    sa_report += 4;
    if(sa_score >= required_hits) {
	snprintf(fbuf, sizeof(fbuf), "X-spasm-SA: %.2f ; ", sa_score);
	len = strlen(fbuf) + 3;
	header = calloc(len, 1);
	sprintf(header, "%s", fbuf);
	while(1) {
	    char *chr2;
	    if((chr = strchr(sa_report, ',')) == NULL) break;
	    *chr = '\0';
	    while(strlen(sa_report) < 50) {
		chr2 = chr;
		*chr = ',';
		chr++;
		if((chr = strchr(chr, ',')) != NULL) {
		    *chr = '\0';
		} else {
		    chr2 = NULL;
		    break;
		}
	    }
	    if(chr2 != NULL) {
		*chr = ',';
		*chr2 = '\0';
	    }
	    len += (strlen(sa_report) + 4);
	    header = realloc(header, len);
	    strcat(header, "\r\n\t");
	    strcat(header, sa_report);
	    if(chr2 == NULL) break;
	    sa_report = chr2 + 1;
	}
	add_header(qfile, header);
	free(header);
	rv = 1;
    }
    free(response);
    return rv;
}

/* deliverQuarantine() - Deliver a quarantine file */
int deliverQuarantine(char *from, char *rcpt, char *qfile)
{
    int pfd[2];
    int pid;
    char fbuf[1024];
    int fd;
    int status;
    int size;

    if(from == NULL || rcpt == NULL || qfile == NULL) {
	return -1;
    }
    if((fd = open(qfile, O_RDONLY)) < 0) {
	syslog(LOG_ERR, "(deliverQuarantine) Error opening qfile (%s): %m", qfile);
	return -1;
    }
    if(pipe(pfd) < 0) {
        syslog(LOG_ERR, "(deliverQuarantine) Error creating mail pipe: %m");
	close(fd);
        return -1;
    }
    if((pid = fork()) < 0) {
        syslog(LOG_ERR, "(deliverQuarantine) Error forking mail process: %m");
	close(fd);
        close(pfd[0]);
        close(pfd[1]);
        return -1;
    }
    if(pid == 0) { /* Child process */
        extern int errno;
        dup2(pfd[0], 0);
        close(pfd[1]);
	close(fd);
        execl(SENDMAIL_PATH, "sendmail", "-f", from, "--", rcpt, 0);
        /* Shouldn't get here */
        syslog(LOG_ERR, "(deliverQuarantine) Error executing sendmail (%d): %m", errno);
        exit(128);
    }
    close(pfd[0]);
    while((size = read(fd, fbuf, sizeof(fbuf))) > 0) {
        write(pfd[1], fbuf, size);
    }
    close(fd);
    close(pfd[1]);
    if(waitpid(pid, &status, 0) == -1) {
	syslog(LOG_ERR, "(deliverQuarantine) Error waiting for child: %m");
    }
    else if(WIFEXITED(status) == 0) {
	syslog(LOG_ERR, "(deliverQuarantine) Exited non-normally: (%d)!", status);
    }
    else if(WEXITSTATUS(status) != 0) {
        syslog(LOG_ERR, "(deliverQuarantine) Error delivering quarantine file (%d): %s", WEXITSTATUS(status), strerror(WEXITSTATUS(status)));
        return -1;
    }
    return 0;
}

sfsistat mlfi_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr)
{
    struct mlfiPriv *priv;
    regmatch_t pmatch;
    struct spasm_list *first, *cur;
    int i;

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_connect) Starting");

    /* Don't scan messages from localhost */
    if(inCIDR(NULL, ((struct sockaddr_in *)hostaddr)->sin_addr.s_addr, "127.0.0.1/32"))
	return SMFIS_ACCEPT;

    /* Don't scan messages from global IP whitelist */
    i = 0;
    first = spasm_get_wl(GLOBAL_IP_WL);
    cur = first;
    while(cur != NULL) {
	if(i != 1 && isCIDR(cur->entry) && inCIDR(NULL, ((struct sockaddr_in *)hostaddr)->sin_addr.s_addr, cur->entry)) i = 1;
	first = cur->next;
	free(cur->entry);
	free(cur);
	cur = first;
    }
    if(i) return SMFIS_ACCEPT;

    if((priv = calloc(1, sizeof(*priv))) == NULL) {
	syslog(LOG_ERR, "(mlfi_connect) Error allocating memory for priv: %m");
	return SMFIS_CONTINUE;
    }
    priv->hostname = strdup(hostname);
    if(priv->hostname == NULL) {
	syslog(LOG_ERR, "(mlfi_connect) Error allocating memory for priv->hostname: %m");
	free(priv);
	return SMFIS_CONTINUE;
    }
    lowercase(priv->hostname);
    priv->ipaddress = ((struct sockaddr_in *) hostaddr)->sin_addr.s_addr;
    if((priv->ipstring = calloc(16, 1)) == NULL) {
	syslog(LOG_ERR, "(mlfi_connect) Error allocating memory for priv->ipstring: %m");
	free(priv->hostname);
	free(priv);
	return SMFIS_CONTINUE;
    }
    sprintf(priv->ipstring, "%u.%u.%u.%u", NIPQUAD(priv->ipaddress));
    if(regexec(&re_sld, priv->hostname, 1, &pmatch, 0) == 0) {
	if((priv->hostdomain = calloc(pmatch.rm_eo - pmatch.rm_so + 1, 1)) == NULL) {
	    syslog(LOG_ERR, "(mlfi_connect) Error allocating memory for priv->hostdomain: %m");
	} else {
	    strncpy(priv->hostdomain, (priv->hostname)+pmatch.rm_so, pmatch.rm_eo-pmatch.rm_so);
	}
    } else {
	priv->hostdomain = NULL;
    }

    smfi_setpriv(ctx, priv);
    syslog(LOG_INFO, "(mlfi_connect) Connection from %s (%s)", priv->ipstring, priv->hostname);
    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_connect) Finished");
    
    return SMFIS_CONTINUE;
}

sfsistat mlfi_helo(SMFICTX *ctx, char *helohost)
{
    regmatch_t pmatch;

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_helo) Starting");
    if(MLFIPRIV == NULL) return SMFIS_CONTINUE;
    if(helohost == NULL) {
	syslog(LOG_INFO, "(mlfi_helo) No HELO issued");
	return SMFIS_CONTINUE;
    }
    MLFIPRIV->helohost = strdup(helohost);
    if(MLFIPRIV->helohost == NULL) {
	syslog(LOG_WARNING, "(mlfi_helo) Error allocating memory for priv->helohost: %m");
	return SMFIS_CONTINUE;
    }
    lowercase(MLFIPRIV->helohost);
    if(regexec(&re_sld, MLFIPRIV->helohost, 1, &pmatch, 0) == 0) {
        if((MLFIPRIV->helodomain = calloc(pmatch.rm_eo - pmatch.rm_so + 1, 1)) == NULL) {
            syslog(LOG_ERR, "(mlfi_helo) Error allocating memory for priv->helodomain: %m");
        } else {
            strncpy(MLFIPRIV->helodomain, (MLFIPRIV->helohost)+pmatch.rm_so, pmatch.rm_eo-pmatch.rm_so);
        }
    }

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_helo) Finished");
    return SMFIS_CONTINUE;
}

sfsistat mlfi_envfrom(SMFICTX *ctx, char **argv)
{
    char *chr;
    regmatch_t pmatch;

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_envfrom) Starting");
    if(MLFIPRIV == NULL) return SMFIS_CONTINUE;

    if(strcmp(*argv, "<>") == 0) MLFIPRIV->mailfrom = strdup("<>");
    else if(**argv == '<') MLFIPRIV->mailfrom = strdup((*argv)+1);
    else MLFIPRIV->mailfrom = strdup(*argv);
    if(MLFIPRIV->mailfrom == NULL) {
	syslog(LOG_WARNING, "(mlfi_envfrom) Error allocating memory for priv->mailfrom: %m");
	return SMFIS_CONTINUE;
    }
    if(strcmp(*argv, "<>") != 0) {
        chr = strchr(MLFIPRIV->mailfrom, '>');
        if(chr != NULL) *chr = '\0';
    }
    lowercase(MLFIPRIV->mailfrom);

    chr = strchr(MLFIPRIV->mailfrom, '@');
    if(chr != NULL) {
	chr++;
        if(regexec(&re_sld, chr, 1, &pmatch, 0) == 0) {
            if((MLFIPRIV->mailfromdomain = calloc(pmatch.rm_eo - pmatch.rm_so + 1, 1)) == NULL) {
                syslog(LOG_ERR, "(mlfi_envfrom) Error allocating memory for priv->mailfromdomain: %m");
            } else {
                strncpy(MLFIPRIV->mailfromdomain, chr+pmatch.rm_so, pmatch.rm_eo-pmatch.rm_so);
            }
        }
    }

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_envfrom) Finished");
    return SMFIS_CONTINUE;
}


/* common things to do when a filter triggers a rejected message */
static int
filter_reject_ops(SMFICTX *ctx,
                  struct spasmRecipient *cur,
                  char **argv,
                  const SPASM_FILTER *pfltr)
{
    char reject_msg[256];

    log_rejection(cur->logging,
                  cur->rcptfile,
                  *argv,
                  MLFIPRIV->mailfrom,
                  MLFIPRIV->hostname,
                  MLFIPRIV->ipstring,
                  MLFIPRIV->helohost,
                  cur->fstatus);

    if (pfltr->adhoc_id == SPASM_HELO) {
        /* create custom rejection message ... */

        snprintf(reject_msg,
                 sizeof(reject_msg),
                 "Rejected: You are not %s",
                 MLFIPRIV->helohost);
    } else {
        /*
         * generate rejection message from smtp_rmsg, which can include up
         * to include up to 3 instances of "%s", which are substibuted with
         * the name of the triggered dnsbl.
         */

        snprintf(reject_msg,
                 sizeof(reject_msg),
                 pfltr->smtp_rmsg,
                 pfltr->dnsbl,
                 pfltr->dnsbl,
                 pfltr->dnsbl);
    }

    return (smfi_setreply(ctx, pfltr->smtp_rc, pfltr->smtp_xrc, reject_msg));
}

static int
test_personal_bl(SMFICTX *ctx, struct spasmRecipient *cur)
{
    return (filter_local(cur->rcptfile, 
                    MLFIPRIV->mailfrom,
		    MLFIPRIV->mailfromdomain,
		    MLFIPRIV->hostname,
		    MLFIPRIV->ipstring) ||
	    filter_local(cur->domain,
                    MLFIPRIV->mailfrom,
		    MLFIPRIV->mailfromdomain,
		    MLFIPRIV->hostname,
		    MLFIPRIV->ipstring));
}

static int
test_local_bl(SMFICTX *ctx)
{
    return (filter_local(LOCAL_BL,
                         MLFIPRIV->mailfrom,
                         MLFIPRIV->mailfromdomain,
                         MLFIPRIV->hostname, MLFIPRIV->ipstring) ||
            search_regex_envfrom(MLFIPRIV->mailfrom));
}

static sfsistat
mlfi_envrcpt(SMFICTX *ctx, char **argv)
{
    char *domain;
    char *chr;
    struct spasmRecipient *cur = NULL, *tmpcur;
    char buf[256];
    int result;
    int fd = -1;
    regmatch_t pmatch;
    const SPASM_FILTER *pfltr;

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_envrcpt) Starting");
    if(MLFIPRIV == NULL) return SMFIS_CONTINUE;

    /* Add to MAX_UNKNOWN_USERS blacklist if threshold has been reached */
    if(MLFIPRIV->unknownusers > MAX_UNKNOWN_USERS) {
	add_to_blacklist(MAX_UNKNOWN_USERS_BL, MLFIPRIV->ipstring);
    }

    /* Prepare recipient info*/
    if ((tmpcur = calloc(sizeof(struct spasmRecipient), 1)) == NULL) {
	syslog(LOG_ERR,
	    "(mlfi_envrcpt) Error allocating memory for MLFIPRIV->rcpts: %m");
	return SMFIS_CONTINUE;
    }
    if(MLFIPRIV->rcpts == NULL)
	MLFIPRIV->rcpts = tmpcur;
    else {
	cur = MLFIPRIV->rcpts;
	while(cur->next != NULL) cur = cur->next;
	cur->next = tmpcur;
    }
    cur = tmpcur;
    cur->rcpt = strdup(*argv);
    if(cur->rcpt == NULL) {
	syslog(LOG_ERR, "(mlfi_envrcpt) Error allocating memory for envrcpt");
	return SMFIS_CONTINUE;
    }
    if(**argv == '<') cur->username = strdup((*argv)+1);
    else cur->username = strdup(*argv);
    if(cur->username == NULL) {
	syslog(LOG_ERR, "(mlfi_envrcpt) Error allocating memory for username");
	return SMFIS_CONTINUE;
    }
    lowercase(cur->username);
    chr = strchr(cur->username, '>');
    if(chr != NULL) *chr = '\0';
    domain = strchr(cur->username, '@');
    if(domain == NULL) {
	cur->domain = calloc(sizeof(DEFAULT_DOMAIN), 1);
	if(cur->domain == NULL) {
	    syslog(LOG_ERR, "(mlfi_envrcpt) Error allocating memory for priv->domain: %m");
	    return SMFIS_CONTINUE;
	} else {
	    strcpy(cur->domain, DEFAULT_DOMAIN);
	}
    } else {
	*domain = '\0';
	domain++;
        if(regexec(&re_sld, domain, 1, &pmatch, 0) == 0) {
            if((cur->domain = calloc(pmatch.rm_eo - pmatch.rm_so + 1, 1)) == NULL) {
                syslog(LOG_ERR, "(mlfi_envrcpt) Error allocating memory for priv->domain: %m");
	        return SMFIS_CONTINUE;
            } else {
                strncpy(cur->domain, domain+pmatch.rm_so, pmatch.rm_eo-pmatch.rm_so);
            }
        } else {
	    size_t len = strlen(domain);

	    if((cur->domain = calloc(len+1,1)) == NULL) {
		syslog(LOG_ERR, "(mlfi_envrcpt) Error allocating memory for priv->domain: %m");
		return SMFIS_CONTINUE;
	    } else {
		strncpy(cur->domain, domain, len);
	    }
	}
    }

    /* Verify User (needs work) */
    if(user_lookup(cur->username, cur->domain) == 0) {
	if(DEBUGGING) syslog(LOG_WARNING, "Unknown user from %s (%s - %s)", MLFIPRIV->mailfrom, MLFIPRIV->hostname, MLFIPRIV->ipstring);
	MLFIPRIV->unknownusers++;
	smfi_setreply(ctx, "550", "5.1.1", "User unknown");
	return SMFIS_REJECT;
    }

    /* Check for USE_DOMAIN_LOGGING */
    if((USE_DOMAIN_LOGGING == 1 && strcmp(cur->domain, DEFAULT_DOMAIN) != 0) || filter_lookup(cur->username, cur->domain, "USE_DOMAIN_LOGGING")) {
	if((cur->rcptfile = strdup(cur->domain)) == NULL) {
	    syslog(LOG_WARNING, "(mlfi_envrcpt) Error allocating memory for rcptfile: %m");
	    return SMFIS_CONTINUE;
	}
    } else if((cur->rcptfile = calloc(strlen(cur->username) + strlen(cur->domain) + 2, 1)) == NULL) {
	syslog(LOG_WARNING, "(mlfi_envrcpt) Error allocating memory for rcptfile: %m");
	return SMFIS_CONTINUE;
    } else {
        sprintf(cur->rcptfile, "%s@%s", cur->username, cur->domain);
    }

    /* Is recipient a spamtrap? */
    if(search_regex_spamtraps(cur->rcpt) != 0) {
	/*log_rejection(cur->logging, SPAMTRAP_LOG, *argv, MLFIPRIV->mailfrom, MLFIPRIV->hostname, MLFIPRIV->ipstring, MLFIPRIV->helohost, "SPAMTRAP");*/
	add_to_blacklist(SPAMTRAP_BL, MLFIPRIV->ipstring);
	smfi_setreply(ctx, "550", "5.7.1", "Rejected");
	return SMFIS_REJECT;
    }

    /* Check whitelist */
    if(userwl_lookup(cur->username, cur->domain, MLFIPRIV->mailfrom, MLFIPRIV->hostname, MLFIPRIV->ipstring))
	return SMFIS_CONTINUE;

    /* Get logging and subject tagging options */
    cur->logging = filter_lookup(cur->username, cur->domain, "USE_LOGGING");
    cur->subject_tagging = filter_lookup(cur->username, cur->domain, "USE_SUBJECT_TAGGING");

    /*
     * Begin processing the various anti-spam filters.  Start with
     * "whitelist only" because if the recipient has enabled this filter,
     * s/he doesn't want e-mail from any source other than what's been
     * listed on his/her personal whitelist.
     */
    if (pwl_only_fltr) {
	if((result = filter_lookup(cur->username,
				   cur->domain,
				   pwl_only_fltr->db_name))) {
	    if (DEBUGGING) 
		syslog(LOG_DEBUG, "(mlfi_envrcpt) %s", pwl_only_fltr->db_name);
	    add_filter_status(cur, pwl_only_fltr->db_name, result);
	    if(result == SPASM_REJECT) { /* Reject outright */
		(void) filter_reject_ops(ctx, cur, argv, pwl_only_fltr);
		return SMFIS_REJECT;
	    }
	}
    }

    /* and now...the remainder of the anti-spam filters */
    pfltr = spasm_filters;
    while (pfltr->db_name) {
	switch (pfltr->adhoc_id)
	{
	    case SPASM_WL_ONLY:      /* did that */
		break;
	    case SPASM_SPAMASSASSIN: /* do that later */
	    case SPASM_VIRUS_SCAN:   /* ditto         */
		break;
	    default:
		/* did user enable filter? */

		if((result = filter_lookup(cur->username,
						   cur->domain,
						       pfltr->db_name))) {
		    /* Yes, as a matter of fact... */

		    int fltrd = 0;

		    if(DEBUGGING)
			syslog(LOG_DEBUG, "(mlfi_envrcpt) %s", pfltr->db_name);
		    switch(pfltr->adhoc_id)
		    {
			case 0:    /* filter uses non-adhoc processing */
                            if (pfltr->uses_ip_addr) {
                                snprintf(buf,
                                         sizeof(buf),
                                         "%u.%u.%u.%u.%s",
                                         RNIPQUAD(MLFIPRIV->ipaddress),
                                         pfltr->dnsbl);
                            } else {
                                snprintf(buf,
                                         sizeof(buf),
                                         "%s.%s",
                                         MLFIPRIV->hostname,
                                         pfltr->dnsbl);
                            }
                            fltrd = dnsbl_lookup(buf);
			    break;
			case SPASM_HELO:
                            fltrd = filter_HELO(MLFIPRIV->helodomain,
                                                MLFIPRIV->hostdomain);
			    break;
			case SPASM_RDNS:
                            fltrd = filter_rDNS(MLFIPRIV->hostname);
			    break;
			case SPASM_SPAMTRAP:
                            fltrd = spasm_lookup_bl(SPAMTRAP_BL,
                                                    MLFIPRIV->ipstring);
			    break;
			case SPASM_MAIL_HOST:
                            fltrd = filter_MAIL_HOST(MLFIPRIV->mailfromdomain,
                                                     MLFIPRIV->hostdomain);
			    break;
			case SPASM_LOCALFILTER:
			    fltrd = test_local_bl(ctx);
			    break;
			case SPASM_MAX_UNK_USERS:
                            fltrd = spasm_lookup_bl(MAX_UNKNOWN_USERS_BL,
                                                    MLFIPRIV->ipstring);
			    break;
			case SPASM_PERSONAL_BL:
			    fltrd = test_personal_bl(ctx, cur);
			    break;
			default:
			    syslog(LOG_ERR,
		       "(mlfi_envrcpt) encountered unknown filter id %d (%s)",
				   pfltr->adhoc_id,
				   pfltr->db_name);
			    break;
		    }
		    if (fltrd) {
			add_filter_status(cur, pfltr->db_name, result);
			if (result == SPASM_REJECT) {
			    (void) filter_reject_ops(ctx, cur, argv, pfltr);
			    return SMFIS_REJECT;
			}
		    }
		}
		break;
	}
	pfltr++;
    }

#ifdef VIRUS_SCAN
    cur->av = filter_lookup(cur->username, cur->domain, "SF_VIRUS_SCAN");
#endif

#ifdef SPAMASSASSIN
    cur->sa = filter_lookup(cur->username, cur->domain, "SF_SPAMASSASSIN");
#endif

    if(cur->sa || cur->av || (cur->status == SPASM_TAG || cur->status == SPASM_QUARANTINE)) {
	char *buf;

	buf = sp_strcat(QUARANTINE_DIR, cur->rcptfile);
	if(buf != NULL) {
            mkdir(buf, 0755);
            if((cur->quarantinefile = calloc(strlen(QUARANTINE_DIR) + strlen(cur->rcptfile) + 32, 1)) == NULL) {
                syslog(LOG_ERR, "(mlfi_envrcpt) Error allocating memory for cur->quarantinefile: %m");
            } else {
		struct timeval tv;
		gettimeofday(&tv, NULL);
                sprintf(cur->quarantinefile, "%s/%s/%ld%ld.XXXXXX", QUARANTINE_DIR, cur->rcptfile, tv.tv_sec, tv.tv_usec);
                if((fd = mkstemp(cur->quarantinefile)) < 0 || (cur->quarantinefp = fdopen(fd, "a")) == NULL) {
                    syslog(LOG_ERR, "(mlfi_envrcpt) Error opening quarantine file %s: %m", cur->quarantinefile);
                } else {
                    chmod(cur->quarantinefile, 0644);
		    /* Write new Received: line */
		    fprintf(cur->quarantinefp, "Received: from %s (%s [%s])\r\n", MLFIPRIV->helohost, MLFIPRIV->hostname, MLFIPRIV->ipstring);
		    fprintf(cur->quarantinefp, "\tby localhost (spasm) for %s\r\n", cur->rcpt);
		}
            }
	    free(buf);
        }
    }

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_envrcpt) Finished");

    return SMFIS_CONTINUE;
}

sfsistat mlfi_header(SMFICTX *ctx, char *headerf, char *headerv)
{
    struct spasmRecipient *cur;

    if(MLFIPRIV == NULL) return SMFIS_CONTINUE;
    cur = MLFIPRIV->rcpts;
    while(cur != NULL) {
	if((cur->sa || cur->av || cur->status) && (cur->status < SPASM_REJECT) && (cur->quarantinefp != NULL)) {
	    if(cur->subject_tagging && (strcasecmp(headerf, "subject") == 0)) { /* Subject Tagging */
		fprintf(cur->quarantinefp, "%s: **** X-spasm **** %s\r\n", headerf, headerv);
	    } else {
		fprintf(cur->quarantinefp, "%s: %s\r\n", headerf, headerv);
	    }
	}
	cur = cur->next;
    }

    return SMFIS_CONTINUE;
}

sfsistat mlfi_eoh(SMFICTX *ctx)
{
    struct spasmRecipient *cur;
    struct filterStatus *fstatus;

    if(MLFIPRIV == NULL) return SMFIS_CONTINUE;
    cur = MLFIPRIV->rcpts;
    while(cur != NULL) {
	if((cur->sa || cur->av || cur->status) && (cur->status < SPASM_REJECT) && (cur->quarantinefp != NULL)) {
	    fstatus = cur->fstatus;
	    while(fstatus != NULL) {
		if(fstatus->status) fprintf(cur->quarantinefp, "X-spasm: %s\r\n", fstatus->filter);
		fstatus = fstatus->next;
	    }
	    fprintf(cur->quarantinefp, "X-spasm-rcpt: %s@%s\r\n", cur->username, cur->domain);
	    fprintf(cur->quarantinefp, "X-spasm-sender: %s\r\n", MLFIPRIV->mailfrom);
	    fprintf(cur->quarantinefp, "X-spasm-sender-IP: %s\r\n", MLFIPRIV->ipstring);
	    fprintf(cur->quarantinefp, "X-spasm-sender-host: %s\r\n", MLFIPRIV->hostname);
	    fprintf(cur->quarantinefp, "\r\n");
	}
	cur = cur->next;
    }

    return SMFIS_CONTINUE;
}

sfsistat mlfi_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen)
{
    struct spasmRecipient *cur;

    if(MLFIPRIV == NULL) return SMFIS_CONTINUE;
    cur = MLFIPRIV->rcpts;
    while(cur != NULL) {
	if((cur->sa || cur->av || cur->status) && (cur->status < SPASM_REJECT) && (cur->quarantinefp != NULL)) {
	    fwrite(bodyp, bodylen, 1, cur->quarantinefp);
	}
	cur = cur->next;
    }

    return SMFIS_CONTINUE;
}

sfsistat mlfi_eom(SMFICTX *ctx)
{
    struct mlfiPriv *priv = MLFIPRIV;
    struct spasmRecipient *cur;
    struct filterStatus *fstatus;

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_eom) Starting");
    if(priv != NULL) {
        cur = MLFIPRIV->rcpts;
        while(cur != NULL && cur->quarantinefp != NULL && cur->quarantinefile != NULL) {
	    /* Flush and close quarantine file */
	    fflush(cur->quarantinefp);
	    fclose(cur->quarantinefp);
	    cur->quarantinefp = NULL;

#ifdef VIRUS_SCAN
	    /* Perform virus scan */
	    if(cur->av && cur->status != SPASM_REJECT) {
		if(filter_AV(cur->quarantinefile) == 1) {
		    syslog(LOG_NOTICE, "(mlfi_eom) Virus found in message from %s", MLFIPRIV->ipstring);
		    add_filter_status(cur, "SF_VIRUS_SCAN", cur->av);
		    if(cur->av == SPASM_REJECT) smfi_delrcpt(ctx, cur->rcpt);
		}
	    }
#endif

#ifdef SPAMASSASSIN
	    /* Run SpamAssassin */
	    if(cur->sa && cur->status != SPASM_REJECT) {
		int required_hits;
		required_hits = filter_lookup(cur->username, cur->domain, SA_REQUIRED_HITS);
		if(filter_SA(cur->quarantinefile, required_hits) == 1) {
		    add_filter_status(cur, "SF_SPAMASSASSIN", cur->sa);
		    if(cur->sa == SPASM_REJECT) smfi_delrcpt(ctx, cur->rcpt);
		}
	    }
#endif

	    /* Remove quarantined recipients and retain file */
	    if(cur->status == SPASM_QUARANTINE) {
		smfi_delrcpt(ctx, cur->rcpt);
		free(cur->quarantinefile);
		cur->quarantinefile = NULL;
	    }
	    /* Deliver to tag-only recipients and delete file */
	    if(cur->status == SPASM_TAG) {
                if(deliverQuarantine(priv->mailfrom, cur->rcpt, cur->quarantinefile) != 0) {
                    syslog(LOG_NOTICE, "(mlfi_eom) Error delivering quarantine file");
                } else {
                    smfi_delrcpt(ctx, cur->rcpt);
                    if((unlink(cur->quarantinefile)) != 0) {
                        syslog(LOG_ERR, "(mlfi_eom) Error unlinking quarantine file: %m");
                    } else cur->quarantinefile = NULL;
                }
	    }
	    if(cur->status == SPASM_OFF || cur->status == SPASM_REJECT) {
                if((unlink(cur->quarantinefile)) != 0) {
                    syslog(LOG_ERR, "(mlfi_eom) Error unlinking quarantine file: %m");
                } else cur->quarantinefile = NULL;
	    }
	    if(cur->status) log_rejection(cur->logging, cur->rcptfile, cur->rcpt, MLFIPRIV->mailfrom, MLFIPRIV->hostname, MLFIPRIV->ipstring, MLFIPRIV->helohost, cur->fstatus);
	    cur = cur->next;
        }

    /* End of message - Clear message-related vars, keep private data
       and connection-related vars (hostname, ipaddress, ipstring) */
	if(priv->helohost != NULL) { free(priv->helohost); priv->helohost = NULL; }
	if(priv->helodomain != NULL) { free(priv->helodomain); priv->helodomain = NULL; }
	if(priv->mailfrom != NULL) { free(priv->mailfrom); priv->mailfrom = NULL; }
	if(priv->mailfromdomain != NULL) { free(priv->mailfromdomain); priv->mailfromdomain = NULL; }
	cur = priv->rcpts;
	while(cur != NULL) {
	    if(cur->rcpt != NULL) free(cur->rcpt);
	    if(cur->username != NULL) free(cur->username);
	    if(cur->domain != NULL) free(cur->domain);
	    if(cur->quarantinefp != NULL) { fflush(cur->quarantinefp); fclose(cur->quarantinefp); }
	    if(cur->quarantinefile != NULL) { unlink(cur->quarantinefile); free(cur->quarantinefile); cur->quarantinefile = NULL; }
	    if(cur->rcptfile != NULL) free(cur->rcptfile);
	    fstatus = cur->fstatus;
	    while(fstatus != NULL) {
		if(fstatus->filter != NULL) { free(fstatus->filter); fstatus->filter = NULL; }
		cur->fstatus = fstatus->next;
		free(fstatus);
		fstatus = cur->fstatus;
	    }
	    priv->rcpts = cur->next;
	    free(cur);
	    cur = priv->rcpts;
	}
	priv->rcpts = NULL;
    }
    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_eom) Finished");
    return SMFIS_CONTINUE;
}

sfsistat mlfi_abort(SMFICTX *ctx)
{
    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_abort) Starting");

    if(MLFIPRIV != NULL && MLFIPRIV->rcpts != NULL) {
	struct spasmRecipient *cur;
	cur = MLFIPRIV->rcpts;
	while(cur != NULL) {
	    if(cur->quarantinefp != NULL) {
		fflush(cur->quarantinefp);
		fclose(cur->quarantinefp);
		cur->quarantinefp = NULL;
	    }
	    if(cur->quarantinefile != NULL) {
		if(unlink(cur->quarantinefile) != 0) {
		    syslog(LOG_ERR, "(mlfi_abort) Error unlinking quarantine file: %m");
		} else {
                    free(cur->quarantinefile);
                    cur->quarantinefile = NULL;
		}
	    }
	    cur = cur->next;
	}
    }

    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_abort) Finished");
    return SMFIS_CONTINUE;
}

sfsistat mlfi_close(SMFICTX *ctx)
{
    struct mlfiPriv *priv = MLFIPRIV;
    struct spasmRecipient *cur;
    struct filterStatus *fstatus;

    /* Connection Closed - Free all allocated memory, set private data to NULL */
    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_close) Starting");
    if(priv != NULL) {
        if(priv->hostname != NULL) free(priv->hostname);
	if(priv->hostdomain != NULL) free(priv->hostdomain);
        if(priv->helohost != NULL) free(priv->helohost);
	if(priv->helodomain != NULL) free(priv->helodomain);
        if(priv->mailfrom != NULL) free(priv->mailfrom);
	if(priv->mailfromdomain != NULL) free(priv->mailfromdomain);
	if(priv->ipstring != NULL) free(priv->ipstring);
	cur = priv->rcpts;
	while(cur != NULL) {
	    if(cur->rcpt != NULL) free(cur->rcpt);
	    if(cur->username != NULL) free(cur->username);
	    if(cur->domain != NULL) free(cur->domain);
	    if(cur->quarantinefp != NULL) { fflush(cur->quarantinefp); fclose(cur->quarantinefp); }
	    if(cur->quarantinefile != NULL) {
		if(cur->status == SPASM_REJECT || cur->status == SPASM_TAG || cur->status == SPASM_OFF) {
		    if((unlink(cur->quarantinefile)) != 0) {
		        syslog(LOG_ERR, "(mlfi_close) Error unlinking quarantine file: %m");
		    }
		}
		free(cur->quarantinefile);
	    }
	    if(cur->rcptfile != NULL) free(cur->rcptfile);
            fstatus = cur->fstatus;
            while(fstatus != NULL) {
		if(fstatus->filter != NULL) free(fstatus->filter);
                cur->fstatus = fstatus->next;
                free(fstatus);
                fstatus = cur->fstatus;
            }
	    priv->rcpts = cur->next;
	    free(cur);
	    cur = priv->rcpts;
	}
	priv->rcpts = NULL;
        free(priv);
    }
    smfi_setpriv(ctx, NULL);
    if(DEBUGGING) syslog(LOG_DEBUG, "(mlfi_close) Finished");
    return SMFIS_CONTINUE;
}

struct smfiDesc smfilter =
{
    "spasm",
    SMFI_VERSION,
    SMFIF_ADDHDRS|SMFIF_DELRCPT,
    mlfi_connect,
    mlfi_helo,
    mlfi_envfrom,
    mlfi_envrcpt,
    mlfi_header,
    mlfi_eoh,
    mlfi_body,
    mlfi_eom,
    mlfi_abort,
    mlfi_close
};

static void
die(char *msg, int rc)
{
    char errstring[FILENAME_MAX + 128];

    snprintf(errstring, sizeof(errstring), "%s: %s", progname, msg);
    perror(errstring);
    exit(rc);
}

static void
usage()
{
    fprintf(stderr, "usage: %s -p <connection>\n", progname);
    exit(EX_USAGE);
}

int main(int argc, char **argv)
{
    int c, rc;
    const char *args = "p:";
    struct passwd *pw;
    char msg[128];

    progname = argv[0];

    /* Drop privs immediately */
    if((pw = getpwnam(SPASM_USER)) == NULL) {
	snprintf(msg, sizeof(msg), "error reading passwd entry for \"%s\"", SPASM_USER);
	die(msg, 1);
    }
    if((setgid(pw->pw_gid) != 0) || (setuid(pw->pw_uid) != 0)) {
	die("error dropping privileges", 1);
    }

    /* Did they give a "-p <connection>" argument? */
    if(argc != 3) usage();

    /* Get arguments, setup milter connection */
    opterr = 0;
    while((c = getopt(argc, argv, args)) != EOF) {
	switch(c) {
	    case 'p':
		if(*optarg == '\0') {
		    fprintf(stderr, "%s: Illegal conn arg: \"%s\"\n", progname, optarg);
		    exit(EX_USAGE);
		}
		if(smfi_setconn(optarg) == MI_FAILURE) {
		    fprintf(stderr, "%s: mlfi_setconn() failed\n", progname);
		    exit(EX_UNAVAILABLE);
		}
		break;
	    default:
		usage();
	}
    }

    /* Register the milter */
    if(smfi_register(smfilter) == MI_FAILURE) {
	fprintf(stderr, "%s: smfi_register() failed\n", progname);
	exit(EX_UNAVAILABLE);
    }

    /* Compile our regex to return (SLD.TLD) of a FQDN */
    if((rc = regcomp(&re_sld, DOMAIN_NAME_REGEX, REG_EXTENDED)) != 0) {
	fprintf(stderr, "%s: error compiling regex \"%s\" (errcode: %d)\n",
		progname, DOMAIN_NAME_REGEX, rc);
	exit(EX_UNAVAILABLE);
    }
    /* Compile our regex to check for a CIDR entry */
    if((rc = regcomp(&re_cidr, CIDR_REGEX, REG_EXTENDED)) != 0) {
	fprintf(stderr, "%s: error compiling regex \"%s\" (errcode: %d)\n",
		progname, CIDR_REGEX, rc);
	exit(EX_UNAVAILABLE);
    }

    /* cache pointer(s) to "important" filter data structure(s) */
    cache_filter_ptrs();

    /* Set Signal Handlers */
#ifdef WANT_SOME_SIGNALS_SOMEDAY
    signal(SIGCHLD, sigCHLD);
    signal(SIGINT, cleanup);
    signal(SIGTERM, cleanup);
    signal(SIGSEGV, sigSEGV);
#endif

    /* Fork daemon. Looping code adapted from vile's npopen.c */
    {
        int tries = 0, pid, nap = 1;

        while ((pid = fork()) < 0) {
            if (++tries > 5) {
                die("daemon fork() failed", EX_UNAVAILABLE);
            }
            sleep(nap);
            nap <<= 1;
        }
	/* We have forkage! */
        if (pid != 0)
	    /* parental unit returns */
            return (0);
        else {
            /* child goes out to play -- let the spam rejection begin! */
            openlog("spasmd", LOG_PID, LOGFACILITY);
            syslog(LOG_INFO, "(main) Starting");
            return (smfi_main());
        }
    }
}
