11.09.2015 12:01, Bron Gondwana wrote:
On Fri, Sep 11, 2015, at 16:48, Vladislav Bogdanov wrote:
11.09.2015 03:52, Bron Gondwana wrote:
sort: spamscore search: spamabove / spambelow

These use the X-Spam-score header which is a floating point number
with a single decimal place usually, i.e. 5.0, 17.3.  spamabove is GE
and spambelow is LT.

I'm going to push this back, because it doesn't clash with anything.
It's kinda nice to be able to sort by spamscore to quickly put the
focus on the most likely to be be wrongly classified messages, and
we're going to support that in our interface at some stage.

Bron.


Ah, I have a nice patch for spamtest extension against 2.4.17.

It connects to spamd itself from lmtpd, checks the message and sets
additional headers. Sieve integration is done too.

Need to send it here.

That would be great.

Attached. Just found that it inconsistently uses tabs/spaces. I hope that is not an issue at least for initial review.


Thanks,

Bron.


>From 7fd7f4eca45accc8b4ccb72ab2baf6f26042db36 Mon Sep 17 00:00:00 2001
From: Vladislav Bogdanov <bub...@hoster-ok.com>
Date: Fri, 15 May 2015 05:37:30 +0000
Subject: [PATCH] Add SPAMTEST sieve extension

---
 doc/README.spamtest     |  161 ++++++++++++
 imap/lmtp_sieve.c       |  623 +++++++++++++++++++++++++++++++++++++++++++++++
 imap/lmtpd.c            |   16 ++
 imap/lmtpd.h            |    8 +
 lib/imapoptions         |   42 +++-
 sieve/bc_emit.c         |   43 ++++
 sieve/bc_eval.c         |   31 +++-
 sieve/bc_generate.c     |   14 +
 sieve/bytecode.h        |    3 +-
 sieve/interp.c          |    9 +
 sieve/interp.h          |    1 +
 sieve/script.c          |    7 +
 sieve/script.h          |    1 +
 sieve/sieve-lex.l       |    1 +
 sieve/sieve.y           |   81 ++++++
 sieve/sieve_interface.h |    2 +
 sieve/sievec.c          |    6 +
 sieve/sieved.c          |   10 +
 sieve/tree.h            |    7 +
 timsieved/scripttest.c  |    6 +
 20 files changed, 1069 insertions(+), 3 deletions(-)
 create mode 100644 doc/README.spamtest

diff --git a/doc/README.spamtest b/doc/README.spamtest
new file mode 100644
index 0000000..8b39753
--- /dev/null
+++ b/doc/README.spamtest
@@ -0,0 +1,161 @@
+'spamtest' extension for cyrus-imap sieve engine.
+=================================================
+
+README
+=================================================
+This patch is written by Vladislav Bogdanov <bub...@hoster-ok.com> and based on
+contrib/sieve-spamasssassin and its updated version which was found
+somewhere in mailing-lists.
+
+This patch introduces strict RFC-3685 compliant 'spamtest' extension
+for cyrus-imap server. For more information look at
+http://www.ietf.org/rfc/rfc3685.txt .
+
+This document assumes that you have already got working spamd installation
+and know how to use spamd/spamc pair for SPAM filtering.
+
+New features:
+
+* Strict RFC-3685 compliance.
+* Ability to replace message body with output from spamd in PROCESS mode.
+* Round-robin load-balancing between several spamd daemons.
+* Ability to use UNIX socket to connect to spamd.
+* Choice, whether to execute spamtest for every recipient separately (as
+  they could have different settings for 'what is SPAM'), or to use cached
+  result from already done check.
+* Header with 'spamtest' value could be added to a message.
+
+'spamtest' value is set in accordance with the score received from spamd and
+threshold set by user or administrator (see spamassassin documentation for
+that settings).
+
+Following algorithm is used:
+---------------------------
+        if (spamd_score >= spamd_threshold) {
+                return 10;
+        } else if (spamd_threshold <= 0) {
+                if (spamd_score <= spamd_threshold) {
+                        return 1;
+                } else {
+                        return 10;
+                }
+        } else if (spamd_score <= 0) {
+                return 1;
+        } else {
+                return (abs((int) 8 * spamd_score / spamd_threshold) + 2);
+        }
+--------------------------
+This algorithm ensures that value returned from 'spamtest' will be
+compliant with RFC-3685.
+
+If 'spamtest_spamd_process' option is enabled and spam check is performed
+without errors then 'spamtest' will add its own header
+"X-Sieve-Spamtest-Score: <score>"
+to a message.
+
+------------------------------
+!!! WARNING WARNING WARNING !!!
+-------------------------------
+If 'spamtest_spamd_process' option is enabled, then your users may receive
+messages that have been incorrectly (from users point of view) marked as SPAM
+(in Subject:, body, etc.) but delivered as normal, not-SPAM messages.
+How this could be? In this case message headers and body are replaced with what
+is returned from the spamd after it has been asked to 'PROCESS' message (not
+'CHECK'). Spamd itself can replace body of message to something like
+'Spam detection software, running on the system "localhost", has identified
+this incoming email as possible spam. ... Content analysis details:   (6.8 points,
+5.0 required) ...', but user has threshold set to 10, not to 5.
+This could happen if the message was delivered to several recipients in one shot,
+and first (in delivery order) of users who have 'spamtest' enabled had theshold
+set to 5 and he/she set up spamd to replace a message body for messages
+identified as 'SPAM'.
+'spamtest' replaced the message body, and all other recipients received that
+message with the body replaced. 
+
+How to avoid such behaviour? There are two possibilities:
+
+1. Disable 'spamtest_spamd_process'. 'spamtest' checks will still be performed,
+   but users will not be able to see headers added by the spamd and by the 'spamtest'
+   extension itself. 
+2. Tell your MTA to limit the number of recipients per the LMTP transfer to 1.
+   This will increase number of messages transfered from MTA to cyrus via LMTP,
+   but ensure that each recipient will receive a message checked for SPAM accordingly
+   to his/her spamd settings. If you use sendmail then you can use 
+   define(`CYRUSV2_MAILER_MAXRCPTS', `1')
+   in sendmail.mc for this.
+
+Note that the only way to inform lmtpd that users could have different settings for
+SPAM checks is the 'spamtest_spamd_userconf' option ('spamtest' does not parse any
+spamd configs, especially at remote hosts :) ). 'spamtest' will check message
+via spamd only once if it is disabled.
+
+'spamtest' checks if both 'spamtest_spamd_process' and 'spamtest_spamd_userconf'
+options are enabled (that means: "Replace message body if spamd told to do so", and
+"Different users could have different setting for 'What is SPAM'"), and sends message
+"Both 'spamtest_spamd_process' and 'spamtest_spamd_userconf' are enabled. Please read
+'README.spamtest'." to syslog, hoping that you will read this file, and setup your
+MTA accordingly.
+
+To disable this message configure your MTA and enable "spamtest_one_rcpt" option.
+
+------------------------------
+!!!      End of warning    !!!
+------------------------------
+
+CONFIGURATION:
+=================================================
+
+There are new options to imapd.conf (exerpt from lib/imapoptions):
+----------------------------------
+{ "spamtest_enabled", 0, SWITCH }
+/* Enables use of 'spamtest' sieve extension. See 'spamtest_*' options
+   for details. */
+
+{ "spamtest_spamd_conntype", "tcp", STRINGLIST("tcp", "unix")}
+/* Type of socket to connect to spamd. If "unix" is selected, then
+   "spamtest_spamd_socket" will be used, overvise "spamtest_spamd_hosts" will be used. */
+
+{ "spamtest_max_size", 256000, INT }
+/* Maximum size of message could be checked be spamd. */
+
+{ "spamtest_spamd_hosts", "127.0.0.1", STRING }
+/* Space-separated list of address[:port] pairs of running spamd processes.
+   If multiple pairs are specified, they will be used in 'pseudo-round-robin'
+   order. If port is not specified, default port 783 will be used. */
+
+{ "spamtest_spamd_socket", "", STRING }
+/* Filename of UNIX socket spamd is listening on. */
+
+{ "spamtest_spamd_process", 0, SWITCH }
+/* If enabled, then content of the message will be replaced by what is
+   received from spamd. If disabled, then message will remain as it is. */
+
+{ "spamtest_spamd_userconf", 0, SWITCH }
+/* Are users have possibility to set spamd parameters.
+   If enabled, then spamtest sieve test will be executed for every recipient
+   separately, as different users could have different thresholds, and spamtest
+   result value may differ for different users.
+   If disabled, then only one spamtest check will be done for each message. */
+
+{ "spamtest_one_rcpt", 0, SWITCH }
+/* Enabling this indicates, that you have read the documentation about issues
+   that could raise if both 'spamtest_spamd_userconf' and 'spamtest_spamd_process'
+   are enabled, and disables annoing warnings about these issues. */
+
+{ "spamtest_append_default_domain", 0, SWITCH }
+/* Whether to append default domain to domain-less username which checking message for spam.
+   Set this to comply with a way how spamassassin user-preferences are stored in your
+   installation. */
+----------------------------------
+
+
+TODO:
+=================================================
+* Write more documentation, include examples (from RFC too).
+* Fix english.
+* MAYBE make message replacement via same functions as lmtpd do instead of fwrite.
+* MAYBE update headers and body cache after message is replaced.
+* Double check for possible race conditions, memory leaks and so on.
+* Do more testing.
+* Make load-balancing work if hostname that resolve to multiple A records is used
+  in "spamtest_spamd_hosts".
diff --git a/imap/lmtp_sieve.c b/imap/lmtp_sieve.c
index 9573e3b..f632a38 100644
--- a/imap/lmtp_sieve.c
+++ b/imap/lmtp_sieve.c
@@ -56,6 +56,8 @@
 #include <sys/types.h>
 #include <sys/wait.h>
 
