/*
    This program is RSPAMD agent for use with 
    exim (http://www.exim.org) MTA by its local_scan feature.

    To enable exim local scan please copy this file to exim source tree
    Local/local_scan.c, edit Local/Makefile to add  
    
    LOCAL_SCAN_SOURCE=Local/local_scan.c 
    LOCAL_SCAN_HAS_OPTIONS=yes
    
    and compile exim. 
    
    Comment out RSPAM_UNIXSOCKET definition below if you have remote RSPAMD
    daemon
    
    AND
    
    use Exim parameters daemonIP and daemonPort to configure remote
    RSPAMD daemon.
    
    For exim compilation with local scan feature details please visit 
    http://www.exim.org/exim-html-4.50/doc/html/spec_toc.html#TOC333
    
    For RSPAMD details please visit
    http://rspamd.sourceforge.net
*/

/* Comment out the row below to use socket type AF_INET
   to connect RSPAMD daemon */
//#define RSPAM_UNIXSOCKET

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <stdlib.h>

#include "local_scan.h"

extern uschar *sender_helo_name;
extern int     message_size;

#define READ_FAIL(x)    ((x) < 0)
#define RSPAMD_FAILURE_HDR 	"X-Spam-Flag"
#define RSPAMD_SCORE_HDR 	"X-Spam-Status"
#define REJECT_ON_ERROR 0

static int _OK = 0;
static int ERR_WRITE = 53;
static int ERR_READ  = 54;
static int MAX_FAILS_C = 256;
static int MAX_PATH  = 256;
static int MAX_SIZE_FILE = 64*1024;

static uschar *daemonIP = US"127.0.0.1";
static int daemonPort = 11333;
static uschar *temp_dir = US"/var/tmp";
static uschar *socket_name = US"/var/run/rspamd.sock";
static int strange = 0;

optionlist local_scan_options[] = 
{ 
    {"rspam_ip", opt_stringptr, &daemonIP}, 
    {"rspam_port", opt_int, &daemonPort},
    {"rspam_tmp", opt_stringptr, &temp_dir},
    {"rspam_sock", opt_stringptr, &socket_name},
    
};

int local_scan_options_count = sizeof (local_scan_options) / sizeof (optionlist);

typedef int socket_t;
static socket_t sock = -1;

int iFdInp;
struct sockaddr_un	ssun;
struct sockaddr_in	ssin;

static int mOpenTmp (char *pszDir, char *pszPrefix, char *pszPath)
{
    int iLen;
    int iFd = -1;
    char *pszSep = "";

    iLen = (int)strlen(pszDir);
    if (iLen > MAX_PATH)
        return -1;

    if (pszDir[iLen - 1] != '/')
        pszSep = "/";

    sprintf (pszPath, "%s%s%sXXXXXX", pszDir, pszSep, pszPrefix);
    iFd = mkstemp (pszPath);

    if (iFd < 0)
        log_write (0, LOG_MAIN, "rspam-exim: Temp file create error %d", errno);

    return iFd;
}

static int ReadFd (int iFdMsg, int fd)
{
    char psMsg [MAX_SIZE_FILE]; /* max size SO can swallow */
    int iLen, result = _OK;

    if ((iLen = read (fd, psMsg, sizeof (psMsg))) > 0) 
    {
        if (write (iFdMsg, psMsg, (unsigned int) iLen) != iLen) 
            result = ERR_WRITE;
    }
    else
        result = ERR_READ;

    close (iFdMsg);

    return result;
}


void CleanupInp (char *sName)
{
    if (sName) unlink (sName); 

    close (iFdInp);
    return;
}


int FakeSMTPCommand (socket_t sock,
                     char *command,
                     char *value,
                     char *sName,
                     int Cleanup,
            	     int wa)
{
    char sCommand[1024];
    char answ [3];
    int  Len;

    sprintf (sCommand, "%s %s\r\n", command, value);

    if (send (sock, sCommand, strlen (sCommand), 0) != (int) strlen (sCommand))
    {
        log_write (0, LOG_MAIN, "rspam-exim: socket sending '%s' error %d", sCommand, errno);
        if (Cleanup)
            CleanupInp (sName);
        return ERR_WRITE;
    }

    if(wa) {
	memset (answ, '\0', sizeof (answ));
        Len = read (sock, answ, sizeof (answ));
	if (READ_FAIL (Len))
	{
    	    log_write (0, LOG_MAIN, "rspam-exim: read() error %d, len=%d", errno, Len);
    	    if (Cleanup)
        	CleanupInp (sName);
    	    return ERR_WRITE;
	}

	if (strncmp (answ, "OK", 2) != 0)
	{
    	    log_write (0, LOG_MAIN, "rspam-exim: server did not confirm, answ=%s", answ);
    	    if (Cleanup)
        	CleanupInp (sName);
    	    return ERR_WRITE;	/* Cannot read message error code */
	}
    }

    return OK;
}


