/* Spamassassin in local_scan by Marc MERLIN */ /* The inline comments and minidocs were moved to the distribution tarball You can get the up to date version of this file and full tarball here: http://marc.merlins.org/linux/exim/sa.html The discussion list is here: http://lists.merlins.org/lists/listinfo/sa-exim */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "local_scan.h" #ifdef DLOPEN_LOCAL_SCAN /* Return the verion of the local_scan ABI, if being compiled as a .so */ int local_scan_version(void) { return LOCAL_SCAN_ABI_VERSION ; } #endif /******************************/ /* Compile time config values */ /******************************/ #ifndef SPAMASSASSIN_CONF #define SPAMASSASSIN_CONF "/etc/mail/spamassassin.conf" #endif static const char conffile[]=SPAMASSASSIN_CONF; /* How much message body you want to feed to SA. By Default SA only processes 250k If you increase this value, you'll have to change the call to spamc and give it -s size */ static const int samaxbody=250*1024; /********************/ /* Code starts here */ /********************/ static const char nospamstatus[]=""; static char *buffera[4096]; static char *buffer=(char *)buffera; static int SAEximDebug=0; static jmp_buf jmp_env; static char *where="Error handler called without error string"; static int line=-1; static char *panicerror; #define CHECKERR(mret, mwhere, mline) \ if (mret < 0) \ { \ where=mwhere; \ line=mline; \ goto errexit; \ } #define PANIC(merror) \ panicerror=merror; \ goto panicexit; static void alarm_handler(int sig) { sig = sig; /* Keep picky compilers happy */ longjmp(jmp_env, 1); } /* Comparing header lines isn't fun, especially since the comparison has to be caseless, so we offload this to this function Return 1 if the header was found, 0 otherwise */ static int compare_header(char *buffertocompare, char *referenceheader) { int idx; int same=1; for (idx=0; idx 7) { if (same) { log_write(0, LOG_MAIN, "SA: Found %s in %s", referenceheader, buffertocompare); } else if (SAEximDebug > 9) { log_write(0, LOG_MAIN, "SA: Did not find %s in %s", referenceheader, buffertocompare); } } return same; } /* Rejected mails can be archived in a spool directory */ /* filename will contain a double / before the filename, I prefer two to none */ static int savemail(int readfd, char *dir, char *dirvarname, char *filename, int SAmaxarchivebody, char *condition) { header_line *hl; FILE *readfh; int writefd; int ret; int chunk; char *expand; char *fake_env_from; if (dir == NULL) { if (SAEximDebug>4) { log_write(0, LOG_MAIN, "SA: Not saving message because %s in undefined", dirvarname); } return 0; } if (condition[0] != '1' || condition[1] != 0) { expand=expand_string(condition); if (expand == NULL) { /* Can't use PANIC within this function :( */ CHECKERR(-1, string_sprintf("savemail condition expansion failure on %s", condition), __LINE__ - 1); } if (SAEximDebug > 2) { log_write(0, LOG_MAIN, "SA: savemail condition expand returned: '%s'", expand); } if (expand[0] == 0 || (expand[0] == '0' && expand[1] == 0)) { log_write(0, LOG_MAIN, "SA: savemail condition expanded to false, not saving message to disk"); return 0; } } if (SAEximDebug) { log_write(0, LOG_MAIN, "SA: Writing message to %s/%s", dir, filename); } /* Unchecked mkdir (fails if dir has already been made or can't be made) */ mkdir (dir, 0700); /* Let's not worry about you receiving two spams at the same second * with the same message ID. If you do, the second one will overwrite * the first one */ writefd=creat(string_sprintf("%s/%s", dir, filename), S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP); CHECKERR(writefd, string_sprintf("creat %s", filename),__LINE__); /* make the file look like a valid mbox -- idea from dman */ fake_env_from=string_sprintf("From %s Wed Dec 31 23:59:59 UTC 1969\n",sender_address); ret=write(writefd, fake_env_from, strlen(fake_env_from)); CHECKERR(ret,string_sprintf("'From ' line write in %s", filename),__LINE__); /* First we need to get the header lines from exim, and then we can read the body from writefd */ hl=header_list; while (hl != NULL) { /* type '*' means the header is internal, don't print it */ if (hl->type == '*') { hl=hl->next; continue; } ret=write(writefd,hl->text,strlen(hl->text)); CHECKERR(ret,string_sprintf("header line write in %s", filename),__LINE__); hl=hl->next; } ret=write(writefd,"\n",1); CHECKERR(ret,string_sprintf("header separation write in %s", filename),__LINE__); /* Now copy the body to the save file */ /* we already read from readfd, so we need to reset it */ ret=lseek(readfd, SEEK_SET, 0); CHECKERR(ret, "lseek reset on spooled message", __LINE__); /* This sets us back a bit too far, we need to skip the first line which */ /* contains the message ID on disk */ readfh=fdopen(readfd, "r"); CHECKERR(readfh, "fdopen",__LINE__); fgets((char *)buffer, sizeof(buffera), readfh); chunk=(SAmaxarchivebody / (sizeof(buffera)-1))+1; if (SAEximDebug > 4) { log_write(0, LOG_MAIN, "SA: Body write chunk starts with %d/%d=%d", SAmaxarchivebody , sizeof(buffera), chunk); } while ((ret=fread(buffer, 1, sizeof(buffera),readfh)) > 0 && chunk-- > 0) { if (SAEximDebug > 4) { log_write(0, LOG_MAIN, "SA: Processing body chunk %d", chunk); } ret=write(writefd, buffer, ret); CHECKERR(ret,string_sprintf("body write in %s", filename),__LINE__); } ret=ferror(readfh); CHECKERR(ret, "read body of spooled message for archival", __LINE__); ret=close(writefd); CHECKERR(ret, "Closing spooled message",__LINE__); return 0; errexit: close(writefd); return -1; } /* * Headers can be multi-line (in theory all of them can I think). Parsing them * is a little more work than a simple line scan, so we're off-loading this to * a function */ int parsemlheader(char *buffer, FILE *readfh, char *headername, char **header) { header_line *hl; if (compare_header(buffer, string_sprintf("%s: ", headername))) { *header=string_copy(buffer); while ((fgets((char *)buffer,sizeof(buffera),readfh)) != NULL) { /* Remove trailing newline */ if (buffer[strlen(buffer)-1] == '\n') { buffer[strlen(buffer)-1]=0; } if (SAEximDebug > 5) { log_write(0, LOG_MAIN, "SA: spamc, while parsing header %s, read %s", headername, buffer); } if (buffer[0] != ' ' && buffer[0] != '\t') { break; } /* Slight waste of memory here, oh well... */ *header=string_sprintf("%s\n%s", *header, buffer); } if (SAEximDebug > 5) { log_write(0, LOG_MAIN, "SA: spamc pieced up %s as: '%s'", headername, *header); } /* Slight waste of memory here, oh well... */ *header=string_sprintf("%s\n", *header); /* Mark the former header as deleted if it's already present */ hl=header_list; while (hl != NULL) { /* type '*' means the header is internal or deleted */ if (hl->type == '*') { hl=hl->next; continue; } if ( compare_header((char *)hl->text, headername) ) { if (SAEximDebug > 5) { log_write(0, LOG_MAIN, "SA: removing old copy of header '%s' and replacing with new one: '%s'", (char *)hl->text, *header); } hl->type = '*'; break; } hl=hl->next; } header_add(' ', "%s", *header); return 1; } return 0; } /* Exim calls us here, feeds us a fd on the message body, and expects a return message in *return_text */ int local_scan(int fd, uschar **return_text) { int ret; int pid; int writefd[2]; int readfd[2]; FILE *readfh; char *mesgfn=NULL; char *safemesgid=NULL; int isspam=0; int gotsa=0; int rantoolong=0; int chunk; float spamvalue=0.0; char *spamstatus=NULL; time_t beforescan; time_t afterscan; time_t afterwait; time_t scantime; time_t fulltime; uschar *expand; header_line *hl; static int readconffile=0; static int wrotedebugenabled=0; /* Options we read from /etc/mail/spamassassin.conf */ #ifndef SPAMC_LOCATION #define SPAMC_LOCATION "/usr/bin/spamc" #endif static char *SAspamcpath=SPAMC_LOCATION; static char *SAEximRunCond="0"; static char *SAEximRejCond="1"; static int SAmaxarchivebody=20*1048576; static int SAerrmaxarchivebody=1024*1048576; static int SAtimeout=240; static char *SAtimeoutsave=NULL; static char *SAtimeoutSavCond="1"; static char *SAerrorsave=NULL; static char *SAerrorSavCond="1"; static int SAtemprejectonerror=0; static float SAstallsender=1<<30; static int SAstallsendertime=900; static char *SAstallsenderSavCond="1"; static char *SAstallsendersave=NULL; static int SAstallsenderoverwrite=1; static float SAdevnull=1<<30; static char *SAdevnullSavCond="1"; static char *SAdevnullsave=NULL; static float SApermreject=1<<30; static char *SApermrejectSavCond="1"; static char *SApermrejectsave=NULL; static float SAtempreject=1<<30; static char *SAtemprejectSavCond="1"; static char *SAtemprejectsave=NULL; static int SAtemprejectoverwrite=1; static char *SAspamacceptsave=NULL; static char *SAspamacceptSavCond="0"; static char *SAnotspamsave=NULL; static char *SAnotspamSavCond="0"; /* We read and use the Message-Id to save the message on disk if needed */ char *mesgid=NULL; /* We tell exim to ignore those headers and replace them with the values * we read back from spamc */ char *subject=NULL; char *contenttype=NULL; char *contenttransferencoding=NULL; /* New values we read from spamassassin */ char *xspamstatus=NULL; char *xspamflag=NULL; char *xspamlevel=NULL; /* SA 2.20 or better */ char *xspamcheckerversion=NULL; char *xspamprevcontenttype=NULL; char *xspamprevcontenttransferencoding=NULL; char *xspamreport=NULL; /* Any error can write the faulty message to mesgfn, so we need to give it *some* value right now, and improve it as we go along */ mesgfn=string_sprintf("%d", time(NULL)); /* We won't scan local messages. I think exim bypasses local_scan for a * bounce generated after a locally submitted message, but better be safe */ /* This is commented out now, because you can control it with SAEximRunCond if (!sender_host_address) { return LOCAL_SCAN_ACCEPT; } */ /* * We keep track of whether we've alrady read the config file, but since * exim spawns itself, it will get read by exim children even though you * didn't restart exim. That said, after you change the config file, you * should restart exim to make sure all the instances pick up the new * config file */ if (!readconffile) { ret=open(conffile, 0); CHECKERR(ret,"conf file open",__LINE__); readfh=fdopen(ret, "r"); CHECKERR(readfh,"fdopen",__LINE__); while ((fgets((char *)buffer,sizeof(buffera),readfh)) != NULL) { if (*buffer == '#' || *buffer == '\n' ) { continue; } if (*buffer != 'S' || *(buffer+1) != 'A') { log_write(0, LOG_MAIN, "SA: error while reading configuration file %s. Line does not begin with a SA directive: '%s', ignoring", conffile, buffer); continue; } #define M_CHECKFORVAR(VAR, TYPE) \ if (strstr(buffer, #VAR ## ": ") == buffer) \ { \ if (sscanf(buffer, #VAR ## ": " ## TYPE, &VAR)) \ { \ if (SAEximDebug > 3) \ { \ if (SAEximDebug && ! wrotedebugenabled) \ { \ log_write(0, LOG_MAIN, "SA: Debug enabled, reading config from file %s", conffile); \ wrotedebugenabled=1; \ } \ else \ { \ log_write(0, LOG_MAIN, "SA: config read "## #VAR ##" = "## TYPE, VAR); \ }\ }\ } \ else \ { \ log_write(0, LOG_MAIN, "SA: error while reading configuration file %s. Can't parse value in: '%s', ignoring", conffile, buffer); \ } \ continue; \ } #define M_CHECKFORSTR(VAR) \ if (strstr(buffer, #VAR ## ": ") == buffer) \ { \ VAR = strdup(buffer+strlen( #VAR )+2); \ if (VAR == NULL) \ { \ log_write(0, LOG_MAIN, "SA: Fatal: malloc failed, quitting..."); \ exit -1; \ } \ \ if (VAR[strlen(VAR)-1] == '\n') \ { \ VAR[strlen(VAR)-1]=0; \ } \ if (SAEximDebug > 3) \ { \ log_write(0, LOG_MAIN, "SA: config read "## #VAR ##" = %s", VAR); \ } \ continue; \ } M_CHECKFORVAR(SAEximDebug, "%d"); M_CHECKFORSTR(SAspamcpath); M_CHECKFORSTR(SAEximRunCond); M_CHECKFORSTR(SAEximRejCond); M_CHECKFORVAR(SAmaxarchivebody, "%d"); M_CHECKFORVAR(SAerrmaxarchivebody, "%d"); M_CHECKFORVAR(SAtimeout, "%d"); M_CHECKFORSTR(SAtimeoutsave); M_CHECKFORSTR(SAtimeoutSavCond); M_CHECKFORSTR(SAerrorsave); M_CHECKFORSTR(SAerrorSavCond); M_CHECKFORVAR(SAtemprejectonerror, "%d"); M_CHECKFORVAR(SAstallsender, "%f"); M_CHECKFORVAR(SAstallsendertime, "%d"); M_CHECKFORSTR(SAstallsenderSavCond); M_CHECKFORSTR(SAstallsendersave); M_CHECKFORVAR(SAstallsenderoverwrite, "%d"); M_CHECKFORVAR(SAdevnull, "%f"); M_CHECKFORSTR(SAdevnullSavCond); M_CHECKFORSTR(SAdevnullsave); M_CHECKFORVAR(SApermreject, "%f"); M_CHECKFORSTR(SApermrejectsave); M_CHECKFORSTR(SApermrejectSavCond); M_CHECKFORVAR(SAtempreject, "%f"); M_CHECKFORSTR(SAtemprejectSavCond); M_CHECKFORSTR(SAtemprejectsave); M_CHECKFORVAR(SAtemprejectoverwrite, "%d"); M_CHECKFORSTR(SAspamacceptsave); M_CHECKFORSTR(SAspamacceptSavCond); M_CHECKFORSTR(SAnotspamsave); M_CHECKFORSTR(SAnotspamSavCond); } readconffile=1; } expand=expand_string(SAEximRunCond); if (expand == NULL) { PANIC(string_sprintf("SAEximRunCond expansion failure on %s", SAEximRunCond)); } if (SAEximDebug) { log_write(0, LOG_MAIN, "SA: SAEximRunCond expand returned: '%s'", expand); } /* Bail from SA if the expansion string says so */ if (expand[0] == 0 || (expand[0] == '0' && expand[1] == 0)) { log_write(0, LOG_MAIN, "SA: check skipped due to expansion to false"); return LOCAL_SCAN_ACCEPT; } if (SAEximDebug) { log_write(0, LOG_MAIN, "SA: check succeeded, running spamc"); } beforescan=time(NULL); /* Fork off spamc, and get ready to talk to it */ ret=pipe(writefd); CHECKERR(ret,"write pipe",__LINE__); ret=pipe(readfd); CHECKERR(ret,"read pipe",__LINE__); if ((pid=fork()) < 0) { CHECKERR(pid, "fork", __LINE__ - 1); } if (pid == 0) { close(readfd[0]); close(writefd[1]); ret=dup2(writefd[0],0); CHECKERR(ret,"dup2 stdin",__LINE__); ret=dup2(readfd[1],1); CHECKERR(ret,"dup2 stdout",__LINE__); ret=dup2(readfd[1],2); CHECKERR(ret,"dup2 stderr",__LINE__); /* * If you are really smart, you'd implement the spamc protocol and * talk to spamd directly instead of forking spamc, but considering * the overhead spent in spamd, forking off spamc seemed acceptable * rather than re-implementing and tracking the spamc/spamd protocol */ ret=execl(SAspamcpath, "spamc", NULL); CHECKERR(ret,string_sprintf("exec %s", SAspamcpath),__LINE__); } ret=close(readfd[1]); CHECKERR(ret,"close r",__LINE__); ret=close(writefd[0]); CHECKERR(ret,"close w",__LINE__); readfh=fdopen(readfd[0], "r"); /* Ok, we're ready for spewing the mail at spamc */ /* First we need to get the header lines from exim, and then we can read the body from fd */ hl=header_list; while (hl != NULL) { /* type '*' means the header is internal, don't print it */ if (hl->type == '*') { hl=hl->next; continue; } /* Let's not send X-Spam headers, we're generating new ones */ if (! compare_header((char *)hl->text, "X-Spam-")) { ret=write(writefd[1],hl->text,strlen(hl->text)); CHECKERR(ret,"header line write",__LINE__); } hl=hl->next; } ret=write(writefd[1],"\n",1); CHECKERR(ret,"header separation write",__LINE__); /* Now read the body and send it to SA */ /* Let's not send more body data than SA is going to process though */ chunk=(samaxbody / sizeof(buffera)); while ((ret=read(fd, buffer, sizeof(buffera))) > 0 && chunk-- > 0) { ret=write(writefd[1], buffer, ret); CHECKERR(ret,"body write",__LINE__); } CHECKERR(ret, "read body", __LINE__ - 4); close(writefd[1]); if (SAEximDebug > 5) { log_write(0, LOG_MAIN, "SA: fed spam to spamc, reading result", expand); } if (SAtimeout) { if (SAEximDebug > 2) { log_write(0, LOG_MAIN, "SA: Setting timeout of %d secs before reading from spamc", SAtimeout); } /* SA can take very long to run for various reasons, let's not wait * forever, that's just bad at SMTP time */ if (setjmp(jmp_env) == 0) { signal(SIGALRM, alarm_handler); alarm (SAtimeout); } else { rantoolong=1; fclose(readfh); *return_text=string_sprintf("Accepted because scan took too long"); /* We sent it to LOG_REJECT too so that we get a header dump */ log_write(0, LOG_MAIN | LOG_REJECT, "SA: spamd took more than %d secs to run, accepting message", SAtimeout); ret=savemail(fd, SAtimeoutsave, "SAtimeoutsave", mesgfn, SAerrmaxarchivebody, SAtimeoutSavCond); CHECKERR(ret,where,line); /* Make sure we kill spamc in case SIGPIPE from fclose didn't */ kill(pid, SIGTERM); return LOCAL_SCAN_ACCEPT; } } /* Let's see what SA has to tell us about this mail and store the headers */ while ((fgets((char *)buffer,sizeof(buffera),readfh)) != NULL) { /* Remove trailing newline */ if (buffer[strlen(buffer)-1] == '\n') { buffer[strlen(buffer)-1]=0; } restart: if (SAEximDebug > 5) { log_write(0, LOG_MAIN, "SA: spamc read: %s", buffer); } /* Let's handle special multi-line headers first, what a pain... */ /* This is ugly, there is an order dependency so we return to the beginning of the loop without reading a new line since we already did that */ if (parsemlheader(buffer, readfh, "Subject", &subject)) goto restart; if (parsemlheader(buffer, readfh, "Content-Type", &contenttype)) goto restart; if (parsemlheader(buffer, readfh, "Content-Transfer-Encoding", &contenttransferencoding)) goto restart; if (parsemlheader(buffer, readfh, "X-Spam-Prev-Content-Type", &xspamprevcontenttype)) goto restart; if (parsemlheader(buffer, readfh, "X-Spam-Report", &xspamreport)) goto restart; if (parsemlheader(buffer, readfh, "X-Spam-Level", &xspamlevel)) goto restart; if (parsemlheader(buffer, readfh, "X-Spam-Checker-Version", &xspamcheckerversion)) goto restart; if (parsemlheader(buffer, readfh, "X-Spam-Prev-Content-Transfer-Encoding", &xspamprevcontenttransferencoding)) goto restart; if (parsemlheader(buffer, readfh, "X-Spam-Flag", &xspamflag)) { if (xspamflag[13] == 'Y') { isspam=1; } if (SAEximDebug > 2) { log_write(0, LOG_MAIN, "SA: Is Spam read from X-Spam-Flag: %d", isspam); } goto restart; } if (parsemlheader(buffer, readfh, "X-Spam-Status", &xspamstatus)) { char *start; char *end; gotsa=1; start=strstr(xspamstatus, "hits="); end=strstr(xspamstatus, " tests="); if (end == NULL) { if (SAEximDebug > 5) { log_write(0, LOG_MAIN, "SA: Could not find old spamstatus format, trying new one..."); } end=strstr(xspamstatus, "\n tests="); } if (start!=NULL && end!=NULL) { spamstatus=string_copyn(start, end-start); if (SAEximDebug > 2) { log_write(0, LOG_MAIN, "SA: Read from X-Spam-Status: %s", spamstatus); } } else { PANIC(string_sprintf("SA: could not parse X-Spam-Status: to extract hits and required. Bad!. Got: '%s'", xspamstatus)); } start=strstr(spamstatus, "="); end=strstr(spamstatus, " "); if (start!=NULL && end!=NULL) { start++; sscanf(start, "%f", &spamvalue); } else { PANIC(string_sprintf("SA: spam value extract failed in '%s'. Bad!", xspamstatus)); } goto restart; } /* Ok, now we can do normal processing */ /* If no more headers here, we're done */ if (buffer[0] == 0) { if (SAEximDebug > 5) { log_write(0, LOG_MAIN, "SA: spamc read got newline, end of headers", buffer); } goto exit; } if (compare_header(buffer, "Message-Id: ")) { /* This assumes a normal Message-Id: , I won't bother * trying to parse the other possible variants */ if (buffer[12] == '<' && (mesgid=string_copy(buffer+13)) && mesgid[strlen(mesgid)-1] == '>' && mesgid[0] != 0) { char *ptr; mesgid[strlen(mesgid)-1]=0; /* Update mesg filename in case it needs to be saved on disk */ safemesgid=string_copy(mesgid); ptr=safemesgid; /* Clean Message-ID to make sure people can't write on our FS */ while (*ptr) { if (*ptr == '/') { *ptr='-'; } ptr++; } if (SAEximDebug > 5) { log_write(0, LOG_MAIN, "SA: Message-Id received and cleaned as: %s", safemesgid); } mesgfn=string_sprintf("%d_%s", time(NULL), safemesgid); continue; } /* else we keep the default mesgfn (unix date in seconds) */ } } exit: fclose(readfh); afterscan=time(NULL); scantime=afterscan-beforescan; wait(&ret); if (ret) { sprintf(buffer, "%d", ret); PANIC(string_sprintf("wait on spamc child yielded, %s", buffer)); } afterwait=time(NULL); fulltime=afterwait-beforescan; if (gotsa == 0) { log_write(0, LOG_MAIN, "SA: SA didn't successfully run against message (time: %d/%d secs)", scantime, fulltime); return LOCAL_SCAN_ACCEPT; } if (spamstatus == NULL) { spamstatus = (char *) nospamstatus; } if (isspam) { char *mailinfo; int i; int dorej; if (SAEximRejCond[0] == '1' && SAEximRejCond[1] == 0) { dorej=1; } else { dorej=1; expand=expand_string(SAEximRejCond); if (expand == NULL) { PANIC(string_sprintf("SAEximRejCond expansion failure on %s", SAEximRejCond)); } if (SAEximDebug) { log_write(0, LOG_MAIN, "SA: SAEximRejCond expand returned: '%s'", expand); } if (expand[0] == 0 || (expand[0] == '0' && expand[1] == 0)) { log_write(0, LOG_MAIN, "SA: SAEximRejCond expanded to false, not applying reject rules"); dorej=0; } } if (sender_host_address != NULL) { mailinfo=string_sprintf("From <%s> (host=%s [%s]) for", sender_address, sender_host_name, sender_host_address); } else { mailinfo=string_sprintf("From <%s> (local) for", sender_address); } for (i=0; i < recipients_count; i++) { mailinfo=string_sprintf("%s %s", mailinfo, recipients_list[i].address); } if (dorej && spamvalue > SAstallsender) { /* By default, we'll only save temp bounces by message ID so * that when the same message is submitted several times, we * overwrite the same file on disk and not create a brand new * one every single time */ if (SAstallsenderoverwrite && safemesgid) { ret=savemail(fd, SAstallsendersave, "SAstallsendersave", safemesgid, SAmaxarchivebody, SAstallsenderSavCond); CHECKERR(ret,where,line); } else { ret=savemail(fd, SAstallsendersave, "SAstallsendersave", mesgfn, SAmaxarchivebody, SAstallsenderSavCond); CHECKERR(ret,where,line); } spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SAstallsender); log_write(0, LOG_MAIN, "SA: local_scan will stall the sender for %d secs: %s (scanned in %d/%d secs). %s", SAstallsendertime, spamstatus, scantime, fulltime, mailinfo); /* Exim might want to stop us if we run for too long, but that's * exactly what we're trying to do, so let's override that */ alarm(0); /* Mmmh, unfortunately, exim doesn't seem to realize when the other * side breaks the connection, so we'll sleep for all of * SAstallsendertime, even if the other side has already * disconnected. * Patches to detect that the other side has disconnected are * wecome :-) */ while (SAstallsendertime--) { /* I tried to cut the big sleep in slices, but it didn't help */ sleep(1); } /* Let's be rude and die after the timeout */ log_write(0, LOG_MAIN, "SA: local_scan stall completed. Committing hara kiri"); exit(0); } else if (dorej && spamvalue > SAdevnull) { ret=savemail(fd, SAdevnullsave, "SAdevnullsave", mesgfn, SAmaxarchivebody, SAdevnullSavCond); CHECKERR(ret,where,line); recipients_count=0; spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SAdevnull); log_write(0, LOG_REJECT | LOG_MAIN, "SA: local_scan silently tossed message: %s (scanned in %d/%d secs). %s", spamstatus, scantime, fulltime, mailinfo); /* As of exim 4.04, you can't return a custom ok message :( */ *return_text=string_sprintf("Message is spam (score: %s)\nand is being fed to the bit bucket\n(please don't feed it too much, it might get fat)", spamstatus); return LOCAL_SCAN_ACCEPT; } else if (dorej && spamvalue > SApermreject) { ret=savemail(fd, SApermrejectsave, "SApermrejectsave", mesgfn, SAmaxarchivebody, SApermrejectSavCond); CHECKERR(ret,where,line); spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SApermreject); log_write(0, LOG_MAIN, "SA: local_scan permanently rejected message: %s (scanned in %d/%d secs). %s", spamstatus, scantime, fulltime, mailinfo); *return_text=string_sprintf("Heuristics guessed that this message was spam:\n%s", spamstatus); return LOCAL_SCAN_REJECT; } else if (dorej && spamvalue > SAtempreject) { /* By default, we'll only save temp bounces by message ID so * that when the same message is submitted several times, we * overwrite the same file on disk and not create a brand new * one every single time */ if (SAtemprejectoverwrite && safemesgid) { ret=savemail(fd, SAtemprejectsave, "SAtemprejectsave", safemesgid, SAmaxarchivebody, SAtemprejectSavCond); CHECKERR(ret,where,line); } else { ret=savemail(fd, SAtemprejectsave, "SAtemprejectsave", mesgfn, SAmaxarchivebody, SAtemprejectSavCond); CHECKERR(ret,where,line); } spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SAtempreject); log_write(0, LOG_MAIN, "SA: local_scan temporarily rejected message: %s (scanned in %d/%d secs). %s", spamstatus, scantime, fulltime, mailinfo); *return_text=string_sprintf("Heuristics guessed that this message was spam:\n%s, so it is temporarilty rejected.\nAdmins may whitelist this and it may be accepted when you resubmit it.", spamstatus); return LOCAL_SCAN_TEMPREJECT; } else { ret=savemail(fd, SAspamacceptsave, "SAspamacceptsave", mesgfn, SAmaxarchivebody, SAspamacceptSavCond); CHECKERR(ret,where,line); log_write(0, LOG_MAIN, "SA: Flagged as Spam but accepted: Score %s (scanned in %d/%d secs)", spamstatus, scantime, fulltime); /* As of exim 4.04, you can't return a custom ok message :( */ *return_text=string_sprintf("Message accepted, heuristics found a spam likelyhood score of: %s", spamstatus); return LOCAL_SCAN_ACCEPT; } } else { ret=savemail(fd, SAnotspamsave, "SAnotspamsave", mesgfn, SAmaxarchivebody, SAnotspamSavCond); CHECKERR(ret,where,line); log_write(0, LOG_MAIN, "SA: score %s (scanned in %d/%d secs)", spamstatus, scantime, fulltime); return LOCAL_SCAN_ACCEPT; } errexit: if (SAtemprejectonerror) { /* Unfortunately, this spits up the message twice, once here and once by * the return TEMPREJECT */ log_write(0, LOG_MAIN | LOG_REJECT, "SA: Unexpected error on %s, file "__FILE__", line %d: %s", where, line-1, strerror(errno)); } else { log_write(0, LOG_MAIN, "SA: Unexpected error on %s (but message was accepted), file "__FILE__", line %d: %s", where, line-1, strerror(errno)); } ret=savemail(fd, SAerrorsave, "SAerrorsave", mesgfn, SAerrmaxarchivebody, SAerrorSavCond); if (ret < 0) { log_write(0, LOG_MAIN, "SA: Error in error handler while trying to save mail to %s, file "__FILE__", line %d: %s", string_sprintf("%s/%s", SAerrorsave, mesgfn), __LINE__ - 3, strerror(errno)); } if (SAtemprejectonerror) { *return_text=string_sprintf("Temporary local error while processing message, please contact postmaster"); return LOCAL_SCAN_TEMPREJECT; } else { return LOCAL_SCAN_ACCEPT; } panicexit: if (SAtemprejectonerror) { log_write(0, LOG_MAIN | LOG_REJECT, "SA: PANIC: %s", panicerror); } else { log_write(0, LOG_MAIN, "SA: PANIC: %s (but message was accepted)", panicerror); } ret=savemail(fd, SAerrorsave, "SAerrorsave", mesgfn, SAerrmaxarchivebody, SAerrorSavCond); if (ret < 0) { log_write(0, LOG_MAIN, "SA: PANIC: Error in error handler while trying to save mail to %s, file "__FILE__", line %d: %s", string_sprintf("%s/%s", SAerrorsave, mesgfn), __LINE__ - 3, strerror(errno)); } if (SAtemprejectonerror) { *return_text=string_sprintf("Temporary local error while processing message, please contact postmaster"); return LOCAL_SCAN_TEMPREJECT; } else { return LOCAL_SCAN_ACCEPT; } } /* End of local_scan.c */ /* To ask Philip: 1) read/use return_text on 2xx 2) optional LOCAL_SCAN_REJECT without triggering a full dump of the rejected headers so that I can return a different message than what I log 3) I need to return '\n' in return_text, but it gets logged, and log_write isn't supposed to get newlines... See #2 */