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
11 /* Code for calling spamassassin's spamd. Called from acl.c. */
14 #ifdef WITH_CONTENT_SCAN
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;
25 uschar *prev_spamd_address_work = NULL;
27 static const uschar * loglabel = US"spam acl condition:";
31 spamd_param_init(spamd_address_container *spamd)
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;
39 spamd->priority = SPAMD_PRIORITY;
45 spamd_param(const uschar * param, spamd_address_container * spamd)
47 static int timesinceday = -1;
51 /*XXX more clever parsing could discard embedded spaces? */
53 if (sscanf(CCS param, "pri=%u", &spamd->priority))
56 if (sscanf(CCS param, "weight=%u", &spamd->weight))
58 if (spamd->weight == 0) /* this server disabled: skip it */
63 if (Ustrncmp(param, "time=", 5) == 0)
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;
72 if ((end_string = Ustrchr(s, '-')))
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
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;
90 time_start = start_h*3600 + start_m*60 + start_s;
91 time_end = end_h*3600 + end_m*60 + end_s;
93 if (timesinceday < time_start || timesinceday >= time_end)
94 return 1; /* skip spamd server */
99 if (Ustrcmp(param, "variant=rspamd") == 0)
101 spamd->is_rspamd = TRUE;
105 if (Ustrncmp(param, "tmo=", 4) == 0)
107 int sec = readconf_readtime((s = param+4), '\0', FALSE);
111 spamd->timeout = sec;
115 if (Ustrncmp(param, "retry=", 6) == 0)
117 int sec = readconf_readtime((s = param+6), '\0', FALSE);
125 log_write(0, LOG_MAIN, "%s warning - invalid spamd parameter: '%s'",
127 return -1; /* syntax error */
130 log_write(0, LOG_MAIN,
131 "%s warning - invalid spamd %s value: '%s'", loglabel, name, s);
132 return -1; /* syntax error */
137 spamd_get_server(spamd_address_container ** spamds, int num_servers)
140 spamd_address_container * sd;
144 /* speedup, if we have only 1 server */
145 if (num_servers == 1)
146 return (spamds[0]->is_failed ? -1 : 0);
148 /* scan for highest pri */
149 for (pri = 0, i = 0; i < num_servers; i++)
152 if (!sd->is_failed && sd->priority > pri) pri = sd->priority;
155 /* get sum of weights */
156 for (weights = 0, i = 0; i < num_servers; i++)
159 if (!sd->is_failed && sd->priority == pri) weights += sd->weight;
161 if (weights == 0) /* all servers failed */
164 for (long rnd = random_number(weights), i = 0; i < num_servers; i++)
167 if (!sd->is_failed && sd->priority == pri)
168 if ((rnd -= sd->weight) < 0)
172 log_write(0, LOG_MAIN|LOG_PANIC,
173 "%s unknown error (memory/cpu corruption?)", loglabel);
179 spam(const uschar **listptr)
182 const uschar *list = *listptr;
184 unsigned long mbox_size;
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;
198 uschar *spamd_address_work;
199 spamd_address_container * sd;
201 /* stop compiler warning */
204 /* find the username from the option list */
205 if (!(user_name = string_nextinlist(&list, &sep, NULL, 0)))
207 /* no username given, this means no scanning should be done */
211 /* if username is "0" or "false", do not scan */
212 if (Ustrcmp(user_name, "0") == 0 || strcmpic(user_name, US"false") == 0)
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 */
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)))
225 log_write(0, LOG_MAIN|LOG_PANIC,
226 "%s spamd_address starts with $, but expansion failed: %s",
227 loglabel, expand_string_message);
231 DEBUG(D_acl) debug_printf_indent("spamd: addrlist '%s'\n", spamd_address_work);
233 /* check if previous spamd_address was expanded and has changed. dump cached results if so */
235 && prev_spamd_address_work != NULL
236 && Ustrcmp(prev_spamd_address_work, spamd_address_work) != 0
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;
244 /* make sure the eml mbox file is spooled up */
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);
259 const uschar * spamd_address_list_ptr = spamd_address_work;
260 spamd_address_container * spamd_address_vector[32];
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)))
267 const uschar * sublist;
268 int sublist_sep = -(int)' '; /* default space-sep */
272 DEBUG(D_acl) debug_printf_indent("spamd: addr entry '%s'\n", address);
273 sd = store_get(sizeof(spamd_address_container), GET_UNTAINTED);
275 for (sublist = address, args = 0, spamd_param_init(sd);
276 (s = string_nextinlist(&sublist, &sublist_sep, NULL, 0));
280 DEBUG(D_acl) debug_printf_indent("spamd: addr parm '%s'\n", s);
283 case 0: sd->hostspec = s;
284 if (*s == '/') args++; /* local; no port */
286 case 1: sd->hostspec = string_sprintf("%s %s", sd->hostspec, s);
288 default: spamd_param(s, sd);
294 log_write(0, LOG_MAIN,
295 "%s warning - invalid spamd address: '%s'", loglabel, address);
299 spamd_address_vector[num_servers] = sd;
300 if (++num_servers > 31)
304 /* check if we have at least one server */
307 log_write(0, LOG_MAIN|LOG_PANIC,
308 "%s no useable spamd server addresses in spamd_address configuration option.",
313 current_server = spamd_get_server(spamd_address_vector, num_servers);
314 sd = spamd_address_vector[current_server];
319 DEBUG(D_acl) debug_printf_indent("spamd: trying server %s\n", sd->hostspec);
323 /*XXX could potentially use TFO early-data here */
324 if ( (spamd_cctx.sock = ip_streamsocket(sd->hostspec, &errstr, 5, NULL)) >= 0
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);
331 if (spamd_cctx.sock >= 0)
334 log_write(0, LOG_MAIN, "%s spamd: %s", loglabel, errstr);
335 sd->is_failed = TRUE;
337 current_server = spamd_get_server(spamd_address_vector, num_servers);
338 if (current_server < 0)
340 log_write(0, LOG_MAIN|LOG_PANIC, "%s all spamd servers failed", loglabel);
343 sd = spamd_address_vector[current_server];
347 (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
348 /* now we are connected to spamd on spamd_cctx.sock */
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));
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);
375 { /* spamassassin variant */
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);
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));
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.
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).
403 # error Need poll(2) support
406 (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
409 read = fread(spamd_buffer,1,sizeof(spamd_buffer),mbox_file);
414 result = poll_one_fd(spamd_cctx.sock, POLLOUT, 1000);
415 if (result == -1 && errno == EINTR)
420 log_write(0, LOG_MAIN|LOG_PANIC,
421 "%s %s on spamd %s socket", loglabel, callout_address, strerror(errno));
424 if (time(NULL) - start < sd->timeout)
426 log_write(0, LOG_MAIN|LOG_PANIC,
427 "%s timed out writing spamd %s, socket", loglabel, callout_address);
429 (void)close(spamd_cctx.sock);
433 wrote = send(spamd_cctx.sock,spamd_buffer + offset,read - offset,0);
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);
441 if (offset + wrote != read)
448 while (!feof(mbox_file) && !ferror(mbox_file));
450 if (ferror(mbox_file))
452 log_write(0, LOG_MAIN|LOG_PANIC,
453 "%s error reading spool file: %s", loglabel, strerror(errno));
454 (void)close(spamd_cctx.sock);
458 (void)fclose(mbox_file);
460 /* we're done sending, close socket for writing */
462 shutdown(spamd_cctx.sock,SHUT_WR);
464 /* read spamd response using what's left of the timeout. */
465 memset(spamd_buffer, 0, sizeof(spamd_buffer));
467 while ((i = ip_recv(&spamd_cctx,
468 spamd_buffer + offset,
469 sizeof(spamd_buffer) - offset - 1,
470 sd->timeout + start)) > 0)
472 spamd_buffer[offset] = '\0'; /* guard byte */
475 if (i <= 0 && errno != 0)
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);
484 (void)close(spamd_cctx.sock);
487 { /* rspamd variant of reply */
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 */
496 log_write(0, LOG_MAIN|LOG_PANIC,
497 "%s cannot parse spamd %s, output: %d", loglabel, callout_address, r);
500 /* now parse action */
501 p = &spamd_buffer[spamd_report_offset];
503 if (Ustrncmp(p, "Action: ", sizeof("Action: ") - 1) == 0)
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)
514 /* dig in the spamd output and put the report in a multiline header,
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)
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)
525 log_write(0, LOG_MAIN|LOG_PANIC,
526 "%s cannot parse spamd %s output", loglabel, callout_address);
531 Ustrcpy(spam_action_buffer,
532 spamd_score >= spamd_threshold ? US"reject" : US"no action");
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;
550 /* add an extra space after the newline to ensure
551 that it is treated as a header continuation line */
557 /* cut off trailing leftovers */
561 spam_report = spam_report_buffer;
562 spam_action = spam_action_buffer;
564 /* create spam bar */
565 spamd_score_char = spamd_score > 0 ? '+' : '-';
566 j = abs((int)(spamd_score));
569 while ((i < j) && (i <= MAX_SPAM_BAR_CHARS))
570 spam_bar_buffer[i++] = spamd_score_char;
573 spam_bar_buffer[0] = '/';
576 spam_bar_buffer[i] = '\0';
577 spam_bar = spam_bar_buffer;
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;
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),
588 spam_score_int = spam_score_int_buffer;
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 */
595 /* remember expanded spamd_address if needed */
596 if (spamd_address_work != spamd_address)
597 prev_spamd_address_work = string_copy(spamd_address_work);
599 /* remember user name and "been here" for it */
600 prev_user_name = user_name;
604 ? OK /* always return OK, no matter what the score */
608 (void)fclose(mbox_file);