static int written (socket_t fd, const char *vptr, int n)
{
    size_t nleft;
    int nwritten;
    const char *ptr;
      
    ptr = vptr;
    nleft = n;
    while (nleft > 0)
    {
        if ((nwritten = send (fd, ptr, nleft, 0)) <= 0)
        {
            if (errno == EINTR)
                nwritten = 0;
            else
                return (-1);
        }
    
        nleft -= nwritten;
        ptr += nwritten;
    }
    
    return (n);
}


static int SendEnvelope (char *sFile)
{
    int i;
    char str [256], *rh;
    void *psBuf;
    int fd, bytesRead;

    if(message_size > MAX_SIZE_FILE) {
	log_write (0, LOG_MAIN, "rspam-exim: file %s is great %d bytes", sFile, MAX_SIZE_FILE);
        return ERR_WRITE;
    }
    
    /* send greeting */
//    if(FakeSMTPCommand(sock, "PROCESS", "RSPAMC/1.0", sFile, 1, 0) != _OK)
//        return ERR_WRITE;
    if(FakeSMTPCommand(sock, "SYMBOLS", "RSPAMC/1.1", sFile, 1, 0) != _OK)
//    if(FakeSMTPCommand(sock, "CHECK", "RSPAMC/1.0", sFile, 1, 0) != _OK)
        return ERR_WRITE;
	
	

    /* sender IP */
    if (FakeSMTPCommand (sock, "IP:", sender_host_address, sFile, 1, 0) != _OK)
        return ERR_WRITE;

    /* mail from */
    if (FakeSMTPCommand (sock, "From:", 
                         strlen (sender_address) == 0 ?  "MAILER-DAEMON" : (char*) sender_address, sFile, 1, 0) != _OK)
        return ERR_WRITE;

    /* send helo */
    if (FakeSMTPCommand (sock, "Helo:", sender_helo_name, sFile, 1, 0) != _OK)
        return ERR_WRITE;

    /* send helo */
    sprintf(str, "%d", message_size);
    if (FakeSMTPCommand (sock, "Content-Length:", str, sFile, 1, 0) != _OK)
        return ERR_WRITE;

    /* number of recipient */
    sprintf(str, "%d", recipients_count);
    if (FakeSMTPCommand (sock, "Recipient-Number:", str, sFile, 1, 0) != _OK)
        return ERR_WRITE;
    
    /* envelope rcpto */
    for (i = 0; i < recipients_count; i ++)
    {
        if (FakeSMTPCommand (sock, "Rcpt:", recipients_list[i].address, sFile, 1, 0) != _OK)
            return ERR_WRITE;
    }

    psBuf = store_get (MAX_SIZE_FILE);

    fd = open (sFile, O_RDONLY);
    if (fd > 0)
    {
        bytesRead = read (fd, psBuf, MAX_SIZE_FILE);
        close (fd);

        if (FakeSMTPCommand (sock, "\r\n", "", sFile, 1, 0) != _OK)
            return ERR_WRITE;
            
        if (written (sock, psBuf, bytesRead) != bytesRead)
            return ERR_WRITE;
    }
    else
    {
        log_write (0, LOG_MAIN, "rspam-exim: file %s open error %d", sFile, errno);
        return ERR_WRITE;
    }

    return _OK;
}


int GetFiles (char *pInpFile, int local_scan_fd)
{
    /*
        Returns OK if no errors, else error code.
        On successful return, pEnvFile points to Envelope file name and
        pInpFile points to Message filename
    */
    int iStatus;
    struct header_line *h_line;
            
    iFdInp = mOpenTmp ((char *)temp_dir, "sp-inp", pInpFile);
    if (iFdInp == -1) 
    {
        return ERR_WRITE;
    }

    /* Emit headers */
    h_line = header_list;
    while (h_line != NULL)
    {
        if (h_line->type == '*') /* internal header */
        {
            h_line = h_line->next;
            continue;
        }
        
        if (write (iFdInp, h_line->text, strlen (h_line->text)) != strlen (h_line->text))
        {
            CleanupInp ("");
            return ERR_WRITE;
        }
        h_line = h_line->next;
    }
    if (write (iFdInp, "\n", 1) != 1)
    {
        CleanupInp ("");
        return ERR_WRITE;
    }
    
    /* Read msg */
    if ((iStatus = ReadFd (iFdInp, local_scan_fd))) 
    {
        return iStatus;
    }

    /* Return success */
    return _OK;
}


