.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
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
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
.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
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
{ "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 },
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
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
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;
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;
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);
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",
}
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;
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,
/* 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,
*q-- = '\0';
spam_report = spam_report_buffer;
+ spam_action = spam_action_buffer;
/* create spam bar */
spamd_score_char = spamd_score > 0 ? '+' : '-';
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)
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
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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+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: <userx@test.ex> R=r
+1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
--- /dev/null
+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: <userx@test.ex> R=r
+1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
--- /dev/null
+# content scan interface: rspamd
+server 11333
+<CHECK RSPAMC/1.3\r
+<Content-length:
+<Queue-Id:
+<From:
+<Recipient-Number: 1\r
+<Rcpt:
+<Helo:
+<\r
+<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\r
+>Metric: default; True; 15.00 / 15.00 / 0.0\r
+>Action: reject\r
+>Symbol: FAKE_SYMBOL_A(15.00)\r
+>Symbol: FAKE_SYMBOL_B(0.00)\r
+>Message-ID: undef\r
+*eof
+****
+exim -odi -bs
+ehlo test.ex
+mail from:<>
+rcpt to:<userx@test.ex>
+data
+Content-type: text/plain
+
+test
+.
+quit
+****
--- /dev/null
+# 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@test.ex>
+data
+Content-type: text/plain
+
+test
+.
+quit
+****
--- /dev/null
+220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000\r
+250-myhost.test.ex Hello CALLER at test.ex\r
+250-SIZE 52428800\r
+250-8BITMIME\r
+250-PIPELINING\r
+250 HELP\r
+250 OK\r
+250 Accepted\r
+354 Enter message, ending with "." on a line by itself\r
+250 OK id=10HmaX-0005vi-00\r
+221 myhost.test.ex closing connection\r
+
+******** 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@myhost.test.ex>
+<Recipient-Number: 1
+<Rcpt: <userx@test.ex>
+<Helo: test.ex
+<
+<From MAILER-DAEMON Tue Mar 02 09:44:33 1999
+<X-Envelope-From: <CALLER@myhost.test.ex>
+<X-Envelope-To: userx@test.ex
+<Received: from CALLER (helo=test.ex)
+< by myhost.test.ex with local-esmtp (Exim x.yz)
+< (envelope-from <CALLER@myhost.test.ex>)
+< id 10HmaX-0005vi-00
+< for userx@test.ex; Tue, 2 Mar 1999 09:44:33 +0000
+<Content-type: text/plain
+<Message-Id: <E10HmaX-0005vi-00@myhost.test.ex>
+<From: CALLER_NAME <CALLER@myhost.test.ex>
+<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
--- /dev/null
+220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000\r
+250-myhost.test.ex Hello CALLER at test.ex\r
+250-SIZE 52428800\r
+250-8BITMIME\r
+250-PIPELINING\r
+250 HELP\r
+250 OK\r
+250 Accepted\r
+354 Enter message, ending with "." on a line by itself\r
+250 OK id=10HmaX-0005vi-00\r
+221 myhost.test.ex closing connection\r
+
+******** 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@myhost.test.ex>
+<X-Envelope-To: userx@test.ex
+<Received: from CALLER (helo=test.ex)
+< by myhost.test.ex with local-esmtp (Exim x.yz)
+< (envelope-from <CALLER@myhost.test.ex>)
+< id 10HmaX-0005vi-00
+< for userx@test.ex; Tue, 2 Mar 1999 09:44:33 +0000
+<Content-type: text/plain
+<Message-Id: <E10HmaX-0005vi-00@myhost.test.ex>
+<From: CALLER_NAME <CALLER@myhost.test.ex>
+<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