make-config.in: complete path (leftover of [807f64e2], 2015-12-26!)
[s-mailx.git] / spam.c
blob3f32f1001b2cd83c209f53b984f7e71bae5ce4b6
1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
2 *@ Spam related facilities.
4 * Copyright (c) 2013 - 2018 Steffen (Daode) Nurpmeso <steffen@sdaoden.eu>.
5 * SPDX-License-Identifier: ISC
7 * Permission to use, copy, modify, and/or distribute this software for any
8 * purpose with or without fee is hereby granted, provided that the above
9 * copyright notice and this permission notice appear in all copies.
11 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 #undef n_FILE
20 #define n_FILE spam
22 #ifndef HAVE_AMALGAMATION
23 # include "nail.h"
24 #endif
26 EMPTY_FILE()
27 #ifdef HAVE_SPAM
29 #ifdef HAVE_SPAM_SPAMD
30 # include <sys/socket.h>
31 # include <sys/un.h>
32 #endif
34 /* This is chosen rather arbitrarily.
35 * It must be able to swallow the first line of a rate response,
36 * and an entire CHECK/TELL spamd(1) response */
37 #if BUFFER_SIZE < 1024
38 # error *spam-interface* BUFFER_SIZE constraints are not matched
39 #endif
41 #ifdef HAVE_SPAM_SPAMD
42 # define SPAMD_IDENT "SPAMC/1.5"
43 # ifndef SUN_LEN
44 # define SUN_LEN(SUP) \
45 (sizeof(*(SUP)) - sizeof((SUP)->sun_path) + strlen((SUP)->sun_path))
46 # endif
47 #endif
49 #ifdef HAVE_SPAM_FILTER
50 /* n_NELEM() of regmatch_t groups */
51 # define SPAM_FILTER_MATCHES 32u
52 #endif
54 enum spam_action {
55 _SPAM_RATE,
56 _SPAM_HAM,
57 _SPAM_SPAM,
58 _SPAM_FORGET
61 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
62 struct spam_cf {
63 char const *cf_cmd;
64 char *cf_result; /* _SPAM_RATE: first response line */
65 int cf_waitstat;
66 ui8_t __pad[3];
67 bool_t cf_useshell;
68 /* .cf_cmd may be adjusted for each call (`spamforget')... */
69 char const *cf_acmd;
70 char const *cf_a0;
71 char const *cf_env[4];
72 sighandler_type cf_otstp;
73 sighandler_type cf_ottin;
74 sighandler_type cf_ottou;
75 sighandler_type cf_ohup;
76 sighandler_type cf_opipe;
77 sighandler_type cf_oint;
78 sighandler_type cf_oquit;
80 #endif
82 #ifdef HAVE_SPAM_SPAMC
83 struct spam_spamc {
84 struct spam_cf c_super;
85 char const *c_cmd_arr[9];
87 #endif
89 #ifdef HAVE_SPAM_SPAMD
90 struct spam_spamd {
91 struct str d_user;
92 sighandler_type d_otstp;
93 sighandler_type d_ottin;
94 sighandler_type d_ottou;
95 sighandler_type d_ohup;
96 sighandler_type d_opipe;
97 sighandler_type d_oint;
98 sighandler_type d_oquit;
99 struct sockaddr_un d_sun;
101 #endif
103 #ifdef HAVE_SPAM_FILTER
104 struct spam_filter {
105 struct spam_cf f_super;
106 char const *f_cmd_nospam; /* Working relative to current message.. */
107 char const *f_cmd_noham;
108 # ifdef HAVE_REGEX
109 ui8_t __pad[4];
110 ui32_t f_score_grpno; /* 0 for not set */
111 regex_t f_score_regex;
112 # endif
114 #endif
116 struct spam_vc {
117 enum spam_action vc_action;
118 bool_t vc_verbose; /* Verbose output */
119 bool_t vc_progress; /* "Progress meter" (mutual verbose) */
120 ui8_t __pad[2];
121 bool_t (*vc_act)(struct spam_vc *);
122 void (*vc_dtor)(struct spam_vc *);
123 char *vc_buffer; /* I/O buffer, BUFFER_SIZE bytes */
124 size_t vc_mno; /* Current message number */
125 struct message *vc_mp; /* Current message */
126 FILE *vc_ifp; /* Input stream on .vc_mp */
127 union {
128 #ifdef HAVE_SPAM_SPAMC
129 struct spam_spamc spamc;
130 #endif
131 #ifdef HAVE_SPAM_SPAMD
132 struct spam_spamd spamd;
133 #endif
134 #ifdef HAVE_SPAM_FILTER
135 struct spam_filter filter;
136 #endif
137 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
138 struct spam_cf cf;
139 #endif
140 } vc_t;
141 char const *vc_esep; /* Error separator for progress mode */
144 /* Indices according to enum spam_action */
145 static char const _spam_cmds[][16] = {
146 "spamrate", "spamham", "spamspam", "spamforget"
149 /* Shared action setup */
150 static bool_t _spam_action(enum spam_action sa, int *ip);
152 /* *spam-interface*=spamc: initialize, communicate */
153 #ifdef HAVE_SPAM_SPAMC
154 static bool_t _spamc_setup(struct spam_vc *vcp);
155 static bool_t _spamc_interact(struct spam_vc *vcp);
156 static void _spamc_dtor(struct spam_vc *vcp);
157 #endif
159 /* *spam-interface*=spamd: initialize, communicate */
160 #ifdef HAVE_SPAM_SPAMD
161 static bool_t _spamd_setup(struct spam_vc *vcp);
162 static bool_t _spamd_interact(struct spam_vc *vcp);
163 #endif
165 /* *spam-interface*=filter: initialize, communicate */
166 #ifdef HAVE_SPAM_FILTER
167 static bool_t _spamfilter_setup(struct spam_vc *vcp);
168 static bool_t _spamfilter_interact(struct spam_vc *vcp);
169 static void _spamfilter_dtor(struct spam_vc *vcp);
170 #endif
172 /* *spam-interface*=(spamc|filter): create child + communication */
173 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
174 static void _spam_cf_setup(struct spam_vc *vcp, bool_t useshell);
175 static bool_t _spam_cf_interact(struct spam_vc *vcp);
176 #endif
178 /* Convert a floating-point spam rate into message.m_spamscore */
179 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_SPAMD ||\
180 (defined HAVE_SPAM_FILTER && defined HAVE_REGEX)
181 static void _spam_rate2score(struct spam_vc *vcp, char *buf);
182 #endif
184 static bool_t
185 _spam_action(enum spam_action sa, int *ip)
187 struct spam_vc vc;
188 size_t maxsize, skipped, cnt, curr;
189 char const *cp;
190 bool_t ok = FAL0;
191 NYD_ENTER;
193 memset(&vc, 0, sizeof vc);
194 vc.vc_action = sa;
195 vc.vc_verbose = ((n_poption & n_PO_VERB) != 0);
196 vc.vc_progress = (!vc.vc_verbose && ((n_psonce & n_PSO_INTERACTIVE) != 0));
197 vc.vc_esep = vc.vc_progress ? "\n" : n_empty;
199 /* Check and setup the desired spam interface */
200 if ((cp = ok_vlook(spam_interface)) == NULL) {
201 n_err(_("`%s': no *spam-interface* set\n"), _spam_cmds[sa]);
202 goto jleave;
203 #ifdef HAVE_SPAM_SPAMC
204 } else if (!asccasecmp(cp, "spamc")) {
205 if (!_spamc_setup(&vc))
206 goto jleave;
207 #endif
208 #ifdef HAVE_SPAM_SPAMD
209 } else if (!asccasecmp(cp, "spamd")) { /* TODO v15: remove */
210 n_OBSOLETE(_("*spam-interface*=spamd is obsolete, please use =spamc"));
211 if (!_spamd_setup(&vc))
212 goto jleave;
213 #endif
214 #ifdef HAVE_SPAM_FILTER
215 } else if (!asccasecmp(cp, "filter")) {
216 if (!_spamfilter_setup(&vc))
217 goto jleave;
218 #endif
219 } else {
220 n_err(_("`%s': unknown / unsupported *spam-interface*: %s\n"),
221 _spam_cmds[sa], cp);
222 goto jleave;
225 /* *spam-maxsize* we do handle ourselfs instead */
226 if ((cp = ok_vlook(spam_maxsize)) == NULL ||
227 (n_idec_ui32_cp(&maxsize, cp, 0, NULL), maxsize) == 0)
228 maxsize = SPAM_MAXSIZE;
230 /* Finally get an I/O buffer */
231 vc.vc_buffer = n_autorec_alloc(BUFFER_SIZE);
233 skipped = cnt = 0;
234 if (vc.vc_progress) {
235 while (ip[cnt] != 0)
236 ++cnt;
238 for (curr = 0, ok = TRU1; *ip != 0; --cnt, ++curr, ++ip) {
239 vc.vc_mno = (size_t)*ip;
240 vc.vc_mp = message + vc.vc_mno - 1;
241 if (sa == _SPAM_RATE)
242 vc.vc_mp->m_spamscore = 0;
244 if (vc.vc_mp->m_size > maxsize) {
245 if (vc.vc_verbose)
246 n_err(_("`%s': message %lu exceeds maxsize (%lu > %lu), skip\n"),
247 _spam_cmds[sa], (ul_i)vc.vc_mno, (ul_i)(size_t)vc.vc_mp->m_size,
248 (ul_i)maxsize);
249 else if (vc.vc_progress) {
250 fprintf(n_stdout, "\r%s: !%-6" PRIuZ " %6" PRIuZ "/%-6" PRIuZ,
251 _spam_cmds[sa], vc.vc_mno, cnt, curr);
252 fflush(n_stdout);
254 ++skipped;
255 } else {
256 if (vc.vc_verbose)
257 n_err(_("`%s': message %lu\n"), _spam_cmds[sa], (ul_i)vc.vc_mno);
258 else if (vc.vc_progress) {
259 fprintf(n_stdout, "\r%s: .%-6" PRIuZ " %6" PRIuZ "/%-6" PRIuZ,
260 _spam_cmds[sa], vc.vc_mno, cnt, curr);
261 fflush(n_stdout);
264 setdot(vc.vc_mp);
265 if ((vc.vc_ifp = setinput(&mb, vc.vc_mp, NEED_BODY)) == NULL) {
266 n_err(_("%s`%s': cannot load message %lu: %s\n"),
267 vc.vc_esep, _spam_cmds[sa], (ul_i)vc.vc_mno,
268 n_err_to_doc(n_err_no));
269 ok = FAL0;
270 break;
273 if (!(ok = (*vc.vc_act)(&vc)))
274 break;
277 if (vc.vc_progress) {
278 if (curr > 0)
279 fprintf(n_stdout, _(" %s (%" PRIuZ "/%" PRIuZ " all/skipped)\n"),
280 (ok ? _("done") : V_(n_error)), curr, skipped);
281 fflush(n_stdout);
284 if (vc.vc_dtor != NULL)
285 (*vc.vc_dtor)(&vc);
286 jleave:
287 NYD_LEAVE;
288 return !ok;
291 #ifdef HAVE_SPAM_SPAMC
292 static bool_t
293 _spamc_setup(struct spam_vc *vcp)
295 struct spam_spamc *sscp;
296 struct str str;
297 char const **args, *cp;
298 bool_t rv = FAL0;
299 NYD2_ENTER;
301 sscp = &vcp->vc_t.spamc;
302 args = sscp->c_cmd_arr;
304 if ((cp = ok_vlook(spamc_command)) == NULL) {
305 # ifdef SPAM_SPAMC_PATH
306 cp = SPAM_SPAMC_PATH;
307 # else
308 n_err(_("`%s': *spamc-command* is not set\n"),
309 _spam_cmds[vcp->vc_action]);
310 goto jleave;
311 # endif
313 *args++ = cp;
315 switch (vcp->vc_action) {
316 case _SPAM_RATE:
317 *args = "-c";
318 break;
319 case _SPAM_HAM:
320 args[1] = "ham";
321 goto jlearn;
322 case _SPAM_SPAM:
323 args[1] = "spam";
324 goto jlearn;
325 case _SPAM_FORGET:
326 args[1] = "forget";
327 jlearn:
328 *args = "-L";
329 ++args;
330 break;
332 ++args;
334 *args++ = "-l"; /* --log-to-stderr */
335 *args++ = "-x"; /* No "safe callback", we need to react on errors! */
337 if ((cp = ok_vlook(spamc_arguments)) != NULL)
338 *args++ = cp;
340 if ((cp = ok_vlook(spamc_user)) != NULL) {
341 if (*cp == '\0')
342 cp = ok_vlook(LOGNAME);
343 *args++ = "-u";
344 *args++ = cp;
346 assert(PTR2SIZE(args - sscp->c_cmd_arr) <= n_NELEM(sscp->c_cmd_arr));
348 *args = NULL;
349 sscp->c_super.cf_cmd = str_concat_cpa(&str, sscp->c_cmd_arr, " ")->s;
350 if (vcp->vc_verbose)
351 n_err(_("spamc(1) via %s\n"),
352 n_shexp_quote_cp(sscp->c_super.cf_cmd, FAL0));
354 _spam_cf_setup(vcp, FAL0);
356 vcp->vc_act = &_spamc_interact;
357 vcp->vc_dtor = &_spamc_dtor;
358 rv = TRU1;
359 # ifndef SPAM_SPAMC_PATH
360 jleave:
361 # endif
362 NYD2_LEAVE;
363 return rv;
366 static bool_t
367 _spamc_interact(struct spam_vc *vcp)
369 bool_t rv;
370 NYD2_ENTER;
372 if (!(rv = _spam_cf_interact(vcp)))
373 goto jleave;
375 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
376 if (vcp->vc_action != _SPAM_RATE) {
377 if (vcp->vc_action == _SPAM_SPAM)
378 vcp->vc_mp->m_flag |= MSPAM;
379 } else {
380 char *buf, *cp;
382 switch (WEXITSTATUS(vcp->vc_t.spamc.c_super.cf_waitstat)) {
383 case 1:
384 vcp->vc_mp->m_flag |= MSPAM;
385 /* FALLTHRU */
386 case 0:
387 break;
388 default:
389 rv = FAL0;
390 goto jleave;
393 if ((cp = strchr(buf = vcp->vc_t.spamc.c_super.cf_result, '/')) != NULL)
394 buf[PTR2SIZE(cp - buf)] = '\0';
395 _spam_rate2score(vcp, buf);
397 jleave:
398 NYD2_LEAVE;
399 return rv;
402 static void
403 _spamc_dtor(struct spam_vc *vcp)
405 NYD2_ENTER;
406 if (vcp->vc_t.spamc.c_super.cf_result != NULL)
407 n_free(vcp->vc_t.spamc.c_super.cf_result);
408 NYD2_LEAVE;
410 #endif /* HAVE_SPAM_SPAMC */
412 #ifdef HAVE_SPAM_SPAMD
413 static bool_t
414 _spamd_setup(struct spam_vc *vcp)
416 struct spam_spamd *ssdp;
417 char const *cp;
418 size_t l;
419 bool_t rv = FAL0;
420 NYD2_ENTER;
422 ssdp = &vcp->vc_t.spamd;
424 if ((cp = ok_vlook(spamd_user)) != NULL) {
425 if (*cp == '\0')
426 cp = ok_vlook(LOGNAME);
427 ssdp->d_user.l = strlen(ssdp->d_user.s = n_UNCONST(cp));
430 if ((cp = ok_vlook(spamd_socket)) == NULL) {
431 n_err(_("`%s': required *spamd-socket* is not set\n"),
432 _spam_cmds[vcp->vc_action]);
433 goto jleave;
435 if ((l = strlen(cp) +1) >= sizeof(ssdp->d_sun.sun_path)) {
436 n_err(_("`%s': *spamd-socket* too long: %s\n"),
437 _spam_cmds[vcp->vc_action], n_shexp_quote_cp(cp, FAL0));
438 goto jleave;
440 ssdp->d_sun.sun_family = AF_UNIX;
441 memcpy(ssdp->d_sun.sun_path, cp, l);
443 vcp->vc_act = &_spamd_interact;
444 rv = TRU1;
445 jleave:
446 NYD2_LEAVE;
447 return rv;
450 static sigjmp_buf __spamd_actjmp; /* TODO oneday, we won't need it no more */
451 static int volatile __spamd_sig; /* TODO oneday, we won't need it no more */
452 static void
453 __spamd_onsig(int sig) /* TODO someday, we won't need it no more */
455 NYD_X; /* Signal handler */
456 __spamd_sig = sig;
457 siglongjmp(__spamd_actjmp, 1);
460 static bool_t
461 _spamd_interact(struct spam_vc *vcp)
463 struct spam_spamd *ssdp;
464 size_t size, i;
465 char *lp, *cp, * volatile headbuf = NULL;
466 int volatile dsfd = -1;
467 bool_t volatile rv = FAL0;
468 NYD2_ENTER;
470 ssdp = &vcp->vc_t.spamd;
472 __spamd_sig = 0;
473 hold_sigs();
474 ssdp->d_otstp = safe_signal(SIGTSTP, SIG_DFL);
475 ssdp->d_ottin = safe_signal(SIGTTIN, SIG_DFL);
476 ssdp->d_ottou = safe_signal(SIGTTOU, SIG_DFL);
477 ssdp->d_opipe = safe_signal(SIGPIPE, SIG_IGN);
478 ssdp->d_ohup = safe_signal(SIGHUP, &__spamd_onsig);
479 ssdp->d_oint = safe_signal(SIGINT, &__spamd_onsig);
480 ssdp->d_oquit = safe_signal(SIGQUIT, &__spamd_onsig);
481 if (sigsetjmp(__spamd_actjmp, 1)) {
482 if (*vcp->vc_esep != '\0')
483 n_err(vcp->vc_esep);
484 goto jleave;
486 rele_sigs();
488 if ((dsfd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
489 n_err(_("%s`%s': can't create unix(4) socket: %s\n"),
490 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
491 goto jleave;
494 if (connect(dsfd, (struct sockaddr*)&ssdp->d_sun, SUN_LEN(&ssdp->d_sun)) ==
495 -1) {
496 n_err(_("%s`%s': can't connect to *spam-socket*: %s\n"),
497 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
498 close(dsfd);
499 dsfd = -1;
500 goto jleave;
503 /* The command header, finalized with an empty line.
504 * This needs to be written in a single write(2)! */
505 # undef _X
506 # define _X(X) do {memcpy(lp, X, sizeof(X) -1); lp += sizeof(X) -1;} while (0)
508 i = ((cp = ssdp->d_user.s) != NULL) ? ssdp->d_user.l : 0;
509 size = sizeof(NETLINE("A_VERY_LONG_COMMAND " SPAMD_IDENT)) +
510 sizeof(NETLINE("Content-length: 9223372036854775807")) +
511 ((cp != NULL) ? sizeof("User: ") + i + sizeof(NETNL) : 0) +
512 sizeof(NETLINE("Message-class: spam")) +
513 sizeof(NETLINE("Set: local")) +
514 sizeof(NETLINE("Remove: local")) +
515 sizeof(NETNL) /*+1*/;
516 lp = headbuf = n_lofi_alloc(size);
518 switch (vcp->vc_action) {
519 case _SPAM_RATE:
520 _X(NETLINE("CHECK " SPAMD_IDENT));
521 break;
522 case _SPAM_HAM:
523 case _SPAM_SPAM:
524 case _SPAM_FORGET:
525 _X(NETLINE("TELL " SPAMD_IDENT));
526 break;
529 lp += snprintf(lp, size, NETLINE("Content-length: %" PRIuZ),
530 (size_t)vcp->vc_mp->m_size);
532 if (cp != NULL) {
533 _X("User: ");
534 memcpy(lp, cp, i);
535 lp += i;
536 _X(NETNL);
539 switch (vcp->vc_action) {
540 case _SPAM_RATE:
541 _X(NETNL);
542 break;
543 case _SPAM_HAM:
544 _X(NETLINE("Message-class: ham")
545 NETLINE("Set: local")
546 NETNL);
547 break;
548 case _SPAM_SPAM:
549 _X(NETLINE("Message-class: spam")
550 NETLINE("Set: local")
551 NETNL);
552 break;
553 case _SPAM_FORGET:
554 if (vcp->vc_mp->m_flag & MSPAM)
555 _X(NETLINE("Message-class: spam"));
556 else
557 _X(NETLINE("Message-class: ham"));
558 _X(NETLINE("Remove: local")
559 NETNL);
560 break;
562 # undef _X
564 i = PTR2SIZE(lp - headbuf);
565 if (n_poption & n_PO_VERBVERB)
566 n_err(">>> %.*s <<<\n", (int)i, headbuf);
567 if (i != (size_t)write(dsfd, headbuf, i))
568 goto jeso;
570 /* Then simply pass through the message "as-is" */
571 for (size = vcp->vc_mp->m_size; size > 0;) {
572 i = fread(vcp->vc_buffer, sizeof *vcp->vc_buffer,
573 n_MIN(size, BUFFER_SIZE), vcp->vc_ifp);
574 if (i == 0) {
575 if (ferror(vcp->vc_ifp))
576 goto jeso;
577 break;
579 size -= i;
581 if (i != (size_t)write(dsfd, vcp->vc_buffer, i)) {
582 jeso:
583 n_err(_("%s`%s': I/O on *spamd-socket* failed: %s\n"),
584 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
585 goto jleave;
589 /* We are finished, say so */
590 shutdown(dsfd, SHUT_WR);
592 /* Be aware on goto: i will be a line counter after this loop! */
593 for (size = 0, i = BUFFER_SIZE -1;;) {
594 ssize_t j = read(dsfd, vcp->vc_buffer + size, i);
595 if (j == -1)
596 goto jeso;
597 if (j == 0)
598 break;
599 size += j;
600 /* For the current way of doing things a single read will suffice.
601 * Note we'll be "penaltized" when awaiting EOF on the socket, at least
602 * in blocking mode, so do avoid that and break off */
603 break;
605 i = 0;
606 vcp->vc_buffer[size] = '\0';
608 if (size == 0 || size == BUFFER_SIZE) {
609 jebogus:
610 n_err(_("%s`%s': bogus spamd(1) I/O interaction (%lu)\n"),
611 vcp->vc_esep, _spam_cmds[vcp->vc_action], (ul_i)i);
612 # ifdef HAVE_DEVEL
613 if (n_poption & n_PO_VERBVERB)
614 n_err(">>> BUFFER: %s <<<\n", vcp->vc_buffer);
615 # endif
616 goto jleave;
619 /* From the response, read those lines that interest us */
620 for (lp = vcp->vc_buffer; size > 0; ++i) {
621 cp = lp;
622 lp = strchr(lp, NETNL[0]);
623 if (lp == NULL)
624 goto jebogus;
625 lp[0] = '\0';
626 if (lp[1] != NETNL[1])
627 goto jebogus;
628 lp += 2;
629 size -= PTR2SIZE(lp - cp);
631 if (i == 0) {
632 if (!strncmp(cp, "SPAMD/1.1 0 EX_OK", sizeof("SPAMD/1.1 0 EX_OK") -1))
633 continue;
634 if (vcp->vc_action != _SPAM_RATE ||
635 strstr(cp, "Service Unavailable") == NULL)
636 goto jebogus;
637 else {
638 /* Unfortunately a missing --allow-tell drops connection.. */
639 n_err(_("%s`%s': service not available in spamd(1) instance\n"),
640 vcp->vc_esep, _spam_cmds[vcp->vc_action]);
641 goto jleave;
643 } else if (i == 1) {
644 switch (vcp->vc_action) {
645 case _SPAM_RATE:
646 if (strncmp(cp, "Spam: ", sizeof("Spam: ") -1))
647 goto jebogus;
648 cp += sizeof("Spam: ") -1;
650 if (!strncmp(cp, "False", sizeof("False") -1)) {
651 cp += sizeof("False") -1;
652 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
653 } else if (!strncmp(cp, "True", sizeof("True") -1)) {
654 cp += sizeof("True") -1;
655 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
656 vcp->vc_mp->m_flag |= MSPAM;
657 } else
658 goto jebogus;
660 while (blankspacechar(*cp))
661 ++cp;
663 if (*cp++ != ';')
664 goto jebogus;
665 else {
666 char *xcp = strchr(cp, '/');
667 if (xcp != NULL) {
668 size = PTR2SIZE(xcp - cp);
669 cp[size] = '\0';
671 _spam_rate2score(vcp, cp);
673 goto jdone;
675 case _SPAM_HAM:
676 case _SPAM_SPAM:
677 /* Empty response means ok but "did nothing" */
678 if (*cp != '\0' &&
679 strncmp(cp, "DidSet: local", sizeof("DidSet: local") -1))
680 goto jebogus;
681 if (*cp == '\0' && vcp->vc_verbose)
682 n_err(_("\tBut spamd(1) \"did nothing\" for message\n"));
683 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
684 if (vcp->vc_action == _SPAM_SPAM)
685 vcp->vc_mp->m_flag |= MSPAM;
686 goto jdone;
688 case _SPAM_FORGET:
689 if (*cp != '\0' &&
690 strncmp(cp, "DidRemove: local", sizeof("DidSet: local") -1))
691 goto jebogus;
692 if (*cp == '\0' && vcp->vc_verbose)
693 n_err(_("\tBut spamd(1) \"did nothing\" for message\n"));
694 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
695 goto jdone;
700 jdone:
701 rv = TRU1;
702 jleave:
703 if (headbuf != NULL)
704 n_lofi_free(headbuf);
705 if (dsfd >= 0)
706 close(dsfd);
708 safe_signal(SIGQUIT, ssdp->d_oquit);
709 safe_signal(SIGINT, ssdp->d_oint);
710 safe_signal(SIGHUP, ssdp->d_ohup);
711 safe_signal(SIGPIPE, ssdp->d_opipe);
712 safe_signal(SIGTSTP, ssdp->d_otstp);
713 safe_signal(SIGTTIN, ssdp->d_ottin);
714 safe_signal(SIGTTOU, ssdp->d_ottou);
716 NYD2_LEAVE;
717 if (__spamd_sig != 0) {
718 sigset_t cset;
719 sigemptyset(&cset);
720 sigaddset(&cset, __spamd_sig);
721 sigprocmask(SIG_UNBLOCK, &cset, NULL);
722 n_raise(__spamd_sig);
723 assert(rv == FAL0);
725 return rv;
727 #endif /* HAVE_SPAM_SPAMD */
729 #ifdef HAVE_SPAM_FILTER
730 static bool_t
731 _spamfilter_setup(struct spam_vc *vcp)
733 struct spam_filter *sfp;
734 char const *cp, *var;
735 bool_t rv = FAL0;
736 NYD2_ENTER;
738 sfp = &vcp->vc_t.filter;
740 switch (vcp->vc_action) {
741 case _SPAM_RATE:
742 cp = ok_vlook(spamfilter_rate);
743 var = "spam-filter-rate";
744 goto jonecmd;
745 case _SPAM_HAM:
746 cp = ok_vlook(spamfilter_ham);
747 var = "spam-filter-ham";
748 goto jonecmd;
749 case _SPAM_SPAM:
750 cp = ok_vlook(spamfilter_spam);
751 var = "spam-filter-spam";
752 jonecmd:
753 if (cp == NULL) {
754 jecmd:
755 n_err(_("`%s': *%s* is not set\n"), _spam_cmds[vcp->vc_action], var);
756 goto jleave;
758 sfp->f_super.cf_cmd = savestr(cp);
759 break;
760 case _SPAM_FORGET:
761 var = "spam-filter-nospam";
762 if ((cp = ok_vlook(spamfilter_nospam)) == NULL)
763 goto jecmd;
764 sfp->f_cmd_nospam = savestr(cp);
765 if ((cp = ok_vlook(spamfilter_noham)) == NULL)
766 goto jecmd;
767 sfp->f_cmd_noham = savestr(cp);
768 break;
771 # ifdef HAVE_REGEX
772 if (vcp->vc_action == _SPAM_RATE &&
773 (cp = ok_vlook(spamfilter_rate_scanscore)) != NULL) {
774 int s;
775 char const *bp;
777 var = strchr(cp, ';');
778 if (var == NULL) {
779 n_err(_("`%s': *spamfilter-rate-scanscore*: missing semicolon;: %s\n"),
780 _spam_cmds[vcp->vc_action], cp);
781 goto jleave;
783 bp = &var[1];
785 if((n_idec_buf(&sfp->f_score_grpno, cp, PTR2SIZE(var - cp), 0,
786 n_IDEC_MODE_LIMIT_32BIT, NULL
787 ) & (n_IDEC_STATE_EMASK | n_IDEC_STATE_CONSUMED)
788 ) != n_IDEC_STATE_CONSUMED){
789 n_err(_("`%s': *spamfilter-rate-scanscore*: bad group: %s\n"),
790 _spam_cmds[vcp->vc_action], cp);
791 goto jleave;
793 if (sfp->f_score_grpno >= SPAM_FILTER_MATCHES) {
794 n_err(_("`%s': *spamfilter-rate-scanscore*: "
795 "group %u excesses limit %u\n"),
796 _spam_cmds[vcp->vc_action], sfp->f_score_grpno,
797 SPAM_FILTER_MATCHES);
798 goto jleave;
801 if ((s = regcomp(&sfp->f_score_regex, bp, REG_EXTENDED | REG_ICASE))
802 != 0) {
803 n_err(_("`%s': invalid *spamfilter-rate-scanscore* regex: %s: %s\n"),
804 _spam_cmds[vcp->vc_action], n_shexp_quote_cp(cp, FAL0),
805 n_regex_err_to_doc(NULL, s));
806 goto jleave;
808 if (sfp->f_score_grpno > sfp->f_score_regex.re_nsub) {
809 regfree(&sfp->f_score_regex);
810 n_err(_("`%s': *spamfilter-rate-scanscore*: no group %u: %s\n"),
811 _spam_cmds[vcp->vc_action], sfp->f_score_grpno, cp);
812 goto jleave;
815 # endif /* HAVE_REGEX */
817 _spam_cf_setup(vcp, TRU1);
819 vcp->vc_act = &_spamfilter_interact;
820 vcp->vc_dtor = &_spamfilter_dtor;
821 rv = TRU1;
822 jleave:
823 NYD2_LEAVE;
824 return rv;
827 static bool_t
828 _spamfilter_interact(struct spam_vc *vcp)
830 # ifdef HAVE_REGEX
831 regmatch_t rem[SPAM_FILTER_MATCHES], *remp;
832 struct spam_filter *sfp;
833 char *cp;
834 # endif
835 bool_t rv;
836 NYD2_ENTER;
838 if (vcp->vc_action == _SPAM_FORGET)
839 vcp->vc_t.cf.cf_cmd = (vcp->vc_mp->m_flag & MSPAM)
840 ? vcp->vc_t.filter.f_cmd_nospam : vcp->vc_t.filter.f_cmd_noham;
842 if (!(rv = _spam_cf_interact(vcp)))
843 goto jleave;
845 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
846 if (vcp->vc_action != _SPAM_RATE) {
847 if (vcp->vc_action == _SPAM_SPAM)
848 vcp->vc_mp->m_flag |= MSPAM;
849 goto jleave;
850 } else switch (WEXITSTATUS(vcp->vc_t.filter.f_super.cf_waitstat)) {
851 case 2:
852 vcp->vc_mp->m_flag |= MSPAMUNSURE;
853 /* FALLTHRU */
854 case 1:
855 break;
856 case 0:
857 vcp->vc_mp->m_flag |= MSPAM;
858 break;
859 default:
860 rv = FAL0;
861 goto jleave;
864 # ifdef HAVE_REGEX
865 sfp = &vcp->vc_t.filter;
867 if (sfp->f_score_grpno == 0)
868 goto jleave;
869 if (sfp->f_super.cf_result == NULL) {
870 n_err(_("`%s': *spamfilter-rate-scanscore*: filter does not "
871 "produce output!\n"));
872 goto jleave;
875 remp = rem + sfp->f_score_grpno;
877 if (regexec(&sfp->f_score_regex, sfp->f_super.cf_result, n_NELEM(rem), rem,
878 0) == REG_NOMATCH || (remp->rm_so | remp->rm_eo) < 0) {
879 n_err(_("`%s': *spamfilter-rate-scanscore* "
880 "does not match filter output!\n"),
881 _spam_cmds[vcp->vc_action]);
882 sfp->f_score_grpno = 0;
883 goto jleave;
886 cp = sfp->f_super.cf_result;
887 cp[remp->rm_eo] = '\0';
888 cp += remp->rm_so;
889 _spam_rate2score(vcp, cp);
890 # endif /* HAVE_REGEX */
892 jleave:
893 NYD2_LEAVE;
894 return rv;
897 static void
898 _spamfilter_dtor(struct spam_vc *vcp)
900 struct spam_filter *sfp;
901 NYD2_ENTER;
903 sfp = &vcp->vc_t.filter;
905 if (sfp->f_super.cf_result != NULL)
906 n_free(sfp->f_super.cf_result);
907 # ifdef HAVE_REGEX
908 if (sfp->f_score_grpno > 0)
909 regfree(&sfp->f_score_regex);
910 # endif
911 NYD2_LEAVE;
913 #endif /* HAVE_SPAM_FILTER */
915 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
916 static void
917 _spam_cf_setup(struct spam_vc *vcp, bool_t useshell)
919 struct str s;
920 char const *cp;
921 struct spam_cf *scfp;
922 NYD2_ENTER;
923 n_LCTA(2 < n_NELEM(scfp->cf_env), "Preallocated buffer too small");
925 scfp = &vcp->vc_t.cf;
927 if ((scfp->cf_useshell = useshell)) {
928 scfp->cf_acmd = ok_vlook(SHELL);
929 scfp->cf_a0 = "-c";
932 /* MAILX_FILENAME_GENERATED *//* TODO pathconf NAME_MAX; but user can create
933 * TODO a file wherever he wants! *Do* create a zero-size temporary file
934 * TODO and give *that* path as MAILX_FILENAME_TEMPORARY, clean it up once
935 * TODO the pipe returns? Like this we *can* verify path/name issues! */
936 cp = n_random_create_cp(n_MIN(NAME_MAX / 4, 16), NULL);
937 scfp->cf_env[0] = str_concat_csvl(&s,
938 n_PIPEENV_FILENAME_GENERATED, "=", cp, NULL)->s;
939 /* v15 compat NAIL_ environments vanish! */
940 scfp->cf_env[1] = str_concat_csvl(&s,
941 "NAIL_FILENAME_GENERATED", "=", cp, NULL)->s;
942 scfp->cf_env[2] = NULL;
943 NYD2_LEAVE;
946 static sigjmp_buf __spam_cf_actjmp; /* TODO someday, we won't need it */
947 static int volatile __spam_cf_sig; /* TODO someday, we won't need it */
948 static void
949 __spam_cf_onsig(int sig) /* TODO someday, we won't need it no more */
951 NYD_X; /* Signal handler */
952 __spam_cf_sig = sig;
953 siglongjmp(__spam_cf_actjmp, 1);
956 static bool_t
957 _spam_cf_interact(struct spam_vc *vcp)
959 struct spam_cf *scfp;
960 int p2c[2], c2p[2];
961 sigset_t cset;
962 char const *cp;
963 size_t size;
964 pid_t volatile pid;
965 enum {
966 _NONE = 0,
967 _SIGHOLD = 1<<0,
968 _P2C_0 = 1<<1,
969 _P2C_1 = 1<<2,
970 _P2C = _P2C_0 | _P2C_1,
971 _C2P_0 = 1<<3,
972 _C2P_1 = 1<<4,
973 _C2P = _C2P_0 | _C2P_1,
974 _JUMPED = 1<<5,
975 _RUNNING = 1<<6,
976 _GOODRUN = 1<<7,
977 _ERRORS = 1<<8
978 } volatile state = _NONE;
979 NYD2_ENTER;
981 scfp = &vcp->vc_t.cf;
982 if (scfp->cf_result != NULL) {
983 n_free(scfp->cf_result);
984 scfp->cf_result = NULL;
987 /* TODO Avoid that we jump away; yet necessary signal mess */
988 /*__spam_cf_sig = 0;*/
989 hold_sigs();
990 state |= _SIGHOLD;
991 scfp->cf_otstp = safe_signal(SIGTSTP, SIG_DFL);
992 scfp->cf_ottin = safe_signal(SIGTTIN, SIG_DFL);
993 scfp->cf_ottou = safe_signal(SIGTTOU, SIG_DFL);
994 scfp->cf_opipe = safe_signal(SIGPIPE, SIG_IGN);
995 scfp->cf_ohup = safe_signal(SIGHUP, &__spam_cf_onsig);
996 scfp->cf_oint = safe_signal(SIGINT, &__spam_cf_onsig);
997 scfp->cf_oquit = safe_signal(SIGQUIT, &__spam_cf_onsig);
998 /* Keep sigs blocked */
999 pid = 0; /* cc uninit */
1001 if (!pipe_cloexec(p2c)) {
1002 n_err(_("%s`%s': cannot create parent communication pipe: %s\n"),
1003 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
1004 goto jtail;
1006 state |= _P2C;
1008 if (!pipe_cloexec(c2p)) {
1009 n_err(_("%s`%s': cannot create child pipe: %s\n"),
1010 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
1011 goto jtail;
1013 state |= _C2P;
1015 if (sigsetjmp(__spam_cf_actjmp, 1)) {
1016 if (*vcp->vc_esep != '\0')
1017 n_err(vcp->vc_esep);
1018 state |= _JUMPED;
1019 goto jtail;
1021 rele_sigs();
1022 state &= ~_SIGHOLD;
1024 /* Start our command as requested */
1025 sigemptyset(&cset);
1026 if ((pid = n_child_start(
1027 (scfp->cf_acmd != NULL ? scfp->cf_acmd : scfp->cf_cmd),
1028 &cset, p2c[0], c2p[1],
1029 scfp->cf_a0, (scfp->cf_acmd != NULL ? scfp->cf_cmd : NULL), NULL,
1030 scfp->cf_env)) < 0) {
1031 state |= _ERRORS;
1032 goto jtail;
1034 state |= _RUNNING;
1035 close(p2c[0]);
1036 state &= ~_P2C_0;
1038 /* Yes, we could sendmp(SEND_MBOX), but simply passing through the MBOX
1039 * content does the same in effect, however much more efficiently.
1040 * XXX NOTE: this may mean we pass a message without From_ line! */
1041 for (size = vcp->vc_mp->m_size; size > 0;) {
1042 size_t i;
1044 i = fread(vcp->vc_buffer, 1, n_MIN(size, BUFFER_SIZE), vcp->vc_ifp);
1045 if (i == 0) {
1046 if (ferror(vcp->vc_ifp))
1047 state |= _ERRORS;
1048 break;
1050 size -= i;
1051 if (i != (size_t)write(p2c[1], vcp->vc_buffer, i)) {
1052 state |= _ERRORS;
1053 break;
1057 jtail:
1058 /* TODO Quite racy -- block anything for a while? */
1059 if (state & _SIGHOLD) {
1060 state &= ~_SIGHOLD;
1061 rele_sigs();
1064 if (state & _P2C_0) {
1065 state &= ~_P2C_0;
1066 close(p2c[0]);
1068 if (state & _C2P_1) {
1069 state &= ~_C2P_1;
1070 close(c2p[1]);
1072 /* And cause EOF for the reader */
1073 if (state & _P2C_1) {
1074 state &= ~_P2C_1;
1075 close(p2c[1]);
1078 if (state & _RUNNING) {
1079 if (!(state & _ERRORS) &&
1080 vcp->vc_action == _SPAM_RATE && !(state & (_JUMPED | _ERRORS))) {
1081 ssize_t i = read(c2p[0], vcp->vc_buffer, BUFFER_SIZE - 1);
1082 if (i > 0) {
1083 vcp->vc_buffer[i] = '\0';
1084 if ((cp = strchr(vcp->vc_buffer, NETNL[0])) == NULL &&
1085 (cp = strchr(vcp->vc_buffer, NETNL[1])) == NULL) {
1086 n_err(_("%s`%s': program generates too much output: %s\n"),
1087 vcp->vc_esep, _spam_cmds[vcp->vc_action],
1088 n_shexp_quote_cp(scfp->cf_cmd, FAL0));
1089 state |= _ERRORS;
1090 } else {
1091 scfp->cf_result = sbufdup(vcp->vc_buffer,
1092 PTR2SIZE(cp - vcp->vc_buffer));
1093 /* FIXME consume child output until EOF??? */
1095 } else if (i != 0)
1096 state |= _ERRORS;
1099 state &= ~_RUNNING;
1100 n_child_wait(pid, &scfp->cf_waitstat);
1101 if (WIFEXITED(scfp->cf_waitstat))
1102 state |= _GOODRUN;
1105 if (state & _C2P_0) {
1106 state &= ~_C2P_0;
1107 close(c2p[0]);
1110 safe_signal(SIGQUIT, scfp->cf_oquit);
1111 safe_signal(SIGINT, scfp->cf_oint);
1112 safe_signal(SIGHUP, scfp->cf_ohup);
1113 safe_signal(SIGPIPE, scfp->cf_opipe);
1114 safe_signal(SIGTSTP, scfp->cf_otstp);
1115 safe_signal(SIGTTIN, scfp->cf_ottin);
1116 safe_signal(SIGTTOU, scfp->cf_ottou);
1118 NYD2_LEAVE;
1119 if (state & _JUMPED) {
1120 assert(vcp->vc_dtor != NULL);
1121 (*vcp->vc_dtor)(vcp);
1123 sigemptyset(&cset);
1124 sigaddset(&cset, __spam_cf_sig);
1125 sigprocmask(SIG_UNBLOCK, &cset, NULL);
1126 n_raise(__spam_cf_sig);
1128 return !(state & (_JUMPED | _ERRORS));
1130 #endif /* HAVE_SPAM_SPAMC || HAVE_SPAM_FILTER */
1132 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_SPAMD ||\
1133 (defined HAVE_SPAM_FILTER && defined HAVE_REGEX)
1134 static void
1135 _spam_rate2score(struct spam_vc *vcp, char *buf){
1136 ui32_t m, s;
1137 enum n_idec_state ids;
1138 NYD2_ENTER;
1140 /* C99 */{ /* Overcome ISO C / compiler weirdness */
1141 char const *cp;
1143 cp = buf;
1144 ids = n_idec_ui32_cp(&m, buf, 10, &cp);
1145 if((ids & n_IDEC_STATE_EMASK) & ~n_IDEC_STATE_EBASE)
1146 goto jleave;
1147 buf = n_UNCONST(cp);
1150 s = 0;
1151 if(!(ids & n_IDEC_STATE_CONSUMED)){
1152 /* Floating-point rounding for non-mathematicians */
1153 char c1, c2, c3;
1155 ++buf; /* '.' */
1156 if((c1 = buf[0]) != '\0' && (c2 = buf[1]) != '\0' &&
1157 (c3 = buf[2]) != '\0'){
1158 buf[2] = '\0';
1159 if(c3 >= '5'){
1160 if(c2 == '9'){
1161 if(c1 == '9'){
1162 ++m;
1163 goto jscore_ok;
1164 }else
1165 buf[0] = ++c1;
1166 c2 = '0';
1167 }else
1168 ++c2;
1169 buf[1] = c2;
1173 ids = n_idec_ui32_cp(&s, buf, 10, NULL);
1174 if((ids & (n_IDEC_STATE_EMASK | n_IDEC_STATE_CONSUMED)
1175 ) != n_IDEC_STATE_CONSUMED)
1176 goto jleave;
1179 jscore_ok:
1180 vcp->vc_mp->m_spamscore = (m << 8) | s;
1181 jleave:
1182 NYD2_LEAVE;
1184 #endif /* _SPAM_SPAMC || _SPAM_SPAMD || (_SPAM_FILTER && HAVE_REGEX) */
1186 FL int
1187 c_spam_clear(void *v)
1189 int *ip;
1190 NYD_ENTER;
1192 for (ip = v; *ip != 0; ++ip)
1193 message[(size_t)*ip - 1].m_flag &= ~(MSPAM | MSPAMUNSURE);
1194 NYD_LEAVE;
1195 return 0;
1198 FL int
1199 c_spam_set(void *v)
1201 int *ip;
1202 NYD_ENTER;
1204 for (ip = v; *ip != 0; ++ip) {
1205 message[(size_t)*ip - 1].m_flag &= ~(MSPAM | MSPAMUNSURE);
1206 message[(size_t)*ip - 1].m_flag |= MSPAM;
1208 NYD_LEAVE;
1209 return 0;
1212 FL int
1213 c_spam_forget(void *v)
1215 int rv;
1216 NYD_ENTER;
1218 rv = _spam_action(_SPAM_FORGET, (int*)v) ? OKAY : STOP;
1219 NYD_LEAVE;
1220 return rv;
1223 FL int
1224 c_spam_ham(void *v)
1226 int rv;
1227 NYD_ENTER;
1229 rv = _spam_action(_SPAM_HAM, (int*)v) ? OKAY : STOP;
1230 NYD_LEAVE;
1231 return rv;
1234 FL int
1235 c_spam_rate(void *v)
1237 int rv;
1238 NYD_ENTER;
1240 rv = _spam_action(_SPAM_RATE, (int*)v) ? OKAY : STOP;
1241 NYD_LEAVE;
1242 return rv;
1245 FL int
1246 c_spam_spam(void *v)
1248 int rv;
1249 NYD_ENTER;
1251 rv = _spam_action(_SPAM_SPAM, (int*)v) ? OKAY : STOP;
1252 NYD_LEAVE;
1253 return rv;
1255 #endif /* HAVE_SPAM */
1257 /* s-it-mode */