int GetAndTransferMessage (int fd, char *sFile)
{
    char answ [4];
    int	 iStatus;
    int	 Len, ccnt;
    int	 test;

    iStatus = GetFiles ((char *)sFile, fd);

    if (iStatus != _OK)
    {
        log_write (0, LOG_MAIN, "rspam-exim: Error %d getting message", iStatus);
        close (sock);
        return iStatus;
    }

    for (ccnt = 0; ccnt <= MAX_FAILS_C; ccnt ++)
    {
#ifdef RSPAM_UNIXSOCKET
        test = connect (sock, (struct sockaddr *) &ssun, sizeof (struct sockaddr_un)) < 0;
#else
        test = connect (sock, (struct sockaddr *) &ssin, sizeof (struct sockaddr_in)) < 0;
#endif
        if (test)
        {
            if (ccnt < MAX_FAILS_C)
                usleep (1000);
            else
            {
                close (sock);
#ifdef RSPAM_UNIXSOCKET
                log_write (0, LOG_MAIN, "rspam-exim: socket connect to %s failed", (char *)socket_name);
#else
                log_write (0, LOG_MAIN, "rspam-exim: socket connect to %s:%u failed", daemonIP, daemonPort);
#endif
		return REJECT_ON_ERROR ? LOCAL_SCAN_TEMPREJECT:LOCAL_SCAN_ACCEPT;
            }
        }
        else
            break;
    }
    
    iStatus = SendEnvelope (sFile);
    if (iStatus != _OK)
    {
        log_write (0, LOG_MAIN, "rspam-exim: error %d sending envelope data", iStatus);
        close (sock);
        return iStatus;
    }
    
    /* fprintf (stderr, "Transmit OK\n"); */
    return _OK;
}

void header_del (uschar *hdr)
{
    struct header_line *h_line;

    h_line = header_list;
    while (h_line != NULL)
    {
        if (h_line->type == '*') /* internal header */
        {
            h_line = h_line->next;
            continue;
        }
        
        if (strncasecmp (h_line->text, hdr, strlen(hdr)) == 0)
        {
            h_line->type = '*';
            while (h_line->next && 
                   (*h_line->next->text == ' ' || *h_line->next->text == '\t'))
            {
                h_line = h_line->next;
                h_line->type = '*';
            }
        }
        h_line = h_line->next;
    }
}

void AlterSubject (char *label)
{
    struct header_line *h_line;
    char *subject, *strP;

    h_line = header_list;
    
    while (h_line != NULL)
    {
        if (h_line->type == '*') /* internal header */
        {
            h_line = h_line->next;
            continue;
        }
        
        if (strncasecmp (h_line->text, "Subject", strlen("Subject")) == 0)
        {
            strP = strchr (h_line->text, ':');
            subject = string_copy (++strP);
            while (h_line->next &&
                   (*h_line->next->text == ' ' || *h_line->next->text == '\t'))
            {
                h_line = h_line->next;
		subject = string_sprintf ("%s\n%s", subject, h_line->text);
            }
            header_del (US "Subject");
            break;
        }

        h_line = h_line->next;
    }
    header_add (' ', "Subject: %s%s", label, subject ? subject : "");
}

int 
io_read(int fd, char *buf, size_t size)
{
    int nfd, next = 0, rcount = 15;
    size_t len = 0;
    fd_set fds;
    struct timeval tv;
    
    if((sock < 0) || (buf == NULL))
	return -1;
	
    FD_ZERO(&fds);

repeat_read:

    tv.tv_sec = 5;
    tv.tv_usec = 0;
    FD_SET(fd, &fds);
    
//    log_write(0, LOG_MAIN, "rspam-exim: before select");

    if((nfd=select(fd+1, &fds, NULL, NULL, &tv)) == -1) {
//	log_write(0, LOG_MAIN, "rspam-exim: select error: %s", strerror(errno));
	return -1;
    }

//    log_write(0, LOG_MAIN, "rspam-exim: select return %d fds, rcount %d, next %d", nfd, rcount, next);
    
    if((nfd>0) && (FD_ISSET(fd, &fds))) {
	next += len = read(fd, buf + next, size - next);
//        log_write(0, LOG_MAIN, "rspam-exim: read %d bytes", len);
//	if(next<size) 
//	    goto repeat_read;
    }
    rcount--;
    if(rcount>0)
	goto repeat_read;

    return next;
}

