1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
5 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
7 * Copyright (c) The Exim Maintainers 2016 - 2021
10 /* Code for calling spamassassin's spamd. Called from acl.c. */
13 #ifdef WITH_CONTENT_SCAN
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;
24 uschar *prev_spamd_address_work = NULL;
26 static const uschar * loglabel = US"spam acl condition:";
30 spamd_param_init(spamd_address_container *spamd)
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;
38 spamd->priority = SPAMD_PRIORITY;
44 spamd_param(const uschar * param, spamd_address_container * spamd)
46 static int timesinceday = -1;
50 /*XXX more clever parsing could discard embedded spaces? */
52 if (sscanf(CCS param, "pri=%u", &spamd->priority))
55 if (sscanf(CCS param, "weight=%u", &spamd->weight))
57 if (spamd->weight == 0) /* this server disabled: skip it */
62 if (Ustrncmp(param, "time=", 5) == 0)
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;
71 if ((end_string = Ustrchr(s, '-')))
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
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;
89 time_start = start_h*3600 + start_m*60 + start_s;
90 time_end = end_h*3600 + end_m*60 + end_s;
92 if (timesinceday < time_start || timesinceday >= time_end)
93 return 1; /* skip spamd server */
98 if (Ustrcmp(param, "variant=rspamd") == 0)
100 spamd->is_rspamd = TRUE;
104 if (Ustrncmp(param, "tmo=", 4) == 0)
106 int sec = readconf_readtime((s = param+4), '\0', FALSE);
110 spamd->timeout = sec;
114 if (Ustrncmp(param, "retry=", 6) == 0)
116 int sec = readconf_readtime((s = param+6), '\0', FALSE);
124 log_write(0, LOG_MAIN, "%s warning - invalid spamd parameter: '%s'",
126 return -1; /* syntax error */
129 log_write(0, LOG_MAIN,
130 "%s warning - invalid spamd %s value: '%s'", loglabel, name, s);
131 return -1; /* syntax error */
136 spamd_get_server(spamd_address_container ** spamds, int num_servers)
139 spamd_address_container * sd;
143 /* speedup, if we have only 1 server */
144 if (num_servers == 1)
145 return (spamds[0]->is_failed ? -1 : 0);
147 /* scan for highest pri */
148 for (pri = 0, i = 0; i < num_servers; i++)
151 if (!sd->is_failed && sd->priority > pri) pri = sd->priority;
154 /* get sum of weights */
155 for (weights = 0, i = 0; i < num_servers; i++)
158 if (!sd->is_failed && sd->priority == pri) weights += sd->weight;
160 if (weights == 0) /* all servers failed */
163 for (long rnd = random_number(weights), i = 0; i < num_servers; i++)
166 if (!sd->is_failed && sd->priority == pri)
167 if ((rnd -= sd->weight) < 0)
171 log_write(0, LOG_MAIN|LOG_PANIC,
172 "%s unknown error (memory/cpu corruption?)", loglabel);
178 spam(const uschar **listptr)
181 const uschar *list = *listptr;
183 unsigned long mbox_size;
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;
197 uschar *spamd_address_work;
198 spamd_address_container * sd;
200 /* stop compiler warning */
203 /* find the username from the option list */
204 if (!(user_name = string_nextinlist(&list, &sep, NULL, 0)))
206 /* no username given, this means no scanning should be done */
210 /* if username is "0" or "false", do not scan */
211 if (Ustrcmp(user_name, "0") == 0 || strcmpic(user_name, US"false") == 0)
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 */
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)))
224 log_write(0, LOG_MAIN|LOG_PANIC,
225 "%s spamd_address starts with $, but expansion failed: %s",
226 loglabel, expand_string_message);
230 DEBUG(D_acl) debug_printf_indent("spamd: addrlist '%s'\n", spamd_address_work);
232 /* check if previous spamd_address was expanded and has changed. dump cached results if so */
234 && prev_spamd_address_work != NULL
235 && Ustrcmp(prev_spamd_address_work, spamd_address_work) != 0
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;
243 /* make sure the eml mbox file is spooled up */
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);
258 const uschar * spamd_address_list_ptr = spamd_address_work;
259 spamd_address_container * spamd_address_vector[32];
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)))
266 const uschar * sublist;
267 int sublist_sep = -(int)' '; /* default space-sep */
271 DEBUG(D_acl) debug_printf_indent("spamd: addr entry '%s'\n", address);
272 sd = store_get(sizeof(spamd_address_container), GET_UNTAINTED);
274 for (sublist = address, args = 0, spamd_param_init(sd);
275 (s = string_nextinlist(&sublist, &sublist_sep, NULL, 0));
279 DEBUG(D_acl) debug_printf_indent("spamd: addr parm '%s'\n", s);
282 case 0: sd->hostspec = s;
283 if (*s == '/') args++; /* local; no port */
285 case 1: sd->hostspec = string_sprintf("%s %s", sd->hostspec, s);
287 default: spamd_param(s, sd);
293 log_write(0, LOG_MAIN,
294 "%s warning - invalid spamd address: '%s'", loglabel, address);
298 spamd_address_vector[num_servers] = sd;
299 if (++num_servers > 31)
303 /* check if we have at least one server */
306 log_write(0, LOG_MAIN|LOG_PANIC,
307 "%s no useable spamd server addresses in spamd_address configuration option.",
312 current_server = spamd_get_server(spamd_address_vector, num_servers);
313 sd = spamd_address_vector[current_server];
318 DEBUG(D_acl) debug_printf_indent("spamd: trying server %s\n", sd->hostspec);
322 /*XXX could potentially use TFO early-data here */
323 if ( (spamd_cctx.sock = ip_streamsocket(sd->hostspec, &errstr, 5, NULL)) >= 0
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);
330 if (spamd_cctx.sock >= 0)
333 log_write(0, LOG_MAIN, "%s spamd: %s", loglabel, errstr);
334 sd->is_failed = TRUE;
336 current_server = spamd_get_server(spamd_address_vector, num_servers);
337 if (current_server < 0)
339 log_write(0, LOG_MAIN|LOG_PANIC, "%s all spamd servers failed", loglabel);
342 sd = spamd_address_vector[current_server];
346 (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
347 /* now we are connected to spamd on spamd_cctx.sock */
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));
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);
374 { /* spamassassin variant */
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);
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));
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.
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).
402 # error Need poll(2) support
405 (void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
408 read = fread(spamd_buffer,1,sizeof(spamd_buffer),mbox_file);
413 result = poll_one_fd(spamd_cctx.sock, POLLOUT, 1000);
414 if (result == -1 && errno == EINTR)
419 log_write(0, LOG_MAIN|LOG_PANIC,
420 "%s %s on spamd %s socket", loglabel, callout_address, strerror(errno));
423 if (time(NULL) - start < sd->timeout)
425 log_write(0, LOG_MAIN|LOG_PANIC,
426 "%s timed out writing spamd %s, socket", loglabel, callout_address);
428 (void)close(spamd_cctx.sock);
432 wrote = send(spamd_cctx.sock,spamd_buffer + offset,read - offset,0);
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);
440 if (offset + wrote != read)
447 while (!feof(mbox_file) && !ferror(mbox_file));
449 if (ferror(mbox_file))
451 log_write(0, LOG_MAIN|LOG_PANIC,
452 "%s error reading spool file: %s", loglabel, strerror(errno));
453 (void)close(spamd_cctx.sock);
457 (void)fclose(mbox_file);
459 /* we're done sending, close socket for writing */
461 shutdown(spamd_cctx.sock,SHUT_WR);
463 /* read spamd response using what's left of the timeout. */
464 memset(spamd_buffer, 0, sizeof(spamd_buffer));
466 while ((i = ip_recv(&spamd_cctx,
467 spamd_buffer + offset,
468 sizeof(spamd_buffer) - offset - 1,
469 sd->timeout + start)) > 0)
471 spamd_buffer[offset] = '\0'; /* guard byte */
474 if (i <= 0 && errno != 0)
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);
483 (void)close(spamd_cctx.sock);
486 { /* rspamd variant of reply */
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 */
495 log_write(0, LOG_MAIN|LOG_PANIC,
496 "%s cannot parse spamd %s, output: %d", loglabel, callout_address, r);
499 /* now parse action */
500 p = &spamd_buffer[spamd_report_offset];
502 if (Ustrncmp(p, "Action: ", sizeof("Action: ") - 1) == 0)
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)
513 /* dig in the spamd output and put the report in a multiline header,
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)
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)
524 log_write(0, LOG_MAIN|LOG_PANIC,
525 "%s cannot parse spamd %s output", loglabel, callout_address);
530 Ustrcpy(spam_action_buffer,
531 spamd_score >= spamd_threshold ? US"reject" : US"no action");
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;
549 /* add an extra space after the newline to ensure
550 that it is treated as a header continuation line */
556 /* cut off trailing leftovers */
560 spam_report = spam_report_buffer;
561 spam_action = spam_action_buffer;
563 /* create spam bar */
564 spamd_score_char = spamd_score > 0 ? '+' : '-';
565 j = abs((int)(spamd_score));
568 while ((i < j) && (i <= MAX_SPAM_BAR_CHARS))
569 spam_bar_buffer[i++] = spamd_score_char;
572 spam_bar_buffer[0] = '/';
575 spam_bar_buffer[i] = '\0';
576 spam_bar = spam_bar_buffer;
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;
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),
587 spam_score_int = spam_score_int_buffer;
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 */
594 /* remember expanded spamd_address if needed */
595 if (spamd_address_work != spamd_address)
596 prev_spamd_address_work = string_copy(spamd_address_work);
598 /* remember user name and "been here" for it */
599 prev_user_name = user_name;
603 ? OK /* always return OK, no matter what the score */
607 (void)fclose(mbox_file);