1 /* Spamassassin in local_scan by Marc MERLIN <marc_soft@merlins.org> */
2 /* $Id: sa-exim.c,v 1.71 2005/03/08 20:39:51 marcmerlin Exp $ */
3 /*
4 
5 The inline comments and minidocs were moved to the distribution tarball
6 
7 You can get the up to date version of this file and full tarball here:
8 http://sa-exim.sourceforge.net/
9 http://marc.merlins.org/linux/exim/sa.html
10 The discussion list is here:
11 http://lists.merlins.org/lists/listinfo/sa-exim
12 */
13 
14 
15 
16 #include <stdio.h>
17 #include <unistd.h>
18 #include <fcntl.h>
19 #include <errno.h>
20 #include <string.h>
21 #include <stdlib.h>
22 #include <time.h>
23 #include <ctype.h>
24 #include <signal.h>
25 #include <setjmp.h>
26 #include <sys/wait.h>
27 #include <sys/types.h>
28 #include <sys/stat.h>
29 #include "sa-exim.h"
30 
31 /* Exim includes */
32 #include <local_scan.h>
33 
34 #ifdef DLOPEN_LOCAL_SCAN
35 
36 /* Karsten Engelke <me@kaeng.org> says this is missing on openbsd */
37 #ifndef RTLD_NOW
38 #define RTLD_NOW 0x002
39 #endif
40 
41 /* Return the verion of the local_scan ABI, if being compiled as a .so */
local_scan_version_major(void)42 int local_scan_version_major(void)
43 {
44     return LOCAL_SCAN_ABI_VERSION_MAJOR;
45 }
46 
local_scan_version_minor(void)47 int local_scan_version_minor(void)
48 {
49     return LOCAL_SCAN_ABI_VERSION_MINOR;
50 }
51 
52 /* Left over for compatilibility with old patched exims that didn't have
53    a version number with minor an major. Keep in mind that it will not work
54    with older exim4s (I think 4.11 is required) */
55 #ifdef DLOPEN_LOCAL_SCAN_OLD_API
local_scan_version(void)56 int local_scan_version(void)
57 {
58     return 1;
59 }
60 #endif
61 #endif
62 
63 #ifndef SAFEMESGIDCHARS
64 #define SAFEMESGIDCHARS "!#%( )*+,-.0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~";
65 #endif
66 
67 
68 /******************************/
69 /* Compile time config values */
70 /******************************/
71 #ifndef SPAMC_LOCATION
72 #define SPAMC_LOCATION	    "/usr/bin/spamc"
73 #endif
74 
75 #ifndef SPAMASSASSIN_CONF
76 #define SPAMASSASSIN_CONF   "/etc/exim4/sa-exim.conf"
77 #endif
78 static const char conffile[]=SPAMASSASSIN_CONF;
79 
80 
81 /********************/
82 /* Code starts here */
83 /********************/
84 static const char nospamstatus[]="<error finding status>";
85 
86 static char *buffera[4096];
87 static char *buffer=(char *)buffera;
88 static int SAEximDebug=0;
89 static int SAPrependArchiveWithFrom=1;
90 static jmp_buf jmp_env;
91 
92 static char *where="Error handler called without error string";
93 static int line=-1;
94 static char *panicerror;
95 
96 #define MIN(a,b) (a<b?a:b)
97 
98 #define CHECKERR(mret, mwhere, mline) \
99     if (mret < 0) \
100     { \
101         where=mwhere; \
102         line=mline; \
103         goto errexit; \
104     }
105 
106 #define PANIC(merror) \
107     panicerror=merror; \
108     goto panicexit;
109 
110 
alarm_handler(int sig)111 static void alarm_handler(int sig)
112 {
113     sig = sig;    /* Keep picky compilers happy */
114     longjmp(jmp_env, 1);
115 }
116 
117 
118 /* Comparing header lines isn't fun, especially since the comparison has to
119    be caseless, so we offload this to this function
120    You can scan on partial headers, just give the root to scan for
121    Return 1 if the header was found, 0 otherwise */
compare_header(char * buffertocompare,char * referenceheader)122 static int compare_header(char *buffertocompare, char *referenceheader)
123 {
124     int idx;
125     int same=1;
126 
127     for (idx=0; idx<strlen(referenceheader); idx++)
128     {
129 	if ( tolower(referenceheader[idx]) != tolower(buffertocompare[idx]) )
130 	{
131 	    same=0;
132 	    break;
133 	}
134     }
135 
136     if (SAEximDebug > 7)
137     {
138 	if (same)
139 	{
140 	    log_write(0, LOG_MAIN, "SA: Debug8: Found %s in %s", referenceheader, buffertocompare);
141 	}
142 	else if (SAEximDebug > 8)
143 	{
144 	    log_write(0, LOG_MAIN, "SA: Debug9: Did not find %s in %s", referenceheader, buffertocompare);
145 	}
146     }
147 
148     return same;
149 }
150 
151 
152 /* returns a header from a buffer line */
get_header(char * buffer)153 static char *get_header(char *buffer)
154 {
155     char *start;
156     char *end;
157     char *header;
158 
159     start=buffer;
160     end=strstr(buffer, ":");
161 
162     header=string_copyn(start, end-start);
163 
164     if (SAEximDebug>5)
165     {
166 	log_write(0, LOG_MAIN, "SA: Debug6: Extracted header %s in buffer %s", header, buffer);
167     }
168 
169     return header;
170 }
171 
172 
173 /* Rejected mails can be archived in a spool directory */
174 /* filename will contain a double / before the filename, I prefer two to none */
savemail(int readfd,off_t fdstart,char * dir,char * dirvarname,char * filename,int SAmaxarchivebody,char * condition)175 static int savemail(int readfd, off_t fdstart, char *dir, char *dirvarname,
176 			char *filename, int SAmaxarchivebody, char *condition)
177 {
178     header_line *hl;
179     int writefd=0;
180     int ret;
181     ssize_t stret;
182     off_t otret;
183     char *expand;
184     char *fake_env_from;
185     int towrite;
186     int chunk;
187     struct stat bufst;
188 
189     if (dir == NULL)
190     {
191 	if (SAEximDebug>4)
192 	{
193 	    log_write(0, LOG_MAIN, "SA: Debug5: Not saving message because %s in undefined", dirvarname);
194 	}
195 	return 0;
196     }
197 
198     if (condition[0] != '1' || condition[1] != 0)
199     {
200 	expand=expand_string(condition);
201 	if (expand == NULL)
202 	{
203 	    /* Can't use PANIC within this function :( */
204 	    CHECKERR(-1, string_sprintf("savemail condition expansion failure on %s", condition), __LINE__ - 1);
205 	}
206 
207 	if (SAEximDebug > 2)
208 	{
209 	    log_write(0, LOG_MAIN, "SA: Debug3: savemail condition expand returned: '%s'", expand);
210 	}
211 
212 	if (expand[0] == 0 || (expand[0] == '0' && expand[1] == 0))
213 	{
214 	    if (SAEximDebug > 1)
215 	    {
216 		log_write(0, LOG_MAIN, "SA: Debug2: savemail condition expanded to false, not saving message to disk");
217 	    }
218 	    return 0;
219 	}
220     }
221 
222     if (SAEximDebug)
223     {
224 	log_write(0, LOG_MAIN, "SA: Debug: Writing message to %s/new/%s", dir, filename);
225 
226     }
227 
228     if (stat(string_sprintf("%s/new/", dir), &bufst) == -1)
229     {
230 	log_write(0, LOG_MAIN, "SA: Notice: creating maildir tree in  %s", dir);
231 	if (stat(dir, &bufst) == -1)
232 	{
233 	    ret=mkdir (dir, 0770);
234 	    CHECKERR(ret,string_sprintf("mkdir %s", dir),__LINE__);
235 	}
236 	ret=mkdir (string_sprintf("%s/new", dir), 0770);
237 	CHECKERR(ret,string_sprintf("mkdir %s/new/", dir),__LINE__);
238 	ret=mkdir (string_sprintf("%s/cur", dir), 0770);
239 	CHECKERR(ret,string_sprintf("mkdir %s/cur/", dir),__LINE__);
240 	ret=mkdir (string_sprintf("%s/tmp", dir), 0770);
241 	CHECKERR(ret,string_sprintf("mkdir %s/tmp/", dir),__LINE__);
242     }
243 
244     /* Let's not worry about you receiving two spams at the same second
245      * with the same message ID. If you do, the second one will overwrite
246      * the first one */
247     writefd=creat(string_sprintf("%s/new/%s", dir, filename), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
248     CHECKERR(writefd, string_sprintf("creat %s/new/%s", dir, filename),__LINE__);
249 
250     /* make the file look like a valid mbox -- idea from dman */
251     /* Although now that we use maildir format, this isn't really necessary */
252     /* Richard Lithvall made this an option */
253     if(SAPrependArchiveWithFrom == 1)
254     {
255 	fake_env_from=string_sprintf("From %s Thu Jan  1 00:00:01 1970\n",sender_address);
256 	stret=write(writefd, fake_env_from, strlen(fake_env_from));
257 	CHECKERR(stret,string_sprintf("'From ' line write in %s", filename),__LINE__);
258     }
259 
260     /* First we need to get the header lines from exim, and then we can read
261        the body from writefd */
262     hl=header_list;
263     while (hl != NULL)
264     {
265 	/* type '*' means the header is internal, don't print it */
266 	if (hl->type == '*')
267 	{
268 	    hl=hl->next;
269 	    continue;
270 	}
271 	stret=write(writefd,hl->text,strlen(hl->text));
272 	CHECKERR(stret,string_sprintf("header line write in %s", filename),__LINE__);
273 	hl=hl->next;
274     }
275     stret=write(writefd,"\n",1);
276     CHECKERR(stret,string_sprintf("header separation write in %s", filename),__LINE__);
277 
278     /* Now copy the body to the save file */
279     /* we already read from readfd, so we need to reset it */
280     otret=lseek(readfd, fdstart, SEEK_SET);
281     CHECKERR(otret, "lseek reset on spooled message", __LINE__);
282 
283     if (SAEximDebug > 8)
284     {
285 	log_write(0, LOG_MAIN, "SA: Debug9: Archive body write starts: writing up to %d bytes in %d byte blocks", SAmaxarchivebody, sizeof(buffera));
286     }
287 
288     towrite=SAmaxarchivebody;
289     chunk=0;
290     while (towrite>0 && (stret=read(readfd, buffer, MIN(sizeof(buffera), towrite))) > 0)
291     {
292 	chunk++;
293 	if (SAEximDebug > 8)
294 	{
295 	    log_write(0, LOG_MAIN, "SA: Debug9: Processing archive body chunk %d (read %.0f, and %.0f can still be written)", chunk, (double)stret, (double)towrite);
296 	}
297 	towrite-=stret;
298 	stret=write(writefd, buffer, stret);
299 	CHECKERR(stret,string_sprintf("body write in %s", filename),__LINE__);
300     }
301     CHECKERR(stret, "read body for archival", __LINE__ - 8);
302     ret=close(writefd);
303     CHECKERR(ret, "Closing spooled message",__LINE__);
304     return 0;
305 
306     /* catch the global errexit, clean up, and return the error up */
307     errexit:
308     close(writefd);
309     return -1;
310 }
311 
312 /*
313  * let's add the X-SA-Exim-Connect-IP, X-SA-Exim-Rcpt-To, and
314  * X-SA-Exim-Mail-From headers.
315  * Those are all required by the greylisting with SA implementation
316  * And From/Rcpt-To can also be used for personalized SA rules
317  */
AddSAEheaders(char * rcptlist,int SAmaxrcptlistlength)318 void AddSAEheaders(char *rcptlist, int SAmaxrcptlistlength)
319 {
320     if (sender_host_address)
321     {
322 	header_add(' ', "X-SA-Exim-Connect-IP: %s\n", sender_host_address);
323     }
324     else
325     {
326 	header_add(' ', "X-SA-Exim-Connect-IP: <locally generated>\n");
327     }
328 
329     /* Create a mega envelope-to header with all the recipients */
330     /* Note, if you consider this a privacy violation, you can remove the header
331      * in exim's system filter.
332      * This is very useful to see who a message was really sent to, and can
333      * be used by Spamassassin to do additional scoring */
334     if (strlen(rcptlist) <= SAmaxrcptlistlength)
335     {
336 	header_add(' ', "X-SA-Exim-Rcpt-To: %s\n", rcptlist);
337     }
338     /* Therefore SAmaxrcptlistlength set to 0 disables the header completely */
339     else if (SAmaxrcptlistlength)
340     {
341 	header_add(' ', "X-SA-Exim-Rcpt-To: too long (recipient list exceeded maximum allowed size of %d bytes)\n", SAmaxrcptlistlength);
342     }
343 
344     header_add(' ', "X-SA-Exim-Mail-From: %s\n", sender_address);
345 }
346 
RemoveHeaders(char * headername)347 void RemoveHeaders(char *headername)
348 {
349     header_line *hl;
350 
351     /* Remove headers that SA can set */
352     hl=header_list;
353     while (hl != NULL)
354     {
355 
356 	/* type '*' means the header is internal or deleted */
357 	if (hl->type == '*')
358 	{
359 	    hl=hl->next;
360 	    continue;
361 	}
362 
363 	/* Strip all SA and SA-Exim headers on incoming mail */
364 	if ( compare_header((char *)hl->text, headername) )
365 	{
366 	    if (SAEximDebug > 2)
367 	    {
368 		log_write(0, LOG_MAIN, "SA: Debug3: removing header %s on incoming mail '%s'", headername, (char *)hl->text);
369 	    }
370 	    hl->type = '*';
371 	}
372 	hl=hl->next;
373     }
374 }
375 
376 
377 /*
378  * Headers can be multi-line (in theory all of them can I think). Parsing them
379  * is a little more work than a simple line scan, so we're off-loading this to
380  * a function
381  */
parsemlheader(char * buffer,FILE * readfh,char * headername,char ** header)382 int parsemlheader(char *buffer, FILE *readfh, char *headername, char **header)
383 {
384     header_line *hl;
385     char *dummy;
386     char *foundheadername;
387 
388     if (SAEximDebug > 4)
389     {
390 	log_write(0, LOG_MAIN, "SA: Debug5: looking for header %s", headername);
391     }
392 
393     if (header == NULL)
394     {
395 	header=&dummy;
396     }
397 
398     if (compare_header(buffer, string_sprintf("%s", headername)))
399     {
400 	*header=string_copy(buffer);
401 
402 	/* Read the next line(s) in case this is a multi-line header */
403 	while ((fgets((char *)buffer,sizeof(buffera),readfh)) != NULL)
404 	{
405 	    /* Remove trailing newline */
406 	    if (buffer[strlen(buffer)-1] == '\n')
407 	    {
408 		buffer[strlen(buffer)-1]=0;
409 		/* and any carriage return */
410 		if (buffer[strlen(buffer)-1] == '\r')
411 		{
412 		    buffer[strlen(buffer)-1]=0;
413 		}
414 	    }
415 	    if (SAEximDebug > 5)
416 	    {
417 		log_write(0, LOG_MAIN, "SA: Debug6: while parsing header %s, read %s", headername, buffer);
418 	    }
419 	    /* concatenated lines only start with space or tab. right? */
420 	    if (buffer[0] != ' ' && buffer[0] != '\t')
421 	    {
422 		break;
423 	    }
424 
425 	    /* Guard against humongous header lines */
426 	    if (strlen(*header) < 8000)
427 	    {
428 		/* Slight waste of memory here, oh well... */
429 		*header=string_sprintf("%s\n%s", *header, buffer);
430 	    }
431 	    else
432 	    {
433 		log_write(0, LOG_MAIN, "SA: Warning: while parsing header %s, ignoring the following trailing line due to header size overflow: %s", headername, buffer);
434 
435 	    }
436 	}
437 	if (SAEximDebug > 5)
438 	{
439 	    log_write(0, LOG_MAIN, "SA: Debug6: header pieced up %s as: '%s'", headername, *header);
440 	}
441 
442 	/* Headers need a newline at the end before being handed out to exim */
443 	/* Slight waste of memory here, oh well... */
444 	*header=string_sprintf("%s\n", *header);
445 
446 	foundheadername=get_header(*header);
447 
448 	/* Mark the former header as deleted if it's already present */
449 	/* Note that for X-Spam, it won't since we already deleted it earlier */
450 	hl=header_list;
451 	while (hl != NULL)
452 	{
453 	    /* type '*' means the header is internal or deleted */
454 	    if (hl->type == '*')
455 	    {
456 		hl=hl->next;
457 		continue;
458 	    }
459 
460 	    if ( compare_header((char *)hl->text, foundheadername) )
461 	    {
462 		if (SAEximDebug > 5)
463 		{
464 		    log_write(0, LOG_MAIN, "SA: Debug6: removing old copy of header '%s' and replacing with new one: '%s'", (char *)hl->text, *header);
465 		}
466 		hl->type = '*';
467 		break;
468 	    }
469 	    hl=hl->next;
470 	}
471 
472 	header_add(' ', "%s", *header);
473 	return 1;
474     }
475     return 0;
476 }
477 
478 
cleanmsgid(char * msgid,char * SAsafemesgidchars)479 char *cleanmsgid(char *msgid, char *SAsafemesgidchars)
480 {
481     char *safemesgid;
482     char *ptr;
483 
484     /* In case the message-Id is too long, let's truncate it */
485     safemesgid=string_copyn(msgid, 220);
486     ptr=safemesgid;
487 
488     /* Clean Message-ID to make sure people can't write on our FS */
489     while (*ptr)
490     {
491 	/* This might be more aggressive than you want, but since you
492 	 * potentially have shell programs dealing with the resulting filenames
493 	 * let's make it a bit safer */
494 	if (strchr(SAsafemesgidchars, *ptr) == NULL)
495 	{
496 	    *ptr='_';
497 	}
498 	ptr++;
499     }
500 
501     if (SAEximDebug > 1)
502     {
503 	log_write(0, LOG_MAIN, "SA: Debug2: Message-Id taken from Exim and cleaned from: %s to: %s", msgid, safemesgid);
504     }
505 
506     return safemesgid;
507 }
508 
509 
510 /* Exim calls us here, feeds us a fd on the message body, and expects a return
511    message in *return_text */
local_scan(volatile int fd,uschar ** return_text)512 int local_scan(volatile int fd, uschar **return_text)
513 {
514 #warning you should not worry about the "might be clobbered by longjmp", see source
515     int ret;
516     ssize_t stret;
517     int pid;
518     int writefd[2];
519     int readfd[2];
520     char *spamc_argv[10];
521     int i;
522     /* These are the only values that we want working after the longjmp
523      * The automatic ones can be clobbered, but we don't really care */
524     volatile FILE *readfh;
525     volatile char *mesgfn=NULL;
526     volatile off_t fdsize;
527     volatile off_t scansize;
528     volatile off_t fdstart;
529     volatile char *rcptlist;
530     volatile void *old_sigchld;
531     char *safemesgid=NULL;
532     int isspam=0;
533     int gotsa=0;
534     int chunk;
535     off_t towrite;
536     char *mailinfo;
537     float spamvalue=0.0;
538     char *spamstatus=NULL;
539     time_t beforescan;
540     time_t afterscan;
541     time_t afterwait;
542     time_t scantime=0;
543     time_t fulltime=0;
544     struct stat stbuf;
545 
546     uschar *expand;
547     header_line *hl;
548 
549     static int readconffile=0;
550     static int wrotedebugenabled=0;
551 
552     /* Options we read from /etc/exim4/sa-exim.conf */
553     static char *SAspamcpath=SPAMC_LOCATION;
554     static char *SAsafemesgidchars=SAFEMESGIDCHARS
555     static char *SAspamcSockPath=NULL;
556     static char *SAspamcPort=NULL;
557     static char *SAspamcHost=NULL;
558     static char *SAspamcUser=NULL;
559     static char *SAEximRunCond="0";
560     static char *SAEximRejCond="1";
561     static int SAmaxbody=250*1024;
562     static char *SATruncBodyCond="0";
563     static int SARewriteBody=0;
564     static int SAmaxarchivebody=20*1048576;
565     static int SAerrmaxarchivebody=1024*1048576;
566     static int SAmaxrcptlistlength=0;
567     static int SAaddSAEheaderBeforeSA=1;
568     static int SAtimeout=240;
569     static char *SAtimeoutsave=NULL;
570     static char *SAtimeoutSavCond="1";
571     static char *SAerrorsave=NULL;
572     static char *SAerrorSavCond="1";
573     static int SAtemprejectonerror=0;
574     static char *SAteergrube="1048576";
575     static float SAteergrubethreshold;
576     /* This is obsolete, since SAteergrube (now a condition) can do the same */
577     static char *SAteergrubecond="1";
578     static int SAteergrubetime=900;
579     static char *SAteergrubeSavCond="1";
580     static char *SAteergrubesave=NULL;
581     static int SAteergrubeoverwrite=1;
582     static char *SAdevnull="1048576";
583     static float SAdevnullthreshold;
584     static char *SAdevnullSavCond="1";
585     static char *SAdevnullsave=NULL;
586     static char *SApermreject="1048576";
587     static float SApermrejectthreshold;
588     static char *SApermrejectSavCond="1";
589     static char *SApermrejectsave=NULL;
590     static char *SAtempreject="1048576";
591     static float SAtemprejectthreshold;
592     static char *SAtemprejectSavCond="1";
593     static char *SAtemprejectsave=NULL;
594     static int SAtemprejectoverwrite=1;
595     static char *SAgreylistiswhitestr="GREYLIST_ISWHITE";
596     static float SAgreylistraisetempreject=3.0;
597     static char *SAspamacceptsave=NULL;
598     static char *SAspamacceptSavCond="0";
599     static char *SAnotspamsave=NULL;
600     static char *SAnotspamSavCond="0";
601     /* Those variables can take a %s to show the spam info */
602     static char *SAmsgteergrubewait="wait for more output";
603     static char *SAmsgteergruberej="Please try again later";
604     static char *SAmsgpermrej="Rejected";
605     static char *SAmsgtemprej="Please try again later";
606     /* Do not put a %s in there, or you'll segfault */
607     static char *SAmsgerror="Temporary local error while processing message, please contact postmaster";
608 
609     /* This needs to be retrieved through expand_string in order
610        not to violate the API. */
611     uschar *primary_hostname=expand_string("$primary_hostname");
612 
613     /* New values we read from spamassassin */
614     char *xspamstatus=NULL;
615     char *xspamflag=NULL;
616 
617 
618     /* Any error can write the faulty message to mesgfn, so we need to
619        give it a value right now. We'll set the real value later */
620     /* message_id here comes from Exim, it's an internal disk Mesg-Id format
621        which doesn't correlate to the actual message's Mesg-Id. We shouldn't
622        need to clean it, and besides, SAsafemesgidchars hasn't been read from
623        the config file yet, but eh, safety is always a good thing, right? */
624     safemesgid=cleanmsgid(message_id, SAsafemesgidchars);
625     mesgfn=string_sprintf("%d_%s", time(NULL), safemesgid);
626 
627     /* We won't scan local messages. I think exim bypasses local_scan for a
628      * bounce generated after a locally submitted message, but better be safe */
629     /* This is commented out now, because you can control it with SAEximRunCond
630     if (!sender_host_address)
631     {
632 	return LOCAL_SCAN_ACCEPT;
633     }
634     */
635 
636     /* If you discard a mail with exim ACLs, we get 0 recipients, so let's just
637      * accept the mail, which won't matter either way since it'll get dropped
638      * (thanks to John Horne for reporting this corner case) */
639     if (recipients_count == 0)
640     {
641 	return LOCAL_SCAN_ACCEPT;
642     }
643 
644     /*
645      * We keep track of whether we've alrady read the config file, but since
646      * exim spawns itself, it will get read by exim children even though you
647      * didn't restart exim. That said, after you change the config file, you
648      * should restart exim to make sure all the instances pick up the new
649      * config file
650      */
651     if (!readconffile)
652     {
653 	ret=open(conffile, 0);
654 	CHECKERR(ret,string_sprintf("conf file open for %s", conffile),__LINE__);
655 	readfh=fdopen(ret, "r");
656 	CHECKERR(readfh,"fdopen",__LINE__);
657 
658 	while ((fgets((char *)buffer, sizeof(buffera), (FILE *)readfh)) != NULL)
659 	{
660 	    if (*buffer == '#' || *buffer == '\n' )
661 	    {
662 		continue;
663 	    }
664 
665 	    if (*buffer != 'S' || *(buffer+1) != 'A')
666 	    {
667 		log_write(0, LOG_MAIN, "SA: Warning: error while reading configuration file %s. Line does not begin with a SA directive: '%s', ignoring", conffile, buffer);
668 		continue;
669 	    }
670 
671 #define     M_CHECKFORVAR(VAR, TYPE) \
672 	    if (strstr(buffer, #VAR ": ") == buffer) \
673 	    { \
674 		if (sscanf(buffer, #VAR ": " TYPE, &VAR)) \
675 		{ \
676 		    if (SAEximDebug > 3) \
677 		    { \
678 			if (SAEximDebug && ! wrotedebugenabled) \
679 			{ \
680 			    log_write(0, LOG_MAIN, "SA: Debug4: Debug enabled, reading config from file %s", conffile); \
681 			    wrotedebugenabled=1; \
682 			} \
683 			else \
684 			{ \
685 			    log_write(0, LOG_MAIN, "SA: Debug4: config read " #VAR " = " TYPE, VAR); \
686 			}\
687 		    }\
688 		} \
689 		else \
690 		{ \
691 		    log_write(0, LOG_MAIN, "SA: Warning: error while reading configuration file %s. Can't parse value in: '%s', ignoring", conffile, buffer); \
692 		} \
693 		continue; \
694 	    }
695 
696 #define	    M_CHECKFORSTR(VAR) \
697 	    if (strstr(buffer, #VAR  ": ") == buffer) \
698 	    { \
699 		VAR = strdup(buffer+strlen( #VAR )+2); \
700 		if (VAR == NULL) \
701 		{ \
702 		    log_write(0, LOG_MAIN, "SA: PANIC: malloc failed, quitting..."); \
703 		    exit(-1); \
704 		} \
705 		\
706 		if (VAR[strlen(VAR)-1] == '\n') \
707 		{ \
708 		    VAR[strlen(VAR)-1]=0; \
709 		} \
710 		if (SAEximDebug > 3) \
711 		{ \
712 		    log_write(0, LOG_MAIN, "SA: Debug4: config read " #VAR " = %s", VAR); \
713 		} \
714 		continue; \
715 	    }
716 
717 	    M_CHECKFORVAR(SAEximDebug, "%d");
718 	    M_CHECKFORSTR(SAspamcpath);
719 	    M_CHECKFORSTR(SAsafemesgidchars);
720 	    M_CHECKFORSTR(SAspamcSockPath);
721 	    M_CHECKFORSTR(SAspamcPort);
722 	    M_CHECKFORSTR(SAspamcHost);
723 	    M_CHECKFORSTR(SAspamcUser);
724 	    M_CHECKFORSTR(SAEximRunCond);
725 	    M_CHECKFORSTR(SAEximRejCond);
726 	    M_CHECKFORVAR(SAmaxbody, "%d");
727 	    M_CHECKFORSTR(SATruncBodyCond);
728 	    M_CHECKFORVAR(SARewriteBody, "%d");
729 	    M_CHECKFORVAR(SAPrependArchiveWithFrom, "%d");
730 	    M_CHECKFORVAR(SAmaxarchivebody, "%d");
731 	    M_CHECKFORVAR(SAerrmaxarchivebody, "%d");
732 	    M_CHECKFORVAR(SAmaxrcptlistlength, "%d");
733 	    M_CHECKFORVAR(SAaddSAEheaderBeforeSA, "%d");
734 	    M_CHECKFORVAR(SAtimeout, "%d");
735 	    M_CHECKFORSTR(SAtimeoutsave);
736 	    M_CHECKFORSTR(SAtimeoutSavCond);
737 	    M_CHECKFORSTR(SAerrorsave);
738 	    M_CHECKFORSTR(SAerrorSavCond);
739 	    M_CHECKFORVAR(SAtemprejectonerror, "%d");
740 	    M_CHECKFORSTR(SAteergrube);
741 	    M_CHECKFORSTR(SAteergrubecond);
742 	    M_CHECKFORVAR(SAteergrubetime, "%d");
743 	    M_CHECKFORSTR(SAteergrubeSavCond);
744 	    M_CHECKFORSTR(SAteergrubesave);
745 	    M_CHECKFORVAR(SAteergrubeoverwrite, "%d");
746 	    M_CHECKFORSTR(SAdevnull);
747 	    M_CHECKFORSTR(SAdevnullSavCond);
748 	    M_CHECKFORSTR(SAdevnullsave);
749 	    M_CHECKFORSTR(SApermreject);
750 	    M_CHECKFORSTR(SApermrejectsave);
751 	    M_CHECKFORSTR(SApermrejectSavCond);
752 	    M_CHECKFORSTR(SAtempreject);
753 	    M_CHECKFORSTR(SAtemprejectSavCond);
754 	    M_CHECKFORSTR(SAtemprejectsave);
755 	    M_CHECKFORVAR(SAtemprejectoverwrite, "%d");
756 	    M_CHECKFORSTR(SAgreylistiswhitestr);
757 	    M_CHECKFORVAR(SAgreylistraisetempreject, "%f");
758 	    M_CHECKFORSTR(SAspamacceptsave);
759 	    M_CHECKFORSTR(SAspamacceptSavCond);
760 	    M_CHECKFORSTR(SAnotspamsave);
761 	    M_CHECKFORSTR(SAnotspamSavCond);
762 	    M_CHECKFORSTR(SAmsgteergrubewait);
763 	    M_CHECKFORSTR(SAmsgteergruberej);
764 	    M_CHECKFORSTR(SAmsgpermrej);
765 	    M_CHECKFORSTR(SAmsgtemprej);
766 	    M_CHECKFORSTR(SAmsgerror);
767 
768 
769 	}
770 
771 	readconffile=1;
772     }
773 
774 #define M_CONDTOFLOAT(VAR) \
775     if ((expand=expand_string( VAR )) == NULL) \
776     { \
777 	PANIC(string_sprintf(#VAR " config expansion failure on %s", #VAR ));\
778     } \
779     sscanf(expand, "%f", &VAR ## threshold); \
780     if (SAEximDebug > 2) \
781     { \
782 	log_write(0, LOG_MAIN, "SA: Debug3: expanded " #VAR " = %.2f", VAR ## threshold); \
783     }\
784 
785     M_CONDTOFLOAT(SAteergrube);
786     M_CONDTOFLOAT(SAdevnull);
787     M_CONDTOFLOAT(SApermreject);
788     M_CONDTOFLOAT(SAtempreject);
789 
790     /* Initialize the list of recipients here */
791     rcptlist=string_copy(recipients_list[0].address);
792     for (i=1; i < recipients_count && strlen((char *)rcptlist) < 7998 - strlen(recipients_list[i].address); i++)
793     {
794 	rcptlist=string_sprintf("%s, %s", rcptlist, recipients_list[i].address);
795     }
796 
797     if (sender_host_address != NULL)
798     {
799 	mailinfo=string_sprintf("From <%s> (host=%s [%s]) for",
800 		sender_address, sender_host_name, sender_host_address);
801     }
802     else
803     {
804 	mailinfo=string_sprintf("From <%s> (local) for", sender_address);
805     }
806     mailinfo=string_sprintf("%s %s", mailinfo, rcptlist);
807 
808 
809     /* Remove SA-Exim headers that could have been set before we add ours*/
810     RemoveHeaders("X-SA-Exim-");
811 
812     if(SAaddSAEheaderBeforeSA)
813     {
814 	AddSAEheaders((char *)rcptlist, SAmaxrcptlistlength);
815     }
816 
817     /* This is used later if we need to rewind and save the body elsewhere */
818     fdstart=lseek(fd, 0, SEEK_CUR);
819     CHECKERR(fdstart,"lseek SEEK_CUR",__LINE__);
820 
821     ret=fstat(fd, &stbuf);
822     CHECKERR(ret,"fstat fd",__LINE__);
823     /* this is the body size plus a few bytes (exim msg ID) */
824     /* it should be 18 bytes, but I'll assume it could be more or less */
825     fdsize=stbuf.st_size;
826 
827     if (SAEximDebug > 3)
828     {
829 	log_write(0, LOG_MAIN, "SA: Debug4: Message body is about %.0f bytes and the initial offset is %.0f", (double)(fdsize-18), (double)fdstart);
830     }
831 
832     if (fdsize > SAmaxbody)
833     {
834 	if (SATruncBodyCond[0] != '1' || SATruncBodyCond[1] != 0)
835 	{
836 	    expand=expand_string(SATruncBodyCond);
837 	    if (expand == NULL)
838 	    {
839 		PANIC(string_sprintf("SATruncBodyCond expansion failure on %s", SATruncBodyCond));
840 	    }
841 
842 	    if (SAEximDebug)
843 	    {
844 		log_write(0, LOG_MAIN, "SA: Debug: SATruncBodyCond expand returned: '%s'", expand);
845 	    }
846 
847 	    if (expand[0] == 0 || (expand[0] == '0' && expand[1] == 0))
848 	    {
849 		log_write(0, LOG_MAIN, "SA: Action: check skipped due to message size (%.0f bytes) and SATruncBodyCond expanded to false (Message-Id: %s). %s", (double)(fdsize-18), safemesgid, mailinfo);
850 		header_add(' ', "X-SA-Exim-Scanned: No (on %s); Message bigger than SAmaxbody (%d)\n", primary_hostname, SAmaxbody);
851 		return LOCAL_SCAN_ACCEPT;
852 	    }
853 	}
854 
855 	if (SAEximDebug > 1)
856 	{
857 	    log_write(0, LOG_MAIN, "SA: Debug2: Message body is about %.0f bytes and SATruncBodyCond expanded to true, will feed a truncated body to SA", (double)(fdsize-18));
858 	}
859 
860 	/* Let's feed exactly spamc will accept */
861 	scansize=SAmaxbody;
862 	header_add(' ', "X-SA-Exim-Scan-Truncated: Fed %.0f bytes of the body to SA instead of %.0f\n", (double)scansize, (double)fdsize);
863     }
864     else
865     {
866 	scansize=fdsize;
867     }
868 
869     expand=expand_string(SAEximRunCond);
870     if (expand == NULL)
871     {
872 	PANIC(string_sprintf("SAEximRunCond expansion failure on %s", SAEximRunCond));
873     }
874 
875     if (SAEximDebug)
876     {
877 	log_write(0, LOG_MAIN, "SA: Debug: SAEximRunCond expand returned: '%s'", expand);
878     }
879 
880 
881     /* Bail from SA if the expansion string says so */
882     if (expand[0] == 0 || (expand[0] == '0' && expand[1] == 0))
883     {
884 	log_write(0, LOG_MAIN, "SA: Action: Not running SA because SAEximRunCond expanded to false (Message-Id: %s). %s", safemesgid, mailinfo);
885 	header_add(' ', "X-SA-Exim-Scanned: No (on %s); SAEximRunCond expanded to false\n", primary_hostname);
886 	return LOCAL_SCAN_ACCEPT;
887     }
888 
889     if (SAEximDebug)
890     {
891 	log_write(0, LOG_MAIN, "SA: Debug: check succeeded, running spamc");
892     }
893 
894     /* Ok, so now that we know we're running SA, we remove the X-Spam headers */
895     /* that might have been there */
896     RemoveHeaders("X-Spam-");
897 
898 
899     beforescan=time(NULL);
900     /* Fork off spamc, and get ready to talk to it */
901     ret=pipe(writefd);
902     CHECKERR(ret,"write pipe",__LINE__);
903     ret=pipe(readfd);
904     CHECKERR(ret,"read pipe",__LINE__);
905 
906     /* Ensure that SIGCHLD isn't being ignored. */
907     old_sigchld = signal(SIGCHLD, SIG_DFL);
908 
909     if ((pid=fork()) < 0)
910     {
911 	CHECKERR(pid, "fork", __LINE__ - 1);
912     }
913 
914     if (pid == 0)
915     {
916 	close(readfd[0]);
917 	close(writefd[1]);
918 
919 	ret=dup2(writefd[0],0);
920 	CHECKERR(ret,"dup2 stdin",__LINE__);
921 	ret=dup2(readfd[1],1);
922 	CHECKERR(ret,"dup2 stdout",__LINE__);
923 	ret=dup2(readfd[1],2);
924 	CHECKERR(ret,"dup2 stderr",__LINE__);
925 
926 	i = 0;
927 	spamc_argv[i++] = "spamc";
928 	if (SAspamcUser && SAspamcUser[0])
929 	{
930 	    expand=expand_string(SAspamcUser);
931 	    if (expand == NULL)
932 	    {
933 		log_write(0, LOG_MAIN | LOG_PANIC, "SA: SAspamcUser expansion failure on %s, will run as Exim user instead.", SAspamcUser);
934 	    }
935 	    else if (expand[0] != '\0')
936 	    {
937 		spamc_argv[i++] = "-u";
938 		spamc_argv[i++] = expand;
939 	    }
940 	}
941 
942 	/*
943          * I could implement the spamc protocol and talk to spamd directly
944          * instead of forking spamc, but considering the overhead spent
945          * in spamd, forking off spamc seemed acceptable rather than
946          * re-implementing and tracking the spamc/spamd protocol or linking
947 	 * with a possibly changing library
948 	 */
949 	/* Ok, we cheat, spamc cares about how big the whole message is and
950          * we only know about the body size, so I'll  give an extra 16K
951          * to account for any headers that can accompany the message */
952 
953 	spamc_argv[i++] = "-s";
954 	spamc_argv[i++] = string_sprintf("%d", SAmaxbody+16384);
955 
956 	if(SAspamcSockPath)
957 	{
958     	    spamc_argv[i++] = "-U";
959 	    spamc_argv[i++] = SAspamcSockPath;
960 	}
961 	else
962 	{
963 	    if (SAspamcHost) {
964 		spamc_argv[i++] = "-d";
965 		spamc_argv[i++] = SAspamcHost;
966 	    }
967 	    if (SAspamcPort) {
968 		spamc_argv[i++] = "-p";
969 		spamc_argv[i++] = SAspamcPort;
970 	    }
971 	}
972 	spamc_argv[i++] = NULL;
973 
974 	ret=execv(SAspamcpath, spamc_argv);
975 	CHECKERR(ret,string_sprintf("exec %s", SAspamcpath),__LINE__);
976     }
977 
978     if (SAEximDebug > 8)
979     {
980 	log_write(0, LOG_MAIN, "SA: Debug9: forked spamc");
981     }
982 
983     ret=close(readfd[1]);
984     CHECKERR(ret,"close r",__LINE__);
985     ret=close(writefd[0]);
986     CHECKERR(ret,"close w",__LINE__);
987     readfh=fdopen(readfd[0], "r");
988 
989     if (SAEximDebug > 8)
990     {
991 	log_write(0, LOG_MAIN, "SA: Debug9: closed filehandles");
992     }
993 
994     /* Ok, we're ready for spewing the mail at spamc */
995     /* First we need to get the header lines from exim, and then we can read
996        the body from fd */
997     hl=header_list;
998     while (hl != NULL)
999     {
1000 	/* type '*' means the header is internal, don't print it */
1001 	if (hl->type == '*')
1002 	{
1003 	    hl=hl->next;
1004 	    continue;
1005 	}
1006 
1007 	stret=write(writefd[1],hl->text,strlen(hl->text));
1008 	CHECKERR(stret,"header line write",__LINE__);
1009 
1010 	hl=hl->next;
1011     }
1012     stret=write(writefd[1],"\n",1);
1013     CHECKERR(stret,"header separation write",__LINE__);
1014 
1015     if (SAEximDebug > 6)
1016     {
1017 	log_write(0, LOG_MAIN, "SA: Debug7: sent headers to spamc pipe. Sending body...");
1018     }
1019 
1020     towrite=scansize;
1021     chunk=0;
1022     while (towrite>0 && (stret=read(fd, buffer, MIN(sizeof(buffera), towrite))) > 0)
1023     {
1024 	chunk++;
1025 	if (SAEximDebug > 8)
1026 	{
1027 	    log_write(0, LOG_MAIN, "SA: Debug9: spamc body going to write chunk %d (read %.0f, %.0f left to write)", chunk, (double)stret, (double)towrite);
1028 	}
1029 	towrite-=stret;
1030 	stret=write(writefd[1], buffer, stret);
1031 	CHECKERR(stret,"body write in",__LINE__);
1032 	if (SAEximDebug > 8)
1033 	{
1034 	    log_write(0, LOG_MAIN, "SA: Debug9: Spamc body wrote chunk %d (wrote %.0f, %.0f left to write)", chunk, (double)stret, (double)towrite);
1035 	}
1036     }
1037     CHECKERR(stret, "read body", __LINE__ - 14);
1038     close(writefd[1]);
1039 
1040     if (SAEximDebug > 5)
1041     {
1042 	log_write(0, LOG_MAIN, "SA: Debug6: fed spam to spamc, reading result");
1043     }
1044 
1045     if (SAtimeout)
1046     {
1047 	if (SAEximDebug > 2)
1048 	{
1049 	    log_write(0, LOG_MAIN, "SA: Debug3: Setting timeout of %d secs before reading from spamc", SAtimeout);
1050 	}
1051 	/* SA can take very long to run for various reasons, let's not wait
1052 	 * forever, that's just bad at SMTP time */
1053 	if (setjmp(jmp_env) == 0)
1054 	{
1055 	    signal(SIGALRM, alarm_handler);
1056 	    alarm (SAtimeout);
1057 	}
1058 	else
1059 	{
1060 	    /* Make sure that all your variables here are volatile or static */
1061 	    signal(SIGCHLD, old_sigchld);
1062 	    fclose((FILE *)readfh);
1063 
1064 	    header_add(' ', "X-SA-Exim-Scanned: No (on %s); SA Timed out after %d secs\n", primary_hostname, SAtimeout);
1065 
1066 	    /* We sent it to LOG_REJECT too so that we get a header dump */
1067 	    log_write(0, LOG_MAIN | LOG_REJECT, "SA: Action: spamd took more than %d secs to run, accepting message (scanned in %d/%d secs | Message-Id: %s). %s", SAtimeout, scantime, fulltime, safemesgid, mailinfo);
1068 
1069 	    ret=savemail(fd, fdstart, SAtimeoutsave, "SAtimeoutsave", (char *)mesgfn, SAerrmaxarchivebody, SAtimeoutSavCond);
1070 	    CHECKERR(ret,where,line);
1071 
1072 	    /* Make sure we kill spamc in case SIGPIPE from fclose didn't */
1073 	    kill(pid, SIGTERM);
1074 	    return LOCAL_SCAN_ACCEPT;
1075 
1076 	}
1077     }
1078 
1079     /* Let's see what SA has to tell us about this mail and store the headers */
1080     while ((fgets((char *)buffer,sizeof(buffera),(FILE *) readfh)) != NULL)
1081     {
1082 	/* Remove trailing newline */
1083 	if (buffer[strlen(buffer)-1] == '\n')
1084 	{
1085 	    buffer[strlen(buffer)-1]=0;
1086 	    /* and any carriage return */
1087 	    if (buffer[strlen(buffer)-1] == '\r')
1088 	    {
1089 		buffer[strlen(buffer)-1]=0;
1090 	    }
1091 	}
1092 restart:
1093 	if (SAEximDebug > 5)
1094 	{
1095 	    log_write(0, LOG_MAIN, "SA: Debug6: spamc read: %s", buffer);
1096 	}
1097 
1098 	/* Let's handle special multi-line headers first, what a pain... */
1099 	/* We feed the one line we read and the filehandle because we'll need
1100 	   to check whether more lines need to be concatenated */
1101         /* This is ugly, there is an order dependency so we return to the
1102            beginning of the loop without reading a new line since we already
1103 	   did that */
1104 	if (parsemlheader(buffer, (FILE *)readfh, "Subject", NULL)) goto restart;
1105 	if ((SARewriteBody == 1) && parsemlheader(buffer, (FILE *)readfh, "Content-Type", NULL)) goto restart;
1106 	if ((SARewriteBody == 1) && parsemlheader(buffer, (FILE *)readfh, "Content-Transfer-Encoding", NULL)) goto restart;
1107 
1108 	if (parsemlheader(buffer, (FILE *)readfh, "X-Spam-Flag", &xspamflag))
1109 	{
1110 	    if (xspamflag[13] == 'Y')
1111 	    {
1112 		isspam=1;
1113 	    }
1114 	    if (SAEximDebug > 2)
1115 	    {
1116 		log_write(0, LOG_MAIN, "SA: Debug3: isspam read from X-Spam-Flag: %d", isspam);
1117 	    }
1118 	    goto restart;
1119 	}
1120 
1121 	if (parsemlheader(buffer, (FILE *)readfh, "X-Spam-Status", &xspamstatus))
1122 	{
1123 	    char *start;
1124 	    char *end;
1125 
1126 	    gotsa=1;
1127 
1128 	    /* if we find the preconfigured greylist string (and it is defined
1129 	     * in sa-exim.conf), we can raise the threshold for tempreject just
1130 	     * for this mail, since it's been whitelisted */
1131 	    if (SAgreylistiswhitestr && strstr(xspamstatus, SAgreylistiswhitestr))
1132 	    {
1133 	        SAtemprejectthreshold+=SAgreylistraisetempreject;
1134 		if (SAEximDebug > 2)
1135 		{
1136 		    log_write(0, LOG_MAIN, "SA: Debug3: read %s string, SAtempreject is now changed to %f", SAgreylistiswhitestr, SAtemprejectthreshold);
1137 		}
1138 	    }
1139 	    else
1140 	    {
1141 		if (SAEximDebug > 2)
1142 		{
1143 		    log_write(0, LOG_MAIN, "SA: Debug3: did not find read GREYLIST_ISWHITE string in X-Spam-Status");
1144 		}
1145 	    }
1146 
1147 	    start=strstr(xspamstatus, "hits=");
1148 	    /* Support SA 3.0 format */
1149 	    if (start == NULL)
1150 	    {
1151 	    	start=strstr(xspamstatus, "score=");
1152 	    }
1153 
1154 	    end=strstr(xspamstatus, " tests=");
1155 	    if (end == NULL)
1156 	    {
1157 		if (SAEximDebug > 5)
1158 		{
1159 		    log_write(0, LOG_MAIN, "SA: Debug6: Could not find old spamstatus format, trying new one...");
1160 		}
1161 		end=strstr(xspamstatus, "\n	tests=");
1162 	    }
1163 	    if (start!=NULL && end!=NULL)
1164 	    {
1165 		spamstatus=string_copyn(start, end-start);
1166 		if (SAEximDebug > 2)
1167 		{
1168 		    log_write(0, LOG_MAIN, "SA: Debug3: Read from X-Spam-Status: %s", spamstatus);
1169 		}
1170 	    }
1171 	    else
1172 	    {
1173 		PANIC(string_sprintf("SA: could not parse X-Spam-Status: to extract hits and required. Bad!. Got: '%s'", xspamstatus));
1174 	    }
1175 
1176 	    start=strstr(spamstatus, "=");
1177 	    end=strstr(spamstatus, " ");
1178 	    if (start!=NULL && end!=NULL)
1179 	    {
1180 		start++;
1181 		sscanf(start, "%f", &spamvalue);
1182 	    }
1183 	    else
1184 	    {
1185 		PANIC(string_sprintf("SA: spam value extract failed in '%s'. Bad!", xspamstatus));
1186 	    }
1187 
1188 	    goto restart;
1189 	}
1190 
1191 	if (parsemlheader(buffer, (FILE *)readfh, "X-Spam-", NULL)) goto restart;
1192 
1193 	/* Ok, now we can do normal processing */
1194 
1195 	/* If no more headers here, we're done */
1196 	if (buffer[0] == 0)
1197 	{
1198 	    if (SAEximDebug > 5)
1199 	    {
1200 		log_write(0, LOG_MAIN, "SA: Debug6: spamc read got newline, end of headers", buffer);
1201 	    }
1202 	    goto exit;
1203 	}
1204 
1205 	if (compare_header(buffer, "Message-Id: "))
1206 	{
1207 	    char *start;
1208 	    char *end;
1209 	    char *mesgid=NULL;
1210 
1211 	    start=strchr(buffer, '<');
1212 	    end=strchr(buffer, '>');
1213 
1214 	    if (start == NULL || end == NULL)
1215 	    {
1216 	        /* we keep the default mesgfn (unix date in seconds) */
1217 		if (SAEximDebug)
1218 		{
1219 		    log_write(0, LOG_MAIN, "SA: Debug: Could not get Message-Id from %s", buffer);
1220 		}
1221 	    }
1222 	    else if ((mesgid=string_copyn(start+1,end-start-1)) && mesgid[0])
1223 	    {
1224                 /* We replace the exim Message-ID with the one read from
1225                 the message * as we use this to detect dupes when we
1226                 send 45x and get the same * message multiple times */
1227 		safemesgid=cleanmsgid(mesgid, SAsafemesgidchars);
1228 		mesgfn=string_sprintf("%d_%s", time(NULL), safemesgid);
1229 
1230 		if (SAEximDebug > 5)
1231 		{
1232 		    log_write(0, LOG_MAIN, "SA: Debug6: Message-Id received and cleaned as: %s", safemesgid);
1233 		}
1234 	    }
1235 	    continue;
1236 	}
1237     }
1238 
1239     exit:
1240 
1241 
1242     if (isspam && SARewriteBody == 1)
1243     {
1244 	int line;
1245 
1246 	if (SAEximDebug)
1247 	{
1248 	    log_write(0, LOG_MAIN, "SA: Debug: SARewriteBody == 1, rewriting message body");
1249 	}
1250 
1251 	/* already read from fd? Better reset it... */
1252 	ret=lseek(fd, fdstart, SEEK_SET);
1253 	CHECKERR(ret, "lseek reset on spooled message", __LINE__);
1254 
1255 	line=1;
1256 	while ((fgets((char *)buffer,sizeof(buffera),(FILE *) readfh)) != NULL)
1257 	{
1258 	    if (SAEximDebug > 8)
1259 	    {
1260 		log_write(0, LOG_MAIN, "SA: Debug9: Read body from SA; line %d (read %d)", line, strlen(buffer));
1261 	    }
1262 
1263 	    stret=write(fd, buffer, strlen(buffer));
1264 	    CHECKERR(stret,string_sprintf("%s", "SA body write to msg"),__LINE__);
1265 	    if (SAEximDebug > 8)
1266 	    {
1267 		log_write(0, LOG_MAIN, "SA: Debug9: Wrote to msg; line %d (wrote %d)", line, ret);
1268 	    }
1269 	    if (buffer[strlen(buffer)-1] == '\n')
1270 	    {
1271 		line++;
1272 	    }
1273 	}
1274 
1275 
1276 	if (SAEximDebug > 1)
1277 	{
1278 	    log_write(0, LOG_MAIN, "SA: Debug2: body_linecount before SA: %d", body_linecount);
1279 	}
1280 
1281 	/* update global variable $body_linecount to reflect the new body size*/
1282 	if (body_linecount > 0) body_linecount = (line - 1); // Not updating if zero, indicating spool_wireformat
1283 
1284 	if (SAEximDebug > 1)
1285 	{
1286 	    log_write(0, LOG_MAIN, "SA: Debug2: body_linecount after SA: %d", body_linecount);
1287 	}
1288 
1289     }
1290 
1291     fclose((FILE *)readfh);
1292 
1293     afterscan=time(NULL);
1294     scantime=afterscan-beforescan;
1295 
1296     wait(&ret);
1297     signal(SIGCHLD, old_sigchld);
1298 
1299     if (ret)
1300     {
1301 	sprintf(buffer, "%d", ret);
1302 	PANIC(string_sprintf("wait on spamc child yielded, %s", buffer));
1303     }
1304 
1305     afterwait=time(NULL);
1306     fulltime=afterwait-beforescan;
1307 
1308     if(!SAaddSAEheaderBeforeSA)
1309     {
1310 	AddSAEheaders((char *)rcptlist, SAmaxrcptlistlength);
1311     }
1312 
1313     header_add(' ', "X-SA-Exim-Version: %s\n",version);
1314 
1315     if (gotsa == 0)
1316     {
1317 	header_add(' ', "X-SA-Exim-Scanned: No (on %s); Unknown failure\n", primary_hostname);
1318 	log_write(0, LOG_MAIN, "SA: Action: SA didn't successfully run against message, accepting (time: %d/%d secs | Message-Id: %s). %s", scantime, fulltime, safemesgid, mailinfo);
1319 	return LOCAL_SCAN_ACCEPT;
1320     }
1321 
1322     header_add(' ', "X-SA-Exim-Scanned: Yes (on %s)\n", primary_hostname);
1323 
1324     if (spamstatus == NULL)
1325     {
1326 	spamstatus = (char *) nospamstatus;
1327     }
1328     if (isspam)
1329     {
1330 	int dorej=1;
1331 	int doteergrube=0;
1332 
1333 	if (SAEximRejCond[0] != '1' || SAEximRejCond[1] != 0)
1334 	{
1335 	    expand=expand_string(SAEximRejCond);
1336 	    if (expand == NULL)
1337 	    {
1338 		PANIC(string_sprintf("SAEximRejCond expansion failure on %s", SAEximRejCond));
1339 	    }
1340 
1341 	    if (SAEximDebug)
1342 	    {
1343 		log_write(0, LOG_MAIN, "SA: Debug: SAEximRejCond expand returned: '%s'", expand);
1344 	    }
1345 
1346 	    if (expand[0] == 0 || (expand[0] == '0' && expand[1] == 0))
1347 	    {
1348 		log_write(0, LOG_MAIN, "SA: Notice: SAEximRejCond expanded to false, not applying reject rules");
1349 		dorej=0;
1350 	    }
1351 	}
1352 
1353 	if (dorej && spamvalue >= SAteergrubethreshold)
1354 	{
1355 	    doteergrube=1;
1356 	    if (SAteergrubecond[0] != '1' || SAteergrubecond[1] != 0)
1357 	    {
1358 		expand=expand_string(SAteergrubecond);
1359 		if (expand == NULL)
1360 		{
1361 		    PANIC(string_sprintf("SAteergrubecond expansion failure on %s", SAteergrubecond));
1362 		}
1363 
1364 		if (SAEximDebug)
1365 		{
1366 		    log_write(0, LOG_MAIN, "SA: Debug: SAteergrubecond expand returned: '%s'", expand);
1367 		}
1368 
1369 		if (expand[0] == 0 || (expand[0] == '0' && expand[1] == 0))
1370 		{
1371 		    log_write(0, LOG_MAIN, "SA: Notice: SAteergrubecond expanded to false, not teergrubing known peer");
1372 		    doteergrube=0;
1373 		}
1374 	    }
1375 	}
1376 
1377 	if (dorej && doteergrube)
1378 	{
1379 	    char *teergrubewaitstr;
1380 	    teergrubewaitstr=string_sprintf(SAmsgteergrubewait, spamstatus);
1381 
1382 	    /* By default, we'll only save temp bounces by message ID so
1383 	     * that when the same message is submitted several times, we
1384 	     * overwrite the same file on disk and not create a brand new
1385 	     * one every single time */
1386 	    if (SAteergrubeoverwrite)
1387 	    {
1388 		ret=savemail(fd, fdstart, SAteergrubesave, "SAteergrubesave", safemesgid, SAmaxarchivebody, SAteergrubeSavCond);
1389 		CHECKERR(ret,where,line);
1390 	    }
1391 	    else
1392 	    {
1393 		ret=savemail(fd, fdstart, SAteergrubesave, "SAteergrubesave", (char *)mesgfn, SAmaxarchivebody, SAteergrubeSavCond);
1394 		CHECKERR(ret,where,line);
1395 	    }
1396 
1397 	    spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SAteergrubethreshold);
1398 	    /* Exim might want to stop us if we run for too long, but that's
1399 	     * exactly what we're trying to do, so let's override that */
1400 	    alarm(0);
1401 
1402 	    for (i=0;i<SAteergrubetime/10;i++)
1403 	    {
1404 		smtp_printf("451-%s\r\n", teergrubewaitstr);
1405 		ret=smtp_fflush();
1406 		if (ret != 0)
1407 		{
1408 		    log_write(0, LOG_MAIN | LOG_REJECT, "SA: Action: teergrubed sender for %d secs until it closed the connection: %s (scanned in %d/%d secs | Message-Id: %s). %s", i*10, spamstatus, scantime, fulltime, safemesgid, mailinfo);
1409 		    /* The other side closed the connection, nothing to print */
1410 		    *return_text="";
1411 		    return LOCAL_SCAN_TEMPREJECT_NOLOGHDR;
1412 		}
1413 		sleep(10);
1414 	    }
1415 
1416 	    log_write(0, LOG_MAIN | LOG_REJECT, "SA: Action: teergrubed sender until full configured duration of %d secs: %s (scanned in %d/%d secs | Message-Id: %s). %s", SAteergrubetime, spamstatus, scantime, fulltime, safemesgid, mailinfo);
1417 	    *return_text=string_sprintf(SAmsgteergruberej, spamstatus);
1418 	    return LOCAL_SCAN_TEMPREJECT_NOLOGHDR;
1419 	}
1420 	else if (dorej && spamvalue >= SAdevnullthreshold)
1421 	{
1422 	    ret=savemail(fd, fdstart, SAdevnullsave, "SAdevnullsave", (char *)mesgfn, SAmaxarchivebody, SAdevnullSavCond);
1423 	    CHECKERR(ret,where,line);
1424 
1425 	    recipients_count=0;
1426 	    spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SAdevnullthreshold);
1427 	    log_write(0, LOG_REJECT | LOG_MAIN, "SA: Action: silently tossed message: %s (scanned in %d/%d secs | Message-Id: %s). %s", spamstatus, scantime, fulltime, safemesgid, mailinfo);
1428 	    return LOCAL_SCAN_ACCEPT;
1429 	}
1430 	else if (dorej && spamvalue >= SApermrejectthreshold)
1431 	{
1432 	    ret=savemail(fd, fdstart, SApermrejectsave, "SApermrejectsave", (char *)mesgfn, SAmaxarchivebody, SApermrejectSavCond);
1433 	    CHECKERR(ret,where,line);
1434 
1435 	    spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SApermrejectthreshold);
1436 	    log_write(0, LOG_MAIN | LOG_REJECT, "SA: Action: permanently rejected message: %s (scanned in %d/%d secs | Message-Id: %s). %s", spamstatus, scantime, fulltime, safemesgid, mailinfo);
1437 	    *return_text=string_sprintf(SAmsgpermrej, spamstatus);
1438 	    return LOCAL_SCAN_REJECT_NOLOGHDR;
1439 	}
1440 	else if (dorej && spamvalue >= SAtemprejectthreshold)
1441 	{
1442 	    /* Yeah, gotos are harmful, but that'd be a function with a lot
1443 	     * of options to send, so, here's a small shortcut */
1444 	    goto dotempreject;
1445 	}
1446 	else
1447 	{
1448 	    ret=savemail(fd, fdstart, SAspamacceptsave, "SAspamacceptsave", (char *)mesgfn, SAmaxarchivebody, SAspamacceptSavCond);
1449 	    CHECKERR(ret,where,line);
1450 	    log_write(0, LOG_MAIN, "SA: Action: flagged as Spam but accepted: %s (scanned in %d/%d secs | Message-Id: %s). %s", spamstatus, scantime, fulltime, safemesgid, mailinfo);
1451 	    return LOCAL_SCAN_ACCEPT;
1452 	}
1453     }
1454     else
1455     {
1456 	/* This is an exception to the rule, for grey listing, we allow for
1457 	 * sending back a tempreject on SA scores that aren't considered as
1458 	 * spam (greylisting is now done directly in spamassassin though */
1459 	if (spamvalue >= SAtemprejectthreshold)
1460 	{
1461 	    dotempreject:
1462 
1463 	    /* By default, we'll only save temp bounces by message ID so
1464 	     * that when the same message is submitted several times, we
1465 	     * overwrite the same file on disk and not create a brand new
1466 	     * one every single time */
1467 	    if (SAtemprejectoverwrite)
1468 	    {
1469 		ret=savemail(fd, fdstart, SAtemprejectsave, "SAtemprejectsave", safemesgid, SAmaxarchivebody, SAtemprejectSavCond);
1470 		CHECKERR(ret,where,line);
1471 	    }
1472 	    else
1473 	    {
1474 		ret=savemail(fd, fdstart, SAtemprejectsave, "SAtemprejectsave", (char *)mesgfn, SAmaxarchivebody, SAtemprejectSavCond);
1475 		CHECKERR(ret,where,line);
1476 	    }
1477 
1478 	    spamstatus=string_sprintf("%s trigger=%.1f", spamstatus, SAtemprejectthreshold);
1479 	    log_write(0, LOG_MAIN | LOG_REJECT, "SA: Action: temporarily rejected message: %s (scanned in %d/%d secs | Message-Id: %s). %s", spamstatus, scantime, fulltime, safemesgid, mailinfo);
1480 	    *return_text=string_sprintf(SAmsgtemprej, spamstatus);
1481 	    return LOCAL_SCAN_TEMPREJECT_NOLOGHDR;
1482 	}
1483 	else
1484 	{
1485 	    ret=savemail(fd, fdstart, SAnotspamsave, "SAnotspamsave", (char *)mesgfn, SAmaxarchivebody, SAnotspamSavCond);
1486 	    CHECKERR(ret,where,line);
1487 	    log_write(0, LOG_MAIN, "SA: Action: scanned but message isn't spam: %s (scanned in %d/%d secs | Message-Id: %s). %s", spamstatus, scantime, fulltime, safemesgid, mailinfo);
1488 	    return LOCAL_SCAN_ACCEPT;
1489 	}
1490     }
1491 
1492 
1493 
1494     errexit:
1495     if (SAtemprejectonerror)
1496     {
1497 	log_write(0, LOG_MAIN | LOG_PANIC, "SA: PANIC: Unexpected error on %s, file "__FILE__", line %d: %s", where, line-1, strerror(errno));
1498     }
1499     else
1500     {
1501 	log_write(0, LOG_MAIN, "SA: PANIC: Unexpected error on %s (but message was accepted), file "__FILE__", line %d: %s", where, line-1, strerror(errno));
1502     }
1503 
1504     header_add(' ', "X-SA-Exim-Scanned: No (on %s); Exit with error (see exim mainlog)\n", primary_hostname);
1505 
1506     ret=savemail(fd, fdstart, SAerrorsave, "SAerrorsave", (char *)mesgfn, SAerrmaxarchivebody, SAerrorSavCond);
1507     if (ret < 0)
1508     {
1509 	log_write(0, LOG_MAIN | LOG_PANIC, "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));
1510     }
1511 
1512     if (SAtemprejectonerror)
1513     {
1514 	*return_text=SAmsgerror;
1515 	return LOCAL_SCAN_TEMPREJECT_NOLOGHDR;
1516     }
1517     else
1518     {
1519 	return LOCAL_SCAN_ACCEPT;
1520     }
1521 
1522 
1523     panicexit:
1524     if (SAtemprejectonerror)
1525     {
1526 	log_write(0, LOG_MAIN | LOG_PANIC, "SA: PANIC: %s", panicerror);
1527     }
1528     else
1529     {
1530 	log_write(0, LOG_MAIN | LOG_PANIC, "SA: PANIC: %s (but message was accepted)", panicerror);
1531     }
1532 
1533     header_add(' ', "X-SA-Exim-Scanned: No (on %s); Panic (see exim mainlog)\n", primary_hostname);
1534 
1535     ret=savemail(fd, fdstart, SAerrorsave, "SAerrorsave", (char *)mesgfn, SAerrmaxarchivebody, SAerrorSavCond);
1536     if (ret < 0)
1537     {
1538 	log_write(0, LOG_MAIN | LOG_PANIC , "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));
1539     }
1540 
1541     if (SAtemprejectonerror)
1542     {
1543 	*return_text=SAmsgerror;
1544 	return LOCAL_SCAN_TEMPREJECT_NOLOGHDR;
1545     }
1546     else
1547     {
1548 	return LOCAL_SCAN_ACCEPT;
1549     }
1550 }
1551