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