int WaitForScanResult (uschar **resStr)
{
    int Len, i;
    int rej = 0, result = LOCAL_SCAN_ACCEPT, answer_size, spm = 0, code = 0, ns = 0, smb = 0, urf = 0;
    char *strP, *tok, *tmp;
    char *hdr = NULL, *hdrv = NULL, *spmStr = NULL, *symbols=NULL, *urls=NULL;
    char answ [4096], state[6], metric[128], back;
    float sm=0, smd=0, smr=0;
    
    memset (answ, '\0', sizeof (answ));
//    log_write(0, LOG_MAIN, "rspam-exim: before read from %d", sock);
//    Len = read (sock, answ, sizeof (answ) - 1);
    Len = io_read(sock, answ, sizeof (answ) - 1);
    log_write(0, LOG_MAIN, "rspam-exim: read %d bytes", Len);

    if (strncmp (answ, "RSPAMD/1.1 ", 11) == 0)
    {
        strP = (char *)answ;
        for (tok = strtok (strP, "\n"); tok; tok = strtok (NULL, "\n"))
        {
//            log_write(0, LOG_MAIN, "rspam-exim: process line '%s'", tok);

            if (strncmp (tok, "RSPAMD/1.1 ", 11) == 0)
            {
                if (sscanf (tok, "%*s %d %s", &code, state) == 2)
                {
//                    log_write(0, LOG_MAIN, "rspam-exim: daemon reports code %d %s", code, state);
            	    if ((code == 0) && (strcmp(state,"OK")==0)) {
                	    header_del ((uschar *) RSPAMD_FAILURE_HDR);
                	    header_add (' ', "%s: SKIP\n", RSPAMD_FAILURE_HDR);
                	    strange = 1;
            		    continue;
            	    } else {
                	    header_del ((uschar *) RSPAMD_FAILURE_HDR);
                	    header_add (' ', "%s: SKIP\n", RSPAMD_FAILURE_HDR);
                            log_write(0, LOG_MAIN, "rspam-exim: daemon reports code %d %s", code, state);
                    	    return LOCAL_SCAN_ACCEPT;
            	    }
                }
                continue;
            }

	    /* Metric: default; False; 6.00 / 10.00 */           
            /* Process metric */
            if (strncmp (tok, "Metric:", 7) == 0)
            {
        	tmp = tok;
        	while(	(*tmp++) && 
        		((*tmp!='\r') || (*tmp!='\n'))
        		);
        	back = *tmp;
        	*tmp = '\0';
                if (sscanf (tok, "Metric: %[^';']; %[^';']; %f / %f / %f", metric, state, &sm, &smd, &smr) == 5) {
            		log_write(0, LOG_MAIN, "rspam-exim: metric: %s; %s; %f / %f / %f", metric, state, sm, smd, smr );
            		if(strcasecmp(state,"true")==0) {
                		header_del ((uschar *) RSPAMD_FAILURE_HDR);
                		header_add (' ', "%s: %s\n", RSPAMD_FAILURE_HDR, "Yes");
            		} else if(strcasecmp(state,"skip")==0) {
                		header_del ((uschar *) RSPAMD_FAILURE_HDR);
                		header_add (' ', "%s: %s\n", RSPAMD_FAILURE_HDR, "Skip");
            		} else {
                		header_del ((uschar *) RSPAMD_FAILURE_HDR);
                		header_add (' ', "%s: %s\n", RSPAMD_FAILURE_HDR, "No");
            		}
                	header_del ((uschar *) RSPAMD_SCORE_HDR);
                	header_add (' ', "%s: %.2f / %.2f / %.2f\n", RSPAMD_SCORE_HDR, sm, smd, smr);
                	strange = 0;
        	}
		*tmp = back;
                continue;
            }
            
            if (strncmp (tok, "Symbol:", 7) == 0)
            {
        	tmp = tok;
        	while(	(*tmp++) && 
        		((*tmp!='\r') || (*tmp!='\n'))
        		);
        	back = *tmp;
        	*tmp = '\0';
		if(smb>0) {
		    tok += 7;
		    while(*tok && isspace(*tok)) tok++;
		    if(strlen(tok)>0) {
			symbols = string_sprintf ("%s\n %s", symbols, tok);
		    }
		} else {
		    tok += 7;
		    while(*tok && isspace(*tok)) tok++;
		    symbols = string_copy (tok);
		}
        	smb = 1;
		*tmp = back;
        	continue;
	    }

            if (strncmp (tok, "Urls:", 5) == 0)
            {
        	tmp = tok;
        	while(	(*tmp++) && 
        		((*tmp!='\r') || (*tmp!='\n'))
        		);
        	back = *tmp;
        	*tmp = '\0';
		if(urf>0) {
		    tok[0] = tok[1]= tok[2]= tok[3]= tok[4] = ' ';
		    urls = string_sprintf ("%s\n%s", urls, tok+3);    
		} else {
		    tok += 5;
		    while(*tok && isspace(*tok)) tok++;
		    urls = string_copy (tok);
		}
        	urf = 1;
		*tmp = back;
        	continue;
	    }
        }

        
        /* do not forget the symbols */
        if (symbols != NULL && strlen(symbols))
        {
    	    i = 0;
    	    tmp = tok = string_copy(symbols);
            header_del ((uschar *) "X-Spam-Sybmols");
            header_add (' ', "%s: %s\n", "X-Spam-Sybmols", symbols);
            while(*tmp!='\0') {
        	if(*tmp == '\r')
			*tmp = ' ';
        	if(*tmp == '\n')
        		*tmp = ',';
        	tmp++;
            }
	    *tmp = '\0';
            log_write(0, LOG_MAIN, "rspam-exim: symbols: %s", tok);
        }

        /* do not forget the urls */
        if (urls != NULL && strlen(urls))
        {
            log_write(0, LOG_MAIN, "rspam-exim: urls: %s", urls);
            header_del ((uschar *) "X-Spam-Urls");
            header_add (' ', "%s: %s\n", "X-Spam-Urls", urls);
        }
        
        log_write (0, LOG_MAIN, "rspam-exim: For message from %s will return %s, mailfrom: <%s>, rcpto: <%s>", sender_host_address, rej == 2 ? "DISCARD" : rej == 1 ? "REJECT" : "ACCEPT", sender_address, recipients_list[0].address);
        
    }
    else
    {
        result = LOCAL_SCAN_ACCEPT;
        log_write(0, LOG_MAIN, "rspam-exim: wrong signature in answer: %s", answ);
    }

    if((sm>0) && (smr>0) && (sm>=smr)) {
	result = LOCAL_SCAN_REJECT;
    }        
    return result;
}


