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