Check query strings of query-style lookups for quoting. Bug 2850
[exim.git] / src / src / spam.c
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 - 2021
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
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
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
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
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), GET_UNTAINTED);
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 */