summaryrefslogtreecommitdiffstats
path: root/contrib/exim
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/exim')
-rw-r--r--contrib/exim/dlfunc-json/README10
-rw-r--r--contrib/exim/dlfunc-json/exim-example.txt75
-rw-r--r--contrib/exim/dlfunc-json/rspamd.c576
-rw-r--r--contrib/exim/local_scan.c700
-rw-r--r--contrib/exim/local_scan.c.in377
-rw-r--r--contrib/exim/patch-exim-src_spam.c.diff338
-rw-r--r--contrib/exim/patch-exim-src_spam.c.diff.exim-4.85.diff332
-rw-r--r--contrib/exim/shutdown.patch14
8 files changed, 2422 insertions, 0 deletions
diff --git a/contrib/exim/dlfunc-json/README b/contrib/exim/dlfunc-json/README
new file mode 100644
index 0000000..0e52270
--- /dev/null
+++ b/contrib/exim/dlfunc-json/README
@@ -0,0 +1,10 @@
+FEATURES
+1) Support new http-protocol (/checkv2)
+2) Return action, symbols, symbol options, messages, scan time
+
+INSTALL
+
+1) Get cJSON.c, cJSON.h, put to dlfunc src dir (https://github.com/DaveGamble/cJSON)
+2) Compile dlfunc library:
+ cc rspamd.c -fPIC -fpic -shared -I/root/rpmbuild/BUILD/exim-4.89/build-Linux-x86_64/ -o exim-rspamd-http-dlfunc.so
+3) See exim-example.txt for exim configure
diff --git a/contrib/exim/dlfunc-json/exim-example.txt b/contrib/exim/dlfunc-json/exim-example.txt
new file mode 100644
index 0000000..e6da334
--- /dev/null
+++ b/contrib/exim/dlfunc-json/exim-example.txt
@@ -0,0 +1,75 @@
+acl_smtp_data = acl_check_data
+
+.....
+
+acl_check_data:
+
+.....
+
+# RSPAMD: START
+ warn
+ !authenticated = *
+ add_header = X-Spam-Checker-Version: Rspamd
+ add_header = :at_start:Authentication-Results: ip=$sender_host_address:$sender_host_port, host=$sender_host_name, helo=$sender_helo_name, mailfrom=$sender_address
+ warn
+ #spam = nobody:true
+ #set acl_m0_rspamd = $spam_report
+ set acl_m0_rspamd = ${dlfunc{/usr/local/libexec/exim/exim-rspamd-http-dlfunc.so}{rspamd}{/var/run/rspamd/rspamd.sock}{defer_ok}}
+ accept
+ authenticated = *
+ warn
+ condition = ${if eq{$acl_m0_rspamd}{}}
+ logwrite = RSPAMD check failed
+ add_header = X-Spam-Info: Check failed
+ warn
+ condition = ${if match{$acl_m0_rspamd}{\N^rspamd dlfunc:\s*\N}{yes}{no}}
+ logwrite = RSPAMD check defer: ${sg{$acl_m0_rspamd}{\N^rspamd dlfunc:\s*\N}{}}
+ add_header = X-Spam-Info: Check deffered
+
+ warn
+ remove_header = X-Spam-Checker-Version:X-Spam-Status:X-Spam-Info:X-Spam-Result
+ set acl_m1 = No
+ warn
+ condition = ${if !eq{$acl_m0_rspamd}{}}
+ set acl_m1_yesno = ${if match{$acl_m0_rspamd}{\NAction: (.+?)\n\N}{$1}{}}
+ set acl_m2_status = ${if eq{$acl_m1_yesno}{reject}{REJECT}{\
+ ${if eq{$acl_m1_yesno}{add header}{PROBABLY}{\
+ ${if eq{$acl_m1_yesno}{rewrite subject}{PROBABLY}{\
+ ${if eq{$acl_m1_yesno}{soft reject}{SOFT}{\
+ ${if eq{$acl_m1_yesno}{greylist}{GREYLIST}{NO}}\
+ }}\
+ }}\
+ }}\
+ }}
+ set acl_m1_yesno = ${if eq{$acl_m1_yesno}{}{unknown}{\
+ ${if eq{$acl_m1_yesno}{reject}{Yes}{\
+ ${if eq{$acl_m1_yesno}{add header}{Yes}{\
+ ${if eq{$acl_m1_yesno}{rewrite subject}{Yes}{\
+ ${if eq{$acl_m1_yesno}{soft reject}{Probably}{\
+ ${if eq{$acl_m1_yesno}{greylist}{Probably}{No}}\
+ }}\
+ }}\
+ }}\
+ }}\
+ }}
+ #logwrite = RSPAMD: status: $acl_m2_status
+ #logwrite = RSPAMD DEBUG: $acl_m0_rspamd
+ set acl_m0_rspamd = ${sg{$acl_m0_rspamd}{ Action:.+\n}{}}
+ warn
+ condition = ${if !eq{$acl_m0_rspamd}{}}
+ logwrite = RSPAMD: $acl_m2_status, $acl_m0_rspamd
+ add_header = X-Spam-Result: $acl_m0_rspamd
+ add_header = X-Spam-Status: $acl_m1_yesno
+ defer
+ condition = ${if eq{$acl_m2_status}{GREYLIST}}
+ log_message = Rspamd $acl_m2_status
+ message = Try again later. Message greylisted
+ defer
+ condition = ${if eq{$acl_m2_status}{SOFT}}
+ log_message = Rspamd $acl_m2_status
+ message = Try again later. Message previously greylisted
+ deny
+ condition = ${if eq{$acl_m2_status}{REJECT}}
+ log_message = Rspamd $acl_m2_status
+ message = This message detected as SPAM and rejected
+# RSPAMD: END
diff --git a/contrib/exim/dlfunc-json/rspamd.c b/contrib/exim/dlfunc-json/rspamd.c
new file mode 100644
index 0000000..2623c7d
--- /dev/null
+++ b/contrib/exim/dlfunc-json/rspamd.c
@@ -0,0 +1,576 @@
+/*-
+ * Copyright (c) 2013-2015, Alexey Savelyev <info@homeweb.ru>
+ * Copyright 2017 Vsevolod Stakhov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+
+ * Based on source code from http://www.ols.es/exim/dlext/ by David Saez <david@ols.es>,
+ * source code of exim by Philip Hazel <ph10@cam.ac.uk>
+ * and source code of exiscan by Tom Kistner <tom@duncanthrax.net>
+ * and source code of Victor Ustugov http://mta.org.ua/
+*/
+
+#include "exim.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <stdarg.h>
+#include <errno.h>
+#include "cJSON.h"
+#include "cJSON.c"
+
+#define RSPAMD_TIMEOUT 120
+
+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 */
+
+//-------------------------------------------------------------------------
+
+int
+rspamd (uschar **yield, int argc, uschar *argv[]) {
+ char *arg_socket_addr;
+ char *arg_defer_ok;
+ int defer_ok;
+ int rspamd_command;
+ char tcp_addr[15];
+ int tcp_port;
+ FILE *mbox_file = NULL;
+// unsigned long mbox_size;
+ off_t mbox_size;
+ uschar *s, *p;
+ header_line *my_header, *header_new, *header_last, *tmp_headerlist;
+ header_line *last_received = NULL;
+ uschar *address;
+ uschar *helo;
+ uschar *sender_host_name;
+ uschar *authenticated_id;
+ char mbox_path[8192];
+ int max_len, len;
+ int rspamd_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 spamd_buffer[32600];
+ uschar spamd_buffer2[32600];
+ time_t start;
+ size_t read, wrote;
+ int i, j, c;
+
+ arg_socket_addr = argv[0];
+ arg_defer_ok = argv[1];
+
+ if (argc < 2) {
+ 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;
+ }
+
+ if ((arg_socket_addr == NULL) || (arg_socket_addr[0] == 0)) {
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: Socket address expected");
+ *yield = string_sprintf ("rspamd dlfunc: Socket address expected");
+ goto RETURN_DEFER;
+ }
+
+
+ if (split_spool_directory == 0) {
+ snprintf (mbox_path, sizeof (mbox_path), "%s/input/%s-D",
+ spool_directory,
+ message_id);
+ } else {
+ snprintf (mbox_path, sizeof (mbox_path), "%s/input/%s/%s-D",
+ spool_directory, message_subdir, message_id);
+ }
+
+ mbox_file = fopen (mbox_path, "rb");
+
+ if (!mbox_file) {
+ *yield = string_sprintf ("rspamd 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: %d\n", mbox_size);
+ mbox_size -= SPOOL_DATA_START_OFFSET;
+//debug_printf(" Spool file size: %d\n", 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] != '/') {
+ if (sscanf (CS arg_socket_addr, "%s %u", tcp_addr, &tcp_port) != 2 ) {
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: Invalid rspamd address: '%s'",
+ arg_socket_addr);
+ *yield = string_sprintf (
+ "rspamd dlfunc: Invalid rspamd 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,
+ "rspamd dlfunc: failed to lookup host '%s'", tcp_addr);
+ *yield = string_sprintf (
+ "rspamd dlfunc: failed to lookup host '%s'", tcp_addr);
+ goto RETURN_DEFER;
+ }
+
+ in = *(struct in_addr *) he->h_addr_list[0];
+
+/* contact a rspamd */
+
+ if ((rspamd_sock = ip_socket (SOCK_STREAM, AF_INET)) < 0) {
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: TCP socket creation failed: %s",
+ strerror (errno));
+ *yield = string_sprintf (
+ "rspamd dlfunc: TCP socket creation failed: %s",
+ strerror (errno));
+ goto RETURN_DEFER;
+ };
+
+ if (ip_connect (rspamd_sock, AF_INET, (uschar *) inet_ntoa (in),
+ tcp_port, 5, FALSE) < 0) {
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: connection to %s, port %u failed: %s",
+ tcp_addr, tcp_port, strerror (errno));
+ *yield = string_sprintf (
+ "rspamd 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 ((rspamd_sock = socket (AF_UNIX, SOCK_STREAM, 0)) < 0) {
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: Unable to acquire socket (%s)",
+ strerror (errno));
+ *yield = string_sprintf (
+ "rspamd 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 (rspamd_sock, (struct sockaddr *) &server,
+ sizeof (struct sockaddr_un)) < 0) {
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: Unable to connect to UNIX socket %s (%s)",
+ socket, strerror (errno));
+ *yield = string_sprintf (
+ "rspamd 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 rspamd on rspamd_sock
+
+ memset (spamd_buffer2, 0, sizeof (spamd_buffer2));
+ offset = 0;
+
+ // 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 = my_header->text;
+
+ header_new->slen = my_header->slen;
+ header_new->type = my_header->type;
+ header_new->next = NULL;
+ //debug_printf(" copy header item: '%s'\n", my_header->text);
+
+ max_len = sizeof (spamd_buffer2) - offset - 1;
+ len = my_header->slen;
+ if (len > max_len) len = max_len;
+ Ustrncpy (spamd_buffer2 + offset, my_header->text, len);
+ offset += len;
+
+ if (header_last != NULL) header_last->next = header_new;
+ header_last = header_new;
+ }
+ }
+
+ s = string_sprintf ("\n");
+ max_len = sizeof (spamd_buffer2) - offset - 1;
+ len = Ustrlen (s);
+ if (len > max_len) len = max_len;
+ Ustrncpy (spamd_buffer2 + offset, s, len);
+ offset += len;
+
+//debug_printf(" Headers size: %d\n", offset);
+ mbox_size += offset;
+//debug_printf(" Total message size: %d\n", mbox_size);
+
+// copy request to buffer
+ memset (spamd_buffer, 0, sizeof (spamd_buffer));
+ string_format (spamd_buffer,
+ sizeof (spamd_buffer),
+ "POST /checkv2 HTTP/1.0\r\nContent-length: "OFF_T_FMT
+ "\r\nQueue-Id: %s\r\nFrom: %s\r\n",
+ mbox_size, message_id, sender_address);
+
+ for (i = 0; i < recipients_count; i++) {
+ string_format (spamd_buffer + Ustrlen (spamd_buffer),
+ sizeof (spamd_buffer) - Ustrlen (spamd_buffer), "Rcpt: %s\r\n",
+ recipients_list[i].address);
+ }
+
+ if ((helo = expand_string (US"$sender_helo_name")) != NULL && *helo != '\0') {
+ string_format (spamd_buffer + Ustrlen (spamd_buffer),
+ sizeof (spamd_buffer) - Ustrlen (spamd_buffer), "Helo: %s\r\n",
+ helo);
+ }
+
+ if ((sender_host_name = expand_string (US"$sender_host_name")) != NULL &&
+ *sender_host_name != '\0') {
+ string_format (spamd_buffer + Ustrlen (spamd_buffer),
+ sizeof (spamd_buffer) - Ustrlen (spamd_buffer),
+ "Hostname: %s\r\n",
+ sender_host_name);
+ }
+ //else
+ //string_format(spamd_buffer+Ustrlen(spamd_buffer), sizeof(spamd_buffer)-Ustrlen(spamd_buffer), "Hostname: unknown\r\n");
+
+ if (sender_host_address != NULL) {
+ string_format (spamd_buffer + Ustrlen (spamd_buffer),
+ sizeof (spamd_buffer) - Ustrlen (spamd_buffer), "IP: %s\r\n",
+ sender_host_address);
+ }
+
+ //authenticated_id
+ if ((authenticated_id = expand_string (US"$authenticated_id")) != NULL &&
+ *authenticated_id != '\0') {
+ string_format (spamd_buffer + Ustrlen (spamd_buffer),
+ sizeof (spamd_buffer) - Ustrlen (spamd_buffer), "User: %s\r\n",
+ authenticated_id);
+ }
+
+ string_format (spamd_buffer + Ustrlen (spamd_buffer),
+ sizeof (spamd_buffer) - Ustrlen (spamd_buffer), "\r\n");
+
+ if (send (rspamd_sock, spamd_buffer, Ustrlen (spamd_buffer), 0) < 0) {
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: rspamd send failed: %s", strerror (errno));
+ goto RETURN_DEFER;
+ }
+
+ /*
+ * now send the data buffer and spool file
+ */
+ Ustrcpy (big_buffer, "sending data block");
+
+ wrote = send (rspamd_sock, spamd_buffer2, strlen (spamd_buffer2), 0);
+ if (wrote == -1) {
+ goto WRITE_FAILED;
+ }
+
+ /*
+ * Note: poll() is not supported in OSX 10.2.
+ */
+
+#ifndef NO_POLL_H
+ pollfd.fd = rspamd_sock;
+ pollfd.events = POLLOUT;
+#endif
+// (void)fcntl(rspamd_sock, F_SETFL, O_NONBLOCK);
+ do {
+ read = fread (spamd_buffer, 1, sizeof (spamd_buffer) - 1, mbox_file);
+
+ if (read < sizeof (spamd_buffer)) {
+ spamd_buffer[read] = 0;
+ }
+//debug_printf(" Read from spool file: %s", spamd_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,
+ "rspamd dlfunc: %s on rspamd socket",
+ strerror (errno));
+ else {
+ if (time (NULL) - start < RSPAMD_TIMEOUT)
+ goto again;
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: timed out writing rspamd socket");
+ *yield = string_sprintf (
+ "rspamd dlfunc: timed out writing rspamd socket");
+ }
+ goto RETURN_DEFER;
+ }
+#endif
+ wrote = send (rspamd_sock, spamd_buffer + offset, read - offset, 0);
+
+ 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,
+ "rspamd dlfunc: error reading spool file: %s",
+ strerror (errno));
+ *yield = string_sprintf ("rspamd dlfunc: error reading spool file: %s",
+ strerror (errno));
+ goto RETURN_DEFER;
+ }
+
+ /*
+ read rspamd response using what's left of the timeout.
+ */
+
+ memset (spamd_buffer, 0, sizeof (spamd_buffer));
+ offset = 0;
+ while ((i = ip_recv (rspamd_sock,
+ spamd_buffer + offset,
+ sizeof (spamd_buffer) - offset - 1,
+ RSPAMD_TIMEOUT - time (NULL) + start)) > 0
+ ) {
+ //debug_printf(" read %d bytes from socket\n", i);
+ offset += i;
+ }
+//debug_printf(" total read %d bytes from socket\n", offset);
+
+/* error handling */
+ if ((i <= 0) && (errno != 0)) {
+ log_write (0, LOG_MAIN | LOG_PANIC,
+ "rspamd dlfunc: error reading from rspamd socket: %s",
+ strerror (errno));
+ *yield = string_sprintf (
+ "rspamd dlfunc: error reading from rspamd socket: %s",
+ strerror (errno));
+ goto RETURN_DEFER;
+ }
+
+//debug_printf("read from socket: %s\n", spamd_buffer);
+
+ if (rspamd_sock > 0) {
+ (void) close (rspamd_sock);
+ rspamd_sock = 0;
+ }
+ if (mbox_file != NULL) {
+ (void) fclose (mbox_file);
+ mbox_file = NULL;
+ }
+
+ //Parse http response code
+ if (strstr (spamd_buffer, "HTTP/1.1 200 OK") == NULL &&
+ strstr (spamd_buffer, "HTTP/1.0 200 OK") == NULL) {
+ *yield = string_sprintf ("rspamd dlfunc: HTTP return code != 200: %s",
+ spamd_buffer);
+ goto RETURN_DEFER;
+ }
+
+ //Parse http response
+ const char *crlf_pos = strstr (spamd_buffer, "\r\n\r\n");
+ if (crlf_pos == NULL) {
+ *yield = string_sprintf ("rspamd dlfunc: HTTP response error: %s",
+ spamd_buffer);
+ goto RETURN_DEFER;
+ }
+
+ char *json_answer = string_sprintf ("%s", crlf_pos + 4);
+
+ //Parse json
+ cJSON *json = NULL;
+ json = cJSON_Parse (json_answer);
+
+ if (!json) {
+ *yield = string_sprintf ("rspamd dlfunc: Json parse error, json: %s",
+ spamd_buffer);
+ goto RETURN_DEFER;
+ }
+
+ //Score
+ cJSON *score = cJSON_GetObjectItem (json, "score");
+ if (!cJSON_IsNumber (score)) {
+ *yield = string_sprintf (
+ "rspamd dlfunc: Json parse error, no found 'score'");
+ goto RETURN_DEFER;
+ }
+ //required_score
+ cJSON *required_score = cJSON_GetObjectItem (json, "required_score");
+ if (!cJSON_IsNumber (required_score)) {
+ *yield = string_sprintf (
+ "rspamd dlfunc: Json parse error, no found 'required_score'");
+ goto RETURN_DEFER;
+ }
+ //Action
+ cJSON *action = cJSON_GetObjectItem (json, "action");
+ if (!cJSON_IsString (action)) {
+ *yield = string_sprintf (
+ "rspamd dlfunc: Json parse error, no found 'action'");
+ goto RETURN_DEFER;
+ }
+ *yield = string_sprintf ("[%.2f / %.2f]", score->valuedouble,
+ required_score->valuedouble);
+
+ //Parse scan time
+ cJSON *time_real = cJSON_GetObjectItem (json, "time_real");
+ cJSON *time_virtual = cJSON_GetObjectItem (json, "time_virtual");
+ if (cJSON_IsNumber (time_real) && cJSON_IsNumber (time_virtual))
+ *yield = string_sprintf ("%s [time: %.6f, %.6f]", *yield,
+ time_real->valuedouble, time_virtual->valuedouble);
+
+ *yield = string_sprintf ("%s\n Action: %s\n", *yield, action->valuestring);
+
+ cJSON *symbol = NULL;
+ cJSON *symbol_name = NULL;
+ cJSON *symbol_score = NULL;
+ cJSON *symbol_options = NULL;
+ cJSON *option = NULL;
+
+ //parse symbols
+ cJSON *symbols = cJSON_GetObjectItem (json, "symbols");
+ for (i = 0; i < cJSON_GetArraySize (symbols); i++) {
+ symbol = cJSON_GetArrayItem (symbols, i);
+ symbol_name = cJSON_GetObjectItem (symbol, "name");
+ symbol_score = cJSON_GetObjectItem (symbol, "score");
+ symbol_options = cJSON_GetObjectItem (symbol, "options");
+
+ if (cJSON_IsString (symbol_name)) {
+ *yield = string_sprintf ("%s %s", *yield, symbol_name->valuestring);
+ }
+ if (cJSON_IsNumber (symbol_score)) {
+ *yield = string_sprintf ("%s(%.2f)", *yield,
+ symbol_score->valuedouble);
+ }
+
+ //parse options
+ c = cJSON_GetArraySize (symbol_options);
+ if (c > 0) {
+ *yield = string_sprintf ("%s[", *yield);
+ }
+
+ for (j = 0; j < c; j++) {
+ option = cJSON_GetArrayItem (symbol_options, j);
+
+ if (cJSON_IsString (option)) {
+ *yield = string_sprintf ("%s%s", *yield, option->valuestring);
+ if (j < c - 1) *yield = string_sprintf ("%s, ", *yield);
+ }
+ }
+ if (c > 0) {
+ *yield = string_sprintf ("%s]", *yield);
+ }
+
+ *yield = string_sprintf ("%s\n", *yield);
+ }
+
+ //Parse messages
+ cJSON *mess = NULL;
+ cJSON *messages = cJSON_GetObjectItem (json, "messages");
+ c = cJSON_GetArraySize (messages);
+
+ for (i = 0; i < c; i++) {
+ mess = cJSON_GetArrayItem (messages, i);
+ if (cJSON_IsString (mess)) {
+ *yield = string_sprintf ("%s %s", *yield, mess->valuestring);
+ }
+
+ if (i < c - 1) {
+ *yield = string_sprintf ("%s\n", *yield);
+ }
+ }
+
+ 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,
+ "rspamd dlfunc: %s on rspamd socket", strerror (errno));
+ *yield = string_sprintf ("rspamd dlfunc: %s on rspamd socket",
+ strerror (errno));
+ goto RETURN_DEFER;
+ }
+
+ RESPONSE_FAILED:
+ {
+ int code;
+ int save_errno;
+ int more_errno;
+ uschar message_buffer[256];
+ uschar *message;
+
+ save_errno = errno;
+
+ message = &message_buffer[0];
+
+ log_write (0, LOG_MAIN | LOG_PANIC, "rspamd dlfunc: %s", message);
+ *yield = string_sprintf ("rspamd dlfunc: %s", message);
+
+ goto RETURN_DEFER;
+ }
+
+ RETURN_DEFER:
+ {
+ if (rspamd_sock > 0) {
+ (void) close (rspamd_sock);
+ rspamd_sock = 0;
+ }
+ if (mbox_file != NULL) {
+ (void) fclose (mbox_file);
+ mbox_file = NULL;
+ }
+
+ return (defer_ok ? OK : ERROR);
+ }
+
+ return OK;
+}
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;
+}
+
diff --git a/contrib/exim/patch-exim-src_spam.c.diff b/contrib/exim/patch-exim-src_spam.c.diff
new file mode 100644
index 0000000..f3ac788
--- /dev/null
+++ b/contrib/exim/patch-exim-src_spam.c.diff
@@ -0,0 +1,338 @@
+diff -ru exim-4.70.orig/src/expand.c exim-4.70/src/expand.c
+--- exim-4.70.orig/src/expand.c 2016-04-09 13:42:00.227625074 +0200
++++ exim-4.70/src/expand.c 2016-04-09 13:42:37.183633096 +0200
+@@ -578,6 +578,7 @@
+ { "sn8", vtype_filter_int, &filter_sn[8] },
+ { "sn9", vtype_filter_int, &filter_sn[9] },
+ #ifdef WITH_CONTENT_SCAN
++ { "spam_action", vtype_stringptr, &spam_action },
+ { "spam_bar", vtype_stringptr, &spam_bar },
+ { "spam_report", vtype_stringptr, &spam_report },
+ { "spam_score", vtype_stringptr, &spam_score },
+diff -ru exim-4.70.orig/src/globals.c exim-4.70/src/globals.c
+--- exim-4.70.orig/src/globals.c 2016-04-09 13:42:00.219625073 +0200
++++ exim-4.70/src/globals.c 2016-04-09 13:42:37.187633096 +0200
+@@ -1136,6 +1136,7 @@
+ uschar *spamd_address = US"127.0.0.1 783";
+ uschar *spam_bar = NULL;
+ uschar *spam_report = NULL;
++uschar *spam_action = NULL;
+ uschar *spam_score = NULL;
+ uschar *spam_score_int = NULL;
+ #endif
+diff -ru exim-4.70.orig/src/globals.h exim-4.70/src/globals.h
+--- exim-4.70.orig/src/globals.h 2016-04-09 13:42:00.219625073 +0200
++++ exim-4.70/src/globals.h 2016-04-09 13:42:37.187633096 +0200
+@@ -703,6 +703,7 @@
+ extern uschar *spamd_address; /* address for the spamassassin daemon */
+ extern uschar *spam_bar; /* the spam "bar" (textual representation of spam_score) */
+ extern uschar *spam_report; /* the spamd report (multiline) */
++extern uschar *spam_action; /* the spamd action */
+ extern uschar *spam_score; /* the spam score (float) */
+ extern uschar *spam_score_int; /* spam_score * 10 (int) */
+ #endif
+diff -ru exim-4.70.orig/src/spam.c exim-4.70/src/spam.c
+--- exim-4.70.orig/src/spam.c 2016-04-09 13:42:00.231625075 +0200
++++ exim-4.70/src/spam.c 2016-04-09 13:43:43.927647677 +0200
+@@ -16,6 +16,7 @@
+ uschar spam_score_buffer[16];
+ uschar spam_score_int_buffer[16];
+ uschar spam_bar_buffer[128];
++uschar spam_action_buffer[32];
+ uschar spam_report_buffer[32600];
+ uschar prev_user_name[128] = "";
+ int spam_ok = 0;
+@@ -31,9 +32,11 @@
+ int spamd_sock;
+ uschar spamd_buffer[32600];
+ int i, j, offset, result;
++ BOOL is_rspamd;
+ uschar spamd_version[8];
++ uschar spamd_short_result[8];
+ uschar spamd_score_char;
+- double spamd_threshold, spamd_score;
++ double spamd_threshold, spamd_score, spamd_reject_score;
+ int spamd_report_offset;
+ uschar *p,*q;
+ int override = 0;
+@@ -122,8 +125,15 @@
+ spamd_address_container *this_spamd =
+ (spamd_address_container *)store_get(sizeof(spamd_address_container));
+
++ /* Check for spamd variant */
++ if( Ustrstr(address, "variant=rspamd") != NULL ) {
++ this_spamd->is_rspamd = TRUE;
++ }
++ else {
++ this_spamd->is_rspamd = FALSE;
++ }
+ /* grok spamd address and port */
+- if( sscanf(CS address, "%s %u", this_spamd->tcp_addr, &(this_spamd->tcp_port)) != 2 ) {
++ if( sscanf(CS address, "%23s %hu", this_spamd->tcp_addr, &(this_spamd->tcp_port)) != 2 ) {
+ log_write(0, LOG_MAIN,
+ "spam acl condition: warning - invalid spamd address: '%s'", address);
+ continue;
+@@ -165,6 +175,7 @@
+ spamd_address_vector[current_server]->tcp_port,
+ 5 ) > -1) {
+ /* connection OK */
++ is_rspamd = spamd_address_vector[current_server]->is_rspamd;
+ break;
+ };
+
+@@ -197,12 +208,28 @@
+ }
+
+ server.sun_family = AF_UNIX;
+- Ustrcpy(server.sun_path, spamd_address_work);
++ p = Ustrstr(spamd_address_work, "variant=rspamd");
++ if( p != NULL ) {
++ is_rspamd = TRUE;
++ /* strip spaces */
++ p --;
++ while (p > spamd_address_work && isspace (*p)) {
++ p --;
++ }
++ Ustrncpy(server.sun_path, spamd_address_work, p - spamd_address_work + 1);
++ /* zero terminate */
++ server.sun_path[p - spamd_address_work + 1] = 0;
++ }
++ else {
++ is_rspamd = FALSE;
++ Ustrcpy(server.sun_path, spamd_address_work);
++ }
++
+
+ if (connect(spamd_sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "malware acl condition: spamd: unable to connect to UNIX socket %s (%s)",
+- spamd_address_work, strerror(errno) );
++ server.sun_path, strerror(errno) );
+ (void)fclose(mbox_file);
+ (void)close(spamd_sock);
+ return DEFER;
+@@ -210,22 +237,50 @@
+
+ }
+
++ (void)fcntl(spamd_sock, F_SETFL, O_NONBLOCK);
+ /* now we are connected to spamd on spamd_sock */
+- (void)string_format(spamd_buffer,
+- sizeof(spamd_buffer),
+- "REPORT SPAMC/1.2\r\nUser: %s\r\nContent-length: %ld\r\n\r\n",
+- user_name,
+- mbox_size);
+-
+- /* send our request */
+- if (send(spamd_sock, spamd_buffer, Ustrlen(spamd_buffer), 0) < 0) {
++ if (is_rspamd) {
++ /* rspamd variant */
++ const char *helo;
++ const char *fcrdns;
++ const char *authid;
++ uschar *req_str;
++
++ req_str = string_sprintf("CHECK RSPAMC/1.3\r\nContent-length: %lu\r\n"
++ "Queue-Id: %s\r\nFrom: <%s>\r\nRecipient-Number: %d\r\n", mbox_size,
++ message_id, sender_address, recipients_count);
++ for (i = 0; i < recipients_count; i ++)
++ req_str = string_sprintf("%sRcpt: <%s>\r\n", req_str, recipients_list[i].address);
++ if ((helo = expand_string(US"$sender_helo_name")) != NULL && *helo != '\0')
++ req_str = string_sprintf("%sHelo: %s\r\n", req_str, helo);
++ if ((fcrdns = expand_string(US"$sender_host_name")) != NULL && *fcrdns != '\0')
++ req_str = string_sprintf("%sHostname: %s\r\n", req_str, fcrdns);
++ if (sender_host_address != NULL)
++ req_str = string_sprintf("%sIP: %s\r\n", req_str, sender_host_address);
++ if ((authid = expand_string(US"$authenticated_id")) != NULL && *authid != '\0')
++ req_str = string_sprintf("%sUser: %s\r\n", req_str, authid);
++ req_str = string_sprintf("%s\r\n", req_str);
++ wrote = send(spamd_sock, req_str, Ustrlen(req_str), 0);
++ }
++ else {
++ /* spamassassin variant */
++ (void)string_format(spamd_buffer,
++ sizeof(spamd_buffer),
++ "REPORT SPAMC/1.2\r\nUser: %s\r\nContent-length: %ld\r\n\r\n",
++ user_name,
++ mbox_size);
++ /* send our request */
++ wrote = send(spamd_sock, spamd_buffer, Ustrlen(spamd_buffer), 0);
++ }
++ if(wrote == -1)
++ {
+ (void)close(spamd_sock);
+ log_write(0, LOG_MAIN|LOG_PANIC,
+- "spam acl condition: spamd send failed: %s", strerror(errno));
++ "spam acl condition: spamd send failed: %s", strerror(errno));
+ (void)fclose(mbox_file);
+ (void)close(spamd_sock);
+ return DEFER;
+- };
++ }
+
+ /* now send the file */
+ /* spamd sometimes accepts connections but doesn't read data off
+@@ -304,7 +359,9 @@
+ (void)fclose(mbox_file);
+
+ /* we're done sending, close socket for writing */
+- shutdown(spamd_sock,SHUT_WR);
++ if (!is_rspamd) {
++ shutdown(spamd_sock,SHUT_WR);
++ }
+
+ /* read spamd response using what's left of the timeout.
+ */
+@@ -328,60 +385,93 @@
+ /* reading done */
+ (void)close(spamd_sock);
+
+- /* dig in the spamd output and put the report in a multiline header, if requested */
+- if( sscanf(CS spamd_buffer,"SPAMD/%7s 0 EX_OK\r\nContent-length: %*u\r\n\r\n%lf/%lf\r\n%n",
+- spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
+-
+- /* try to fall back to pre-2.50 spamd output */
+- if( sscanf(CS spamd_buffer,"SPAMD/%7s 0 EX_OK\r\nSpam: %*s ; %lf / %lf\r\n\r\n%n",
+- spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
++ if (!is_rspamd) {
++ /* dig in the spamd output and put the report in a multiline header, if requested */
++ if( sscanf(CS spamd_buffer,"SPAMD/%7s 0 EX_OK\r\nContent-length: %*u\r\n\r\n%lf/%lf\r\n%n",
++ spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
++
++ /* try to fall back to pre-2.50 spamd output */
++ if( sscanf(CS spamd_buffer,"SPAMD/%7s 0 EX_OK\r\nSpam: %*s ; %lf / %lf\r\n\r\n%n",
++ spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
++ log_write(0, LOG_MAIN|LOG_PANIC,
++ "spam acl condition: cannot parse spamd output");
++ return DEFER;
++ };
++ };
++
++ if( spamd_score >= spamd_threshold ) {
++ Ustrcpy(spam_action_buffer, "reject");
++ }
++ else {
++ Ustrcpy(spam_action_buffer, "no action");
++ }
++ }
++ else {
++ /* rspamd variant of reply */
++ int r;
++ if( (r = sscanf(CS spamd_buffer,"RSPAMD/%7s 0 EX_OK\r\nMetric: default; %7s %lf / %lf / %lf\r\n%n",
++ spamd_version,spamd_short_result,&spamd_score,&spamd_threshold,&spamd_reject_score,&spamd_report_offset)) != 5 ) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+- "spam acl condition: cannot parse spamd output");
++ "spam acl condition: cannot parse spamd output: %d", r);
+ return DEFER;
+ };
+- };
++ /* now parse action */
++ p = &spamd_buffer[spamd_report_offset];
++
++ if( Ustrncmp(p, "Action: ", sizeof("Action: ") - 1) == 0 ) {
++ p += sizeof("Action: ") - 1;
++ q = &spam_action_buffer[0];
++ while (*p && *p != '\r' && (q - spam_action_buffer) < sizeof(spam_action_buffer) - 1) {
++ *q++ = *p++;
++ }
++ *q = '\0';
++ }
++ }
+
+ /* Create report. Since this is a multiline string,
+ we must hack it into shape first */
+ p = &spamd_buffer[spamd_report_offset];
+ q = spam_report_buffer;
+ while (*p != '\0') {
+- /* skip \r */
+- if (*p == '\r') {
+- p++;
+- continue;
+- };
+- *q = *p;
+- q++;
+- if (*p == '\n') {
+- /* add an extra space after the newline to ensure
+- that it is treated as a header continuation line */
+- *q = ' ';
+- q++;
+- };
+- p++;
++ /* skip \r */
++ if (*p == '\r') {
++ p++;
++ continue;
++ };
++ *q = *p;
++ q++;
++ if (*p == '\n') {
++ /* add an extra space after the newline to ensure
++ that it is treated as a header continuation line */
++ *q = ' ';
++ q++;
++ };
++ p++;
+ };
+ /* NULL-terminate */
+ *q = '\0';
+ q--;
+ /* cut off trailing leftovers */
+ while (*q <= ' ') {
+- *q = '\0';
+- q--;
++ *q = '\0';
++ q--;
+ };
++
++ /* common spamd actions */
+ spam_report = spam_report_buffer;
++ spam_action = spam_action_buffer;
+
+ /* create spam bar */
+ spamd_score_char = spamd_score > 0 ? '+' : '-';
+ j = abs((int)(spamd_score));
+ i = 0;
+ if( j != 0 ) {
+- while((i < j) && (i <= MAX_SPAM_BAR_CHARS))
+- spam_bar_buffer[i++] = spamd_score_char;
++ while((i < j) && (i <= MAX_SPAM_BAR_CHARS))
++ spam_bar_buffer[i++] = spamd_score_char;
+ }
+ else{
+- spam_bar_buffer[0] = '/';
+- i = 1;
++ spam_bar_buffer[0] = '/';
++ i = 1;
+ }
+ spam_bar_buffer[i] = '\0';
+ spam_bar = spam_bar_buffer;
+@@ -397,12 +487,12 @@
+
+ /* compare threshold against score */
+ if (spamd_score >= spamd_threshold) {
+- /* spam as determined by user's threshold */
+- spam_rc = OK;
++ /* spam as determined by user's threshold */
++ spam_rc = OK;
+ }
+ else {
+- /* not spam */
+- spam_rc = FAIL;
++ /* not spam */
++ spam_rc = FAIL;
+ };
+
+ /* remember user name and "been here" for it unless spamd_socket was expanded */
+diff -ru exim-4.70.orig/src/spam.h exim-4.70/src/spam.h
+--- exim-4.70.orig/src/spam.h 2016-04-09 13:42:00.235625076 +0200
++++ exim-4.70/src/spam.h 2016-04-09 13:42:37.187633096 +0200
+@@ -24,7 +24,8 @@
+
+ typedef struct spamd_address_container {
+ uschar tcp_addr[24];
+- unsigned int tcp_port;
++ unsigned short int tcp_port;
++ int is_rspamd:1;
+ } spamd_address_container;
+
+ #endif
diff --git a/contrib/exim/patch-exim-src_spam.c.diff.exim-4.85.diff b/contrib/exim/patch-exim-src_spam.c.diff.exim-4.85.diff
new file mode 100644
index 0000000..0389aea
--- /dev/null
+++ b/contrib/exim/patch-exim-src_spam.c.diff.exim-4.85.diff
@@ -0,0 +1,332 @@
+diff -ru exim-4.85.orig/src/expand.c exim-4.85/src/expand.c
+--- exim-4.85.orig/src/expand.c 2016-04-09 13:47:01.707691638 +0200
++++ exim-4.85/src/expand.c 2016-04-09 13:47:29.771697969 +0200
+@@ -652,6 +652,7 @@
+ { "sn8", vtype_filter_int, &filter_sn[8] },
+ { "sn9", vtype_filter_int, &filter_sn[9] },
+ #ifdef WITH_CONTENT_SCAN
++ { "spam_action", vtype_stringptr, &spam_action },
+ { "spam_bar", vtype_stringptr, &spam_bar },
+ { "spam_report", vtype_stringptr, &spam_report },
+ { "spam_score", vtype_stringptr, &spam_score },
+Only in exim-4.85/src: expand.c.orig
+diff -ru exim-4.85.orig/src/globals.c exim-4.85/src/globals.c
+--- exim-4.85.orig/src/globals.c 2016-04-09 13:47:01.695691635 +0200
++++ exim-4.85/src/globals.c 2016-04-09 13:47:29.771697969 +0200
+@@ -1276,6 +1276,7 @@
+ uschar *spamd_address = US"127.0.0.1 783";
+ uschar *spam_bar = NULL;
+ uschar *spam_report = NULL;
++uschar *spam_action = NULL;
+ uschar *spam_score = NULL;
+ uschar *spam_score_int = NULL;
+ #endif
+Only in exim-4.85/src: globals.c.orig
+diff -ru exim-4.85.orig/src/globals.h exim-4.85/src/globals.h
+--- exim-4.85.orig/src/globals.h 2016-04-09 13:47:01.695691635 +0200
++++ exim-4.85/src/globals.h 2016-04-09 13:47:29.771697969 +0200
+@@ -819,6 +819,7 @@
+ extern uschar *spamd_address; /* address for the spamassassin daemon */
+ extern uschar *spam_bar; /* the spam "bar" (textual representation of spam_score) */
+ extern uschar *spam_report; /* the spamd report (multiline) */
++extern uschar *spam_action; /* the spamd action */
+ extern uschar *spam_score; /* the spam score (float) */
+ extern uschar *spam_score_int; /* spam_score * 10 (int) */
+ #endif
+Only in exim-4.85/src: globals.h.orig
+diff -ru exim-4.85.orig/src/spam.c exim-4.85/src/spam.c
+--- exim-4.85.orig/src/spam.c 2016-04-09 13:47:01.711691638 +0200
++++ exim-4.85/src/spam.c 2016-04-09 13:52:12.611762892 +0200
+@@ -14,6 +14,7 @@
+ uschar spam_score_buffer[16];
+ uschar spam_score_int_buffer[16];
+ uschar spam_bar_buffer[128];
++uschar spam_action_buffer[32];
+ uschar spam_report_buffer[32600];
+ uschar prev_user_name[128] = "";
+ int spam_ok = 0;
+@@ -32,9 +33,11 @@
+ int spamd_sock = -1;
+ uschar spamd_buffer[32600];
+ int i, j, offset, result;
++ BOOL is_rspamd;
+ uschar spamd_version[8];
++ uschar spamd_short_result[8];
+ uschar spamd_score_char;
+- double spamd_threshold, spamd_score;
++ double spamd_threshold, spamd_score, spamd_reject_score;
+ int spamd_report_offset;
+ uschar *p,*q;
+ int override = 0;
+@@ -128,8 +131,15 @@
+ spamd_address_container *this_spamd =
+ (spamd_address_container *)store_get(sizeof(spamd_address_container));
+
++ /* Check for spamd variant */
++ if( Ustrstr(address, "variant=rspamd") != NULL ) {
++ this_spamd->is_rspamd = TRUE;
++ }
++ else {
++ this_spamd->is_rspamd = FALSE;
++ }
+ /* grok spamd address and port */
+- if (sscanf(CS address, "%23s %u", this_spamd->tcp_addr, &(this_spamd->tcp_port)) != 2)
++ if (sscanf(CS address, "%23s %hu", this_spamd->tcp_addr, &(this_spamd->tcp_port)) != 2)
+ {
+ log_write(0, LOG_MAIN,
+ "spam acl condition: warning - invalid spamd address: '%s'", address);
+@@ -174,6 +184,7 @@
+ spamd_address_vector[current_server]->tcp_port,
+ 5 ) > -1) {
+ /* connection OK */
++ is_rspamd = spamd_address_vector[current_server]->is_rspamd;
+ break;
+ };
+
+@@ -210,12 +221,28 @@
+ }
+
+ server.sun_family = AF_UNIX;
+- Ustrcpy(server.sun_path, spamd_address_work);
++ p = Ustrstr(spamd_address_work, "variant=rspamd");
++ if( p != NULL ) {
++ is_rspamd = TRUE;
++ /* strip spaces */
++ p --;
++ while (p > spamd_address_work && isspace (*p)) {
++ p --;
++ }
++ Ustrncpy(server.sun_path, spamd_address_work, p - spamd_address_work + 1);
++ /* zero terminate */
++ server.sun_path[p - spamd_address_work + 1] = 0;
++ }
++ else {
++ is_rspamd = FALSE;
++ Ustrcpy(server.sun_path, spamd_address_work);
++ }
++
+
+ if (connect(spamd_sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "malware acl condition: spamd: unable to connect to UNIX socket %s (%s)",
+- spamd_address_work, strerror(errno) );
++ server.sun_path, strerror(errno) );
+ (void)fclose(mbox_file);
+ (void)close(spamd_sock);
+ return DEFER;
+@@ -231,22 +258,50 @@
+ return DEFER;
+ }
+
++ (void)fcntl(spamd_sock, F_SETFL, O_NONBLOCK);
+ /* now we are connected to spamd on spamd_sock */
+- (void)string_format(spamd_buffer,
+- sizeof(spamd_buffer),
+- "REPORT SPAMC/1.2\r\nUser: %s\r\nContent-length: %ld\r\n\r\n",
+- user_name,
+- mbox_size);
+-
+- /* send our request */
+- if (send(spamd_sock, spamd_buffer, Ustrlen(spamd_buffer), 0) < 0) {
++ if (is_rspamd) {
++ /* rspamd variant */
++ const char *helo;
++ const char *fcrdns;
++ const char *authid;
++ uschar *req_str;
++
++ req_str = string_sprintf("CHECK RSPAMC/1.3\r\nContent-length: %lu\r\n"
++ "Queue-Id: %s\r\nFrom: <%s>\r\nRecipient-Number: %d\r\n", mbox_size,
++ message_id, sender_address, recipients_count);
++ for (i = 0; i < recipients_count; i ++)
++ req_str = string_sprintf("%sRcpt: <%s>\r\n", req_str, recipients_list[i].address);
++ if ((helo = expand_string(US"$sender_helo_name")) != NULL && *helo != '\0')
++ req_str = string_sprintf("%sHelo: %s\r\n", req_str, helo);
++ if ((fcrdns = expand_string(US"$sender_host_name")) != NULL && *fcrdns != '\0')
++ req_str = string_sprintf("%sHostname: %s\r\n", req_str, fcrdns);
++ if (sender_host_address != NULL)
++ req_str = string_sprintf("%sIP: %s\r\n", req_str, sender_host_address);
++ if ((authid = expand_string(US"$authenticated_id")) != NULL && *authid != '\0')
++ req_str = string_sprintf("%sUser: %s\r\n", req_str, authid);
++ req_str = string_sprintf("%s\r\n", req_str);
++ wrote = send(spamd_sock, req_str, Ustrlen(req_str), 0);
++ }
++ else {
++ /* spamassassin variant */
++ (void)string_format(spamd_buffer,
++ sizeof(spamd_buffer),
++ "REPORT SPAMC/1.2\r\nUser: %s\r\nContent-length: %ld\r\n\r\n",
++ user_name,
++ mbox_size);
++ /* send our request */
++ wrote = send(spamd_sock, spamd_buffer, Ustrlen(spamd_buffer), 0);
++ }
++ if(wrote == -1)
++ {
+ (void)close(spamd_sock);
+ log_write(0, LOG_MAIN|LOG_PANIC,
+- "spam acl condition: spamd send failed: %s", strerror(errno));
++ "spam acl condition: spamd send failed: %s", strerror(errno));
+ (void)fclose(mbox_file);
+ (void)close(spamd_sock);
+ return DEFER;
+- };
++ }
+
+ /* now send the file */
+ /* spamd sometimes accepts connections but doesn't read data off
+@@ -349,60 +404,93 @@
+ /* reading done */
+ (void)close(spamd_sock);
+
+- /* dig in the spamd output and put the report in a multiline header, if requested */
+- if( sscanf(CS spamd_buffer,"SPAMD/%7s 0 EX_OK\r\nContent-length: %*u\r\n\r\n%lf/%lf\r\n%n",
+- spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
+-
+- /* try to fall back to pre-2.50 spamd output */
+- if( sscanf(CS spamd_buffer,"SPAMD/%7s 0 EX_OK\r\nSpam: %*s ; %lf / %lf\r\n\r\n%n",
+- spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
++ if (!is_rspamd) {
++ /* dig in the spamd output and put the report in a multiline header, if requested */
++ if( sscanf(CS spamd_buffer,"SPAMD/%7s 0 EX_OK\r\nContent-length: %*u\r\n\r\n%lf/%lf\r\n%n",
++ spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
++
++ /* try to fall back to pre-2.50 spamd output */
++ if( sscanf(CS spamd_buffer,"SPAMD/%7s 0 EX_OK\r\nSpam: %*s ; %lf / %lf\r\n\r\n%n",
++ spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
++ log_write(0, LOG_MAIN|LOG_PANIC,
++ "spam acl condition: cannot parse spamd output");
++ return DEFER;
++ };
++ };
++
++ if( spamd_score >= spamd_threshold ) {
++ Ustrcpy(spam_action_buffer, "reject");
++ }
++ else {
++ Ustrcpy(spam_action_buffer, "no action");
++ }
++ }
++ else {
++ /* rspamd variant of reply */
++ int r;
++ if( (r = sscanf(CS spamd_buffer,"RSPAMD/%7s 0 EX_OK\r\nMetric: default; %7s %lf / %lf / %lf\r\n%n",
++ spamd_version,spamd_short_result,&spamd_score,&spamd_threshold,&spamd_reject_score,&spamd_report_offset)) != 5 ) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+- "spam acl condition: cannot parse spamd output");
++ "spam acl condition: cannot parse spamd output: %d", r);
+ return DEFER;
+ };
+- };
++ /* now parse action */
++ p = &spamd_buffer[spamd_report_offset];
++
++ if( Ustrncmp(p, "Action: ", sizeof("Action: ") - 1) == 0 ) {
++ p += sizeof("Action: ") - 1;
++ q = &spam_action_buffer[0];
++ while (*p && *p != '\r' && (q - spam_action_buffer) < sizeof(spam_action_buffer) - 1) {
++ *q++ = *p++;
++ }
++ *q = '\0';
++ }
++ }
+
+ /* Create report. Since this is a multiline string,
+ we must hack it into shape first */
+ p = &spamd_buffer[spamd_report_offset];
+ q = spam_report_buffer;
+ while (*p != '\0') {
+- /* skip \r */
+- if (*p == '\r') {
+- p++;
+- continue;
+- };
+- *q = *p;
+- q++;
+- if (*p == '\n') {
+- /* add an extra space after the newline to ensure
+- that it is treated as a header continuation line */
+- *q = ' ';
+- q++;
+- };
+- p++;
++ /* skip \r */
++ if (*p == '\r') {
++ p++;
++ continue;
++ };
++ *q = *p;
++ q++;
++ if (*p == '\n') {
++ /* add an extra space after the newline to ensure
++ that it is treated as a header continuation line */
++ *q = ' ';
++ q++;
++ };
++ p++;
+ };
+ /* NULL-terminate */
+ *q = '\0';
+ q--;
+ /* cut off trailing leftovers */
+ while (*q <= ' ') {
+- *q = '\0';
+- q--;
++ *q = '\0';
++ q--;
+ };
++
++ /* common spamd actions */
+ spam_report = spam_report_buffer;
++ spam_action = spam_action_buffer;
+
+ /* create spam bar */
+ spamd_score_char = spamd_score > 0 ? '+' : '-';
+ j = abs((int)(spamd_score));
+ i = 0;
+ if( j != 0 ) {
+- while((i < j) && (i <= MAX_SPAM_BAR_CHARS))
+- spam_bar_buffer[i++] = spamd_score_char;
++ while((i < j) && (i <= MAX_SPAM_BAR_CHARS))
++ spam_bar_buffer[i++] = spamd_score_char;
+ }
+ else{
+- spam_bar_buffer[0] = '/';
+- i = 1;
++ spam_bar_buffer[0] = '/';
++ i = 1;
+ }
+ spam_bar_buffer[i] = '\0';
+ spam_bar = spam_bar_buffer;
+@@ -418,12 +506,12 @@
+
+ /* compare threshold against score */
+ if (spamd_score >= spamd_threshold) {
+- /* spam as determined by user's threshold */
+- spam_rc = OK;
++ /* spam as determined by user's threshold */
++ spam_rc = OK;
+ }
+ else {
+- /* not spam */
+- spam_rc = FAIL;
++ /* not spam */
++ spam_rc = FAIL;
+ };
+
+ /* remember expanded spamd_address if needed */
+Only in exim-4.85/src: spam.c.orig
+Only in exim-4.85/src: .spam.c.rej.swp
+diff -ru exim-4.85.orig/src/spam.h exim-4.85/src/spam.h
+--- exim-4.85.orig/src/spam.h 2016-04-09 13:47:01.715691640 +0200
++++ exim-4.85/src/spam.h 2016-04-09 13:47:29.775697969 +0200
+@@ -22,7 +22,8 @@
+
+ typedef struct spamd_address_container {
+ uschar tcp_addr[24];
+- unsigned int tcp_port;
++ unsigned short int tcp_port;
++ int is_rspamd:1;
+ } spamd_address_container;
+
+ #endif
diff --git a/contrib/exim/shutdown.patch b/contrib/exim/shutdown.patch
new file mode 100644
index 0000000..e8bf8a0
--- /dev/null
+++ b/contrib/exim/shutdown.patch
@@ -0,0 +1,14 @@
+--- exim4-4.86.2.orig/src/spam.c
++++ exim4-4.86.2/src/spam.c
+@@ -499,7 +499,10 @@ if (ferror(mbox_file))
+ (void)fclose(mbox_file);
+
+ /* we're done sending, close socket for writing */
+-shutdown(spamd_sock,SHUT_WR);
++if (!sd->is_rspamd)
++ {
++ shutdown(spamd_sock,SHUT_WR);
++ }
+
+ /* read spamd response using what's left of the timeout. */
+ memset(spamd_buffer, 0, sizeof(spamd_buffer));