/* 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://sa-exim.sourceforge.net/ 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/exim/spamassassin.conf" #endif static const char conffile[]=SPAMASSASSIN_CONF; /* How much message body you want to feed to SA. We'll feed spamc a slightly larger message than it it's willing to process so that spamc decides that it's not going to process the message vs spamc getting a truncated message */ 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=0; 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); mkdir (string_sprintf("%s/new", 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/new/%s", dir, filename), S_IRUSR|S_IWUSR); CHECKERR(writefd, string_sprintf("creat %s/new/%s", dir, 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, 0, SEEK_SET); 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(volatile int fd, uschar **return_text) { #warning you shouldn''t worry about the "might be clobbered by longjmp", see source int ret; int pid; int writefd[2]; int readfd[2]; /* These are the only values that we want working after the longjmp * The automatic ones can be clobbered, but we don't really care */ volatile FILE *readfh; volatile char *mesgfn=NULL; char *safemesgid=NULL; int isspam=0; int gotsa=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 SAteergrub=1<<30; static int SAteergrubtime=900; static char *SAteergrubSavCond="1"; static char *SAteergrubsave=NULL; static int SAteergruboverwrite=1; 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), (FILE *)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(SAteergrub, "%f"); M_CHECKFORVAR(SAteergrubtime, "%d"); M_CHECKFORSTR(SAteergrubSavCond); M_CHECKFORSTR(SAteergrubsave); M_CHECKFORVAR(SAteergruboverwrite, "%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"); header_add(' ', "X-SA-Exim-Scanned: No; SAEximRunCond expanded to false\n"); 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", "-s", string_sprintf("%d", samaxbody), 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__); /* We're now feeding the body to SA, but let's not send much more body data * than SA is going to process, but let's send at least one byte more for * spamc to do the size cutoff, not us */ chunk=(samaxbody+1 / 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 { /* Make sure that all your variables here are volatile or static */ fclose((FILE *)readfh); header_add(' ', "X-SA-Exim-Scanned: No; SA Timed out after %d secs\n", SAtimeout); *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", (char *)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),(FILE *) 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, (FILE *)readfh, "Subject", &subject)) goto restart; if (parsemlheader(buffer, (FILE *)readfh, "Content-Type", &contenttype)) goto restart; if (parsemlheader(buffer, (FILE *)readfh, "Content-Transfer-Encoding", &contenttransferencoding)) goto restart; if (parsemlheader(buffer, (FILE *)readfh, "X-Spam-Prev-Content-Type", &xspamprevcontenttype)) goto restart; if (parsemlheader(buffer, (FILE *)readfh, "X-Spam-Report", &xspamreport)) goto restart; if (parsemlheader(buffer, (FILE *)readfh, "X-Spam-Level", &xspamlevel)) goto restart; if (parsemlheader(buffer, (FILE *)readfh, "X-Spam-Checker-Version", &xspamcheckerversion)) goto restart; if (parsemlheader(buffer, (FILE *)readfh, "X-Spam-Prev-Content-Transfer-Encoding", &xspamprevcontenttransferencoding)) goto restart; if (parsemlheader(buffer, (FILE *)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, (FILE *)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: ")) { char *start; char *end; start=index(buffer, '<'); end=index(buffer, '>'); if (start == NULL || end == NULL) { /* we keep the default mesgfn (unix date in seconds) */ if (SAEximDebug) { log_write(0, LOG_MAIN, "SA: Could not get Message-Id from %s", buffer); } } else if ((mesgid=string_copyn(start+1,end-start-1)) && mesgid[0]) { char *ptr; /* Update mesg filename in case it needs to be saved on disk */ safemesgid=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; } } exit: fclose((FILE *)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) { header_add(' ', "X-SA-Exim-Scanned: No; Unknown failure\n"); log_write(0, LOG_MAIN, "SA: SA didn't successfully run against message (time: %d/%d secs)", scantime, fulltime); return LOCAL_SCAN_ACCEPT; } header_add(' ', "X-SA-Exim-Scanned: Yes\n"); 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 >= SAteergrub) { /* 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 (SAteergruboverwrite && safemesgid) { ret=savemail(fd, SAteergrubsave, "SAteergrubsave", safemesgid, SAmaxarchivebody, SAteergrubSavCond); CHECKERR(ret,where,line); } else { ret=savemail(fd, SAteergrubsave, "SAteergrubsave", (char *)mesgfn, SAmaxarchivebody, SAteergrubSavCond); CHECKERR(ret,where,line); } spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SAteergrub); log_write(0, LOG_MAIN, "SA: local_scan will teergrub the sender for %d secs: %s (scanned in %d/%d secs). %s", SAteergrubtime, 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); /* http://www.iks-jena.de/mitarb/lutz/usenet/teergrube.en.html */ for (i=0;i= 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", (char *)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 know that the other side * disconnected until it tries to write to it. * We'll sleep for all of SAstallsendertime, even if the other side * has already disconnected. * In theory, you could query the socket to see if it has been * closed, but not from within local_scan, you'd need a hook inside * exim */ sleep(SAstallsendertime); /* We used to just die, but that leaves exim spool files behind */ /* let's send a tempreject in case anyone is still listening, and * it will also cause exim to clean after itself */ log_write(0, LOG_MAIN, "SA: local_scan stall completed. Sending tempreject"); *return_text=string_sprintf("Please try again later (%s)", spamstatus); return LOCAL_SCAN_TEMPREJECT; } else if (dorej && spamvalue >= SAdevnull) { ret=savemail(fd, SAdevnullsave, "SAdevnullsave", (char *)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", (char *)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", (char *)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", (char *)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", (char *)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)); } header_add(' ', "X-SA-Exim-Scanned: No; Exit with error (see exim mainlog)\n"); ret=savemail(fd, SAerrorsave, "SAerrorsave", (char *)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); } header_add(' ', "X-SA-Exim-Scanned: No; Panic (see exim mainlog)\n"); ret=savemail(fd, SAerrorsave, "SAerrorsave", (char *)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 */