/* 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 #include #include #include #include #include #include #include #include #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 writen (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 (writen (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 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, ccnt; 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 if (connect (sock, (struct sockaddr *) &ssun, sizeof (struct sockaddr_un)) < 0) #else if (connect (sock, (struct sockaddr *) &ssin, sizeof (struct sockaddr_in)) < 0) #endif { 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(next0) goto repeat_read; return next; } int WaitForScanResult (uschar **resStr) { int Len, i; int rej, 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 */