int
local_scan(int fd, uschar **return_text)
{
    int 	retval = _OK;
    char sFileInp [MAX_PATH + 81];

    /* Socket stuff */

    strange = 0;
#ifdef RSPAM_UNIXSOCKET
    if ((sock = socket (AF_UNIX, SOCK_STREAM, 0)) < 0)
    {
        log_write(0, LOG_MAIN, "rspam-exim: socket() failed");
        exit (1);    
    }
    memset (&ssun, '\0', sizeof (struct sockaddr_un));
    ssun.sun_family = AF_UNIX;
    if (sizeof (socket_name) > sizeof (ssun.sun_path))
    {
        close (sock);
        log_write(0, LOG_MAIN, "rspam-exim: UNIX socket name %s too long", socket_name);
        exit (1);
    }
    strcpy (ssun.sun_path, socket_name);
#else
    if ((sock = socket (AF_INET, SOCK_STREAM, 0)) < 0)
    {
        log_write(0, LOG_MAIN, "rspam-exim: socket() failed");
        exit (1);    
    }
    memset (&ssin, '\0', sizeof (struct sockaddr_in));
    ssin.sin_family = AF_INET;
    ssin.sin_addr.s_addr = inet_addr (daemonIP);
    ssin.sin_port = htons (daemonPort);
#endif

    if (GetAndTransferMessage (fd, (char *)sFileInp) != _OK)
    {
        close (sock);
        unlink (sFileInp);
        SPOOL_DATA_START_OFFSET;
	return REJECT_ON_ERROR ? LOCAL_SCAN_TEMPREJECT:LOCAL_SCAN_ACCEPT;
    }
    
    retval = WaitForScanResult (return_text);

    if(!strange)
	unlink (sFileInp);
    close (sock);
    SPOOL_DATA_START_OFFSET;
    
    return retval;
}

/* End of local_scan.c */