+#include <sys/un.h>
+
 #include "annotate.h"
 #include "append.h"
 #include "auth.h"
@@ -714,6 +716,621 @@ sieve_vacation_t vacation = {
 static char *markflags[] = { "\\flagged" };
 static sieve_imapflags_t mark = { markflags, 1 };
 
+/* spam support */
+
+static int spamtest_getline (int s, char *buf, int len)
+{
+	char *bp = buf;
+	int ret = 1;
+	char ch;
+	int orig_len = len;
+
+	while ((ret = read (s, &ch, 1)) == 1 && ch != '\n') {
+		if (len > 0) {
+			*bp++ = ch;
+			len--;
+		}
+	}
+	if (len > 0)
+		*bp = '\0';
+	return (orig_len - len);
+}
+
+static int spamtest_full_write (int s, char *buf, int len)
+{
+	int total;
+	int ret;
+
+	for (total = 0; total < len; total += ret) {
+		ret = write (s, buf + total, len - total);
+		if (ret < 0)
+			return 0;
+	}
+	return total == len;
+}
+
+/* Stolen from libspamc.c, and I'm not sure about license compatibility */
+static float _locale_safe_string_to_float(char *buf, int siz)
+{
+    int is_neg;
+    char *cp, *dot;
+    int divider;
+    float ret, postdot;
+
+    buf[siz - 1] = '\0';	/* ensure termination */
+
+    /* ok, let's illustrate using "100.033" as an example... */
+
+    is_neg = 0;
+    if (*buf == '-') {
+	is_neg = 1;
+    }
+
+    ret = (float) (strtol(buf, &dot, 10));
+    if (dot == NULL) {
+	return 0.0;
+    }
+    if (dot != NULL && *dot != '.') {
+	return ret;
+    }
+
+    /* ex: ret == 100.0 */
+
+    cp = (dot + 1);
+    postdot = (float) (strtol(cp, NULL, 10));
+    /* note: don't compare floats == 0.0, it's unsafe.  use a range */
+    if (postdot >= -0.00001 && postdot <= 0.00001) {
+	return ret;
+    }
+
+    /* ex: postdot == 33.0, cp="033" */
+
+    /* now count the number of decimal places and figure out what power of 10 to use */
+    divider = 1;
+    while (*cp != '\0') {
+	divider *= 10;
+	cp++;
+    }
+
+    /* ex:
+     * cp="033", divider=1
+     * cp="33", divider=10
+     * cp="3", divider=100
+     * cp="", divider=1000
+     */
+
+    if (is_neg) {
+	ret -= (postdot / ((float) divider));
+    }
+    else {
+	ret += (postdot / ((float) divider));
+    }
+    /* ex: ret == 100.033, tada! ... hopefully */
+
+    return ret;
+}
+
+static int spamtest_calculate_score(float spamd_score, float spamd_threshold)
+{
+/*
+    RFC3685:
+    The spamtest result is a string starting with a numeric value in the
+    range "0" (zero) through "10", with meanings summarized below:
+
+    spamtest    interpretation
+    value
+
+        0		message was not tested for spam
+        1		message was tested and is clear of spam
+        2 - 9		message was tested and has a varying likelihood of
+			containing spam in increasing order
+        10		message was tested and definitely contains spam
+	 
+    The underlying SIEVE implementation will map whatever spam check is
+    done into this numeric range, as appropriate.
+
+*/
+
+	if (spamd_score >= spamd_threshold) {
+		return 10;
+	} else if (spamd_threshold <= 0) { /* FIXME: If user set threshold to <= 0, then (s)he wants to receive 
+					      only messages from her(his) whitelist, isn't (s)he?
+					      We need this check to prevent division by zero
+					    */
+		if (spamd_score <= spamd_threshold) {
+			return 1; 
+		} else {
+			return 10;
+		}
+	} else if (spamd_score <= 0) {
+		return 1;
+	} else {
+		return (abs((int) 8 * spamd_score / spamd_threshold) + 2);
+	}
+}
+
+#define SPAMTEST_BUFSIZE 8192
+#define SPAMTEST_DEBUG 0
+
+static int spamtest_process_response (int s, int *spam_score, message_data_t *m)
+{
+	char is_spam_str[6];
+	char buf[SPAMTEST_BUFSIZE];
+	int major;
+	int minor;
+	int response;
+	int size = 0;
+	int new_size = 0;
+	char score_buf[20];
+	char threshold_buf[20];
+
+	float spamd_score = 0.0;
+	float spamd_threshold = 0.0;
+
+	*spam_score = 0;
+
+	if (! spamtest_getline (s, buf, sizeof (buf))) {
+		syslog (LOG_ERR, "read_response: response getline failed");
+		return SIEVE_FAIL;
+	}
+	if (sscanf (buf, "SPAMD/%d.%d %d %*s", &major, &minor, &response) != 3) {
+		syslog (LOG_ERR, "read_response: response sscanf failed, buf: %s",
+			buf);
+		return SIEVE_FAIL;
+	}
+	if (major < 1 || (major == 1 && minor < 1)) {
+		syslog (LOG_ERR, "read_response: bad spamd version: %d.%d",
+			major, minor);
+		return SIEVE_FAIL;
+	}
+	if (m == NULL) {
+		if (! spamtest_getline (s, buf, sizeof (buf))) {
+			syslog (LOG_ERR, "read_response: header getline failed");
+			return SIEVE_FAIL;
+		}
+		if (sscanf (buf, "Spam: %5s ; %20s / %20s", is_spam_str, score_buf, threshold_buf) != 3) {
+			syslog (LOG_ERR, "read_response: header sscanf failed, buf: %s",
+				buf);
+			return SIEVE_FAIL;
+		}
+		spamd_score = _locale_safe_string_to_float(score_buf, 20);
+		spamd_threshold = _locale_safe_string_to_float(threshold_buf, 20);
+		*spam_score = spamtest_calculate_score(spamd_score, spamd_threshold);
+	} else {
+		/* Read spamd headers, catch Content-length */
+		while ((size = spamtest_getline (s, buf, sizeof (buf))) && (buf[0] != '\r' && buf[0] != '\0')) {
+#if SPAMTEST_DEBUG
+			syslog (LOG_DEBUG, "Spamd_header: %s", buf);
+#endif
+			if (strncmp (buf, "Content-length: ", 16) == 0) {
+				if (sscanf (buf, "Content-length: %d", &new_size) < 1) {
+					syslog (LOG_ERR, "read_response: invalid Content-length from spamd");
+				}
+			}
+		}
+
+		if (new_size > 0) {
+			int body_size, ret, headers_size = 0;
+			int sieve_spamtest_header_len = 0;
+
+			rewind (m->f);
+			/* First, read headers one-by one and try to catch X-Spam-Status
+			   Save one character in buf for trailing '\n'
+			 */
+			while ((size = spamtest_getline (s, buf, sizeof (buf) - 1)) && (buf[0] != '\r' && buf[0] != '\0')) {
+#if SPAMTEST_DEBUG
+				syslog (LOG_DEBUG, "Header(rd): %s", buf);
+#endif
+				if (strncmp (buf, "X-Spam-Status: ", 15) == 0) {
+					if (sscanf(buf, "X-Spam-Status: %5s score=%20s required=%20s %*s",
+						is_spam_str, score_buf, threshold_buf) < 3) {
+						syslog (LOG_ERR, "read_response: invalid X-Spam-Status from spamd (%s)", buf);
+					} else {
+						/* Deal with comma after spam status reply
+						   (sscanf understands boundaries only at a whitespace)
+						 */
+						char *p = NULL;
+						char sieve_spamtest_header[SPAMTEST_BUFSIZE];
+
+						if ((p = index(is_spam_str, ','))) {
+							is_spam_str[p - is_spam_str] = '\0';
+						}
+#if SPAMTEST_DEBUG
+						syslog (LOG_DEBUG, "spamtest_process_response: X-Spam-Status = %s", is_spam_str);
+#endif
+						spamd_score = _locale_safe_string_to_float(score_buf, 20);
+						spamd_threshold = _locale_safe_string_to_float(threshold_buf, 20);
+						*spam_score = spamtest_calculate_score(spamd_score, spamd_threshold);
+#if 1
+						snprintf(sieve_spamtest_header, SPAMTEST_BUFSIZE, "X-Sieve-Spamtest-Score: %d\r\n%s", *spam_score, buf);
+						sieve_spamtest_header_len = strlen(sieve_spamtest_header) - strlen(buf);
+						/* It should be fine, 8192 is much bigger then X-Spam-Status header line */
+						strncpy(buf, sieve_spamtest_header, SPAMTEST_BUFSIZE);
+#if SPAMTEST_DEBUG
+						syslog (LOG_DEBUG, "%d; %d => %d, %d => %d", sieve_spamtest_header_len,
+						    size, sieve_spamtest_header_len + size,
+						    new_size, new_size + sieve_spamtest_header_len);
+#endif
+						size += sieve_spamtest_header_len;
+						new_size += sieve_spamtest_header_len;
+#endif
+					}
+				}
+
+				assert(buf[size] == '\0');
+				buf[size] = '\n';
+#if SPAMTEST_DEBUG
+				/* prevent syslog from showing trailing trash */
+				buf[size + 1] = '\0';
+				syslog (LOG_DEBUG, "Header(wr): %s", buf);
+#endif
+				if (fwrite(buf, 1, ++size, m->f) != size) {
+					syslog (LOG_ERR, "spam: error in fwrite: %s", strerror(errno));
+				}
+				headers_size += size;
+			}
+
+			/* Store empty line we stopped at before */
+			fwrite("\r\n", 1, 2, m->f);
+			headers_size += 2;
+
+			/* Store message body in one shot */
+			for (body_size = 0; body_size < new_size - headers_size; body_size += ret) {
+				ret = read (s, buf, sizeof (buf));
+				if (ret <= 0)
+					break;
+#if SPAMTEST_DEBUG
+				syslog (LOG_DEBUG, "Body(%d): %s", ret, buf);
+#endif
+				if (fwrite(buf, 1, ret, m->f) != ret) {
+				    syslog (LOG_ERR, "spam: error in fwrite: %s", strerror(errno));
+				}
+			}
+#if SPAMTEST_DEBUG
+			syslog (LOG_DEBUG, "%d + %d = %d (%d)", body_size, headers_size, body_size + headers_size, new_size);
+#endif
+			assert(body_size + headers_size == new_size);
+                       if (m->size > new_size) {
+                               /* 
+                                * Get rid of excessive data if new size is smaller then original
+                                * (spamassassin may remove headers of previous SA checks)
+                                */
+                               ftruncate(fileno(m->f), new_size);
+                       }
+			m->size = new_size;
+			rewind (m->f);
+			/* Size is likely changed, sync both data and metadata */
+			fflush (m->f);
+			fsync (fileno(m->f));
+		} else {
+			syslog (LOG_ERR, "spam: invalid Content-Length from spamd, received: %s", buf);
+		}
+	}
+
+	syslog(LOG_INFO, "spamtest result: %d, score = %.2f, threshold = %.2f",
+		*spam_score,
+		spamd_score,
+		spamd_threshold);
+
+	return SIEVE_OK;
+}
+
+struct spamtest_host {
+	char *hostname;
+	int port;
+};
+
+void spamtest_free_hosts(struct spamtest_host **list)
+{
+	int i = 0;
+	if (list) {
+		while (list[i]) {
+			if (list[i]->hostname) {
+				free(list[i]->hostname);
+			}
+			free(list[i]);
+			i++;
+		}    
+		free(list);
+		*list = NULL;
+	}
+}
+
+struct spamtest_host **spamtest_parse_hosts(const char *str)
+{
+	struct spamtest_host **hosts = (struct spamtest_host **)malloc(sizeof(struct spamtest_host *) * 2);
+	char buf[1024];
+	int len;
+	int count = 0;
+
+	if (!hosts) {
+		return NULL;
+	}
+	hosts[0] = NULL;
+	hosts[1] = NULL;
+
+#if SPAMTEST_DEBUG
+	syslog (LOG_DEBUG, "spamtest: entered spamtest_parse_hosts with param '%s'", str);
+#endif
+	while (*str) {
+		char *p;
+		char *i;
+		struct spamtest_host *host = NULL;
+	
+		for (p = (char *) str; *p && !isspace((int) *p); p++);
+		len = p - str;
+		if (len >= sizeof(buf))
+			len = sizeof(buf) - 1;
+
+		memcpy(buf, str, len);
+		buf[len] = '\0';
+		
+		host = malloc(sizeof(struct spamtest_host));
+
+		if (!host) {
+			spamtest_free_hosts(hosts);
+			return NULL;
+		}
+
+		host->hostname = NULL;
+		host->port = 783;
+
+		if ((i = index(buf, ':')) != NULL) {
+			int port = 0;
+			buf[i - buf] = '\0';
+			host->hostname = strdup(buf);
+			port = atoi(i);
+			if (port)
+				host->port = port;
+		} else {
+			host->hostname = strdup(buf);
+		}
+
+		if (host->hostname) {
+			if (count != 0) {
+				hosts = (struct spamtest_host **)realloc(hosts, sizeof(struct spamtest_host *) * (count + 1) + 1);
+				hosts[count + 1] = NULL;
+			}
+
+			hosts[count] = host;
+			count++;
+		} else {
+			free(host);
+		}
+
+		str = p;
+		while (*str && isspace((int) *str)) str++;
+	}
+#if SPAMTEST_DEBUG
+	syslog (LOG_DEBUG, "spamtest: returning from spamtest_parse_hosts with %d records", count);
+#endif
+	return hosts;
+}
+
+struct spamtest_host *spamtest_get_random_host(struct spamtest_host **list)
+{
+	int i = 0;
+	int j;
+
+	if (!list)
+		return NULL;
+
+	while (list[i]) {
+	    i++;
+	}
+	
+	if (i == 0)
+		return NULL;
+	else if (i == 1)
+		return (list[0]);
+
+	srand(getpid() ^ time(NULL));
+
+	j = (int) (rand() % i);
+
+	assert (j <= i);
+	assert (list[j]);
+
+	return (list[j]);
+}
+
+static void spamtest_delete_host(struct spamtest_host **list, struct spamtest_host *host)
+{
+	int i = 0;
+	int j = 0;
+	int count = 0;
+	struct spamtest_host **list_tmp = NULL;
+
+	if (!list)
+		return;
+
+	while (list[count]) {
+	    count++;
+	}
+
+	if (count == 0)
+		return;
+
+	list_tmp = (struct spamtest_host **)malloc(sizeof(struct spamtest_host *) * count);
+
+	while (i < count) {
+		if (list[i] == host) {
+			if (list[i]->hostname) free(list[i]->hostname);
+			free(list[i]);
+			list[i] = NULL;
+		} else {
+			list_tmp[j] = list[i];
+			j++;
+		}
+		i++;
+	}
+
+	list_tmp[j] = NULL;
+	memcpy(list, list_tmp, sizeof(struct spamtest_host *) * j);
+}
+
+int spamtest (void *mc, int *spam_score)
+{
+	deliver_data_t *d = (deliver_data_t *) mc;
+	message_data_t *m = d->m;
+	int s;
+	struct sockaddr_in addr_in;
+	struct sockaddr_un addr_un;
+	struct sockaddr *addr = NULL;
+	char header[2048];
+	int is_spamtest_enabled = config_getswitch (IMAPOPT_SPAMTEST_ENABLED);
+	int use_tcp = ! strcasecmp (config_getstring (IMAPOPT_SPAMTEST_SPAMD_CONNTYPE), "tcp");
+	int max_size = config_getint (IMAPOPT_SPAMTEST_MAX_SIZE);
+	const char *socketpath = config_getstring (IMAPOPT_SPAMTEST_SPAMD_SOCKET);
+	const char *default_domain = config_getstring (IMAPOPT_DEFAULTDOMAIN);
+	struct spamtest_host **hosts = NULL;
+	int is_process = config_getswitch (IMAPOPT_SPAMTEST_SPAMD_PROCESS);
+	char *msg_buf;
+	char *spamd_command;
+	int ret;
+	struct spamtest_host *hr = NULL;
+
+	if (!is_spamtest_enabled) {
+		*spam_score = 0;
+		return SIEVE_OK;
+	}
+
+	if (d->spamtest_result_valid != 0) {
+		syslog (LOG_INFO, "spamtest: skipping message, already processed with value %d", d->spamtest_result);
+		*spam_score = d->spamtest_result;
+		return SIEVE_OK;
+	}
+
+	if (is_process) {
+		spamd_command = "PROCESS";
+	} else {
+		spamd_command = "CHECK";
+	}
+
+	/* Assume message isn't spam if it is larger than max_size */
+	if (m->size > max_size) {
+		syslog (LOG_INFO, "spamtest: skipping message bigger than %d", max_size);
+		return SIEVE_FAIL;
+	}
+
+	if (use_tcp) {
+		struct hostent *host;
+
+		hosts = spamtest_parse_hosts(config_getstring(IMAPOPT_SPAMTEST_SPAMD_HOSTS));
+
+		if (!hosts) {
+			syslog (LOG_ERR, "spamtest: can't parse spam_spamd_hosts");
+			return SIEVE_FAIL;
+		}
+
+one_more_tcp_try:
+		hr = spamtest_get_random_host(hosts);
+
+		if (!hr) {
+			syslog (LOG_ERR, "spamtest: can not get random spamd host");
+			spamtest_free_hosts(hosts);
+			return SIEVE_FAIL;
+		}
+
+		memset (&addr_in, 0, sizeof(addr_in));
+		addr_in.sin_family = AF_INET;
+		addr_in.sin_port = htons(hr->port ? hr->port : 783);
+
+		if ((host = gethostbyname (hr->hostname)) == NULL) {
+			syslog (LOG_ERR, "spamtest: gethostbyname failed");
+			spamtest_free_hosts(hosts);
+			return SIEVE_FAIL;
+		}
+
+		memcpy (&addr_in.sin_addr, host->h_addr, sizeof (addr_in.sin_addr));
+		addr = (struct sockaddr *) &addr_in;
+	} else {
+		memset(&addr_un, 0, sizeof addr_un);
+		addr_un.sun_family = AF_UNIX;
+		strncpy(addr_un.sun_path, socketpath, sizeof addr_un.sun_path - 1);
+		addr_un.sun_path[sizeof addr_un.sun_path - 1] = '\0';
+		addr = (struct sockaddr *) &addr_un;
+	}
+
+	if ((s = socket (use_tcp ? PF_INET : PF_UNIX, SOCK_STREAM, 0)) < 0) {
+		syslog (LOG_ERR, "spamtest: socket failed");
+		if (use_tcp)
+			spamtest_free_hosts(hosts);
+		return SIEVE_FAIL;
+	}
+
+	if (connect (s, addr, use_tcp ? sizeof (struct sockaddr_in) : sizeof(struct sockaddr_un)) < 0) {
+		syslog (LOG_ERR, "spamtest: connect failed");
+		close (s);
+		if (use_tcp) {
+			spamtest_delete_host(hosts, hr);
+			goto one_more_tcp_try;
+		} else {
+			return SIEVE_FAIL;
+		}
+	}
+
+	if ((msg_buf = malloc (m->size)) == NULL) { /* This is BAD, but I assume that max_size is set to something sane */
+		syslog (LOG_ERR, "spamtest: malloc(%d) failed", m->size);
+		close (s);
+		return SIEVE_FAIL;
+	}
+
+	rewind (m->f);
+
+	if (fread (msg_buf, 1, m->size, m->f) != m->size || ferror (m->f)) {
+		syslog (LOG_ERR, "spamtest: read message failed");
+		free (msg_buf);
+		close (s);
+		return SIEVE_FAIL;
+	}
+
+	if (d->username) {
+		if (strchr(d->username, '@') == NULL &&
+		    config_getswitch(IMAPOPT_SPAMTEST_APPEND_DEFAULT_DOMAIN) &&
+		    default_domain && default_domain[0])
+			snprintf (header, sizeof (header),
+				"%s SPAMC/1.2\r\nUser: %s@%s\r\nContent-length: %d\r\n\r\n",
+				spamd_command, d->username, default_domain, m->size);
+		else 
+			snprintf (header, sizeof (header),
+				"%s SPAMC/1.2\r\nUser: %s\r\nContent-length: %d\r\n\r\n",
+				spamd_command, d->username, m->size);
+	} else {
+		snprintf (header, sizeof (header),
+			"%s SPAMC/1.2\r\nContent-length: %d\r\n\r\n", spamd_command, m->size);
+	}
+
+	if (! spamtest_full_write (s, header, strlen (header))) {
+		syslog (LOG_ERR, "spamtest: write header failed");
+		free (msg_buf);
+		close (s);
+		return SIEVE_FAIL;
+	}
+
+	if (! spamtest_full_write (s, msg_buf, m->size)) {
+		syslog (LOG_ERR, "spamtest: write message failed");
+		free (msg_buf);
+		close (s);
+		return SIEVE_FAIL;
+	}
+
+	shutdown (s, SHUT_WR);
+
+	ret = spamtest_process_response (s, spam_score, is_process ? m : NULL);
+
+	if (*spam_score != 0) {
+	    d->spamtest_result = *spam_score;
+	    d->spamtest_result_valid = 1;
+	}
+
+	shutdown (s, SHUT_RD);
+	free (msg_buf);
+	close (s);
+
+	return ret;
+}
+
 static int sieve_parse_error_handler(int lineno, const char *msg, 
 				     void *ic __attribute__((unused)),
 				     void *sc)
@@ -827,6 +1444,12 @@ sieve_interp_t *setup_sieve(void)
 	fatal("sieve_register_vacation()", EC_SOFTWARE);
     }
 
