Revert "Support Rspamd. Patch from Andrew Lewis, lightly editorialised"
[exim.git] / src / src / spam.c
CommitLineData
8523533c
TK
1/*************************************************
2* Exim - an Internet mail transport agent *
3*************************************************/
4
80fea873
JH
5/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
6 * License: GPL
f9ba5e22 7 * Copyright (c) The Exim Maintainers 2016 - 2018
80fea873 8 */
8523533c
TK
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
16uschar spam_score_buffer[16];
17uschar spam_score_int_buffer[16];
18uschar spam_bar_buffer[128];
dd998666 19uschar * spam_action_buffer;
8523533c
TK
20uschar spam_report_buffer[32600];
21uschar prev_user_name[128] = "";
22int spam_ok = 0;
23int spam_rc = 0;
f7274286 24uschar *prev_spamd_address_work = NULL;
8523533c 25
fd4d8871 26static const uschar * loglabel = US"spam acl condition:";
8523533c 27
23763898 28
fd4d8871
R
29static int
30spamd_param_init(spamd_address_container *spamd)
31{
dc7b3d36 32/* default spamd server weight, time and priority value */
fd4d8871 33spamd->is_failed = FALSE;
8a512ed5
JH
34spamd->weight = SPAMD_WEIGHT;
35spamd->timeout = SPAMD_TIMEOUT;
36spamd->retry = 0;
dc7b3d36 37spamd->priority = 1;
fd4d8871
R
38return 0;
39}
8523533c 40
8523533c 41
fd4d8871 42static int
fc362fc5 43spamd_param(const uschar * param, spamd_address_container * spamd)
fd4d8871
R
44{
45static int timesinceday = -1;
23763898 46const uschar * s;
8a512ed5 47const uschar * name;
fd4d8871 48
fd4d8871
R
49/*XXX more clever parsing could discard embedded spaces? */
50
fc362fc5 51if (sscanf(CCS param, "pri=%u", &spamd->priority))
dc7b3d36
JH
52 return 0; /* OK */
53
fc362fc5 54if (sscanf(CCS param, "weight=%u", &spamd->weight))
fd4d8871
R
55 {
56 if (spamd->weight == 0) /* this server disabled: skip it */
57 return 1;
58 return 0; /* OK */
59 }
60
23763898 61if (Ustrncmp(param, "time=", 5) == 0)
fd4d8871
R
62 {
63 unsigned int start_h = 0, start_m = 0, start_s = 0;
64 unsigned int end_h = 24, end_m = 0, end_s = 0;
65 unsigned int time_start, time_end;
23763898 66 const uschar * end_string;
fd4d8871 67
8a512ed5 68 name = US"time";
23763898
JH
69 s = param+5;
70 if ((end_string = Ustrchr(s, '-')))
ddcf2b5f 71 {
23763898
JH
72 end_string++;
73 if ( sscanf(CS end_string, "%u.%u.%u", &end_h, &end_m, &end_s) == 0
74 || sscanf(CS s, "%u.%u.%u", &start_h, &start_m, &start_s) == 0
75 )
8a512ed5 76 goto badval;
f7274286 77 }
f7274286 78 else
8a512ed5 79 goto badval;
8523533c 80
fd4d8871 81 if (timesinceday < 0)
ddcf2b5f 82 {
fd4d8871
R
83 time_t now = time(NULL);
84 struct tm *tmp = localtime(&now);
85 timesinceday = tmp->tm_hour*3600 + tmp->tm_min*60 + tmp->tm_sec;
86 }
8e669ac1 87
fd4d8871
R
88 time_start = start_h*3600 + start_m*60 + start_s;
89 time_end = end_h*3600 + end_m*60 + end_s;
8e669ac1 90
fd4d8871
R
91 if (timesinceday < time_start || timesinceday >= time_end)
92 return 1; /* skip spamd server */
c5f280e2 93
fd4d8871
R
94 return 0; /* OK */
95 }
8e669ac1 96
23763898
JH
97if (Ustrncmp(param, "tmo=", 4) == 0)
98 {
99 int sec = readconf_readtime((s = param+4), '\0', FALSE);
8a512ed5 100 name = US"timeout";
23763898 101 if (sec < 0)
8a512ed5
JH
102 goto badval;
103 spamd->timeout = sec;
104 return 0;
105 }
106
107if (Ustrncmp(param, "retry=", 6) == 0)
108 {
109 int sec = readconf_readtime((s = param+6), '\0', FALSE);
110 name = US"retry";
111 if (sec < 0)
112 goto badval;
113 spamd->retry = sec;
23763898
JH
114 return 0;
115 }
116
fd4d8871
R
117log_write(0, LOG_MAIN, "%s warning - invalid spamd parameter: '%s'",
118 loglabel, param);
119return -1; /* syntax error */
8a512ed5
JH
120
121badval:
122 log_write(0, LOG_MAIN,
123 "%s warning - invalid spamd %s value: '%s'", loglabel, name, s);
124 return -1; /* syntax error */
fd4d8871 125}
8523533c 126
8523533c 127
fd4d8871 128static int
dc7b3d36 129spamd_get_server(spamd_address_container ** spamds, int num_servers)
fd4d8871
R
130{
131unsigned int i;
dc7b3d36
JH
132spamd_address_container * sd;
133long rnd, weights;
134unsigned pri;
135static BOOL srandomed = FALSE;
fd4d8871 136
806c3df9 137/* speedup, if we have only 1 server */
dc7b3d36
JH
138if (num_servers == 1)
139 return (spamds[0]->is_failed ? -1 : 0);
8e669ac1 140
dc7b3d36
JH
141/* init ranmod */
142if (!srandomed)
143 {
144 struct timeval tv;
145 gettimeofday(&tv, NULL);
146 srandom((unsigned int)(tv.tv_usec/1000));
147 srandomed = TRUE;
148 }
8e669ac1 149
dc7b3d36
JH
150/* scan for highest pri */
151for (pri = 0, i = 0; i < num_servers; i++)
152 {
153 sd = spamds[i];
154 if (!sd->is_failed && sd->priority > pri) pri = sd->priority;
fd4d8871 155 }
8e669ac1 156
dc7b3d36
JH
157/* get sum of weights */
158for (weights = 0, i = 0; i < num_servers; i++)
159 {
160 sd = spamds[i];
161 if (!sd->is_failed && sd->priority == pri) weights += sd->weight;
162 }
163if (weights == 0) /* all servers failed */
164 return -1;
8e669ac1 165
dc7b3d36
JH
166for (rnd = random() % weights, i = 0; i < num_servers; i++)
167 {
168 sd = spamds[i];
169 if (!sd->is_failed && sd->priority == pri)
170 if ((rnd -= sd->weight) <= 0)
fd4d8871 171 return i;
dc7b3d36 172 }
29cfeb94 173
fd4d8871
R
174log_write(0, LOG_MAIN|LOG_PANIC,
175 "%s unknown error (memory/cpu corruption?)", loglabel);
176return -1;
177}
29cfeb94 178
29cfeb94 179
fd4d8871 180int
55414b25 181spam(const uschar **listptr)
fd4d8871
R
182{
183int sep = 0;
55414b25 184const uschar *list = *listptr;
fd4d8871
R
185uschar *user_name;
186uschar user_name_buffer[128];
187unsigned long mbox_size;
188FILE *mbox_file;
74f1a423 189client_conn_ctx spamd_cctx = {.sock = -1};
fd4d8871
R
190uschar spamd_buffer[32600];
191int i, j, offset, result;
fd4d8871
R
192uschar spamd_version[8];
193uschar spamd_short_result[8];
194uschar spamd_score_char;
195double spamd_threshold, spamd_score, spamd_reject_score;
196int spamd_report_offset;
197uschar *p,*q;
198int override = 0;
199time_t start;
200size_t read, wrote;
fd4d8871
R
201#ifndef NO_POLL_H
202struct pollfd pollfd;
203#else /* Patch posted by Erik ? for OS X */
204struct timeval select_tv; /* and applied by PH */
205fd_set select_fd;
206#endif
207uschar *spamd_address_work;
8a512ed5 208spamd_address_container * sd;
fd4d8871
R
209
210/* stop compiler warning */
211result = 0;
212
213/* find the username from the option list */
214if ((user_name = string_nextinlist(&list, &sep,
215 user_name_buffer,
216 sizeof(user_name_buffer))) == NULL)
217 {
218 /* no username given, this means no scanning should be done */
219 return FAIL;
220 }
221
222/* if username is "0" or "false", do not scan */
223if ( (Ustrcmp(user_name,"0") == 0) ||
224 (strcmpic(user_name,US"false") == 0) )
225 return FAIL;
226
227/* if there is an additional option, check if it is "true" */
228if (strcmpic(list,US"true") == 0)
229 /* in that case, always return true later */
230 override = 1;
231
232/* expand spamd_address if needed */
233if (*spamd_address == '$')
234 {
235 spamd_address_work = expand_string(spamd_address);
236 if (spamd_address_work == NULL)
237 {
238 log_write(0, LOG_MAIN|LOG_PANIC,
239 "%s spamd_address starts with $, but expansion failed: %s",
240 loglabel, expand_string_message);
241 return DEFER;
29cfeb94 242 }
fd4d8871
R
243 }
244else
245 spamd_address_work = spamd_address;
246
e1d04f48 247DEBUG(D_acl) debug_printf_indent("spamd: addrlist '%s'\n", spamd_address_work);
fd4d8871
R
248
249/* check if previous spamd_address was expanded and has changed. dump cached results if so */
250if ( spam_ok
251 && prev_spamd_address_work != NULL
252 && Ustrcmp(prev_spamd_address_work, spamd_address_work) != 0
253 )
254 spam_ok = 0;
255
256/* if we scanned for this username last time, just return */
257if (spam_ok && Ustrcmp(prev_user_name, user_name) == 0)
258 return override ? OK : spam_rc;
259
260/* make sure the eml mbox file is spooled up */
328c5688 261
040721f2 262if (!(mbox_file = spool_mbox(&mbox_size, NULL, NULL)))
328c5688 263 { /* error while spooling */
fd4d8871
R
264 log_write(0, LOG_MAIN|LOG_PANIC,
265 "%s error while creating mbox spool file", loglabel);
266 return DEFER;
267 }
268
269start = time(NULL);
270
271 {
272 int num_servers = 0;
273 int current_server;
dc7b3d36
JH
274 uschar * address;
275 const uschar * spamd_address_list_ptr = spamd_address_work;
fd4d8871 276 spamd_address_container * spamd_address_vector[32];
fd4d8871
R
277
278 /* Check how many spamd servers we have
279 and register their addresses */
dc7b3d36 280 sep = 0; /* default colon-sep */
040721f2 281 while ((address = string_nextinlist(&spamd_address_list_ptr, &sep, NULL, 0)))
ddcf2b5f 282 {
55414b25 283 const uschar * sublist;
fd4d8871
R
284 int sublist_sep = -(int)' '; /* default space-sep */
285 unsigned args;
286 uschar * s;
fd4d8871 287
e1d04f48 288 DEBUG(D_acl) debug_printf_indent("spamd: addr entry '%s'\n", address);
2aad5761 289 sd = (spamd_address_container *)store_get(sizeof(spamd_address_container));
fd4d8871 290
2aad5761 291 for (sublist = address, args = 0, spamd_param_init(sd);
755762fd 292 (s = string_nextinlist(&sublist, &sublist_sep, NULL, 0));
fd4d8871
R
293 args++
294 )
ddcf2b5f 295 {
e1d04f48 296 DEBUG(D_acl) debug_printf_indent("spamd: addr parm '%s'\n", s);
fd4d8871
R
297 switch (args)
298 {
2aad5761 299 case 0: sd->hostspec = s;
fd4d8871
R
300 if (*s == '/') args++; /* local; no port */
301 break;
2aad5761
JH
302 case 1: sd->hostspec = string_sprintf("%s %s", sd->hostspec, s);
303 break;
304 default: spamd_param(s, sd);
305 break;
fd4d8871 306 }
ddcf2b5f 307 }
fd4d8871 308 if (args < 2)
c5f280e2 309 {
fd4d8871
R
310 log_write(0, LOG_MAIN,
311 "%s warning - invalid spamd address: '%s'", loglabel, address);
312 continue;
c5f280e2 313 }
8523533c 314
2aad5761 315 spamd_address_vector[num_servers] = sd;
fd4d8871
R
316 if (++num_servers > 31)
317 break;
8523533c
TK
318 }
319
fd4d8871
R
320 /* check if we have at least one server */
321 if (!num_servers)
ddcf2b5f 322 {
0f501486 323 log_write(0, LOG_MAIN|LOG_PANIC,
fd4d8871
R
324 "%s no useable spamd server addresses in spamd_address configuration option.",
325 loglabel);
8acbb134 326 goto defer;
ddcf2b5f 327 }
0f501486 328
8a512ed5
JH
329 current_server = spamd_get_server(spamd_address_vector, num_servers);
330 sd = spamd_address_vector[current_server];
331 for(;;)
ddcf2b5f 332 {
2aad5761 333 uschar * errstr;
8523533c 334
e1d04f48 335 DEBUG(D_acl) debug_printf_indent("spamd: trying server %s\n", sd->hostspec);
fd4d8871 336
8a512ed5
JH
337 for (;;)
338 {
4a5cbaff 339 /*XXX could potentially use TFO early-data here */
74f1a423 340 if ( (spamd_cctx.sock = ip_streamsocket(sd->hostspec, &errstr, 5)) >= 0
8a512ed5
JH
341 || sd->retry <= 0
342 )
343 break;
e1d04f48 344 DEBUG(D_acl) debug_printf_indent("spamd: server %s: retry conn\n", sd->hostspec);
8a512ed5
JH
345 while (sd->retry > 0) sd->retry = sleep(sd->retry);
346 }
74f1a423 347 if (spamd_cctx.sock >= 0)
2aad5761 348 break;
25257489 349
2aad5761
JH
350 log_write(0, LOG_MAIN, "%s spamd: %s", loglabel, errstr);
351 sd->is_failed = TRUE;
25257489 352
2aad5761
JH
353 current_server = spamd_get_server(spamd_address_vector, num_servers);
354 if (current_server < 0)
fd4d8871 355 {
8a512ed5 356 log_write(0, LOG_MAIN|LOG_PANIC, "%s all spamd servers failed", loglabel);
2aad5761 357 goto defer;
fd4d8871 358 }
8a512ed5 359 sd = spamd_address_vector[current_server];
ddcf2b5f 360 }
fd4d8871 361 }
8523533c 362
74f1a423 363(void)fcntl(spamd_cctx.sock, F_SETFL, O_NONBLOCK);
dd998666
JH
364/* now we are connected to spamd on spamd_sock */
365
366(void)string_format(spamd_buffer,
367 sizeof(spamd_buffer),
368 "REPORT SPAMC/1.2\r\nUser: %s\r\nContent-length: %ld\r\n\r\n",
369 user_name,
370 mbox_size);
371/* send our request */
372wrote = send(spamd_cctx.sock, spamd_buffer, Ustrlen(spamd_buffer), 0);
373
fd4d8871
R
374if (wrote == -1)
375 {
74f1a423 376 (void)close(spamd_cctx.sock);
fd4d8871 377 log_write(0, LOG_MAIN|LOG_PANIC,
0ea02355 378 "%s spamd %s send failed: %s", loglabel, callout_address, strerror(errno));
8acbb134 379 goto defer;
fd4d8871
R
380 }
381
382/* now send the file */
4c04137d 383/* spamd sometimes accepts connections but doesn't read data off
fd4d8871
R
384 * the connection. We make the file descriptor non-blocking so
385 * that the write will only write sufficient data without blocking
4c04137d 386 * and we poll the descriptor to make sure that we can write without
fd4d8871 387 * blocking. Short writes are gracefully handled and if the whole
4c04137d 388 * transaction takes too long it is aborted.
fd4d8871
R
389 * Note: poll() is not supported in OSX 10.2 and is reported to be
390 * broken in more recent versions (up to 10.4).
391 */
392#ifndef NO_POLL_H
74f1a423 393pollfd.fd = spamd_cctx.sock;
fd4d8871
R
394pollfd.events = POLLOUT;
395#endif
fd4d8871
R
396do
397 {
398 read = fread(spamd_buffer,1,sizeof(spamd_buffer),mbox_file);
399 if (read > 0)
ddcf2b5f 400 {
fd4d8871
R
401 offset = 0;
402again:
403#ifndef NO_POLL_H
404 result = poll(&pollfd, 1, 1000);
8523533c 405
fd4d8871
R
406/* Patch posted by Erik ? for OS X and applied by PH */
407#else
408 select_tv.tv_sec = 1;
409 select_tv.tv_usec = 0;
410 FD_ZERO(&select_fd);
74f1a423
JH
411 FD_SET(spamd_cctx.sock, &select_fd);
412 result = select(spamd_cctx.sock+1, NULL, &select_fd, NULL, &select_tv);
fd4d8871
R
413#endif
414/* End Erik's patch */
8523533c 415
fd4d8871
R
416 if (result == -1 && errno == EINTR)
417 goto again;
418 else if (result < 1)
c5f280e2 419 {
fd4d8871
R
420 if (result == -1)
421 log_write(0, LOG_MAIN|LOG_PANIC,
0ea02355 422 "%s %s on spamd %s socket", loglabel, callout_address, strerror(errno));
fd4d8871
R
423 else
424 {
8a512ed5 425 if (time(NULL) - start < sd->timeout)
fd4d8871
R
426 goto again;
427 log_write(0, LOG_MAIN|LOG_PANIC,
0ea02355 428 "%s timed out writing spamd %s, socket", loglabel, callout_address);
fd4d8871 429 }
74f1a423 430 (void)close(spamd_cctx.sock);
8acbb134 431 goto defer;
c5f280e2 432 }
8e669ac1 433
74f1a423 434 wrote = send(spamd_cctx.sock,spamd_buffer + offset,read - offset,0);
fd4d8871 435 if (wrote == -1)
c5f280e2 436 {
fd4d8871 437 log_write(0, LOG_MAIN|LOG_PANIC,
0ea02355 438 "%s %s on spamd %s socket", loglabel, callout_address, strerror(errno));
74f1a423 439 (void)close(spamd_cctx.sock);
8acbb134 440 goto defer;
c5f280e2 441 }
fd4d8871 442 if (offset + wrote != read)
ddcf2b5f 443 {
fd4d8871
R
444 offset += wrote;
445 goto again;
ddcf2b5f
JH
446 }
447 }
fd4d8871
R
448 }
449while (!feof(mbox_file) && !ferror(mbox_file));
8523533c 450
fd4d8871
R
451if (ferror(mbox_file))
452 {
453 log_write(0, LOG_MAIN|LOG_PANIC,
454 "%s error reading spool file: %s", loglabel, strerror(errno));
74f1a423 455 (void)close(spamd_cctx.sock);
8acbb134 456 goto defer;
fd4d8871
R
457 }
458
459(void)fclose(mbox_file);
460
461/* we're done sending, close socket for writing */
dd998666 462shutdown(spamd_cctx.sock, SHUT_WR);
fd4d8871
R
463
464/* read spamd response using what's left of the timeout. */
465memset(spamd_buffer, 0, sizeof(spamd_buffer));
466offset = 0;
74f1a423 467while ((i = ip_recv(&spamd_cctx,
fd4d8871
R
468 spamd_buffer + offset,
469 sizeof(spamd_buffer) - offset - 1,
77560253 470 sd->timeout - time(NULL) + start)) > 0)
fd4d8871 471 offset += i;
77560253 472spamd_buffer[offset] = '\0'; /* guard byte */
fd4d8871
R
473
474/* error handling */
475if (i <= 0 && errno != 0)
476 {
477 log_write(0, LOG_MAIN|LOG_PANIC,
0ea02355 478 "%s error reading from spamd %s, socket: %s", loglabel, callout_address, strerror(errno));
74f1a423 479 (void)close(spamd_cctx.sock);
fd4d8871
R
480 return DEFER;
481 }
482
483/* reading done */
74f1a423 484(void)close(spamd_cctx.sock);
fd4d8871 485
fd4d8871
R
486 { /* spamassassin */
487 /* dig in the spamd output and put the report in a multiline header,
488 if requested */
489 if (sscanf(CS spamd_buffer,
490 "SPAMD/%7s 0 EX_OK\r\nContent-length: %*u\r\n\r\n%lf/%lf\r\n%n",
491 spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3)
ddcf2b5f 492 {
fd4d8871
R
493 /* try to fall back to pre-2.50 spamd output */
494 if (sscanf(CS spamd_buffer,
495 "SPAMD/%7s 0 EX_OK\r\nSpam: %*s ; %lf / %lf\r\n\r\n%n",
496 spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3)
497 {
498 log_write(0, LOG_MAIN|LOG_PANIC,
0ea02355 499 "%s cannot parse spamd %s output", loglabel, callout_address);
fd4d8871
R
500 return DEFER;
501 }
ddcf2b5f 502 }
fd4d8871 503
dd998666 504 spam_action_buffer = spamd_score >= spamd_threshold ? US"reject" : US"no action";
fd4d8871
R
505 }
506
507/* Create report. Since this is a multiline string,
508we must hack it into shape first */
509p = &spamd_buffer[spamd_report_offset];
510q = spam_report_buffer;
511while (*p != '\0')
512 {
513 /* skip \r */
514 if (*p == '\r')
515 {
516 p++;
517 continue;
518 }
519 *q++ = *p;
520 if (*p++ == '\n')
521 {
522 /* add an extra space after the newline to ensure
523 that it is treated as a header continuation line */
524 *q++ = ' ';
525 }
526 }
527/* NULL-terminate */
528*q-- = '\0';
529/* cut off trailing leftovers */
530while (*q <= ' ')
531 *q-- = '\0';
532
533spam_report = spam_report_buffer;
534spam_action = spam_action_buffer;
535
536/* create spam bar */
537spamd_score_char = spamd_score > 0 ? '+' : '-';
538j = abs((int)(spamd_score));
539i = 0;
540if (j != 0)
541 while ((i < j) && (i <= MAX_SPAM_BAR_CHARS))
542 spam_bar_buffer[i++] = spamd_score_char;
543else
544 {
545 spam_bar_buffer[0] = '/';
546 i = 1;
547 }
548spam_bar_buffer[i] = '\0';
549spam_bar = spam_bar_buffer;
550
551/* create "float" spam score */
552(void)string_format(spam_score_buffer, sizeof(spam_score_buffer),
553 "%.1f", spamd_score);
554spam_score = spam_score_buffer;
555
556/* create "int" spam score */
557j = (int)((spamd_score + 0.001)*10);
558(void)string_format(spam_score_int_buffer, sizeof(spam_score_int_buffer),
559 "%d", j);
560spam_score_int = spam_score_int_buffer;
561
562/* compare threshold against score */
563spam_rc = spamd_score >= spamd_threshold
564 ? OK /* spam as determined by user's threshold */
565 : FAIL; /* not spam */
566
567/* remember expanded spamd_address if needed */
568if (spamd_address_work != spamd_address)
569 prev_spamd_address_work = string_copy(spamd_address_work);
570
571/* remember user name and "been here" for it */
572Ustrcpy(prev_user_name, user_name);
573spam_ok = 1;
574
575return override
576 ? OK /* always return OK, no matter what the score */
577 : spam_rc;
8acbb134
JH
578
579defer:
580 (void)fclose(mbox_file);
581 return DEFER;
8523533c
TK
582}
583
584#endif
2aad5761
JH
585/* vi: aw ai sw=2
586*/