/*
    This program is Yandex spamooborona (c) 2005 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 
    
    and compile exim. 
    
    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 Yandex spamooborona details please visit
    http://so.yandex.ru
*/

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#include "local_scan.h"

#define READ_FAIL(x)    ((x) < 0)
#define MAX_SIZE_SO	64 * 1024
#define SO_FAILURE_HDR 	"X-Spam-Flag"

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;

const char temp_dir[] = "/var/spool/spamooborona";
const char socket_name[] = "/var/run/sp-daemon.sock";

typedef int socket_t;
static socket_t sock = -1;

int iFdInp;

static int mOpenTmp (char *pszDir, char *pszPrefix, char *pszPath)
{
/*
	Creates a temp file in pszDir (/tmp if NULL) opened for exclusive access. Will be deleted on close. Avoids using
	tempnam(). Returns fd or -1 if error and sets errno
*/
    int iLen;
    static int miRand = 1;
    int iFd = -1;
    int iI;
    char *pszSep = "";

	if (!pszDir) pszDir = "/var/spool/spamooborona";

	iLen = strlen(pszDir);
	if (iLen > MAX_PATH) { return -1; }
	if (pszDir[iLen -1] != '/') { pszSep = "/"; };

	/* mode 0600 conforms with glibc */
	for (iI = 0; iI < 10; iI++) {
		/* Create path */
		sprintf (pszPath, "%s%s%.8s%8.8d%3.3d%6.6d", pszDir, pszSep, pszPrefix, getpid(), miRand++, time(NULL));

		umask ((mode_t) 0);
		if ((iFd = open (pszPath, O_RDWR | O_EXCL | O_CREAT, 0666)) > 0) break;

		if (errno != EEXIST) return -1;	/* Unexpected error */

		/* Else, for some reason this file exists. Try again after a short sleep */
		sleep(1);
	}
	if (iFd == -1) return -1;	/* Tried 10 times */

	/* Return open file descriptor */
	return iFd;
}


