diff options
Diffstat (limited to '')
-rw-r--r-- | contrib/exim/local_scan.c | 700 | ||||
-rw-r--r-- | contrib/exim/local_scan.c.in | 377 |
2 files changed, 1077 insertions, 0 deletions
diff --git a/contrib/exim/local_scan.c b/contrib/exim/local_scan.c new file mode 100644 index 0000000..42cd5ad --- /dev/null +++ b/contrib/exim/local_scan.c @@ -0,0 +1,700 @@ +/* + 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 (EXIT_FAILURE); + } + 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 (EXIT_FAILURE); + } + 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 (EXIT_FAILURE); + } + 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 */ diff --git a/contrib/exim/local_scan.c.in b/contrib/exim/local_scan.c.in new file mode 100644 index 0000000..72d2f79 --- /dev/null +++ b/contrib/exim/local_scan.c.in @@ -0,0 +1,377 @@ +/* + * 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. + * + * For exim compilation with local scan feature details please visit + * http://www.exim.org/exim-html-current/doc/html/spec_html/ch42.html + * + * For RSPAMD details please visit + * https://bitbucket.org/vstakhov/rspamd/ + * + * Example configuration: + * ********************** + * + * local_scan_timeout = 50s + * + * begin local_scan + * rspam_ip = 127.0.0.1 + * rspam_port = 11333 + * rspam_skip_sasl_authenticated = true + * # don't reject message if on of recipients from this list + * rspam_skip_rcpt = postmaster@example.com : some_user@example.com + * rspam_message = "Spam rejected; If this is not spam, please contact <postmaster@example.com>" + * + * + * $Id: local_scan.c 646 2010-08-11 11:49:36Z ayuzhaninov $ + */ + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/uio.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <errno.h> +#include <math.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include "local_scan.h" + +#define REQUEST_LINES 64 +#define REPLY_BUF_SIZE 16384 +#define HEADER_STATUS "X-Rspam-Status" +#define HEADER_METRIC "X-Rspam-Metric" +#define HEADER_SCORE "X-Rspam-Score" + +/* configuration options */ +static uschar *daemon_ip = US"127.0.0.1"; +static int max_scan_size = 4 * 1024 * 1024; +static uschar *reject_message = US"Spam message rejected"; +static int daemon_port = 11333; +static BOOL skip_authenticated = TRUE; +static uschar *want_spam_rcpt_list = US""; + +/* the entries must appear in alphabetical order */ +optionlist local_scan_options[] = { + { "rspam_ip", opt_stringptr, &daemon_ip }, + { "rspam_max_scan_size", opt_mkint, &max_scan_size }, + { "rspam_message", opt_stringptr, &reject_message }, + { "rspam_port", opt_int, &daemon_port }, + { "rspam_skip_rcpt", opt_stringptr, &want_spam_rcpt_list }, + { "rspam_skip_sasl_authenticated", opt_bool, &skip_authenticated }, +}; + +int local_scan_options_count = sizeof(local_scan_options) / sizeof(optionlist); + +/* push formatted line into vector */ +int push_line(struct iovec *iov, int i, const char *fmt, ...); + +int +local_scan(int fd, uschar **return_text) +{ + struct stat sb; + struct sockaddr_in server_in; + int s, i, r, request_p = 0, headers_count = 0, is_spam = 0, is_reject = 0; + off_t message_size; + struct iovec request_v[REQUEST_LINES], *headers_v; +#if "@CMAKE_SYSTEM_NAME@" == "FreeBSD" + struct sf_hdtr headers_sf; +#endif + uschar *helo, *log_buf; + header_line *header_p; + char reply_buf[REPLY_BUF_SIZE], io_buf[BUFSIZ]; + ssize_t size; + char *tok_ptr, *str; + char mteric[128], result[8]; + float score, required_score; + + *return_text = reject_message; + + /* + * one msaage can be send via exim+rspamd twice + * remove header from previous pass + */ + header_remove(0, US HEADER_STATUS); + header_remove(0, US HEADER_METRIC); + + /* check message size */ + fstat(fd,&sb); /* XXX shuld check error */ + message_size = sb.st_size - SPOOL_DATA_START_OFFSET; + if (message_size > max_scan_size) { + header_add(' ', HEADER_STATUS ": skip_big\n"); + log_write (0, LOG_MAIN, "rspam: message larger than rspam_max_scan_size, accept"); + return LOCAL_SCAN_ACCEPT; + } + + /* don't scan mail from authenticated hosts */ + if (skip_authenticated && sender_host_authenticated != NULL) { + header_add(' ', HEADER_STATUS ": skip_authenticated\n"); + log_write(0, LOG_MAIN, "rspam: from=<%s> ip=%s authenticated (%s), skip check\n", + sender_address, + sender_host_address == NULL ? US"localhost" : sender_host_address, + sender_host_authenticated); + return LOCAL_SCAN_ACCEPT; + } + + /* + * add status header, which mean, that message was not scanned + * if message will be scanned, this header will be replaced + */ + header_add(' ', HEADER_STATUS ": check_error\n"); + + /* create socket */ + memset(&server_in, 0, sizeof(server_in)); + server_in.sin_family = AF_INET; + server_in.sin_port = htons(daemon_port); + server_in.sin_addr.s_addr = inet_addr(daemon_ip); + if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) { + log_write(0, LOG_MAIN, "rspam: socket (%d: %s)", errno, strerror(errno)); + return LOCAL_SCAN_ACCEPT; + } + if (connect(s, (struct sockaddr *) &server_in, sizeof(server_in)) < 0) { + close(s); + log_write(0, LOG_MAIN, "rspam: can't connect to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno)); + return LOCAL_SCAN_ACCEPT; + } + + /* count message headers */ + for (header_p = header_list; header_p != NULL; header_p = header_p->next) { + /* header type '*' is used for replaced or deleted header */ + if (header_p->type == '*') + continue; + headers_count++; + } + + /* write message headers to vector */ +#if "@CMAKE_SYSTEM_NAME@" == "FreeBSD" + memset(&headers_sf, 0, sizeof(headers_sf)); + if (headers_count > 0) { + headers_v = store_get((headers_count + 1)* sizeof(*headers_v)); + i = 0; + for (header_p = header_list; header_p != NULL; header_p = header_p->next) { + if (header_p->type == '*') + continue; + headers_v[i].iov_base = header_p->text; + headers_v[i].iov_len = header_p->slen; + i++; + message_size += header_p->slen; + } + headers_v[i].iov_base = "\n"; + headers_v[i].iov_len = strlen("\n"); + message_size += strlen("\n"); + + headers_sf.headers = headers_v; + headers_sf.hdr_cnt = headers_count + 1; + } +#else + if (headers_count > 0) { + headers_v = store_get((headers_count + 1)* sizeof(*headers_v)); + i = 0; + for (header_p = header_list; header_p != NULL; header_p = header_p->next) { + if (header_p->type == '*') + continue; + headers_v[i].iov_base = header_p->text; + headers_v[i].iov_len = header_p->slen; + i++; + message_size += header_p->slen; + } + headers_v[i].iov_base = "\n"; + headers_v[i].iov_len = strlen("\n"); + message_size += strlen("\n"); +#endif + + /* write request to vector */ + r = 0; + r += push_line(request_v, request_p++, "SYMBOLS RSPAMC/1.1\r\n"); + r += push_line(request_v, request_p++, "Content-length: " OFF_T_FMT "\r\n", message_size); + r += push_line(request_v, request_p++, "Queue-Id: %s\r\n", message_id); + r += push_line(request_v, request_p++, "From: %s\r\n", sender_address); + r += push_line(request_v, request_p++, "Recipient-Number: %d\r\n", recipients_count); + for (i = 0; i < recipients_count; i ++) + r += push_line(request_v, request_p++, "Rcpt: %s\r\n", recipients_list[i].address); + if ((helo = expand_string(US"$sender_helo_name")) != NULL && *helo != '\0') + r += push_line(request_v, request_p++, "Helo: %s\r\n", helo); + if (sender_host_address != NULL) + r += push_line(request_v, request_p++, "IP: %s\r\n", sender_host_address); + r += push_line(request_v, request_p++, "\r\n"); + + if (r < 0) { + close(s); + return LOCAL_SCAN_ACCEPT; + } + + /* send request */ + if (writev(s, request_v, request_p) < 0) { + close(s); + log_write(0, LOG_MAIN, "rspam: can't send request to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno)); + return LOCAL_SCAN_ACCEPT; + } + +#if "@CMAKE_SYSTEM_NAME@" == "FreeBSD" + /* send headers (from iovec) and message body (from file) */ + if (sendfile(fd, s, SPOOL_DATA_START_OFFSET, 0, &headers_sf, NULL, 0) < 0) { + close(s); + log_write(0, LOG_MAIN, "rspam: can't send message to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno)); + return LOCAL_SCAN_ACCEPT; + } +#else + /* send headers */ + if (writev(s, headers_v, headers_count) < 0) { + close(s); + log_write(0, LOG_MAIN, "rspam: can't send headers to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno)); + return LOCAL_SCAN_ACCEPT; + } + + /* Send message */ + while ((r = read (fd, io_buf, sizeof (io_buf))) > 0) { + if (write (s, io_buf, r) < 0) { + close(s); + log_write(0, LOG_MAIN, "rspam: can't send message to %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno)); + return LOCAL_SCAN_ACCEPT; + } + } +#endif + + /* read reply from rspamd */ + reply_buf[0] = '\0'; + size = 0; + while ((r = read(s, reply_buf + size, sizeof(reply_buf) - size - 1)) > 0 && size < sizeof(reply_buf) - 1) { + size += r; + } + + if (r < 0) { + close(s); + log_write(0, LOG_MAIN, "rspam: can't read from %s:%d (%d: %s)", daemon_ip, daemon_port, errno, strerror(errno)); + return LOCAL_SCAN_ACCEPT; + } + reply_buf[size] = '\0'; + close(s); + + if (size >= REPLY_BUF_SIZE - 1) { + log_write(0, LOG_MAIN, "rspam: buffer is full, reply may be truncated"); + } + + /* parse reply */ + tok_ptr = reply_buf; + + /* + * rspamd can use several metrics, logic implemented here: + * if any metric more than reject_score - will reject + * if any metric true - message will be marked as spam + */ + + /* First line is: <PROTOCOL>/<VERSION> <ERROR_CODE> <ERROR_REPLY> */ + str = strsep(&tok_ptr, "\r\n"); + if (str != NULL && sscanf(str, "%*s %d %*s", &i) == 1) { + if (i != 0) { + log_write(0, LOG_MAIN, "rspam: server error: %s", str); + return LOCAL_SCAN_ACCEPT; + } + } else { + log_write(0, LOG_MAIN, "rspam: bad reply from server: %s", str); + return LOCAL_SCAN_ACCEPT; + } + + while ((str = strsep(&tok_ptr, "\r\n")) != NULL) { + /* skip empty tockens */ + if (*str == '\0') + continue; + if (strncmp(str, "Metric:", strlen("Metric:")) == 0) { + /* + * parse line like + * Metric: default; False; 27.00 / 30.00 + */ + if (sscanf(str, "Metric: %s %s %f / %f", + mteric, result, &score, &required_score) == 4) { + log_write(0, LOG_MAIN, "rspam: metric %s %s %.2f / %.2f", + mteric, result, score, required_score); + header_add(' ', HEADER_METRIC ": %s %s %.2f / %.2f\n", + mteric, result, score, required_score); + /* integers score for use in sieve ascii-numeric comparator */ + if (strcmp(mteric, "default;") == 0) + header_add(' ', HEADER_SCORE ": %d\n", + (int)round(score)); + } else { + log_write(0, LOG_MAIN, "rspam: can't parse: %s", str); + return LOCAL_SCAN_ACCEPT; + } + } else if (strncmp(str, "Action:", strlen("Action:")) == 0) { + /* line like Action: add header */ + str += strlen("Action: "); + if (strncmp(str, "reject", strlen("reject")) == 0) { + is_reject = 1; + is_spam = 1; + } else if (strncmp(str, "add header", strlen("add header")) == 0) { + is_spam = 1; + } + } + } + + /* XXX many allocs by string_sprintf() + * better to sprintf() to single buffer allocated by store_get() + */ + log_buf = string_sprintf("message to"); + for (i = 0; i < recipients_count; i ++) { + log_buf = string_sprintf("%s %s", log_buf, recipients_list[i].address); + if (is_reject && lss_match_address(recipients_list[i].address, want_spam_rcpt_list, TRUE) == OK) { + is_reject = 0; + log_write(0, LOG_MAIN, "rspam: %s want spam, don't reject this message", recipients_list[i].address); + } + } + + if (is_reject) { + log_write(0, LOG_MAIN, "rspam: reject %s", log_buf); + return LOCAL_SCAN_REJECT; + } + + header_remove(0, US HEADER_STATUS); + if (is_spam) { + header_add(' ', HEADER_STATUS ": spam\n"); + log_write(0, LOG_MAIN, "rspam: message marked as spam"); + } else { + header_add(' ', HEADER_STATUS ": ham\n"); + log_write(0, LOG_MAIN, "rspam: message marked as ham"); + } + + return LOCAL_SCAN_ACCEPT; +} + +int +push_line(struct iovec *iov, const int i, const char *fmt, ...) +{ + va_list ap; + size_t len; + char buf[512]; + + if (i >= REQUEST_LINES) { + log_write(0, LOG_MAIN, "rspam: %s: index out of bounds", __FUNCTION__); + return (-1); + } + + va_start(ap, fmt); + len = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + iov[i].iov_base = string_copy(US buf); + iov[i].iov_len = len; + + if (len >= sizeof(buf)) { + log_write(0, LOG_MAIN, "rspam: %s: error, string was longer than %d", __FUNCTION__, sizeof(buf)); + return (-1); + } + + return 0; +} + |