// // based on source code from http://www.ols.es/exim/dlext/ by David Saez , // source code of exim by Philip Hazel // and source code of exiscan by Tom Kistner // //#include "local_scan.h" //#include "macros.h" #include "exim.h" #include "transports/lmtp.h" #include #include #include #include #include #include #include extern uschar *tod_stamp(int); //extern uschar *spool_directory; extern uschar *primary_hostname; #define DSPAM_TIMEOUT 120 #define DSPAM_ANSWER_MAX_SIZE 32600 //------------------------------------------------------------------------- int dspam(uschar **yield, int argc, uschar *argv[]) { char *arg_socket_addr; char *arg_ident; char *arg_defer_ok; char *arg_user_name; int defer_ok; char tcp_addr[15]; int tcp_port; FILE *mbox_file = NULL;; FILE *dspam_file = NULL; header_line *my_header, *header_new, *header_last, *tmp_headerlist; header_line *last_received = NULL; uschar *str; uschar *address; char mbox_path[512]; int dspam_sock = 0; struct hostent *he; struct in_addr in; struct sockaddr_un server; #ifndef NO_POLL_H int result; struct pollfd pollfd; #endif int offset; uschar dspam_buffer[32600]; uschar dspam_answer[DSPAM_ANSWER_MAX_SIZE]; time_t start; size_t read, wrote; uschar *p; arg_socket_addr = argv[0]; arg_ident = argv[1]; arg_defer_ok = argv[2]; arg_user_name = argv[3]; if (argc < 4) { 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 < 4) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Bad number of arguments: %d", argc); *yield = string_sprintf("dspam dlfunc: Bad number of arguments: %d", argc); return(defer_ok ? OK : ERROR); } if ((arg_socket_addr == NULL) || (arg_socket_addr[0] == 0)) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Socket address expected"); *yield = string_sprintf("dspam dlfunc: Socket address expected"); return(defer_ok ? OK : ERROR); } if ((arg_ident == NULL) || (arg_ident[0] == 0)) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: DSPAM LMTP ident expected"); *yield = string_sprintf("dspam dlfunc: DSPAM LMTP ident expected"); return(defer_ok ? OK : ERROR); } if ((arg_user_name == NULL) || (arg_user_name[0] == 0)) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Username expected"); *yield = string_sprintf("dspam dlfunc: Username expected"); return(defer_ok ? OK : ERROR); } // get message body stream sprintf(mbox_path, "%s/input/%s-D", spool_directory, message_id); debug_printf(" Open spool file: %s\n", mbox_path); mbox_file = fopen(mbox_path,"rb"); if (!mbox_file) { *yield = string_copy((uschar *)"dspam dlfunc: Unable to spool message"); return(defer_ok ? OK : ERROR); } 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] != '/') { if (sscanf(CS arg_socket_addr, "%s %u", tcp_addr, &tcp_port) != 2 ) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Invalid dspam address: '%s'", arg_socket_addr); *yield = string_sprintf("dspam dlfunc: Invalid dspam address: '%s'", arg_socket_addr); goto RETURN_DEFER; } /* Lookup the host */ if((he = gethostbyname(CS tcp_addr)) == 0) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: failed to lookup host '%s'", tcp_addr); *yield = string_sprintf("dspam dlfunc: failed to lookup host '%s'", tcp_addr); goto RETURN_DEFER; } in = *(struct in_addr *) he->h_addr_list[0]; /* contact a dspam */ if ((dspam_sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: TCP socket creation failed: %s", strerror(errno)); *yield = string_sprintf("dspam dlfunc: TCP socket creation failed: %s", strerror(errno)); goto RETURN_DEFER; }; if (ip_connect(dspam_sock, AF_INET, (uschar*)inet_ntoa(in), tcp_port, 5) < 0) { if (dspam_sock >= 0) (void)close(dspam_sock); log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: connection to %s, port %u failed: %s", tcp_addr, tcp_port, strerror(errno)); *yield = string_sprintf("dspam dlfunc: connection to %s, port %u failed: %s", tcp_addr, tcp_port, strerror(errno)); goto RETURN_DEFER; } debug_printf(" Use TCP socket %s:%d\n", tcp_addr, tcp_port); } else { if ((dspam_sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Unable to acquire socket (%s)", strerror(errno)); *yield = string_sprintf("dspam dlfunc: Unable to acquire socket (%s)", strerror(errno)); goto RETURN_DEFER; } server.sun_family = AF_UNIX; Ustrcpy(server.sun_path, arg_socket_addr); if (connect(dspam_sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Unable to connect to UNIX socket %s (%s)", socket, strerror(errno) ); *yield = string_sprintf("dspam dlfunc: Unable to connect to UNIX socket %s (%s)", socket, strerror(errno) ); goto RETURN_DEFER; } debug_printf(" Use UNIX Domain socket %s\n", arg_socket_addr); } /* now we are connected to dspam on dspam_sock */ dspam_file = fdopen(dspam_sock, "rb"); if (!lmtp_read_response(dspam_file, dspam_buffer, sizeof(dspam_buffer), '2', DSPAM_TIMEOUT - time(NULL) + start)) goto RESPONSE_FAILED; /* send our request */ if (!lmtp_write_command(dspam_sock, "LHLO %s\r\n", primary_hostname)) goto WRITE_FAILED; if (!lmtp_read_response(dspam_file, dspam_buffer, sizeof(dspam_buffer), '2', DSPAM_TIMEOUT - time(NULL) + start)) goto RESPONSE_FAILED; if (!lmtp_write_command(dspam_sock, "MAIL FROM: <%s> DSPAMPROCESSMODE=\"%s \"\r\n", arg_ident, "--client --stdout --deliver=innocent,spam --debug")) goto WRITE_FAILED; if (!lmtp_read_response(dspam_file, dspam_buffer, sizeof(dspam_buffer), '2', DSPAM_TIMEOUT - time(NULL) + start)) { if (errno == 0 && dspam_buffer[0] == '4') errno = ERRNO_MAIL4XX; goto RESPONSE_FAILED; } if (!lmtp_write_command(dspam_sock, "RCPT TO: <%s>\r\n", arg_user_name)) goto WRITE_FAILED; if (!lmtp_read_response(dspam_file, dspam_buffer, sizeof(dspam_buffer), '2', DSPAM_TIMEOUT - time(NULL) + start)) { if (errno == 0 && dspam_buffer[0] == '4') errno = ERRNO_RCPT4XX; goto RESPONSE_FAILED; } if (!lmtp_write_command(dspam_sock, "DATA\r\n")) goto WRITE_FAILED; if (!lmtp_read_response(dspam_file, dspam_buffer, sizeof(dspam_buffer), '3', DSPAM_TIMEOUT - time(NULL) + start)) { if (errno == 0 && dspam_buffer[0] == '4') errno = ERRNO_DATA4XX; goto RESPONSE_FAILED; } /* * now send the file */ Ustrcpy(big_buffer, "sending data block"); /* For error messages */ debug_printf(" LMTP>> writing message and terminating \".\"\n"); address = expand_string(US"${sender_address}"); if (address && *address) { sprintf(dspam_buffer, "Return-path: <%s>\n", address); debug_printf(" Send to socket: %s", dspam_buffer); wrote = send(dspam_sock, dspam_buffer, strlen(dspam_buffer), 0); if (wrote == -1) goto WRITE_FAILED; } // 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}"); if (address && *address) { sprintf(dspam_buffer, "Envelope-To: %s\n", address); debug_printf(" Send to socket: %s", dspam_buffer); wrote = send(dspam_sock, dspam_buffer, strlen(dspam_buffer), 0); if (wrote == -1) goto WRITE_FAILED; } sprintf(dspam_buffer, "Delivery-date: %s\n", tod_stamp(tod_full)); debug_printf(" Send to socket: %s", dspam_buffer); wrote = send(dspam_sock, dspam_buffer, strlen(dspam_buffer), 0); if (wrote == -1) goto WRITE_FAILED; // 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)); header_new->text = string_copyn(my_header->text, my_header->slen); 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 ((str = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) { int len = Ustrlen(str); if (header_testname(my_header, str, 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)); header_new->text = string_copyn(my_header->text, my_header->slen); 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; } for (my_header = tmp_headerlist; my_header; my_header = my_header->next) { if ((my_header->type != '*') && (my_header->type != htype_old)) { debug_printf(" Send to socket: %s", my_header->text); wrote = send(dspam_sock, my_header->text, my_header->slen, 0); if (wrote == -1) goto WRITE_FAILED; } } sprintf(dspam_buffer, "\r\n"); debug_printf(" Send to socket: %s", dspam_buffer); wrote = send(dspam_sock, dspam_buffer, strlen(dspam_buffer), 0); if (wrote == -1) goto WRITE_FAILED; debug_printf("Headers sent\n"); /* * Note: poll() is not supported in OSX 10.2. */ #ifndef NO_POLL_H pollfd.fd = dspam_sock; pollfd.events = POLLOUT; #endif // (void)fcntl(dspam_sock, F_SETFL, O_NONBLOCK); do { read = fread(dspam_buffer,1,sizeof(dspam_buffer),mbox_file); if (read < sizeof(dspam_buffer)) dspam_buffer[read] = 0; //debug_printf(" Read from file: %s", dspam_buffer); if (read > 0) { offset = 0; again: #ifndef NO_POLL_H result = poll(&pollfd, 1, 1000); if (result == -1 && errno == EINTR) continue; else if (result < 1) { if (result == -1) log_write(0, LOG_MAIN|LOG_PANIC, "dspam acl condition: %s on dspam socket", strerror(errno)); else { if (time(NULL) - start < DSPAM_TIMEOUT) goto again; log_write(0, LOG_MAIN|LOG_PANIC, "dspam acl condition: timed out writing dspam socket"); *yield = string_sprintf("dspam acl condition: timed out writing dspam socket"); } goto RETURN_DEFER; } #endif wrote = send(dspam_sock, dspam_buffer + offset, read - offset, 0); //debug_printf(" Send to socket %d bytes: %s", read - offset, dspam_buffer + offset); if (wrote == -1) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam acl condition: %s on dspam socket", strerror(errno)); *yield = string_sprintf("dspam acl condition: %s on dspam socket", strerror(errno)); goto RETURN_DEFER; } 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, "dspam acl condition: error reading spool file: %s", strerror(errno)); *yield = string_sprintf("dspam acl condition: error reading spool file: %s", strerror(errno)); goto RETURN_DEFER; } if (!lmtp_write_command(dspam_sock, ".\r\n")) goto WRITE_FAILED; /* read dspam response using what's left of the timeout. */ { uschar *rc; int save_errno; int done = 0; int len, max_len; int dspam_answer_len = 0; int continue_header = 0; do { dspam_buffer[0] = 0; // In case nothing gets read sigalrm_seen = FALSE; alarm(DSPAM_TIMEOUT - time(NULL) + start); rc = Ufgets(&dspam_buffer, sizeof(dspam_buffer) - 1, dspam_file); save_errno = errno; alarm(0); errno = save_errno; if (rc == NULL) { // Handle timeout; must do this first because it uses EINTR if (sigalrm_seen) { errno = ETIMEDOUT; debug_printf("LMTP timeout while reading data from socket\n"); log_write(0, LOG_MAIN|LOG_PANIC, "dspam acl condition: error reading data from LMTP socket: %s", strerror(errno)); *yield = string_sprintf("dspam acl condition: error reading data from LMTP socket: %s", strerror(errno)); goto RETURN_DEFER; // If some other interrupt arrived, just retry. We presume this to be rare, // but it can happen (e.g. the SIGUSR1 signal sent by exiwhat causes // read() to exit). } else if (errno == EINTR) { debug_printf("EINTR while reading LMTP response\n"); continue; } } //debug_printf("read from LMTP socket: %s", rc); if ((strcmpic(rc, US"\n") == 0) || (strcmpic(rc, US"\r\n") == 0) || (strcmpic(rc, US".\r\n") == 0)) { done = 1; } else { max_len = sizeof(dspam_answer) - dspam_answer_len - 1; p = Ustrstr(rc, "X-Daemon-Classification: "); if ((p == NULL) || (p != rc)) p = Ustrstr(rc, "X-DSPAM-"); if (p == rc) { len = Ustrlen(rc); if (len > max_len) len = max_len; Ustrncpy(dspam_answer + dspam_answer_len, rc, len); dspam_answer_len += len; continue_header = 1; } else { if (((rc[0] == 0x20) || (rc[0] == 0x09)) && (dspam_answer_len > 0) && (continue_header)) { len = Ustrlen(rc); if (len > max_len) len = max_len; Ustrncpy(dspam_answer + dspam_answer_len, rc, len); dspam_answer_len += len; } else { continue_header = 0; } } dspam_answer[dspam_answer_len] = 0; } } while (!feof(dspam_file) && !ferror(dspam_file) && !done); } if (ferror(dspam_file)) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam acl condition: error reading response from LMTP socket: %s", strerror(errno)); *yield = string_sprintf("dspam acl condition: error reading response from LMTP socket: %s", strerror(errno)); goto RETURN_DEFER; } (void)lmtp_write_command(dspam_sock, "QUIT\r\n"); (void)lmtp_read_response(dspam_file, dspam_buffer, sizeof(dspam_buffer), '2', DSPAM_TIMEOUT); if (dspam_file >= 0) (void)fclose(dspam_file); if (dspam_sock >= 0) (void)close(dspam_sock); if (mbox_file >= 0) (void)fclose(mbox_file); *yield = string_sprintf("DSPAM answer: %s", dspam_answer); 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. */ RESPONSE_FAILED: { int code; int save_errno; int more_errno; uschar message_buffer[256]; uschar *message; save_errno = errno; message = &message_buffer[0]; if (check_response(&save_errno, more_errno, dspam_buffer, &code, &message)) { (void) lmtp_write_command(dspam_sock, "QUIT\r\n"); (void) lmtp_read_response(dspam_file, dspam_buffer, sizeof(dspam_buffer), '2', 2); } log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: %s", message); *yield = string_sprintf("dspam dlfunc: %s", message); goto RETURN_DEFER; } /* Come here if there are errors during writing of a command or the message itself. This error will be applied to all the addresses. */ WRITE_FAILED: { if (errno == ERRNO_CHHEADER_FAIL) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Failed to expand headers_add or headers_remove: %s", expand_string_message); *yield = string_sprintf("dspam dlfunc: Failed to expand headers_add or headers_remove: %s", expand_string_message); } else if (errno == ERRNO_FILTER_FAIL) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Filter process failure"); *yield = string_sprintf("dspam dlfunc: Filter process failure"); } else if (errno == ERRNO_WRITEINCOMPLETE) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: Failed repeatedly to write data"); *yield = string_sprintf("dspam dlfunc: Failed repeatedly to write data"); } else if (errno == ERRNO_SMTPFORMAT) { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: overlong LMTP command generated"); *yield = string_sprintf("dspam dlfunc: overlong LMTP command generated"); } else { log_write(0, LOG_MAIN|LOG_PANIC, "dspam dlfunc: write failed: error %d: %s", errno, strerror(errno)); *yield = string_sprintf("dspam dlfunc: write failed: error %d: %s", errno, strerror(errno)); } goto RETURN_DEFER; } RETURN_DEFER: { if (dspam_file >= 0) (void)fclose(dspam_file); if (dspam_sock >= 0) (void)close(dspam_sock); if (mbox_file >= 0) (void)fclose(mbox_file); return(defer_ok ? OK : ERROR); } return(OK); }