[exim-cvs] Support Rspamd. Patch from Andrew Lewis, lightly…

Páxina inicial
Borrar esta mensaxe
Responder a esta mensaxe
Autor: Exim Git Commits Mailing List
Data:  
Para: exim-cvs
Asunto: [exim-cvs] Support Rspamd. Patch from Andrew Lewis, lightly editorialised
Gitweb: http://git.exim.org/exim.git/commitdiff/c5f280e20a8e3ecd5f016b8fb34a436588915ed2
Commit:     c5f280e20a8e3ecd5f016b8fb34a436588915ed2
Parent:     84547130ee72174d019f557dcbc3b0cf42c5ac80
Author:     Andrew Lewis <exim@???>
AuthorDate: Sat Jan 24 23:42:59 2015 +0000
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Sun Jan 25 17:39:41 2015 +0000


    Support Rspamd.  Patch from Andrew Lewis, lightly editorialised
    by JH.  Bug 1573
---
 doc/doc-docbook/spec.xfpt       |   56 +++++++++++----
 doc/doc-txt/ChangeLog           |    3 +
 src/src/expand.c                |    1 +
 src/src/globals.c               |    1 +
 src/src/globals.h               |    1 +
 src/src/spam.c                  |  151 ++++++++++++++++++++++++++++-----------
 src/src/spam.h                  |    3 +-
 test/confs/4008                 |   37 ++++++++++
 test/confs/4009                 |   37 ++++++++++
 test/log/4008                   |    4 +
 test/log/4009                   |    4 +
 test/scripts/4000-scanning/4008 |   43 +++++++++++
 test/scripts/4000-scanning/4009 |   55 ++++++++++++++
 test/stdout/4008                |   45 ++++++++++++
 test/stdout/4009                |   57 +++++++++++++++
 15 files changed, 443 insertions(+), 55 deletions(-)


diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index 77d966d..13d903b 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -30711,14 +30711,23 @@ deny message = This message contains malware ($malware_name)
.endd