static int ReadFd (int iFdMsg, int fd)
{
    char psMsg [MAX_SIZE_SO]; // 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)
{
    char sCommand[1024];
    char answ [3];
    int  Len;

    sprintf (sCommand, "%s %s", command, value);

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

    memset (answ, '\0', sizeof (answ));
    Len = read (sock, answ, sizeof (answ));
    if (READ_FAIL (Len))
    {
        log_write (0, LOG_MAIN, "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, "server did not confirm, answ=%s", answ);
        if (Cleanup)
            CleanupInp (sName);
        return ERR_WRITE;	// Cannot read message error code
    }

    return OK;
}


char *RemoteHost (char	*c_IP,
		  char	*c_host)
{
    char *sRemoteHost;

    sRemoteHost = (char *)store_get (c_IP ? strlen (c_IP) : 0 + 
                                  c_host ? strlen (c_host) : 0 + 4);
    sprintf (sRemoteHost, "%s [%s]", c_host ? c_host : "", c_IP ? c_IP : "");

    return sRemoteHost;
}


static int SendEnvelope (char *sFile)
{
    int i;
    char str [256];
    char *rh = NULL;
    
    // sender IP and hostname
    rh = RemoteHost (sender_host_address, sender_host_name);
    if (FakeSMTPCommand (sock, "CONNECT", rh, sFile, 1) != _OK)
        return ERR_WRITE;

    // envelope from
    if (FakeSMTPCommand (sock, "MAILFROM", 
                         strlen (sender_address) == 0 ?  "MAILER-DAEMON" : (char*) sender_address, sFile, 1) != _OK)
        return ERR_WRITE;
    
    // envelope rcpto
    for (i = 0; i < recipients_count; i ++)
    {
        if (FakeSMTPCommand (sock, "RCPTTO", recipients_list[i].address, sFile, 1) != _OK)
            return ERR_WRITE;
    }

    if (FakeSMTPCommand (sock, "DATA", sFile, sFile, 1) != _OK)
        return ERR_WRITE;
        
    if (FakeSMTPCommand (sock, ".", "", sFile, 1) != _OK)
        return ERR_WRITE;
        
    return _OK;
}


int GetFiles (char *pInpFile, int local_scan_fd)
{
    /*
        Returns OK if no errors, else error code.
        On succesful 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;

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

    if (iStatus != _OK)
    {
        log_write (0, LOG_MAIN, "sp-exim: Error %d getting message", iStatus);
        close (sock);
        return iStatus;
    }
    
    iStatus = SendEnvelope (sFile);
    if (iStatus != _OK)
    {
        log_write (0, LOG_MAIN, "sp-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 (strncmp (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->next->type = '*';
                h_line = h_line->next;
            }
        }
        h_line = h_line->next;
    }
}

void AlterSubject (char *label)
{
    struct header_line *h_line;
    char subject [1024];
    char *strP;
 
    h_line = header_list;
    
    memset (subject, '\0', sizeof (subject));
    while (h_line != NULL)
    {
        if (h_line->type == '*') // internal header
        {
            h_line = h_line->next;
            continue;
        }
        
        if (strncmp (h_line->text, "Subject", strlen("Subject")) == 0)
        {
            strP = strchr (h_line->text, ':');
            strncpy (subject, ++strP, sizeof (subject) - strlen (strP) - 1);
            while (h_line->next &&
                   (*h_line->next->text == ' ' || *h_line->next->text == '\t'))
            {
                strcat (subject, h_line->next->text);
                h_line = h_line->next;
            }
            header_del (US "Subject");
            break;
        }

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

int WaitForScanResult (uschar **resStr)
{
    int Len;
    int rej, result = LOCAL_SCAN_ACCEPT, answer_size, spm = 0;
    char answ [4096];
    char *strP, *tok, *tmp, *spmStr = NULL;
    char hdr [256];
    char hdrv [4096];

    memset (hdr, '\0', sizeof (hdr));
    memset (hdrv, '\0', sizeof (hdrv));
    memset (answ, '\0', sizeof (answ));
    Len = read (sock, answ, sizeof (answ) - 1);

    if (strncmp (answ, "SODAEMON ", 9) == 0)
    {
        strP = (char *)answ;
        for (tok = (char *)strtok (strP, "\n"); tok; tok = (char *)strtok (NULL, "\n"))
        {
            // signature always goes first
            if (strncmp (tok, "SODAEMON ", 9) == 0)
            {
                if (sscanf (tok, "%*s %d", &answer_size) == 1)
                {
                    if (answer_size == 0) // empty reply
                    {
                        strcpy (hdr, SO_FAILURE_HDR);
                        strcpy (hdrv, "SKIP");
                        result = LOCAL_SCAN_ACCEPT;
                        break;
                    }
                    else
                        if (answer_size > sizeof (answ) - 1)
                            log_write(0, LOG_MAIN, "sp-exim: daemon reports %d size answer", answer_size);
                }
                continue;
            }
           
            // reject or accept flag
            if (strncmp (tok, "REJECT ", 7) == 0)
            {
                if (sscanf (tok, "%*s %d", &rej) == 1)
                    result = rej ? LOCAL_SCAN_REJECT : LOCAL_SCAN_ACCEPT;
 
                continue;
            }
           
            // reject string
            if (strncmp (tok, "REJECTSTR ", 10) == 0)
            {
                tmp = strchr (tok, ' ');
                if (result == LOCAL_SCAN_REJECT)
                    *resStr = (uschar *)strdup (++tmp);
               
                continue;
            }
            
            // spam flag
            if (strncmp (tok, "SPAM ", 5) == 0)
            {
                if (sscanf (tok, "%*s %d", &spm) != 1)
                {
                    spm = 0;
                    log_write(0, LOG_MAIN, "sp-exim: Error receiving spam flag");
                }

                continue;
            }
           
            // spam label
            if (strncmp (tok, "SPAMSTR ", 8) == 0)
            {
                tmp = strchr (tok, ' ');
                if (spm)
                    spmStr = strdup (++tmp);
                    
                continue;
            }
           

            if (! isspace (*tok)) // New header
            {
                if (strlen (hdr))
                {
                    header_del ((uschar *) hdr);
                    header_add (' ', "%s: %s\n", hdr, hdrv);
                }

                memset (hdr, '\0', sizeof (hdr));
                memset (hdrv, '\0', sizeof (hdrv));
                tmp = strchr (tok, ':');
                if (tmp)
                   *tmp = '\0';
                strncpy (hdr, tok, sizeof (hdr) - 1);
                ++tmp;
                while (isspace (*tmp))
                   ++tmp;
                strncpy (hdrv, tmp, sizeof (hdrv) - 1);
               
            }
            else // append multiline header value
            {
                hdrv [strlen (hdrv)] = '\n';
                strncpy (&hdrv [strlen (hdrv)], tok, sizeof (hdrv) - strlen (hdrv) - 1);
            }
           
        }
        
        // do not forget the last header
        if (strlen (hdr))
        {
            header_del ((uschar *) hdr);
            header_add (' ', "%s: %s\n", hdr, hdrv);
        }

        if (spm && spmStr)
            AlterSubject (spmStr);
        
    }
    else
    {
        result = LOCAL_SCAN_ACCEPT;
        log_write(0, LOG_MAIN, "sp-exim: wrong signature in answer: %s", answ);
    }
        
    return result;
}


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

    // Socket stuff
    if ((sock = socket (AF_UNIX, SOCK_STREAM, 0)) < 0)
    {
        log_write(0, LOG_MAIN, "sp-exim: socket() failed");
        exit (1);    
    }

    memset (&sockaddr, '\0', sizeof (struct sockaddr_un));
    sockaddr.sun_family = AF_UNIX;
    if (sizeof (socket_name) > sizeof (sockaddr.sun_path))
    {
        close (sock);
        log_write(0, LOG_MAIN, "sp-exim: UNIX socket name %s too long", socket_name);
        exit (1);
    }
    strcpy (sockaddr.sun_path, socket_name);
    
    for (ccnt = 0; ccnt <= MAX_FAILS_C; ccnt ++)
    {
        if (connect (sock, (struct sockaddr *) &sockaddr, sizeof (struct sockaddr_un)) < 0)
        {
            if (ccnt < MAX_FAILS_C)
                usleep (100);
            else
            {
                close (sock);
                log_write(0, LOG_MAIN, "sp-exim: socked connect to %s failed", (char *)socket_name);
                return LOCAL_SCAN_TEMPREJECT;
            }
        }
        else
            break;
    }

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

    unlink (sFileInp);
    close (sock);
    SPOOL_DATA_START_OFFSET;
    
    return retval;
}

/* End of local_scan.c */
