1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4 
5 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
6  * License: GPL
7  * Copyright (c) The Exim Maintainers 2016 - 2020
8  */
9 
10 /* Code for calling spamassassin's spamd. Called from acl.c. */
11 
12 #include "exim.h"
13 #ifdef WITH_CONTENT_SCAN
14 #include "spam.h"
15 
16 uschar spam_score_buffer[16];
17 uschar spam_score_int_buffer[16];
18 uschar spam_bar_buffer[128];
19 uschar spam_action_buffer[32];
20 uschar spam_report_buffer[32600];
21 uschar * prev_user_name = NULL;
22 int spam_ok = 0;
23 int spam_rc = 0;
24 uschar *prev_spamd_address_work = NULL;
25 
26 static const uschar * loglabel = US"spam acl condition:";
27 
28 
29 static int
spamd_param_init(spamd_address_container * spamd)30 spamd_param_init(spamd_address_container *spamd)
31 {
32 /* default spamd server weight, time and priority value */
33 spamd->is_rspamd = FALSE;
34 spamd->is_failed = FALSE;
35 spamd->weight = SPAMD_WEIGHT;
36 spamd->timeout = SPAMD_TIMEOUT;
37 spamd->retry = 0;
38 spamd->priority = SPAMD_PRIORITY;
39 return 0;
40 }
41 
42 
43 static int
spamd_param(const uschar * param,spamd_address_container * spamd)44 spamd_param(const uschar * param, spamd_address_container * spamd)
45 {
46 static int timesinceday = -1;
47 const uschar * s;
48 const uschar * name;
49 
50 /*XXX more clever parsing could discard embedded spaces? */
51 
52 if (sscanf(CCS param, "pri=%u", &spamd->priority))
53   return 0; /* OK */
54 
55 if (sscanf(CCS param, "weight=%u", &spamd->weight))
56   {
57   if (spamd->weight == 0) /* this server disabled: skip it */
58     return 1;
59   return 0; /* OK */
60   }
61 
62 if (Ustrncmp(param, "time=", 5) == 0)
63   {
64   unsigned int start_h = 0, start_m = 0, start_s = 0;
65   unsigned int end_h = 24, end_m = 0, end_s = 0;
66   unsigned int time_start, time_end;
67   const uschar * end_string;
68 
69   name = US"time";
70   s = param+5;
71   if ((end_string = Ustrchr(s, '-')))
72     {
73     end_string++;
74     if (  sscanf(CS end_string, "%u.%u.%u", &end_h,   &end_m,   &end_s)   == 0
75        || sscanf(CS s,          "%u.%u.%u", &start_h, &start_m, &start_s) == 0
76        )
77       goto badval;
78     }
79   else
80     goto badval;
81 
82   if (timesinceday < 0)
83     {
84     time_t now = time(NULL);
85     struct tm *tmp = localtime(&now);
86     timesinceday = tmp->tm_hour*3600 + tmp->tm_min*60 + tmp->tm_sec;
87     }
88 
89   time_start = start_h*3600 + start_m*60 + start_s;
90   time_end = end_h*3600 + end_m*60 + end_s;
91 
92   if (timesinceday < time_start || timesinceday >= time_end)
93     return 1; /* skip spamd server */
94 
95   return 0; /* OK */
96   }
97 
98 if (Ustrcmp(param, "variant=rspamd") == 0)
99   {
100   spamd->is_rspamd = TRUE;
101   return 0;
102   }
103 
104 if (Ustrncmp(param, "tmo=", 4) == 0)
105   {
106   int sec = readconf_readtime((s = param+4), '\0', FALSE);
107   name = US"timeout";
108   if (sec < 0)
109     goto badval;
110   spamd->timeout = sec;
111   return 0;
112   }
113 
114 if (Ustrncmp(param, "retry=", 6) == 0)
115   {
116   int sec = readconf_readtime((s = param+6), '\0', FALSE);
117   name = US"retry";
118   if (sec < 0)
119     goto badval;
120   spamd->retry = sec;
121   return 0;
122   }
123 
124 log_write(0, LOG_MAIN, "%s warning - invalid spamd parameter: '%s'",
125   loglabel, param);
126 return -1; /* syntax error */
127 
128 badval:
129   log_write(0, LOG_MAIN,
130     "%s warning - invalid spamd %s value: '%s'", loglabel, name, s);
131   return -1; /* syntax error */
132 }
133 
134 
135 static int
spamd_get_server(spamd_address_container ** spamds,int num_servers)136 spamd_get_server(spamd_address_container ** spamds, int num_servers)
137 {
138 unsigned int i;
139 spamd_address_container * sd;
140 long weights;
141 unsigned pri;
142 
143 /* speedup, if we have only 1 server */
144 if (num_servers == 1)
145   return (spamds[0]->is_failed ? -1 : 0);
146 
147 /* scan for highest pri */
148 for (pri = 0, i = 0; i < num_servers; i++)
149   {
150   sd = spamds[i];
151   if (!sd->is_failed && sd->priority > pri) pri = sd->priority;
152   }
153 
154 /* get sum of weights */
155 for (weights = 0, i = 0; i < num_servers; i++)
156   {
157   sd = spamds[i];
158   if (!sd->is_failed && sd->priority == pri) weights += sd->weight;
159   }
160 if (weights == 0)	/* all servers failed */
161   return -1;
162 
163 for (long rnd = random_number(weights), i = 0; i < num_servers; i++)
164   {
165   sd = spamds[i];
166   if (!sd->is_failed && sd->priority == pri)
167     if ((rnd -= sd->weight) < 0)
168       return i;
169   }
170 
171 log_write(0, LOG_MAIN|LOG_PANIC,
172   "%s unknown error (memory/cpu corruption?)", loglabel);
173 return -1;
174 }
175 
176 
177 int
spam(const uschar ** listptr)178 spam(const uschar **listptr)
179 {
180 int sep = 0;
181 const uschar *list = *listptr;
182 uschar *user_name;
183 unsigned long mbox_size;
184 FILE *mbox_file;
185 client_conn_ctx spamd_cctx = {.sock = -1};
186 uschar spamd_buffer[32600];
187 int i, j, offset, result;
188 uschar spamd_version[8];
189 uschar spamd_short_result[8];
190 uschar spamd_score_char;
191 double spamd_threshold, spamd_score, spamd_reject_score;
192 int spamd_report_offset;
193 uschar *p,*q;
194 int override = 0;
195 time_t start;
196 size_t read, wrote;
197 uschar *spamd_address_work;
198 spamd_address_container * sd;
199 
200 /* stop compiler warning */
201 result = 0;
202 
203 /* find the username from the option list */
204 if (!(user_name = string_nextinlist(&list, &sep, NULL, 0)))
205   {
206   /* no username given, this means no scanning should be done */
207   return FAIL;
208   }
209 
210 /* if username is "0" or "false", do not scan */
211 if (Ustrcmp(user_name, "0") == 0 || strcmpic(user_name, US"false") == 0)
212   return FAIL;
213 
214 /* if there is an additional option, check if it is "true" */
215 if (strcmpic(list,US"true") == 0)
216   /* in that case, always return true later */
217   override = 1;
218 
219 /* expand spamd_address if needed */
220 if (*spamd_address != '$')
221   spamd_address_work = spamd_address;
222 else if (!(spamd_address_work = expand_string(spamd_address)))
223   {
224   log_write(0, LOG_MAIN|LOG_PANIC,
225     "%s spamd_address starts with $, but expansion failed: %s",
226     loglabel, expand_string_message);
227   return DEFER;
228   }
229 
230 DEBUG(D_acl) debug_printf_indent("spamd: addrlist '%s'\n", spamd_address_work);
231 
232 /* check if previous spamd_address was expanded and has changed. dump cached results if so */
233 if (  spam_ok
234    && prev_spamd_address_work != NULL
235    && Ustrcmp(prev_spamd_address_work, spamd_address_work) != 0
236    )
237   spam_ok = 0;
238 
239 /* if we scanned for this username last time, just return */
240 if (spam_ok && Ustrcmp(prev_user_name, user_name) == 0)
241   return override ? OK : spam_rc;
242 
243 /* make sure the eml mbox file is spooled up */
244 
245 if (!(mbox_file = spool_mbox(&mbox_size, NULL, NULL)))
246   {								/* error while spooling */
247   log_write(0, LOG_MAIN|LOG_PANIC,
248 	 "%s error while creating mbox spool file", loglabel);
249   return DEFER;
250   }
251 
252 start = time(NULL);
253 
254   {
255   int num_servers = 0;
256   int current_server;
257   uschar * address;
258   const uschar * spamd_address_list_ptr = spamd_address_work;
259   spamd_address_container * spamd_address_vector[32];
260 
261   /* Check how many spamd servers we have
262      and register their addresses */
263   sep = 0;				/* default colon-sep */
264   while ((address = string_nextinlist(&spamd_address_list_ptr, &sep, NULL, 0)))
265     {
266     const uschar * sublist;
267     int sublist_sep = -(int)' ';	/* default space-sep */
268     unsigned args;
269     uschar * s;
270 
271     DEBUG(D_acl) debug_printf_indent("spamd: addr entry '%s'\n", address);
272     sd = store_get(sizeof(spamd_address_container), FALSE);
273 
274     for (sublist = address, args = 0, spamd_param_init(sd);
275 	 (s = string_nextinlist(&sublist, &sublist_sep, NULL, 0));
276 	 args++
277 	 )
278       {
279 	DEBUG(D_acl) debug_printf_indent("spamd:  addr parm '%s'\n", s);
280 	switch (args)
281 	{
282 	case 0:   sd->hostspec = s;
283 		  if (*s == '/') args++;	/* local; no port */
284 		  break;
285 	case 1:   sd->hostspec = string_sprintf("%s %s", sd->hostspec, s);
286 		  break;
287 	default:  spamd_param(s, sd);
288 		  break;
289 	}
290       }
291     if (args < 2)
292       {
293       log_write(0, LOG_MAIN,
294 	"%s warning - invalid spamd address: '%s'", loglabel, address);
295       continue;
296       }
297 
298     spamd_address_vector[num_servers] = sd;
299     if (++num_servers > 31)
300       break;
301     }
302 
303   /* check if we have at least one server */
304   if (!num_servers)
305     {
306     log_write(0, LOG_MAIN|LOG_PANIC,
307        "%s no useable spamd server addresses in spamd_address configuration option.",
308        loglabel);
309     goto defer;
310     }
311 
312   current_server = spamd_get_server(spamd_address_vector, num_servers);
313   sd = spamd_address_vector[current_server];
314   for(;;)
315     {
316     uschar * errstr;
317 
318     DEBUG(D_acl) debug_printf_indent("spamd: trying server %s\n", sd->hostspec);
319 
320     for (;;)
321       {
322       /*XXX could potentially use TFO early-data here */
323       if (  (spamd_cctx.sock = ip_streamsocket(sd->hostspec, &errstr, 5, NULL)) >= 0
324          || sd->retry <= 0
325 	 )
326 	break;
327       DEBUG(D_acl) debug_printf_indent("spamd: server %s: retry conn\n", sd->hostspec);
328       while (sd->retry > 0) sd->retry = sleep(sd->retry);
329       }
330     if (spamd_cctx.sock >= 0)
331       break;
332 
333     log_write(0, LOG_MAIN, "%s spamd: %s", loglabel, errstr);
334     sd->is_failed = TRUE;
335 
336     current_server = spamd_get_server(spamd_address_vector, num_servers);
337     if (current_server < 0)
338       {
339       log_write(0, LOG_MAIN|LOG_PANIC, "%s all spamd servers failed", loglabel);
340       goto defer;
341       }
342     sd = spamd_address_vector[current_server];
343     }
344   }
345 
346 (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
347 /* now we are connected to spamd on spamd_cctx.sock */
348 if (sd->is_rspamd)
349   {
350   gstring * req_str;
351   const uschar * s;
352 
353   req_str = string_append(NULL, 8,
354     "CHECK RSPAMC/1.3\r\nContent-length: ", string_sprintf("%lu\r\n", mbox_size),
355     "Queue-Id: ", message_id,
356     "\r\nFrom: <", sender_address,
357     ">\r\nRecipient-Number: ", string_sprintf("%d\r\n", recipients_count));
358 
359   for (int i = 0; i < recipients_count; i++)
360     req_str = string_append(req_str, 3,
361       "Rcpt: <", recipients_list[i].address, ">\r\n");
362   if ((s = expand_string(US"$sender_helo_name")) && *s)
363     req_str = string_append(req_str, 3, "Helo: ", s, "\r\n");
364   if ((s = expand_string(US"$sender_host_name")) && *s)
365     req_str = string_append(req_str, 3, "Hostname: ", s, "\r\n");
366   if (sender_host_address)
367     req_str = string_append(req_str, 3, "IP: ", sender_host_address, "\r\n");
368   if ((s = expand_string(US"$authenticated_id")) && *s)
369     req_str = string_append(req_str, 3, "User: ", s, "\r\n");
370   req_str = string_catn(req_str, US"\r\n", 2);
371   wrote = send(spamd_cctx.sock, req_str->s, req_str->ptr, 0);
372   }
373 else
374   {				/* spamassassin variant */
375   int n;
376   uschar * s = string_sprintf(
377 	  "REPORT SPAMC/1.2\r\nUser: %s\r\nContent-length: %ld\r\n\r\n%n",
378 	  user_name, mbox_size, &n);
379   /* send our request */
380   wrote = send(spamd_cctx.sock, s, n, 0);
381   }
382 
383 if (wrote == -1)
384   {
385   (void)close(spamd_cctx.sock);
386   log_write(0, LOG_MAIN|LOG_PANIC,
387        "%s spamd %s send failed: %s", loglabel, callout_address, strerror(errno));
388   goto defer;
389   }
390 
391 /* now send the file */
392 /* spamd sometimes accepts connections but doesn't read data off the connection.
393 We make the file descriptor non-blocking so that the write will only write
394 sufficient data without blocking and we poll the descriptor to make sure that we
395 can write without blocking.  Short writes are gracefully handled and if the
396 whole transaction takes too long it is aborted.
397 
398 Note: poll() is not supported in OSX 10.2 and is reported to be broken in more
399       recent versions (up to 10.4). Workaround using select() removed 2021/11 (jgh).
400  */
401 #ifdef NO_POLL_H
402 # error Need poll(2) support
403 #endif
404 
405 (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
406 do
407   {
408   read = fread(spamd_buffer,1,sizeof(spamd_buffer),mbox_file);
409   if (read > 0)
410     {
411     offset = 0;
412 again:
413     result = poll_one_fd(spamd_cctx.sock, POLLOUT, 1000);
414     if (result == -1 && errno == EINTR)
415       goto again;
416     else if (result < 1)
417       {
418       if (result == -1)
419 	log_write(0, LOG_MAIN|LOG_PANIC,
420 	  "%s %s on spamd %s socket", loglabel, callout_address, strerror(errno));
421       else
422 	{
423 	if (time(NULL) - start < sd->timeout)
424 	  goto again;
425 	log_write(0, LOG_MAIN|LOG_PANIC,
426 	  "%s timed out writing spamd %s, socket", loglabel, callout_address);
427 	}
428       (void)close(spamd_cctx.sock);
429       goto defer;
430       }
431 
432     wrote = send(spamd_cctx.sock,spamd_buffer + offset,read - offset,0);
433     if (wrote == -1)
434       {
435       log_write(0, LOG_MAIN|LOG_PANIC,
436 	  "%s %s on spamd %s socket", loglabel, callout_address, strerror(errno));
437       (void)close(spamd_cctx.sock);
438       goto defer;
439       }
440     if (offset + wrote != read)
441       {
442       offset += wrote;
443       goto again;
444       }
445     }
446   }
447 while (!feof(mbox_file) && !ferror(mbox_file));
448 
449 if (ferror(mbox_file))
450   {
451   log_write(0, LOG_MAIN|LOG_PANIC,
452     "%s error reading spool file: %s", loglabel, strerror(errno));
453   (void)close(spamd_cctx.sock);
454   goto defer;
455   }
456 
457 (void)fclose(mbox_file);
458 
459 /* we're done sending, close socket for writing */
460 if (!sd->is_rspamd)
461   shutdown(spamd_cctx.sock,SHUT_WR);
462 
463 /* read spamd response using what's left of the timeout.  */
464 memset(spamd_buffer, 0, sizeof(spamd_buffer));
465 offset = 0;
466 while ((i = ip_recv(&spamd_cctx,
467 		   spamd_buffer + offset,
468 		   sizeof(spamd_buffer) - offset - 1,
469 		   sd->timeout + start)) > 0)
470   offset += i;
471 spamd_buffer[offset] = '\0';	/* guard byte */
472 
473 /* error handling */
474 if (i <= 0 && errno != 0)
475   {
476   log_write(0, LOG_MAIN|LOG_PANIC,
477        "%s error reading from spamd %s, socket: %s", loglabel, callout_address, strerror(errno));
478   (void)close(spamd_cctx.sock);
479   return DEFER;
480   }
481 
482 /* reading done */
483 (void)close(spamd_cctx.sock);
484 
485 if (sd->is_rspamd)
486   {				/* rspamd variant of reply */
487   int r;
488   if (  (r = sscanf(CS spamd_buffer,
489 	  "RSPAMD/%7s 0 EX_OK\r\nMetric: default; %7s %lf / %lf / %lf\r\n%n",
490 	  spamd_version, spamd_short_result, &spamd_score, &spamd_threshold,
491 	  &spamd_reject_score, &spamd_report_offset)) != 5
492      || spamd_report_offset >= offset		/* verify within buffer */
493      )
494     {
495     log_write(0, LOG_MAIN|LOG_PANIC,
496 	      "%s cannot parse spamd %s, output: %d", loglabel, callout_address, r);
497     return DEFER;
498     }
499   /* now parse action */
500   p = &spamd_buffer[spamd_report_offset];
501 
502   if (Ustrncmp(p, "Action: ", sizeof("Action: ") - 1) == 0)
503     {
504     p += sizeof("Action: ") - 1;
505     q = &spam_action_buffer[0];
506     while (*p && *p != '\r' && (q - spam_action_buffer) < sizeof(spam_action_buffer) - 1)
507       *q++ = *p++;
508     *q = '\0';
509     }
510   }
511 else
512   {				/* spamassassin */
513   /* dig in the spamd output and put the report in a multiline header,
514   if requested */
515   if (sscanf(CS spamd_buffer,
516        "SPAMD/%7s 0 EX_OK\r\nContent-length: %*u\r\n\r\n%lf/%lf\r\n%n",
517        spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3)
518     {
519       /* try to fall back to pre-2.50 spamd output */
520       if (sscanf(CS spamd_buffer,
521 	   "SPAMD/%7s 0 EX_OK\r\nSpam: %*s ; %lf / %lf\r\n\r\n%n",
522 	   spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3)
523 	{
524 	log_write(0, LOG_MAIN|LOG_PANIC,
525 		  "%s cannot parse spamd %s output", loglabel, callout_address);
526 	return DEFER;
527 	}
528     }
529 
530   Ustrcpy(spam_action_buffer,
531     spamd_score >= spamd_threshold ? US"reject" : US"no action");
532   }
533 
534 /* Create report. Since this is a multiline string,
535 we must hack it into shape first */
536 p = &spamd_buffer[spamd_report_offset];
537 q = spam_report_buffer;
538 while (*p != '\0')
539   {
540   /* skip \r */
541   if (*p == '\r')
542     {
543     p++;
544     continue;
545     }
546   *q++ = *p;
547   if (*p++ == '\n')
548     {
549     /* add an extra space after the newline to ensure
550     that it is treated as a header continuation line */
551     *q++ = ' ';
552     }
553   }
554 /* NULL-terminate */
555 *q-- = '\0';
556 /* cut off trailing leftovers */
557 while (*q <= ' ')
558   *q-- = '\0';
559 
560 spam_report = spam_report_buffer;
561 spam_action = spam_action_buffer;
562 
563 /* create spam bar */
564 spamd_score_char = spamd_score > 0 ? '+' : '-';
565 j = abs((int)(spamd_score));
566 i = 0;
567 if (j != 0)
568   while ((i < j) && (i <= MAX_SPAM_BAR_CHARS))
569      spam_bar_buffer[i++] = spamd_score_char;
570 else
571   {
572   spam_bar_buffer[0] = '/';
573   i = 1;
574   }
575 spam_bar_buffer[i] = '\0';
576 spam_bar = spam_bar_buffer;
577 
578 /* create "float" spam score */
579 (void)string_format(spam_score_buffer, sizeof(spam_score_buffer),
580 	"%.1f", spamd_score);
581 spam_score = spam_score_buffer;
582 
583 /* create "int" spam score */
584 j = (int)((spamd_score + 0.001)*10);
585 (void)string_format(spam_score_int_buffer, sizeof(spam_score_int_buffer),
586 	"%d", j);
587 spam_score_int = spam_score_int_buffer;
588 
589 /* compare threshold against score */
590 spam_rc = spamd_score >= spamd_threshold
591   ? OK	/* spam as determined by user's threshold */
592   : FAIL;	/* not spam */
593 
594 /* remember expanded spamd_address if needed */
595 if (spamd_address_work != spamd_address)
596   prev_spamd_address_work = string_copy(spamd_address_work);
597 
598 /* remember user name and "been here" for it */
599 prev_user_name = user_name;
600 spam_ok = 1;
601 
602 return override
603   ? OK		/* always return OK, no matter what the score */
604   : spam_rc;
605 
606 defer:
607   (void)fclose(mbox_file);
608   return DEFER;
609 }
610 
611 #endif
612 /* vi: aw ai sw=2
613 */
614