1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
6 * Copyright (c) The Exim Maintainers 2016 - 2022
7 * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
9 * SPDX-License-Identifier: GPL-2.0-only
12 /* Code for calling spamassassin's spamd. Called from acl.c. */
15 #ifdef WITH_CONTENT_SCAN
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;
26 uschar *prev_spamd_address_work = NULL;
28 static const uschar * loglabel = US"spam acl condition:";
32 spamd_param_init(spamd_address_container *spamd)
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;
40 spamd->priority = SPAMD_PRIORITY;
46 spamd_param(const uschar * param, spamd_address_container * spamd)
48 static int timesinceday = -1;
52 /*XXX more clever parsing could discard embedded spaces? */
54 if (sscanf(CCS param, "pri=%u", &spamd->priority))
57 if (sscanf(CCS param, "weight=%u", &spamd->weight))
59 if (spamd->weight == 0) /* this server disabled: skip it */
64 if (Ustrncmp(param, "time=", 5) == 0)
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;
73 if ((end_string = Ustrchr(s, '-')))
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
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;
91 time_start = start_h*3600 + start_m*60 + start_s;
92 time_end = end_h*3600 + end_m*60 + end_s;
94 if (timesinceday < time_start || timesinceday >= time_end)
95 return 1; /* skip spamd server */
100 if (Ustrcmp(param, "variant=rspamd") == 0)
102 spamd->is_rspamd = TRUE;
106 if (Ustrncmp(param, "tmo=", 4) == 0)
108 int sec = readconf_readtime((s = param+4), '\0', FALSE);
112 spamd->timeout = sec;
116 if (Ustrncmp(param, "retry=", 6) == 0)
118 int sec = readconf_readtime((s = param+6), '\0', FALSE);
126 log_write(0, LOG_MAIN, "%s warning - invalid spamd parameter: '%s'",
128 return -1; /* syntax error */
131 log_write(0, LOG_MAIN,
132 "%s warning - invalid spamd %s value: '%s'", loglabel, name, s);
133 return -1; /* syntax error */
138 spamd_get_server(spamd_address_container ** spamds, int num_servers)
141 spamd_address_container * sd;
145 /* speedup, if we have only 1 server */
146 if (num_servers == 1)
147 return (spamds[0]->is_failed ? -1 : 0);
149 /* scan for highest pri */
150 for (pri = 0, i = 0; i < num_servers; i++)
153 if (!sd->is_failed && sd->priority > pri) pri = sd->priority;
156 /* get sum of weights */
157 for (weights = 0, i = 0; i < num_servers; i++)
160 if (!sd->is_failed && sd->priority == pri) weights += sd->weight;
162 if (weights == 0) /* all servers failed */
165 for (long rnd = random_number(weights), i = 0; i < num_servers; i++)
168 if (!sd->is_failed && sd->priority == pri)
169 if ((rnd -= sd->weight) < 0)
173 log_write(0, LOG_MAIN|LOG_PANIC,
174 "%s unknown error (memory/cpu corruption?)", loglabel);
180 spam(const uschar **listptr)
183 const uschar *list = *listptr;
185 unsigned long mbox_size;
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;
199 uschar *spamd_address_work;
200 spamd_address_container * sd;
202 /* stop compiler warning */
205 /* find the username from the option list */
206 if (!(user_name = string_nextinlist(&list, &sep, NULL, 0)))
208 /* no username given, this means no scanning should be done */
212 /* if username is "0" or "false", do not scan */
213 if (Ustrcmp(user_name, "0") == 0 || strcmpic(user_name, US"false") == 0)
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 */
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)))
226 log_write(0, LOG_MAIN|LOG_PANIC,
227 "%s spamd_address starts with $, but expansion failed: %s",
228 loglabel, expand_string_message);
232 DEBUG(D_acl) debug_printf_indent("spamd: addrlist '%s'\n", spamd_address_work);
234 /* check if previous spamd_address was expanded and has changed. dump cached results if so */
236 && prev_spamd_address_work != NULL
237 && Ustrcmp(prev_spamd_address_work, spamd_address_work) != 0
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;
245 /* make sure the eml mbox file is spooled up */
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);
260 const uschar * spamd_address_list_ptr = spamd_address_work;
261 spamd_address_container * spamd_address_vector[32];
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)))
268 const uschar * sublist;
269 int sublist_sep = -(int)' '; /* default space-sep */
273 DEBUG(D_acl) debug_printf_indent("spamd: addr entry '%s'\n", address);
274 sd = store_get(sizeof(spamd_address_container), GET_UNTAINTED);
276 for (sublist = address, args = 0, spamd_param_init(sd);
277 (s = string_nextinlist(&sublist, &sublist_sep, NULL, 0));
281 DEBUG(D_acl) debug_printf_indent("spamd: addr parm '%s'\n", s);
284 case 0: sd->hostspec = s;
285 if (*s == '/') args++; /* local; no port */
287 case 1: sd->hostspec = string_sprintf("%s %s", sd->hostspec, s);
289 default: spamd_param(s, sd);
295 log_write(0, LOG_MAIN,
296 "%s warning - invalid spamd address: '%s'", loglabel, address);
300 spamd_address_vector[num_servers] = sd;
301 if (++num_servers > 31)
305 /* check if we have at least one server */
308 log_write(0, LOG_MAIN|LOG_PANIC,
309 "%s no useable spamd server addresses in spamd_address configuration option.",
314 current_server = spamd_get_server(spamd_address_vector, num_servers);
315 sd = spamd_address_vector[current_server];
320 DEBUG(D_acl) debug_printf_indent("spamd: trying server %s\n", sd->hostspec);
324 /*XXX could potentially use TFO early-data here */
325 if ( (spamd_cctx.sock = ip_streamsocket(sd->hostspec, &errstr, 5, NULL)) >= 0
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);
332 if (spamd_cctx.sock >= 0)
335 log_write(0, LOG_MAIN, "%s spamd: %s", loglabel, errstr);
336 sd->is_failed = TRUE;
338 current_server = spamd_get_server(spamd_address_vector, num_servers);
339 if (current_server < 0)
341 log_write(0, LOG_MAIN|LOG_PANIC, "%s all spamd servers failed", loglabel);
344 sd = spamd_address_vector[current_server];
348 (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
349 /* now we are connected to spamd on spamd_cctx.sock */
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));
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);
376 { /* spamassassin variant */
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);
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));
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.
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).
404 # error Need poll(2) support
407 (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
410 read = fread(spamd_buffer,1,sizeof(spamd_buffer),mbox_file);
415 result = poll_one_fd(spamd_cctx.sock, POLLOUT, 1000);
416 if (result == -1 && errno == EINTR)
421 log_write(0, LOG_MAIN|LOG_PANIC,
422 "%s %s on spamd %s socket", loglabel, callout_address, strerror(errno));
425 if (time(NULL) - start < sd->timeout)
427 log_write(0, LOG_MAIN|LOG_PANIC,
428 "%s timed out writing spamd %s, socket", loglabel, callout_address);
430 (void)close(spamd_cctx.sock);
434 wrote = send(spamd_cctx.sock,spamd_buffer + offset,read - offset,0);
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);
442 if (offset + wrote != read)
449 while (!feof(mbox_file) && !ferror(mbox_file));
451 if (ferror(mbox_file))
453 log_write(0, LOG_MAIN|LOG_PANIC,
454 "%s error reading spool file: %s", loglabel, strerror(errno));
455 (void)close(spamd_cctx.sock);
459 (void)fclose(mbox_file);
461 /* we're done sending, close socket for writing */
463 shutdown(spamd_cctx.sock,SHUT_WR);
465 /* read spamd response using what's left of the timeout. */
466 memset(spamd_buffer, 0, sizeof(spamd_buffer));
468 while ((i = ip_recv(&spamd_cctx,
469 spamd_buffer + offset,
470 sizeof(spamd_buffer) - offset - 1,
471 sd->timeout + start)) > 0)
473 spamd_buffer[offset] = '\0'; /* guard byte */
476 if (i <= 0 && errno != 0)
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);
485 (void)close(spamd_cctx.sock);
488 { /* rspamd variant of reply */
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 */
497 log_write(0, LOG_MAIN|LOG_PANIC,
498 "%s cannot parse spamd %s, output: %d", loglabel, callout_address, r);
501 /* now parse action */
502 p = &spamd_buffer[spamd_report_offset];
504 if (Ustrncmp(p, "Action: ", sizeof("Action: ") - 1) == 0)
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)
515 /* dig in the spamd output and put the report in a multiline header,
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)
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)
526 log_write(0, LOG_MAIN|LOG_PANIC,
527 "%s cannot parse spamd %s output", loglabel, callout_address);
532 Ustrcpy(spam_action_buffer,
533 spamd_score >= spamd_threshold ? US"reject" : US"no action");
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;
551 /* add an extra space after the newline to ensure
552 that it is treated as a header continuation line */
558 /* cut off trailing leftovers */
562 spam_report = spam_report_buffer;
563 spam_action = spam_action_buffer;
565 /* create spam bar */
566 spamd_score_char = spamd_score > 0 ? '+' : '-';
567 j = abs((int)(spamd_score));
570 while ((i < j) && (i <= MAX_SPAM_BAR_CHARS))
571 spam_bar_buffer[i++] = spamd_score_char;
574 spam_bar_buffer[0] = '/';
577 spam_bar_buffer[i] = '\0';
578 spam_bar = spam_bar_buffer;
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;
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),
589 spam_score_int = spam_score_int_buffer;
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 */
596 /* remember expanded spamd_address if needed */
597 if (spamd_address_work != spamd_address)
598 prev_spamd_address_work = string_copy(spamd_address_work);
600 /* remember user name and "been here" for it */
601 prev_user_name = user_name;
605 ? OK /* always return OK, no matter what the score */
609 (void)fclose(mbox_file);