From c5f280e20a8e3ecd5f016b8fb34a436588915ed2 Mon Sep 17 00:00:00 2001 From: Andrew Lewis Date: Sat, 24 Jan 2015 23:42:59 +0000 Subject: [PATCH] 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(-) create mode 100644 test/confs/4008 create mode 100644 test/confs/4009 create mode 100644 test/log/4008 create mode 100644 test/log/4009 create mode 100644 test/scripts/4000-scanning/4008 create mode 100644 test/scripts/4000-scanning/4009 create mode 100644 test/stdout/4008 create mode 100644 test/stdout/4009 diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 77d966d6d..13d903b16 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 bfe872503..835417ff2 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 e9112dc16..9f430f803 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 a066f3595..6d7ea33ab 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 b5ea8a49b..98d589f82 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 c0c3fb373..71993fb7f 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 ba700c8b6..e6d27a137 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 000000000..55c290333 --- /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 000000000..b635195b6 --- /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 000000000..d8bbb9b18 --- /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@myhost.test.ex U=CALLER P=local-esmtp S=sss +1999-03-02 09:44:33 10HmaX-0005vi-00 => :blackhole: 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 000000000..0aa7ba3ce --- /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@myhost.test.ex U=CALLER P=local-esmtp S=sss +1999-03-02 09:44:33 10HmaX-0005vi-00 => :blackhole: 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 000000000..30c1d94d0 --- /dev/null +++ b/test/scripts/4000-scanning/4008 @@ -0,0 +1,43 @@ +# content scan interface: rspamd +server 11333 +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: +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 000000000..8e34c9802 --- /dev/null +++ b/test/scripts/4000-scanning/4009 @@ -0,0 +1,55 @@ +# content scan interface: spamassassin +server 7833 +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: +data +Content-type: text/plain + +test +. +quit +**** diff --git a/test/stdout/4008 b/test/stdout/4008 new file mode 100644 index 000000000..957fbd8c5 --- /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] + + + +) +< id 10HmaX-0005vi-00 +< for userx@test.ex; Tue, 2 Mar 1999 09:44:33 +0000 + + +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 000000000..a1d7f2ea7 --- /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] + +) +< id 10HmaX-0005vi-00 +< for userx@test.ex; Tue, 2 Mar 1999 09:44:33 +0000 + + +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 -- 2.25.1