diff -Nru netqmail-1.06_errmsg/Makefile netqmail-1.06_errmsg_spamrefuse/Makefile --- netqmail-1.06_errmsg/Makefile 2009-04-28 16:26:19.000000000 +0100 +++ netqmail-1.06_errmsg_spamrefuse/Makefile 2009-08-23 22:01:17.000000000 +0100 @@ -1425,9 +1425,11 @@ qmail-queue: \ load qmail-queue.o triggerpull.o fmtqfn.o now.o date822fmt.o \ datetime.a seek.a ndelay.a open.a sig.a alloc.a substdio.a error.a \ +getln.a stralloc.a env.a \ str.a fs.a auto_qmail.o auto_split.o auto_uids.o ./load qmail-queue triggerpull.o fmtqfn.o now.o \ date822fmt.o datetime.a seek.a ndelay.a open.a sig.a \ + getln.a stralloc.a env.a \ alloc.a substdio.a error.a str.a fs.a auto_qmail.o \ auto_split.o auto_uids.o @@ -1437,6 +1439,7 @@ qmail-queue.o: \ compile qmail-queue.c readwrite.h sig.h exit.h open.h seek.h fmt.h \ +getln.h stralloc.h env.h \ alloc.h substdio.h datetime.h now.h datetime.h triggerpull.h extra.h \ auto_qmail.h auto_uids.h date822fmt.h fmtqfn.h ./compile qmail-queue.c diff -Nru netqmail-1.06_errmsg/qmail.c netqmail-1.06_errmsg_spamrefuse/qmail.c --- netqmail-1.06_errmsg/qmail.c 2007-11-30 20:22:54.000000000 +0000 +++ netqmail-1.06_errmsg_spamrefuse/qmail.c 2009-08-25 12:16:15.000000000 +0100 @@ -108,6 +108,7 @@ case 115: /* compatibility */ case 11: return "Denvelope address too long for qq (#5.1.3)"; case 31: return "Dmail server permanently rejected message (#5.3.0)"; + case 32: return "DMessage rejected, appears to be spam (#5.3.0)"; /* 5.7.1 maybe better but qmail-smtpd starts error message with 554 which isn't specified for 5.7.1 */ case 51: return "Zqq out of memory (#4.3.0)"; case 52: return "Zqq timeout (#4.3.0)"; case 53: return "Zqq write error or disk full (#4.3.0)"; diff -Nru netqmail-1.06_errmsg/qmail-control.9 netqmail-1.06_errmsg_spamrefuse/qmail-control.9 --- netqmail-1.06_errmsg/qmail-control.9 1998-06-15 11:53:16.000000000 +0100 +++ netqmail-1.06_errmsg_spamrefuse/qmail-control.9 2009-08-28 19:42:19.000000000 +0100 @@ -22,6 +22,7 @@ in .IR badmailfrom , .IR locals , +.IR nospamrefuse , .IR percenthack , .IR qmqpservers , .IR rcpthosts , @@ -56,6 +57,7 @@ .I localiphost \fIme \fRqmail-smtpd .I locals \fIme \fRqmail-send .I morercpthosts \fR(none) \fRqmail-smtpd +.I nospamrefuse \fRpostmaster \fRqmail-queue .I percenthack \fR(none) \fRqmail-send .I plusdomain \fIme \fRqmail-inject .I qmqpservers \fR(none) \fRqmail-qmqpc diff -Nru netqmail-1.06_errmsg/qmail-queue.8 netqmail-1.06_errmsg_spamrefuse/qmail-queue.8 --- netqmail-1.06_errmsg/qmail-queue.8 2007-11-30 20:22:54.000000000 +0000 +++ netqmail-1.06_errmsg_spamrefuse/qmail-queue.8 2009-08-28 19:32:48.000000000 +0100 @@ -58,6 +58,31 @@ subdirectory must be in the same filesystem as the .B intd directory. + +.SH "ENVIRONMENT VARIABLES" +.TP 5 +.I SPAMREFUSE +Maximum acceptable score from Spam Assassin (assumes Spam Assassin +has processed the message prior to reception by +.BR qmail-queue , +typically via Spam Assassin's +.BR qmail-spamc ). +Messages are rejected if the (first, if more than one) +.I X-Spam-Status: +header shows a score higher than +.IR SPAMREFUSE . +If no +.I X-Spam-Status: +header is found the message is accepted. Up to 2 decimal places are +significant in the +.I SPAMREFUSE +value. The +.B nospamrefuse +control file disables +.I SPAMREFUSE +for particular addresses, see the +.B qmail-smtpd +man page. .SH "EXIT CODES" .B qmail-queue does not print diagnostics. @@ -80,6 +105,12 @@ (Not used by .BR qmail-queue , but can be used by programs offering the same interface.) +.TP +.B 32 +Message rejected since it appears to be spam (the +.I X-Spam-Status: +score exceeded +.IR SPAMREFUSE ). .PP All other .B qmail-queue diff -Nru netqmail-1.06_errmsg/qmail-queue.c netqmail-1.06_errmsg_spamrefuse/qmail-queue.c --- netqmail-1.06_errmsg/qmail-queue.c 1998-06-15 11:53:16.000000000 +0100 +++ netqmail-1.06_errmsg_spamrefuse/qmail-queue.c 2009-08-27 21:52:19.000000000 +0100 @@ -7,6 +7,8 @@ #include "seek.h" #include "fmt.h" #include "alloc.h" +#include "str.h" +#include "stralloc.h" #include "substdio.h" #include "datetime.h" #include "now.h" @@ -55,6 +57,7 @@ } void die(e) int e; { _exit(e); } +void die_spam() { cleanup(); die(32); } void die_write() { cleanup(); die(53); } void die_read() { cleanup(); die(54); } void sigalrm() { /* thou shalt not clean up here */ die(52); } @@ -149,12 +152,131 @@ die(63); } +/* Convert the number with up to 2 decimal places in + string s to that number multiplied by 100 as an integer. + Any additional decimals are discarded. The number may be + negative. If no number is found, 0 is returned. + + Enhancement: It will be better to return the number*100 + separately to the result (success or fail) of this + subroutine so the caller can use a sensible fallback + value for number*100 instead of 0. + */ +int getnum100(s) +char *s; +{ + int ret = 0; + int neg = 0; + + /* Find the number or EOS */ + while ((*s != '\0') && (*s != '-') && (*s != '.') && ((*s < '0') || (*s > '9'))) + s++; + if (*s == '-') { neg=1; s++; } + while ((*s >= '0') && (*s <= '9')) + { + ret = ret*10 + (int)(*s)-(int)'0'; + s++; + } + ret *= 100; + if (*s == '.') + { /* Get up to 2 digits after decimal point */ + s++; + if ((*s >= '0') && (*s <= '9')) + { + ret += ((int)(*s) - (int)'0') * 10; + s++; + if ((*s >= '0') && (*s <= '9')) + ret += (int)(*s) - (int)'0'; + } + } + if (neg) return (-ret); + else return ( ret); +} + +/* This function examines the X-Spam-Status header in the + message with a view to causing qmail-queue to reject the + message if the spam score is over a certain limit. + It is assumed that the message has already been processed + by SpamAssassin and its X-Spam-Status header has been + added (such as by setting QMAILQUEUE to qmail-spamc). + Additionally it is assumed that the first number on the + X-Spam-Status: header line is the score - and that this + occurs on the first line of this header. + + If a limit has been set to check against the score + value in the X-Spam-Status line, extract this score and + compare it against the supplied limit. Since the + score may have a fractional (decimal) component but I + don't wish to introduce floating point operations, both + the score and limit are multiplied by 100 before + comparison to enable comparison to 2 decimal places. Also + note that both the score and limit may be negative. + + Inspiration for this patch comes from Erwin Hoffmann's + QHPSI patch; I've chosen to implement this functionality + internally to qmail-queue since relatively little code + is required to examine the already-generated SpamAssassin + headers, I think having an external program to do this + would be overkill. + + Some email addresses may be excluded from spamrefusecheck() + (like postmaster); these exceptions are dealt with in + qmail-smtpd.c rather than here since constmap is used to + manage the list of exceptions, and this is already used + elsewhere in qmail-smtpd.c - exceptions are dealt with by + unsetting the environment variable that would otherwise + cause spamrefusecheck() to be invoked. + Andrew Richards, 23rd August 2009. + */ +void spamrefusecheck(limit_s) +char *limit_s; +{ + int mfd; + struct stat st; + static stralloc mline={0}; + int match; + substdio mss; + char buf[128]; + int score100, limit100; + int n; + + if ((!limit_s) || (*limit_s == '\0')) return; /* Check disabled */ + limit100 = getnum100(limit_s); + + if (stat(messfn,&st) == -1) die_read(); + mfd = open_read(messfn); + substdio_fdbuf(&mss,read,mfd,buf,sizeof(buf)); + do + { + if (getln(&mss,&mline,&match,'\n') != 0) die_read(); + if (mline.len <= 1) return; /* End of headers, give up */ + if (str_start(mline.s,"X-Spam-Status: ")) + { + stralloc_0(&mline); + n = 15; /* 15 = str_len("X-Spam-Status: ") */ + while (n+6 < mline.len) /* 6 = str_len("score=") */ + { + if (str_start(mline.s+n,"score=")) + { + score100 = getnum100(mline.s+n+6); + break; + } + n++; + } + if (score100 && (score100 >= limit100)) die_spam(); + else return; /* message can be accepted */ + } + } + while (match); +} + char tmp[FMT_ULONG]; void main() { unsigned int len; char ch; + char *spamrefuse; sig_blocknone(); umask(033); @@ -165,6 +287,8 @@ uid = getuid(); starttime = now(); datetime_tai(&dt,starttime); + spamrefuse = env_get("SPAMREFUSE"); + if ((spamrefuse) && (*spamrefuse == '\0')) spamrefuse = (char *)0; /* Disable spamrefuse check if SPAMREFUSE="" */ received_setup(); @@ -244,6 +368,9 @@ if (len >= ADDR) die(11); } + /* Reject if X-Spam-Level header shows a score > SPAMREFUSE */ + if (spamrefuse) spamrefusecheck(spamrefuse); + if (substdio_flush(&ssout) == -1) die_write(); if (fsync(intdfd) == -1) die_write(); diff -Nru netqmail-1.06_errmsg/qmail-smtpd.8 netqmail-1.06_errmsg_spamrefuse/qmail-smtpd.8 --- netqmail-1.06_errmsg/qmail-smtpd.8 1998-06-15 11:53:16.000000000 +0100 +++ netqmail-1.06_errmsg_spamrefuse/qmail-smtpd.8 2009-08-28 18:02:56.000000000 +0100 @@ -120,6 +120,31 @@ and the rest into .IR morercpthosts . .TP 5 +.I nospamrefuse +List of destinations that get spam regardless of any +.B SPAMREFUSE +setting (see +.B qmail-queue +man page). +Destinations can be specified as full addresses, every address in a given +domain by using the form +.BR @\fIhost , +or an address at all domains (the full part before the +.B @ +- fractional addresses are not supported). +Examples of each: + +.EX + someone@example.com + @example.com + someone +.EE + +Default: +.I postmaster +(at all domains). + +.TP 5 .I rcpthosts Allowed RCPT domains. If diff -Nru netqmail-1.06_errmsg/qmail-smtpd.c netqmail-1.06_errmsg_spamrefuse/qmail-smtpd.c --- netqmail-1.06_errmsg/qmail-smtpd.c 2009-04-28 16:26:19.000000000 +0100 +++ netqmail-1.06_errmsg_spamrefuse/qmail-smtpd.c 2009-08-27 22:05:51.000000000 +0100 @@ -181,6 +181,10 @@ int bmfok = 0; stralloc bmf = {0}; struct constmap mapbmf; +char *spamrefuse; +int nsrok = 0; +stralloc nsr = {0}; +struct constmap mapnsr; void setup() { @@ -202,6 +206,27 @@ if (bmfok) if (!constmap_init(&mapbmf,bmf.s,bmf.len,0)) die_nomem(); + /* Is SMTP-time spam blocking on > SPAMREFUSE desired: */ + spamrefuse = env_get("SPAMREFUSE"); + if ((spamrefuse) && (*spamrefuse == '\0')) spamrefuse = (char *)0; + if (spamrefuse) + { /* See which recipients omit spamrefuse check */ + nsrok = control_readfile(&nsr,"control/nospamrefuse",0); + if (nsrok == -1) die_control(); + if (nsrok) + { + if (!constmap_init(&mapnsr,nsr.s,nsr.len,0)) die_nomem(); + } + else /* Default to letting spam through for postmaster mailboxes only */ + { /* Note that must use str_len(...)+1 because str_len will treat trailing \0 as EOS */ + /* \0 in string (in addition to \0 for EOS) is to play nicely with constmap_init: */ + if (!constmap_init(&mapnsr,"postmaster\0",str_len("postmaster")+1,0)) die_nomem(); + nsrok = 1; + } + } + else + nsrok = 0; + if (control_readint(&databytes,"control/databytes") == -1) die_control(); x = env_get("DATABYTES"); if (x) { scan_ulong(x,&u); databytes = u; } @@ -292,6 +317,22 @@ return 0; } +int nsrcheck() /* See if recipient omits spamrefuse check */ +{ + int j; + if (!nsrok) return 0; + /* username@domain: */ + if (constmap(&mapnsr,addr.s,addr.len - 1)) return 1; + j = byte_rchr(addr.s,addr.len,'@'); + if ((j < addr.len) && (j > 0 )) + { /* @domain: */ + if (constmap(&mapnsr,addr.s + j,addr.len - j - 1)) return 1; + /* username: */ + if (constmap(&mapnsr,addr.s, j)) return 1; + } + return 0; +} + int addrallowed() { int r; @@ -303,6 +344,7 @@ int seenmail = 0; int flagbarf; /* defined if seenmail */ +int blockspam = 0; void smtp_helo(arg) char *arg; { @@ -319,6 +361,7 @@ void smtp_rset(arg) char *arg; { seenmail = 0; + blockspam = 0; enew(); eout("Session RSET\n"); eflush(); out("250 flushed\r\n"); } @@ -345,6 +388,7 @@ } else if (!addrallowed()) { err_nogateway(); return; } + if ((!blockspam) && (!nsrcheck())) { blockspam = 1; } if (!stralloc_cats(&rcptto,"T")) die_nomem(); if (!stralloc_cats(&rcptto,addr.s)) die_nomem(); if (!stralloc_0(&rcptto)) die_nomem(); @@ -471,6 +515,16 @@ seenmail = 0; if (databytes) bytestooverflow = databytes + 1; messagebytes = 0; + if (spamrefuse) + { + if (!blockspam) { env_unset("SPAMREFUSE"); } + else /* Re-enable SPAMREFUSE if we removed it for a previous message (persistent SMTP session) */ + { + if (!env_get("SPAMREFUSE")) + if (!env_put2("SPAMREFUSE",spamrefuse)) die_nomem(); + } + } + blockspam = 0; /* Reset for any subsequent message in a persistent SMTP session */ if (qmail_open(&qqt) == -1) { err_qqt(); return; } qp = qmail_qp(&qqt); out("354 go ahead\r\n");