+    res = sieve_register_spamtest(interp, &spamtest);
+    if (res != SIEVE_OK) {
+	syslog(LOG_ERR, "sieve_register_spamtest() returns %d\n", res);
+	fatal("sieve_register_spamtest()", EC_SOFTWARE);
+    }
+
     res = sieve_register_parse_error(interp, &sieve_parse_error_handler);
     if (res != SIEVE_OK) {
 	syslog(LOG_ERR, "sieve_register_parse_error() returns %d\n", res);
diff --git a/imap/lmtpd.c b/imap/lmtpd.c
index f70f587..7d12bc9 100644
--- a/imap/lmtpd.c
+++ b/imap/lmtpd.c
@@ -774,6 +774,9 @@ int deliver(message_data_t *msgdata, char *authuser,
     mydata.namespace = &lmtpd_namespace;
     mydata.authuser = authuser;
     mydata.authstate = authstate;
+    mydata.username = NULL;
+    mydata.spamtest_result = 0;
+    mydata.spamtest_result_valid = 0;
     
     /* loop through each recipient, attempting delivery for each */
     for (n = 0; n < nrcpts; n++) {
@@ -816,6 +819,19 @@ int deliver(message_data_t *msgdata, char *authuser,
 	    /* local mailbox */
 	    mydata.cur_rcpt = n;
 #ifdef USE_SIEVE
+	    /* Make a copy of mailbox username for spam stuff */
+	    mydata.username = (char *)user;
+	    /* Score could be different for every user as they could have different thresholds */
+	    if (config_getswitch (IMAPOPT_SPAMTEST_ENABLED) && config_getswitch (IMAPOPT_SPAMTEST_SPAMD_USERCONF)) {
+		mydata.spamtest_result = 0;
+		mydata.spamtest_result_valid = 0;
+		if (config_getswitch (IMAPOPT_SPAMTEST_SPAMD_PROCESS)) {
+		    if (!config_getswitch (IMAPOPT_SPAMTEST_ONE_RCPT)) {
+			syslog (LOG_WARNING, "Both 'spamtest_spamd_process' and 'spamtest_spamd_userconf' are enabled.");
+			syslog (LOG_WARNING, "Please read 'README.spamtest'.");
+		    }
+		}
+	    }
 	    r = run_sieve(user, domain, mailbox, sieve_interp, &mydata);
 	    /* if there was no sieve script, or an error during execution,
 	       r is non-zero and we'll do normal delivery */
diff --git a/imap/lmtpd.h b/imap/lmtpd.h
index 7561a5a..06a697b 100644
--- a/imap/lmtpd.h
+++ b/imap/lmtpd.h
@@ -68,6 +68,14 @@ typedef struct deliver_data {
 
     char *authuser;		/* user who submitted message */
     struct auth_state *authstate;
+
+    /* spam stuff */
+
+    /* This is in script_data, but the spam callback can't get to it */
+    /* so we put a copy here */
+    char *username;           /* Username of mailbox */
+    int spamtest_result_valid;    /* != 0 if spamtest result is valid */
+    int spamtest_result;
 } deliver_data_t;
 
 /* forward declarations */
diff --git a/lib/imapoptions b/lib/imapoptions
index 9cd02d1..36a8279 100644
--- a/lib/imapoptions
+++ b/lib/imapoptions
@@ -1125,7 +1125,7 @@ product version in the capabilities */
    user's scripts reside on a remote server (in a Murder).
    Otherwise, timsieved will proxy traffic to the remote server. */
 
-{ "sieve_extensions", "fileinto reject vacation imapflags notify envelope relational regex subaddress copy", BITFIELD("fileinto", "reject", "vacation", "imapflags", "notify", "include", "envelope", "body", "relational", "regex", "subaddress", "copy") }
+{ "sieve_extensions", "fileinto reject vacation imapflags notify envelope relational regex subaddress copy spamtest", BITFIELD("fileinto", "reject", "vacation", "imapflags", "notify", "include", "envelope", "body", "relational", "regex", "subaddress", "copy", "spamtest") }
 /* Space-separated list of Sieve extensions allowed to be used in
    sieve scripts, enforced at submission by timsieved(8).  Any
    previously installed script will be unaffected by this option and
@@ -1206,6 +1206,46 @@ product version in the capabilities */
 { "sql_usessl", 0, SWITCH }
 /* If enabled, a secure connection will be made to the SQL server. */
 
+{ "spamtest_enabled", 0, SWITCH }
+/* Enables use of 'spamtest' sieve extension. See 'spamtest_*' options
+   for details. */
+
+{ "spamtest_spamd_conntype", "tcp", STRINGLIST("tcp", "unix")}
+/* Type of socket to connect to spamd. If "unix" is selected, then
+   "spamtest_spamd_socket" will be used, overvise "spamtest_spamd_hosts" will be used. */
+
+{ "spamtest_max_size", 256000, INT }
+/* Maximum size of message could be checked be spamd. */
+
+{ "spamtest_spamd_hosts", "127.0.0.1", STRING }
+/* Space-separated list of address[:port] pairs of running spamd processes.
+   If multiple pairs are specified, they will be used in 'pseudo-round-robin'
+   order. If port is not specified, default port 783 will be used. */
+
+{ "spamtest_spamd_socket", "", STRING }
+/* Filename of UNIX socket spamd is listening on. */
+
+{ "spamtest_spamd_process", 0, SWITCH }
+/* If enabled, then content of the message will be replaced by what is
+   received from spamd. If disabled, then message will remain as it is. */
+
+{ "spamtest_spamd_userconf", 0, SWITCH }
+/* Are users have possibility to set spamd parameters.
+   If enabled, then spamtest sieve test will be executed for every recipient
+   separately, as different users could have different thresholds, and spamtest
+   result value may differ for different users.
+   If disabled, then only one spamtest check will be done for each message. */
+
+{ "spamtest_one_rcpt", 0, SWITCH }
+/* Enabling this indicates, that you have read the documentation about issues
+   that could raise if both 'spamtest_spamd_userconf' and 'spamtest_spamd_process'
+   are enabled, and disables annoing warnings about these issues. */
+
+{ "spamtest_append_default_domain", 0, SWITCH }
+/* Whether to append default domain to domain-less username which checking message for spam.
+   Set this to comply with a way which spamassassin user-preferences is stored in your
+   installation. */
+
 { "srvtab", "", STRING }
 /* The pathname of \fIsrvtab\fR file containing the server's private
    key.  This option is passed to the SASL library and overrides its
diff --git a/sieve/bc_emit.c b/sieve/bc_emit.c
index 026c8d6..9a40b11 100644
--- a/sieve/bc_emit.c
+++ b/sieve/bc_emit.c
@@ -201,6 +201,7 @@ static int bc_test_emit(int fd, int *codep, bytecode_info_t *bc)
     
     
     int ret; /* Temporary Return Value Variable */
+    int len;
     
     /* Output this opcode */
     if(write_int(fd, bc->data[(*codep)].op) == -1)
@@ -213,6 +214,48 @@ static int bc_test_emit(int fd, int *codep, bytecode_info_t *bc)
 	/* No parameter opcodes */
 	break;
 	
+    case BC_SPAMTEST:
+    {
+	int ret;
+	/* Drop match type */
+	if(write_int(fd, bc->data[(*codep)].value) == -1)
+	    return -1;
+	wrote += sizeof(int);
+	(*codep)++;
+	/*drop comparator */
+	if(write_int(fd, bc->data[(*codep)].value) == -1)
+	    return -1;
+	wrote += sizeof(int);
+	(*codep)++;
+	/*now drop relation*/
+	if(write_int(fd, bc->data[(*codep)].value) == -1)
+	    return -1;
+	wrote += sizeof(int);
+	(*codep)++;
+	/* Now drop data */
+#if 1
+	/*just a string*/
+	len = bc->data[(*codep)++].len;
+	if(write_int(fd,len) == -1)
+	    return -1;
+	wrote += sizeof(int);
+	    
+	if(write(fd,bc->data[(*codep)++].str,len) == -1)
+	    return -1;
+	    
+	ret = align_string(fd, len);
+	if(ret == -1)
+	    return -1;
+
+	wrote += len + ret;
+#else
+	ret = bc_stringlist_emit(fd, codep, bc);
+	if(ret < 0) return -1;
+	wrote+=ret;
+#endif
+	break;
+    }
+
     case BC_NOT:
     {
 	/* Single parameter: another test */
diff --git a/sieve/bc_eval.c b/sieve/bc_eval.c
index e5de4ae..b6ba111 100644
--- a/sieve/bc_eval.c
+++ b/sieve/bc_eval.c
@@ -902,9 +902,38 @@ static int eval_bc_test(sieve_interp_t *interp, void* m,
 	
 	break;
     }
+    case BC_SPAMTEST:
+    {
+	int spam_score = 0;
+	int len;
+	const char *data;
+	char spamtest_result[10];
+
+	int match = ntohl(bc[i+1].value);
+	int relation = ntohl(bc[i+2].value);
+	int comparator = ntohl(bc[i+3].value);
+
+	i = unwrap_string(bc, i+4, &data, &len);
+
+	comp = lookup_comp(comparator, match, relation, &comprock);
+
+	if(!comp) {
+	    res = SIEVE_RUN_ERROR;
+	    break;
+	}
+
+        if (interp->spamtest == NULL || interp->spamtest(m, &spam_score) != SIEVE_OK)
+            break;
+
+	snprintf(spamtest_result, 10, "%d", spam_score);
+	res |= comp(spamtest_result, strlen(spamtest_result), data, comprock);
+	
+        break;
+    }
+
     default:
 #if VERBOSE
-	printf("WERT, can't evaluate if statement. %d is not a valid command",
+	printf("WERT, can't evaluate if statement. %d is not a valid command\n",
 	       op);
 #endif     
 	return SIEVE_RUN_ERROR;
diff --git a/sieve/bc_generate.c b/sieve/bc_generate.c
index ecffde5..2ee3d8e 100644
--- a/sieve/bc_generate.c
+++ b/sieve/bc_generate.c
@@ -417,6 +417,20 @@ static int bc_test_generate(int codep, bytecode_info_t *retval, test_t *t)
 	if (codep == -1) return -1;
      
 	break;
+    case SPAMTEST:
+	if(!atleast(retval,codep + 1)) return -1;
+	retval->data[codep++].op = BC_SPAMTEST;
+	codep = bc_comparator_generate(codep, retval,t->u.ae.comptag,
+				       t->u.ae.relation, 
+				       t->u.ae.comparator);
+	if (codep == -1) return -1;
+
+	if(!atleast(retval,codep+1)) return -1;
+
+	retval->data[codep++].len = strlen(t->u.sp.s);
+	retval->data[codep++].str = t->u.sp.s;
+
+	break;
     default:
 	return -1;
       
diff --git a/sieve/bytecode.h b/sieve/bytecode.h
index 5b5e262..2dec8d5 100644
--- a/sieve/bytecode.h
+++ b/sieve/bytecode.h
@@ -149,7 +149,8 @@ enum bytecode_comps {
     BC_ADDRESS,
     BC_ENVELOPE,	/* require envelope */
     BC_HEADER,
-    BC_BODY		/* require body */
+    BC_BODY,		/* require body */
+    BC_SPAMTEST
 };
 
 /* currently one enum so as to help determine where values are being misused.
diff --git a/sieve/interp.c b/sieve/interp.c
index 1435640..79870e3 100644
--- a/sieve/interp.c
+++ b/sieve/interp.c
@@ -147,6 +147,8 @@ const char *sieve_listextensions(sieve_interp_t *i)
 	    strlcat(extensions, " subaddress", EXT_LEN);
 	if (config_sieve_extensions & IMAP_ENUM_SIEVE_EXTENSIONS_COPY)
 	    strlcat(extensions, " copy", EXT_LEN);
+	if (config_sieve_extensions & IMAP_ENUM_SIEVE_EXTENSIONS_SPAMTEST)
+	    strlcat(extensions, " spamtest", EXT_LEN);
     }
 
     return extensions;
@@ -213,6 +215,13 @@ int sieve_register_notify(sieve_interp_t *interp, sieve_callback *f)
     return SIEVE_OK;
 }
 
+int sieve_register_spamtest(sieve_interp_t *interp, sieve_spamtest *f)
+{
+    interp->spamtest = f;
+ 
+    return SIEVE_OK;
+}
+
 /* add the callbacks for messages. again, undefined if used after
    sieve_script_parse */
 int sieve_register_size(sieve_interp_t *interp, sieve_get_size *f)
diff --git a/sieve/interp.h b/sieve/interp.h
index 7501ea8..c9d374e 100644
--- a/sieve/interp.h
+++ b/sieve/interp.h
@@ -53,6 +53,7 @@ struct sieve_interp {
     sieve_callback *redirect, *discard, *reject, *fileinto, *keep;
     sieve_callback *notify;
     sieve_vacation_t *vacation;
+    sieve_spamtest *spamtest;
 
     sieve_get_size *getsize;
     sieve_get_header *getheader;
diff --git a/sieve/script.c b/sieve/script.c
index 5e15358..8906226 100644
--- a/sieve/script.c
+++ b/sieve/script.c
@@ -167,6 +167,13 @@ int script_require(sieve_script_t *s, char *req)
 	       (config_sieve_extensions & IMAP_ENUM_SIEVE_EXTENSIONS_COPY)) {
 	s->support.copy = 1;
 	return 1;
+    } else if (!strcmp("spamtest", req)) {
+	if (s->interp.spamtest) {
+	    s->support.spamtest = 1;
+	    return 1;
+	} else {
+	    return 0;
+	}
     }
     return 0;
 }
diff --git a/sieve/script.h b/sieve/script.h
index 014dac5..9077ede 100644
--- a/sieve/script.h
+++ b/sieve/script.h
@@ -66,6 +66,7 @@ struct sieve_script {
 	int notify         : 1;
 	int regex          : 1;
 	int subaddress     : 1;
+	int spamtest       : 1;
 	int relational     : 1;
 	int i_ascii_numeric: 1;
 	int include        : 1;
diff --git a/sieve/sieve-lex.l b/sieve/sieve-lex.l
index 7919953..563578b 100644
--- a/sieve/sieve-lex.l
+++ b/sieve/sieve-lex.l
@@ -118,6 +118,7 @@ CRLF		(\r\n|\r|\n)
 <INITIAL>body		return BODY;
 <INITIAL>not		return NOT;
 <INITIAL>size		return SIZE;
+<INITIAL>spamtest	return SPAMTEST;
 <INITIAL>reject		return REJCT;
 <INITIAL>fileinto	return FILEINTO;
 <INITIAL>redirect	return REDIRECT;
diff --git a/sieve/sieve.y b/sieve/sieve.y
index 4aa3051..1284d33 100644
--- a/sieve/sieve.y
+++ b/sieve/sieve.y
@@ -85,6 +85,12 @@ struct htags {
     int relation;
 };
 
+struct spamtags {
+    char *comparator;
+    int comptag;
+    int relation;
+};
+
 struct aetags {
     int addrtag;
     char *comparator;
@@ -124,6 +130,7 @@ static test_t *build_address(int t, struct aetags *ae,
 static test_t *build_header(int t, struct htags *h,
 			    stringlist_t *sl, stringlist_t *pl);
 static test_t *build_body(int t, struct btags *b, stringlist_t *pl);
+static test_t *build_spamtest(int t, struct spamtags *s, char *value);
 static commandlist_t *build_vacation(int t, struct vtags *h, char *s);
 static commandlist_t *build_notify(int t, struct ntags *n);
 static commandlist_t *build_denotify(int t, struct dtags *n);
@@ -147,6 +154,9 @@ static void free_ntags(struct ntags *n);
 static struct dtags *new_dtags(void);
 static struct dtags *canon_dtags(struct dtags *d);
 static void free_dtags(struct dtags *d);
+static struct spamtags *new_spamtags(void);
+static struct spamtags *canon_spamtags(struct spamtags *s);
+static void free_spamtags(struct spamtags *s);
 
 static int verify_stringlist(stringlist_t *sl, int (*verify)(char *));
 static int verify_mailbox(char *s);
@@ -186,6 +196,7 @@ extern void yyrestart(FILE *f);
     struct btags *btag;
     struct ntags *ntag;
     struct dtags *dtag;
+    struct spamtags *spamtag;
 }
 
 %token <nval> NUMBER
@@ -195,6 +206,7 @@ extern void yyrestart(FILE *f);
 %token SETFLAG ADDFLAG REMOVEFLAG MARK UNMARK
 %token NOTIFY DENOTIFY
 %token ANYOF ALLOF EXISTS SFALSE STRUE HEADER NOT SIZE ADDRESS ENVELOPE BODY
+%token SPAMTEST
 %token COMPARATOR IS CONTAINS MATCHES REGEX COUNT VALUE OVER UNDER
 %token GT GE LT LE EQ NE
 %token ALL LOCALPART DOMAIN USER DETAIL
@@ -210,6 +222,7 @@ extern void yyrestart(FILE *f);
 %type <nval> comptag relcomp sizetag addrparttag addrorenv location copy
 %type <testl> testlist tests
 %type <htag> htags
+%type <spamtag> spamtags
 %type <aetag> aetags
 %type <btag> btags
 %type <vtag> vtags
@@ -543,6 +556,13 @@ test:     ANYOF testlist	 { $$ = new_test(ANYOF); $$->u.tl = $2; }
 	| NOT test		 { $$ = new_test(NOT); $$->u.t = $2; }
 	| SIZE sizetag NUMBER    { $$ = new_test(SIZE); $$->u.sz.t = $2;
 		                   $$->u.sz.n = $3; }
+
+	| SPAMTEST spamtags STRING	 { if (!parse_script->support.spamtest) {
+				     yyerror("spamtest not required");
+				     YYERROR;
+				   }
+				     $2 = canon_spamtags($2);
+				   $$ = build_spamtest(SPAMTEST, $2, $3); }
 	| error			 { $$ = NULL; }
 	;
 
@@ -646,6 +666,24 @@ btags: /* empty */		 { $$ = new_btags(); }
 				     $$->comparator = $3; } }
         ;
 
+spamtags: /* empty */		 { $$ = new_spamtags(); }
+	| spamtags relcomp STRING COMPARATOR STRING { $$ = $1;
+				   if ($$->comptag != -1) { 
+			yyerror("duplicate comparator type tag"); YYERROR; }
+				   else { $$->comptag = $2;
+				   $$->relation = verify_relat($3);
+				   if ($$->relation==-1) 
+				     {YYERROR; /*vr called yyerror()*/ }
+				   }
+				   if ($$->comparator != NULL) { 
+			 yyerror("duplicate comparator tag"); YYERROR; }
+				   else if (!strcmp($5, "i;ascii-numeric") &&
+					    !parse_script->support.i_ascii_numeric) { 
+			 yyerror("comparator-i;ascii-numeric not required");  YYERROR; }
+				   else { 
+				     $$->comparator = $5; } }
+        ;
+
 
 addrparttag: ALL                 { $$ = ALL; }
 	| LOCALPART		 { $$ = LOCALPART; }
@@ -842,6 +880,22 @@ static commandlist_t *build_vacation(int t, struct vtags *v, char *reason)
     return ret;
 }
 
+static test_t *build_spamtest(int t, struct spamtags *s, char *value)
+{
+    test_t *ret = new_test(t);
+
+    assert(t == SPAMTEST);
+
+    if (ret) {
+	ret->u.sp.comptag = s->comptag;
+	ret->u.sp.relation = s->relation;
+	ret->u.sp.comparator = strdup(s->comparator);
+	free_spamtags(s);
+	ret->u.sp.s = strdup(value);
+    }
+    return ret;
+}
+
 static commandlist_t *build_notify(int t, struct ntags *n)
 {
     commandlist_t *ret = new_command(t);
@@ -944,6 +998,7 @@ static struct htags *new_htags(void)
     return r;
 }
 
+
 static struct htags *canon_htags(struct htags *h)
 {
     if (h->comparator == NULL) {
@@ -993,6 +1048,32 @@ static void free_btags(struct btags *b)
     free(b);
 }
 
+static struct spamtags *new_spamtags(void)
+{
+    struct spamtags *r = (struct spamtags *) xmalloc(sizeof(struct spamtags));
+
+    r->comptag = r->relation= -1;
+    
+    r->comparator = NULL;
+
+    return r;
+}
+
+static struct spamtags *canon_spamtags(struct spamtags *s)
+{
+    if (s->comparator == NULL) {
+	s->comparator = xstrdup("i;ascii-casemap");
+    }
+    if (s->comptag == -1) { s->comptag = IS; }
+    return s;
+}
+
+static void free_spamtags(struct spamtags *s)
+{
+    free(s->comparator);
+    free(s);
+}
+
 static struct vtags *new_vtags(void)
 {
     struct vtags *r = (struct vtags *) xmalloc(sizeof(struct vtags));
diff --git a/sieve/sieve_interface.h b/sieve/sieve_interface.h
index c5b4f07..2265393 100644
--- a/sieve/sieve_interface.h
+++ b/sieve/sieve_interface.h
@@ -72,6 +72,7 @@ typedef int sieve_get_envelope(void *message_context,
 			       const char ***contents);
 typedef int sieve_get_include(void *script_context, const char *script,
 			      int isglobal, char *fpath, size_t size);
+typedef int sieve_spamtest(void *message_context, int *spam_score);
 
 /* MUST keep this struct sync'd with bodypart in imap/message.h */
 typedef struct sieve_bodypart {
@@ -154,6 +155,7 @@ int sieve_register_vacation(sieve_interp_t *interp, sieve_vacation_t *v);
 int sieve_register_imapflags(sieve_interp_t *interp, sieve_imapflags_t *mark);
 int sieve_register_notify(sieve_interp_t *interp, sieve_callback *f);
 int sieve_register_include(sieve_interp_t *interp, sieve_get_include *f);
+int sieve_register_spamtest(sieve_interp_t *interp, sieve_spamtest *f);
 
 /* add the callbacks for messages. again, undefined if used after
    sieve_script_parse */
diff --git a/sieve/sievec.c b/sieve/sievec.c
index 69ae43d..6a63a1e 100644
--- a/sieve/sievec.c
+++ b/sieve/sievec.c
@@ -273,6 +273,12 @@ int is_script_parsable(FILE *stream, char **errstr, sieve_script_t **ret)
 	goto done;
     }
 
+    res = sieve_register_spamtest(i, (sieve_spamtest *) &foo);
+    if (res != SIEVE_OK) {
+	syslog(LOG_ERR, "sieve_register_spamtest() returns %d\n", res);
+	return TIMSIEVE_FAIL;
+    }
+
     res = sieve_register_parse_error(i, &mysieve_error);
     if (res != SIEVE_OK) {
 	syslog(LOG_ERR, "sieve_register_parse_error() returns %d\n", res);
diff --git a/sieve/sieved.c b/sieve/sieved.c
index 204a453..5dd1f38 100644
--- a/sieve/sieved.c
+++ b/sieve/sieved.c
@@ -329,6 +329,16 @@ static int dump2_test(bytecode_input_t * d, int i)
 	i=write_list(ntohl(d[i].len), i+1, d);
 	printf("             ]\n");
 	break;
+    case BC_SPAMTEST:
+    {
+    	const char *data;
+	int len;
+	printf("Spamtest [");
+	i = printComparison(d, i+1);
+	i = unwrap_string(d, i, &data, &len);
+	printf("{%d}%s]\n", len, data);
+	break;
+    }
     default:
 	printf("WERT %d ", ntohl(d[i].value));
     }   
diff --git a/sieve/tree.h b/sieve/tree.h
index 2de327c..da6fe89 100644
--- a/sieve/tree.h
+++ b/sieve/tree.h
@@ -109,6 +109,13 @@ struct Test {
 	    int t; /* tag */
 	    int n; /* param */
 	} sz;
+	struct { /* it's a spam test */
+	    int comptag;
+	    char * comparator;
+	    int relation;
+	    void *comprock;
+	    char *s;
+	} sp;
     } u;
 };
 
diff --git a/timsieved/scripttest.c b/timsieved/scripttest.c
index 5c25b86..459bae9 100644
--- a/timsieved/scripttest.c
+++ b/timsieved/scripttest.c
@@ -180,6 +180,12 @@ int build_sieve_interp(void)
 	return TIMSIEVE_FAIL;
     }
 
+    res = sieve_register_spamtest(interp, (sieve_spamtest *) &foo);
+    if (res != SIEVE_OK) {
+	syslog (LOG_ERR, "sieve_register_spamtest() returns %d\n", res);
+	return TIMSIEVE_FAIL;
+    }
+
     res = sieve_register_parse_error(interp, &mysieve_error);
     if (res != SIEVE_OK) {
 	syslog(LOG_ERR, "sieve_register_parse_error() returns %d\n", res);
-- 
1.7.1

Reply via email to