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