-.section "Scanning with SpamAssassin" "SECTscanspamass"
+.section "Scanning with SpamAssassin and Rspamd" "SECTscanspamass"
.cindex "content scanning" "for spam"
.cindex "spam scanning"
.cindex "SpamAssassin"
+.cindex "Rspamd"
The &%spam%& ACL condition calls SpamAssassin's &%spamd%& daemon to get a spam
-score and a report for the message. You can get SpamAssassin at
-&url(http://www.spamassassin.org), or, if you have a working Perl
-installation, you can use CPAN by running:
+score and a report for the message.
+.new
+Support is also provided for Rspamd (which can speak SpamAssassin's protocol but
+provides reduced functionality when used in this mode).
+
+For more information about installation and configuration of SpamAssassin or
+Rspamd refer to their respective websites at
+&url(http://spamassassin.apache.org) and &url(http://www.rspamd.com)
+.wen
+
+SpamAssassin can be installed with CPAN by running:
.code
perl -MCPAN -e 'install Mail::SpamAssassin'
.endd
@@ -30727,17 +30736,27 @@ documentation to see how you can tweak it. The default installation should work
nicely, however.

.oindex "&%spamd_address%&"
-After having installed and configured SpamAssassin, start the &%spamd%& daemon.
-By default, it listens on 127.0.0.1, TCP port 783. If you use another host or
-port for &%spamd%&, you must set the &%spamd_address%& option in the global
-part of the Exim configuration as follows (example):
+By default, SpamAssassin listens on 127.0.0.1, TCP port 783 and if you
+intend to use an instance running on the local host you do not need to set
+&%spamd_address%&. If you intend to use another host or port for SpamAssassin,
+you must set the &%spamd_address%& option in the global part of the Exim
+configuration as follows (example):
.code
spamd_address = 192.168.99.45 387
.endd
-You do not need to set this option if you use the default. As of version 2.60,
-&%spamd%& also supports communication over UNIX sockets. If you want to use
-these, supply &%spamd_address%& with an absolute file name instead of a
-address/port pair:
+
+.new
+To use Rspamd (which by default listens on all local addresses
+on TCP port 11333)
+you should add &%variant=rspamd%& after the address/port pair, for example:
+.code
+spamd_address = 127.0.0.1 11333 variant=rspamd
+.endd
+.wen
+
+As of version 2.60, &%SpamAssassin%& also supports communication over UNIX
+sockets. If you want to us these, supply &%spamd_address%& with an absolute
+file name instead of a address/port pair:
.code
spamd_address = /var/run/spamd_socket
.endd
@@ -30773,7 +30792,10 @@ The right-hand side of the &%spam%& condition specifies a name. This is
relevant if you have set up multiple SpamAssassin profiles. If you do not want
to scan using a specific profile, but rather use the SpamAssassin system-wide
default profile, you can scan for an unknown name, or simply use &"nobody"&.
-However, you must put something on the right-hand side.
+.new
+Rspamd does not use this setting. However, you must put something on the
+right-hand side.
+.wen

The name allows you to use per-domain or per-user antispam profiles in
principle, but this is not straightforward in practice, because a message may
@@ -30827,6 +30849,14 @@ headers, since MUAs can match on such strings.
.vitem &$spam_report$&
A multiline text table, containing the full SpamAssassin report for the
message. Useful for inclusion in headers or reject messages.
+
+.new
+.vitem &$spam_action$&
+For SpamAssassin either 'reject' or 'no action' depending on the
+spam score versus threshold.
+For Rspamd, the recommended action.
+.wen
+
.endlist

 The &%spam%& condition caches its results unless expansion in
diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog
index bfe8725..835417f 100644
--- a/doc/doc-txt/ChangeLog
+++ b/doc/doc-txt/ChangeLog
@@ -50,6 +50,9 @@ JH/12 The cutthrough-routing facility now supports multi-recipient mails,
 JH/13 Bug 344: The verify = reverse_host_lookup ACL condition now accepts a
       /defer_ok option.


+JH/14 Bug 1573: The spam= ACL condition now additionally supports Rspamd.
+      Patch from Andrew Lewis.
+



 Exim version 4.85
diff --git a/src/src/expand.c b/src/src/expand.c
index e9112dc..9f430f8 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -655,6 +655,7 @@ static var_entry var_table[] = {
   { "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 --git a/src/src/globals.c b/src/src/globals.c
index a066f35..6d7ea33 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1275,6 +1275,7 @@ BOOL    smtp_use_size          = FALSE;
 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 --git a/src/src/globals.h b/src/src/globals.h
index b5ea8a4..98d589f 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -827,6 +827,7 @@ extern BOOL    smtp_use_size;          /* Global for passed connections */
 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 recommended-action */
 extern uschar *spam_score;             /* the spam score (float) */
 extern uschar *spam_score_int;         /* spam_score * 10 (int) */
 #endif
diff --git a/src/src/spam.c b/src/src/spam.c
index c0c3fb3..71993fb 100644
--- a/src/src/spam.c
+++ b/src/src/spam.c
@@ -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 @@ spam(uschar **listptr)
   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;
@@ -132,8 +135,11 @@ spam(uschar **listptr)
       spamd_address_container *this_spamd =
         (spamd_address_container *)store_get(sizeof(spamd_address_container));


+      /* Check for spamd variant */
+      this_spamd->is_rspamd = Ustrstr(address, "variant=rspamd") != NULL;
+
       /* 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,
           "%s warning - invalid spamd address: '%s'", loglabel, address);
@@ -181,8 +187,11 @@ spam(uschar **listptr)
                      spamd_address_vector[current_server]->tcp_addr,
                      spamd_address_vector[current_server]->tcp_port,
                      5 ) > -1)
+    {
         /* connection OK */
+        is_rspamd = spamd_address_vector[current_server]->is_rspamd;
         break;
+    }


       log_write(0, LOG_MAIN|LOG_PANIC,
          "%s warning - spamd connection to %s, port %u failed: %s",
@@ -221,14 +230,27 @@ spam(uschar **listptr)
       }


     server.sun_family = AF_UNIX;
-    Ustrcpy(server.sun_path, spamd_address_work);
+
+    is_rspamd = (p = Ustrstr(spamd_address_work, "variant=rspamd")) != NULL;
+    if (is_rspamd)
+      {
+      /* 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
+      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,
                 "%s spamd: unable to connect to UNIX socket %s (%s)",
         loglabel,
-                spamd_address_work, strerror(errno) );
+                server.sun_path, strerror(errno) );
       (void)fclose(mbox_file);
       (void)close(spamd_sock);
       return DEFER;
@@ -244,15 +266,39 @@ spam(uschar **listptr)
     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 */
+    uschar *req_str;
+    const char *helo;
+    const char *fcrdns;
+ 
+    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);
+    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,
@@ -370,23 +416,51 @@ again:
   /* 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)
-    {
+  if (is_rspamd)
+    {                /* 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,
+                  "%s cannot parse spamd output: %d", loglabel, r);
+        return DEFER;
+      }
+    /* now parse action */
+    p = &spamd_buffer[spamd_report_offset];


-    /* try to fall back to pre-2.50 spamd output */
+    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';
+      }
+    }
+  else
+    {                /* spamassassin */
+    /* 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\nSpam: %*s ; %lf / %lf\r\n\r\n%n",
-                spamd_version, &spamd_score, &spamd_threshold,
-        &spamd_report_offset) != 3 )
+     "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)
       {
-      log_write(0, LOG_MAIN|LOG_PANIC,
-         "%s cannot parse spamd output", loglabel);
-      return DEFER;
+        /* 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,
+            "%s cannot parse spamd output", loglabel);
+      return DEFER;
+          }
       }
+
+    Ustrcpy(spam_action_buffer,
+      spamd_score >= spamd_threshold ? "reject" : "no action");
     }


   /* Create report. Since this is a multiline string,
@@ -416,6 +490,7 @@ again:
     *q-- = '\0';


spam_report = spam_report_buffer;
+ spam_action = spam_action_buffer;

/* create spam bar */
spamd_score_char = spamd_score > 0 ? '+' : '-';
@@ -433,25 +508,20 @@ again:
spam_bar = spam_bar_buffer;

   /* create "float" spam score */
-  (void)string_format(spam_score_buffer, sizeof(spam_score_buffer),"%.1f", spamd_score);
+  (void)string_format(spam_score_buffer, sizeof(spam_score_buffer),
+      "%.1f", spamd_score);
   spam_score = spam_score_buffer;


   /* create "int" spam score */
   j = (int)((spamd_score + 0.001)*10);
-  (void)string_format(spam_score_int_buffer, sizeof(spam_score_int_buffer), "%d", j);
+  (void)string_format(spam_score_int_buffer, sizeof(spam_score_int_buffer),
+      "%d", j);
   spam_score_int = spam_score_int_buffer;


   /* compare threshold against score */
-  if (spamd_score >= spamd_threshold)
-    {
-    /* spam as determined by user's threshold */
-    spam_rc = OK;
-    }
-  else
-    {
-    /* not spam */
-    spam_rc = FAIL;
-    }
+  spam_rc = spamd_score >= spamd_threshold
+    ? OK    /* spam as determined by user's threshold */
+    : FAIL;    /* not spam */


/* remember expanded spamd_address if needed */
if (spamd_address_work != spamd_address)
@@ -461,10 +531,9 @@ again:
Ustrcpy(prev_user_name, user_name);
spam_ok = 1;

-  if (override) /* always return OK, no matter what the score */
-    return OK;
-  else
-    return spam_rc;
+  return override
+    ? OK        /* always return OK, no matter what the score */
+    : spam_rc;
 }


#endif
diff --git a/src/src/spam.h b/src/src/spam.h
index ba700c8..e6d27a1 100644
--- a/src/src/spam.h
+++ b/src/src/spam.h
@@ -22,7 +22,8 @@

typedef struct spamd_address_container {
uschar tcp_addr[24];
- unsigned int tcp_port;
+ unsigned short int tcp_port;
+ BOOL is_rspamd;
} spamd_address_container;

 #endif
diff --git a/test/confs/4008 b/test/confs/4008
new file mode 100644
index 0000000..55c2903
--- /dev/null
+++ b/test/confs/4008
@@ -0,0 +1,37 @@
+# Exim test configuration 4008
+# Content-scan: rspamd interface
+
+exim_path = EXIM_PATH
+host_lookup_order = bydns
+primary_hostname = myhost.test.ex
+spool_directory = DIR/spool
+log_file_path = DIR/spool/log/%slog
+gecos_pattern = ""
+gecos_name = CALLER_NAME
+log_selector = +subject
+
+spamd_address = 127.0.0.1 11333 variant=rspamd
+
+# ----- Main settings -----
+
+acl_smtp_rcpt = accept
+acl_smtp_data = c_data
+
+begin acl
+
+c_data:
+  warn
+    spam = nobody
+  warn
+    log_message = $spam_action $spam_report
+  accept
+
+# ----- Routers -----
+
+begin routers
+
+r:
+  driver = redirect
+  data = :blackhole:
+
+# End
diff --git a/test/confs/4009 b/test/confs/4009
new file mode 100644
index 0000000..b635195
--- /dev/null
+++ b/test/confs/4009
@@ -0,0 +1,37 @@
+# Exim test configuration 4009
+# Content-scan: spamassassin interface
+
+exim_path = EXIM_PATH
+host_lookup_order = bydns
+primary_hostname = myhost.test.ex
+spool_directory = DIR/spool
+log_file_path = DIR/spool/log/%slog
+gecos_pattern = ""
+gecos_name = CALLER_NAME
+log_selector = +subject
+
+spamd_address = 127.0.0.1 7833
+
+# ----- Main settings -----
+
+acl_smtp_rcpt = accept
+acl_smtp_data = c_data
+
+begin acl
+
+c_data:
+  warn
+    spam = nobody
+  warn
+    log_message = $spam_action $spam_report
+  accept
+
+# ----- Routers -----
+
+begin routers
+
+r:
+  driver = redirect
+  data = :blackhole:
+
+# End
diff --git a/test/log/4008 b/test/log/4008
new file mode 100644
index 0000000..d8bbb9b
--- /dev/null
+++ b/test/log/4008
@@ -0,0 +1,4 @@
+1999-03-02 09:44:33 10HmaX-0005vi-00 U=CALLER Warning: reject Action: reject\n Symbol: FAKE_SYMBOL_A(15.00)\n Symbol: FAKE_SYMBOL_B(0.00)\n Message-ID: undef
+1999-03-02 09:44:33 10HmaX-0005vi-00 <= CALLER@??? U=CALLER P=local-esmtp S=sss
+1999-03-02 09:44:33 10HmaX-0005vi-00 => :blackhole: <userx@???> R=r
+1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
diff --git a/test/log/4009 b/test/log/4009
new file mode 100644
index 0000000..0aa7ba3
--- /dev/null
+++ b/test/log/4009
@@ -0,0 +1,4 @@
+1999-03-02 09:44:33 10HmaX-0005vi-00 U=CALLER Warning: no action Spam detection software, running on the system "demo",\n has NOT identified this incoming email as spam.  The original\n message has been attached to this so you can view it or label\n similar future email.  If you have any questions, see\n @@CONTACT_ADDRESS@@ for details.\n \n Content preview:  test [...]\n \n Content analysis details:   (4.5 points, 5.0 required)\n \n  pts rule name              description\n ---- ---------------------- --------------------------------------------------\n -1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP\n  1.2 MISSING_HEADERS        Missing To: header\n  1.0 MISSING_FROM           Missing From: header\n  1.8 MISSING_SUBJECT        Missing Subject: header\n  1.4 MISSING_DATE           Missing Date: header\n  0.1 MISSING_MID            Missing Message-Id: header
+1999-03-02 09:44:33 10HmaX-0005vi-00 <= CALLER@??? U=CALLER P=local-esmtp S=sss
+1999-03-02 09:44:33 10HmaX-0005vi-00 => :blackhole: <userx@???> R=r
+1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
diff --git a/test/scripts/4000-scanning/4008 b/test/scripts/4000-scanning/4008
new file mode 100644
index 0000000..30c1d94
--- /dev/null
+++ b/test/scripts/4000-scanning/4008
@@ -0,0 +1,43 @@
+# content scan interface: rspamd
+server 11333
+<CHECK RSPAMC/1.3
+<Content-length: 
+<Queue-Id: 
+<From: 
+<Recipient-Number: 1
+<Rcpt: 
+<Helo:
+<
+<From 
+<X-Envelope-From
+<X-Envelope-To
+<Received: 
+<    by 
+<    (envelope
+<    id 
+<    for 
+<Content-type: text/plain
+<Message-Id:
+<From:
+<Date:
+<
+<test
+>RSPAMD/1.3 0 EX_OK
+>Metric: default; True; 15.00 / 15.00 / 0.0
+>Action: reject
+>Symbol: FAKE_SYMBOL_A(15.00)
+>Symbol: FAKE_SYMBOL_B(0.00)
+>Message-ID: undef
+*eof
+****
+exim -odi -bs
+ehlo test.ex
+mail from:<>
+rcpt to:<userx@???>
+data
+Content-type: text/plain
+
+test
+.
+quit
+****
diff --git a/test/scripts/4000-scanning/4009 b/test/scripts/4000-scanning/4009
new file mode 100644
index 0000000..8e34c98
--- /dev/null
+++ b/test/scripts/4000-scanning/4009
@@ -0,0 +1,55 @@
+# content scan interface: spamassassin
+server 7833
+<REPORT SPAMC
+<User:
+<Content-length:
+<
+<From 
+<X-Envelope-From
+<X-Envelope-To
+<Received: 
+<    by 
+<    (envelope
+<    id 
+<    for 
+<Content-type: text/plain
+<Message-Id:
+<From:
+<Date:
+<
+<test
+>SPAMD/1.1 0 EX_OK
+>Spam: False ; 4.5 / 5.0
+>
+>Spam detection software, running on the system "demo",
+>has NOT identified this incoming email as spam.  The original
+>message has been attached to this so you can view it or label
+>similar future email.  If you have any questions, see
+>@@CONTACT_ADDRESS@@ for details.
+>
+>Content preview:  test [...] 
+>
+>Content analysis details:   (4.5 points, 5.0 required)
+>
+> pts rule name              description
+>---- ---------------------- --------------------------------------------------
+>-1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP
+> 1.2 MISSING_HEADERS        Missing To: header
+> 1.0 MISSING_FROM           Missing From: header
+> 1.8 MISSING_SUBJECT        Missing Subject: header
+> 1.4 MISSING_DATE           Missing Date: header
+> 0.1 MISSING_MID            Missing Message-Id: header
+>
+*eof
+****
+exim -odi -bs
+ehlo test.ex
+mail from:<>
+rcpt to:<userx@???>
+data
+Content-type: text/plain
+
+test
+.
+quit
+****
diff --git a/test/stdout/4008 b/test/stdout/4008
new file mode 100644
index 0000000..957fbd8
--- /dev/null
+++ b/test/stdout/4008
@@ -0,0 +1,45 @@
+220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000
+250-myhost.test.ex Hello CALLER at test.ex
+250-SIZE 52428800
+250-8BITMIME
+250-PIPELINING
+250 HELP
+250 OK
+250 Accepted
+354 Enter message, ending with "." on a line by itself
+250 OK id=10HmaX-0005vi-00
+221 myhost.test.ex closing connection
+
+******** SERVER ********
+Listening on port 11333 ... 
+Connection request from [127.0.0.1]
+<CHECK RSPAMC/1.3
+<Content-length: 479
+<Queue-Id: 10HmaX-0005vi-00
+<From: <CALLER@???>
+<Recipient-Number: 1
+<Rcpt: <userx@???>
+<Helo: test.ex
+<
+<From MAILER-DAEMON Tue Mar 02 09:44:33 1999
+<X-Envelope-From: <CALLER@???>
+<X-Envelope-To: userx@???
+<Received: from CALLER (helo=test.ex)
+<    by myhost.test.ex with local-esmtp (Exim x.yz)
+<    (envelope-from <CALLER@???>)
+<    id 10HmaX-0005vi-00
+<    for userx@???; Tue, 2 Mar 1999 09:44:33 +0000
+<Content-type: text/plain
+<Message-Id: <E10HmaX-0005vi-00@???>
+<From: CALLER_NAME <CALLER@???>
+<Date: Tue, 2 Mar 1999 09:44:33 +0000
+<
+<test
+>RSPAMD/1.3 0 EX_OK
+>Metric: default; True; 15.00 / 15.00 / 0.0
+>Action: reject
+>Symbol: FAKE_SYMBOL_A(15.00)
+>Symbol: FAKE_SYMBOL_B(0.00)
+>Message-ID: undef
+Expected EOF read from client
+End of script
diff --git a/test/stdout/4009 b/test/stdout/4009
new file mode 100644
index 0000000..a1d7f2e
--- /dev/null
+++ b/test/stdout/4009
@@ -0,0 +1,57 @@
+220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000
+250-myhost.test.ex Hello CALLER at test.ex
+250-SIZE 52428800
+250-8BITMIME
+250-PIPELINING
+250 HELP
+250 OK
+250 Accepted
+354 Enter message, ending with "." on a line by itself
+250 OK id=10HmaX-0005vi-00
+221 myhost.test.ex closing connection
+
+******** SERVER ********
+Listening on port 7833 ... 
+Connection request from [127.0.0.1]
+<REPORT SPAMC/1.2
+<User: nobody
+<Content-length: 479
+<
+<From MAILER-DAEMON Tue Mar 02 09:44:33 1999
+<X-Envelope-From: <CALLER@???>
+<X-Envelope-To: userx@???
+<Received: from CALLER (helo=test.ex)
+<    by myhost.test.ex with local-esmtp (Exim x.yz)
+<    (envelope-from <CALLER@???>)
+<    id 10HmaX-0005vi-00
+<    for userx@???; Tue, 2 Mar 1999 09:44:33 +0000
+<Content-type: text/plain
+<Message-Id: <E10HmaX-0005vi-00@???>
+<From: CALLER_NAME <CALLER@???>
+<Date: Tue, 2 Mar 1999 09:44:33 +0000
+<
+<test
+>SPAMD/1.1 0 EX_OK
+>Spam: False ; 4.5 / 5.0
+>
+>Spam detection software, running on the system "demo",
+>has NOT identified this incoming email as spam.  The original
+>message has been attached to this so you can view it or label
+>similar future email.  If you have any questions, see
+>@@CONTACT_ADDRESS@@ for details.
+>
+>Content preview:  test [...]
+>
+>Content analysis details:   (4.5 points, 5.0 required)
+>
+> pts rule name              description
+>---- ---------------------- --------------------------------------------------
+>-1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP
+> 1.2 MISSING_HEADERS        Missing To: header
+> 1.0 MISSING_FROM           Missing From: header
+> 1.8 MISSING_SUBJECT        Missing Subject: header
+> 1.4 MISSING_DATE           Missing Date: header
+> 0.1 MISSING_MID            Missing Message-Id: header
+>
+Expected EOF read from client
+End of script