/* * spamd.c - SpamAssassin spamd dlfunc for Exim * https://mta.org.ua/exim-4.94-conf/dlfunc/spamd/spamd.c * * Copyright (C) 2006-2020 Victor Ustugov * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library 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 Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Based on examples from http://www.ols.es/exim/dlext/ by David Saez , * source code of exim by Philip Hazel , * source code of exiscan by Tom Kistner 2003 - 2015 * source code of exim by The Exim Maintainers 2016 - 2018 */ // // warn set acl_m0 = ${dlfunc{/usr/local/libexec/exim/exim-dlfunc.so}{spamd}\ // {127.0.0.1 783}{defer_ok}{spamd}{report}} // #include #include #include #include #include #include #include //#include "local_scan.h" //#include "macros.h" #include "exim.h" //# define string_copy(s) string_copy_function(s) //# define string_copyn(s, n) string_copyn_function((s), (n)) //# define string_copy_taint(s, t) string_copy_taint_function((s), (t)) extern uschar * string_copy_function(const uschar *); extern uschar * string_copyn_function(const uschar *, int n); extern uschar * string_copy_taint_function(const uschar *, BOOL tainted); #define string_sprintf(fmt, ...) \ string_sprintf_trc(fmt, US __FUNCTION__, __LINE__, __VA_ARGS__) extern uschar *string_sprintf_trc(const char *, const uschar *, unsigned, ...) ALMOST_PRINTF(1,4); extern uschar *tod_stamp(int); //extern BOOL split_spool_directory; /* TRUE to use multiple subdirs */ //extern uschar *spool_directory; /* Name of spool directory */ //extern uschar message_subdir[]; /* Subdirectory for messages */ /***************************************************************************** * Configuration settings: * *****************************************************************************/ #define WITH_PROTO_SPAMC_1_3 #define SPAMD_TIMEOUT 90 #define SPAM_COMMAND_REPORT 1 #define SPAM_COMMAND_PROCESS 2 //------------------------------------------------------------------------- int spamd(uschar **yield, int argc, uschar *argv[]) { uschar *arg_socket_addr; uschar *arg_defer_ok; uschar *arg_user_name; uschar *arg_spamd_command; int defer_ok; int spamd_command; char tcp_addr[15]; int tcp_port; FILE *mbox_file = NULL; unsigned long mbox_size; uschar *s, *p; header_line *my_header, *header_new, *header_last, *tmp_headerlist; header_line *last_received = NULL; uschar *address; char mbox_path[512]; int max_len, len; client_conn_ctx spamd_cctx = {.sock = -1}; struct hostent *he; struct in_addr in; int result; #ifndef NO_POLL_H struct pollfd pollfd; // patch posted by Erik ? for OS X and applied by PH #else fd_set select_fd; // /patch posted by Erik ? for OS X and applied by PH #endif int offset; #define SPAMD_BUFFER_SIZE 32600 uschar * spamd_buffer = NULL; #define SPAMD_BUFFER2_SIZE 32600 uschar * spamd_buffer2 = NULL; time_t start; size_t read, wrote; int i; uschar * errstr; arg_socket_addr = argv[0]; arg_defer_ok = argv[1]; arg_user_name = argv[2]; arg_spamd_command = argv[3]; if (argc < 3) { defer_ok = 0; } else if ( (strcmpic(arg_defer_ok,US"1") == 0) || (strcmpic(arg_defer_ok,US"yes") == 0) || (strcmpic(arg_defer_ok,US"true") == 0) || (strcmpic(arg_defer_ok,US"defer_ok") == 0) ) { defer_ok = 1; } else { defer_ok = 0; } debug_printf(" defer_ok: %d\n", defer_ok); if (argc < 3) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: Invalid number of arguments: %d", argc); *yield = string_sprintf("spamd dlfunc: Invalid number of arguments: %d", argc); goto RETURN_DEFER; } if ((arg_socket_addr == NULL) || (arg_socket_addr[0] == 0)) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: Socket address expected"); *yield = string_copy(US"spamd dlfunc: Socket address expected"); goto RETURN_DEFER; } if ((arg_user_name == NULL) || (arg_user_name[0] == 0)) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: Username expected"); *yield = string_copy(US"spamd dlfunc: Username expected"); goto RETURN_DEFER; } if ((arg_spamd_command == NULL) || (arg_spamd_command[0] == 0)) { spamd_command = SPAM_COMMAND_REPORT; } else if (strcmpic(arg_spamd_command,US"process") == 0) { spamd_command = SPAM_COMMAND_PROCESS; } else { spamd_command = SPAM_COMMAND_REPORT; } debug_printf(" SPAMD command: %s\n", (spamd_command == SPAM_COMMAND_REPORT ? "REPORT" : "PROCESS")); // get message body stream if (split_spool_directory == 0) { sprintf(mbox_path, "%s/input/%s-D", spool_directory, message_id); } else { sprintf(mbox_path, "%s/input/%s/%s-D", spool_directory, message_subdir, message_id); } debug_printf(" Open spool file: %s\n", mbox_path); mbox_file = fopen(mbox_path,"rb"); if (!mbox_file) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: Unable to spool message '%s'", mbox_path); *yield = string_sprintf("spamd dlfunc: Unable to spool message '%s'", mbox_path); return(defer_ok ? OK : ERROR); } (void)fseek(mbox_file, 0, SEEK_END); mbox_size = ftell(mbox_file); debug_printf(" Total spool file size: %ld\n", (long)mbox_size); mbox_size -= SPOOL_DATA_START_OFFSET; debug_printf(" Spool file size: %ld\n", (long)mbox_size); debug_printf(" fseek %d, %d\n", SPOOL_DATA_START_OFFSET, SEEK_SET); (void)fseek(mbox_file, SPOOL_DATA_START_OFFSET, SEEK_SET); start = time(NULL); // socket does not start with '/' -> network socket if ((arg_socket_addr[0] != '/') && (sscanf(CS arg_socket_addr, "%s %u", tcp_addr, &tcp_port) != 2 )) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: Invalid spamd address: '%s'", arg_socket_addr); *yield = string_sprintf("spamd dlfunc: Invalid spamd address: '%s'", arg_socket_addr); goto RETURN_DEFER; } debug_printf(" Try to connect to spamd on socket: %s\n", arg_socket_addr); if ((spamd_cctx.sock = ip_streamsocket(arg_socket_addr, &errstr, 5, NULL)) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: %s", errstr); *yield = string_sprintf("spamd dlfunc: %s", errstr); goto RETURN_DEFER; }; (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK); // now we are connected to spamd on spamd_cctx.sock // spamd_buffer2 = store_get(SPAMD_BUFFER2_SIZE, is_tainted(sender_address)); spamd_buffer2 = store_get(SPAMD_BUFFER2_SIZE, TRUE); memset(spamd_buffer2, 0, SPAMD_BUFFER2_SIZE); // address = expand_string(US"${if def:received_for{$received_for}}"); address = expand_string(US"${received_for}"); if (!address || !*address) address = expand_string(US"${recipients}"); if (!address || !*address) address = expand_string(US"${local_part}@${domain}"); (void)string_format(spamd_buffer2, SPAMD_BUFFER2_SIZE, "Return-path: <%s>\n%sDelivery-date: %s\n", ((sender_address && *sender_address) ? sender_address : (uschar *)""), ((address && *address) ? string_sprintf("Envelope-To: %s\n", address) : (uschar *)""), tod_stamp(tod_full) ); offset = Ustrlen(spamd_buffer2); // create a copy of original headers list tmp_headerlist = NULL; header_last = NULL; for (my_header = header_list; my_header; my_header = my_header->next) { if ((my_header->type != '*') && (my_header->type != htype_old)) { header_new = store_get(sizeof(header_line), FALSE); //// header_new->text = string_copyn(my_header->text, my_header->slen); // header_new->text = store_get(my_header->slen, FALSE); // memcpy(header_new->text, my_header->text, my_header->slen); header_new->text = my_header->text; header_new->slen = my_header->slen; header_new->type = my_header->type; header_new->next = NULL; //debug_printf(" create a copy of header item: '%s'\n", header_new->text); if (tmp_headerlist == NULL) tmp_headerlist = header_new; if (header_last != NULL) header_last->next = header_new; header_last = header_new; } } // headers removed by acl_check_data if (acl_removed_headers != NULL) { for (my_header = tmp_headerlist; my_header != NULL; my_header = my_header->next) { uschar *list; list = acl_removed_headers; int sep = ':'; // This is specified as a colon-separated list uschar buffer[128]; while ((s = string_nextinlist((const uschar **)&list, &sep, buffer, sizeof(buffer))) != NULL) { int len = Ustrlen(s); if (header_testname(my_header, s, len, FALSE)) { //debug_printf(" header removed by acl_check_data: '%s'; '%s'\n", s, my_header->text); my_header->type = htype_old; } } } } // headers added by acl_check_data my_header = acl_added_headers; while (my_header != NULL) { //debug_printf(" header added by acl_check_data: '%s'\n", my_header->text); header_new = store_get(sizeof(header_line), FALSE); //// header_new->text = string_copyn(my_header->text, my_header->slen); // header_new->text = store_get(my_header->slen, FALSE); // memcpy(header_new->text, my_header->text, my_header->slen); header_new->text = my_header->text; header_new->slen = my_header->slen; switch(my_header->type) { case htype_add_top: // add header at top header_new->next = tmp_headerlist; tmp_headerlist = header_new; break; case htype_add_rec: // add header after Received: if (last_received == NULL) { last_received = tmp_headerlist; while (!header_testname(last_received, US"Received", 8, FALSE)) last_received = last_received->next; while (last_received->next != NULL && header_testname(last_received->next, US"Received", 8, FALSE)) last_received = last_received->next; } header_new->next = last_received->next; last_received->next = header_new; break; case htype_add_rfc: // add header before any header which is NOT Received: or Resent- last_received = tmp_headerlist; while ( (last_received->next != NULL) && ( (header_testname(last_received->next, US"Received", 8, FALSE)) || (header_testname_incomplete(last_received->next, US"Resent-", 7, FALSE)) ) ) last_received = last_received->next; // last_received now points to the last Received: or Resent-* header // in an uninterrupted chain of those header types (seen from the beginning // of all headers. Our current header must follow it. header_new->next = last_received->next; last_received->next = header_new; break; default: // htype_add_bot // add header at bottom header_new->next = NULL; header_last->next = header_new; break; } if (header_new->next == NULL) header_last = header_new; my_header = my_header->next; } // copy all the headers to data buffer my_header = tmp_headerlist; while (my_header) { if (my_header->type != htype_old) { max_len = SPAMD_BUFFER2_SIZE - offset - 1; len = my_header->slen; if (len > max_len) len = max_len; Ustrncpy(spamd_buffer2 + offset, my_header->text, len); offset += len; //debug_printf(" copy header item: '%s'\n", my_header->text); } //// if (my_header->text) store_release(my_header->text); header_last = my_header; my_header = my_header->next; // if (header_last) store_release(header_last); } // s = string_copy(US"\r\n"); s = string_copy(US"\n"); max_len = SPAMD_BUFFER2_SIZE - offset - 1; len = Ustrlen(s); if (len > max_len) len = max_len; Ustrncpy(spamd_buffer2 + offset, s, len); offset += len; debug_printf(" headers:\n%s\n", spamd_buffer2); debug_printf(" Headers size: %d\n", offset); mbox_size += offset; debug_printf(" Total message size: %ld\n", (long)mbox_size); spamd_buffer = store_get(SPAMD_BUFFER_SIZE, is_tainted(sender_address)); // spamd_buffer = store_get(SPAMD_BUFFER_SIZE, TRUE); memset(spamd_buffer, 0, SPAMD_BUFFER_SIZE); // copy request to buffer #ifdef WITH_PROTO_SPAMC_1_3 string_format(spamd_buffer, SPAMD_BUFFER_SIZE, "%s SPAMC/1.3\r\nUser: %s\r\nContent-length: %ld\r\nSender: %s\r\n\r\n", (spamd_command == SPAM_COMMAND_PROCESS ? "PROCESS" : "REPORT"), arg_user_name, mbox_size, sender_address), #else string_format(spamd_buffer, SPAMD_BUFFER_SIZE, "%s SPAMC/1.2\r\nUser: %s\r\nContent-length: %ld\r\n\r\n", (spamd_command == SPAM_COMMAND_PROCESS ? "PROCESS" : "REPORT"), arg_user_name, mbox_size); #endif debug_printf("sending request: %s\n", spamd_buffer); // send our request if (send(spamd_cctx.sock, spamd_buffer, Ustrlen(spamd_buffer), 0) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: spamd send failed: %s", strerror(errno)); *yield = string_sprintf("spamd dlfunc: spamd send failed: %s", strerror(errno)); goto RETURN_DEFER; } // // now send the data buffer and spool file // debug_printf("sending data block\n"); debug_printf(" Send to socket: %s\n", spamd_buffer2); wrote = send(spamd_cctx.sock, spamd_buffer2, Ustrlen(spamd_buffer2), 0); if (wrote == -1) goto WRITE_FAILED; // // Note: poll() is not supported in OSX 10.2. // memset(spamd_buffer, 0, SPAMD_BUFFER_SIZE); #ifndef NO_POLL_H pollfd.fd = spamd_cctx.sock; pollfd.events = POLLOUT; #endif (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK); do { read = fread(spamd_buffer,1,SPAMD_BUFFER_SIZE,mbox_file); debug_printf(" read mbox file: %ld bytes\n", (long)read); if (read < SPAMD_BUFFER_SIZE) spamd_buffer[read] = 0; //debug_printf(" Read from file: %s", spamd_buffer); if (read > 0) { offset = 0; again: #ifndef NO_POLL_H result = poll(&pollfd, 1, 1000); // patch posted by Erik ? for OS X and applied by PH #else select_tv.tv_sec = 1; select_tv.tv_usec = 0; FD_ZERO(&select_fd); FD_SET(spamd_cctx.sock, &select_fd); result = select(spamd_cctx.sock+1, NULL, &select_fd, NULL, &select_tv); // /patch posted by Erik ? for OS X and applied by PH */ #endif if (result == -1 && errno == EINTR) goto again; else if (result < 1) { if (result == -1) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: %s on spamd socket", strerror(errno)); *yield = string_sprintf("spamd dlfunc: spamd send failed: %s", strerror(errno)); } else { if (time(NULL) - start < SPAMD_TIMEOUT) goto again; log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: timed out writing spamd socket"); *yield = string_copy(US"spamd dlfunc: timed out writing spamd socket"); } goto RETURN_DEFER; } wrote = send(spamd_cctx.sock, spamd_buffer + offset, read - offset, 0); //debug_printf(" Send to socket %ld bytes: %s", long(read) - (long)offset, spamd_buffer + offset); debug_printf(" wrote to spamd socket: %ld bytes\n", (long)wrote); if (wrote == -1) goto WRITE_FAILED; if (offset + wrote != read) { offset += wrote; goto again; } } } while (!feof(mbox_file) && !ferror(mbox_file)); if (ferror(mbox_file)) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: error reading spool file: %s", strerror(errno)); *yield = string_sprintf("spamd dlfunc: error reading spool file: %s", strerror(errno)); goto RETURN_DEFER; } /* we're done sending, close socket for writing */ shutdown(spamd_cctx.sock,SHUT_WR); /* read spamd response using what's left of the timeout. */ memset(spamd_buffer, 0, SPAMD_BUFFER_SIZE); offset = 0; debug_printf(" read from spamd socket\n"); while ((i = ip_recv(&spamd_cctx, spamd_buffer + offset, SPAMD_BUFFER_SIZE - offset - 1, SPAMD_TIMEOUT + start)) > 0 ) { offset += i; debug_printf(" read from spamd socket: %d bytes/%d bytes\n", i, offset); } spamd_buffer[offset] = '\0'; /* guard byte */ debug_printf(" total read %d bytes from socket\n", offset); /* error handling */ if((i <= 0) && (errno != 0)) { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: error reading from spamd socket: %s", strerror(errno)); *yield = string_sprintf("spamd dlfunc: error reading from spamd socket: %s", strerror(errno)); goto RETURN_DEFER; } //debug_printf("read from socket: %s", spamd_buffer); if (spamd_cctx.sock > 0) { (void)close(spamd_cctx.sock); spamd_cctx.sock = 0; } if (mbox_file != NULL) { (void)fclose(mbox_file); mbox_file = NULL; } if (spamd_command == SPAM_COMMAND_PROCESS) { s = Ustrstr(spamd_buffer, "\n\r\n"); if (s == NULL) s = Ustrstr(spamd_buffer, "\n\n"); p = s + 1; if (p != NULL) { s = Ustrstr(p, "\n\r\n"); if (s == NULL) s = Ustrstr(p, "\n\n"); if (s != NULL) s[1] = '\0'; } *yield = string_sprintf("SPAMD answer: %s", spamd_buffer); } else { memset(spamd_buffer2, 0, SPAMD_BUFFER2_SIZE); s = Ustrstr(spamd_buffer, "\n\r\n"); if (s == NULL) { s = Ustrstr(spamd_buffer, "\n\n"); Ustrncpy(spamd_buffer2, spamd_buffer, s-spamd_buffer+2); s += 2; } else { Ustrncpy(spamd_buffer2, spamd_buffer, s-spamd_buffer+3); s += 3; } p = &spamd_buffer2[0]; p = p + (s - &spamd_buffer[0]); while (*s != '\0') { /* skip \r */ if (*s == '\r') { s++; continue; }; *p = *s; p++; if (*s == '\n') { *p = ' '; p++; s++; if (*s == '\n') { s++; } } else { s++; }; }; /* NULL-terminate */ *p = '\0'; p--; /* cut off trailing leftovers */ while (*p <= ' ') { *p = '\0'; p--; }; *yield = string_sprintf("SPAMD answer: %s", spamd_buffer2); } return OK; /* Come here if any call to read_response, other than a response after the data phase, failed. Analyse the error, and if isn't too bad, send a QUIT command. Wait for the response with a short timeout, so we don't wind up this process before the far end has had time to read the QUIT. */ WRITE_FAILED: { log_write(0, LOG_MAIN|LOG_PANIC, "spamd dlfunc: %s on spamd socket", strerror(errno)); *yield = string_sprintf("spamd dlfunc: %s on spamd socket", strerror(errno)); goto RETURN_DEFER; } RETURN_DEFER: { if (spamd_cctx.sock > 0) { (void)close(spamd_cctx.sock); spamd_cctx.sock = 0; } if (mbox_file != NULL) { (void)fclose(mbox_file); mbox_file = NULL; } return(defer_ok ? OK : ERROR); } return OK; }