Fix _CLOEXEC_SET() fallback implementation
[s-mailx.git] / spam.c
blobac12d439b9254e18695e0f57b802b52f77a763aa
1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
2 *@ Spam related facilities.
4 * Copyright (c) 2013 - 2017 Steffen (Daode) Nurpmeso <steffen@sdaoden.eu>.
6 * Permission to use, copy, modify, and/or distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18 #undef n_FILE
19 #define n_FILE spam
21 #ifndef HAVE_AMALGAMATION
22 # include "nail.h"
23 #endif
25 EMPTY_FILE()
26 #ifdef HAVE_SPAM
27 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
28 # include <sys/wait.h>
29 #endif
31 #ifdef HAVE_SPAM_SPAMD
32 # include <sys/socket.h>
33 # include <sys/un.h>
34 #endif
36 /* This is chosen rather arbitrarily.
37 * It must be able to swallow the first line of a rate response,
38 * and an entire CHECK/TELL spamd(1) response */
39 #if BUFFER_SIZE < 1024
40 # error *spam-interface* BUFFER_SIZE constraints are not matched
41 #endif
43 #ifdef HAVE_SPAM_SPAMD
44 # define SPAMD_IDENT "SPAMC/1.5"
45 # ifndef SUN_LEN
46 # define SUN_LEN(SUP) \
47 (sizeof(*(SUP)) - sizeof((SUP)->sun_path) + strlen((SUP)->sun_path))
48 # endif
49 #endif
51 #ifdef HAVE_SPAM_FILTER
52 /* n_NELEM() of regmatch_t groups */
53 # define SPAM_FILTER_MATCHES 32u
54 #endif
56 enum spam_action {
57 _SPAM_RATE,
58 _SPAM_HAM,
59 _SPAM_SPAM,
60 _SPAM_FORGET
63 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
64 struct spam_cf {
65 char const *cf_cmd;
66 char *cf_result; /* _SPAM_RATE: first response line */
67 int cf_waitstat;
68 ui8_t __pad[3];
69 bool_t cf_useshell;
70 /* .cf_cmd may be adjusted for each call (`spamforget')... */
71 char const *cf_acmd;
72 char const *cf_a0;
73 char const *cf_env[4];
74 sighandler_type cf_otstp;
75 sighandler_type cf_ottin;
76 sighandler_type cf_ottou;
77 sighandler_type cf_ohup;
78 sighandler_type cf_opipe;
79 sighandler_type cf_oint;
80 sighandler_type cf_oquit;
82 #endif
84 #ifdef HAVE_SPAM_SPAMC
85 struct spam_spamc {
86 struct spam_cf c_super;
87 char const *c_cmd_arr[8];
89 #endif
91 #ifdef HAVE_SPAM_SPAMD
92 struct spam_spamd {
93 struct str d_user;
94 sighandler_type d_otstp;
95 sighandler_type d_ottin;
96 sighandler_type d_ottou;
97 sighandler_type d_ohup;
98 sighandler_type d_opipe;
99 sighandler_type d_oint;
100 sighandler_type d_oquit;
101 struct sockaddr_un d_sun;
103 #endif
105 #ifdef HAVE_SPAM_FILTER
106 struct spam_filter {
107 struct spam_cf f_super;
108 char const *f_cmd_nospam; /* Working relative to current message.. */
109 char const *f_cmd_noham;
110 # ifdef HAVE_REGEX
111 ui8_t __pad[4];
112 ui32_t f_score_grpno; /* 0 for not set */
113 regex_t f_score_regex;
114 # endif
116 #endif
118 struct spam_vc {
119 enum spam_action vc_action;
120 bool_t vc_verbose; /* Verbose output */
121 bool_t vc_progress; /* "Progress meter" (mutual verbose) */
122 ui8_t __pad[2];
123 bool_t (*vc_act)(struct spam_vc *);
124 void (*vc_dtor)(struct spam_vc *);
125 char *vc_buffer; /* I/O buffer, BUFFER_SIZE bytes */
126 size_t vc_mno; /* Current message number */
127 struct message *vc_mp; /* Current message */
128 FILE *vc_ifp; /* Input stream on .vc_mp */
129 union {
130 #ifdef HAVE_SPAM_SPAMC
131 struct spam_spamc spamc;
132 #endif
133 #ifdef HAVE_SPAM_SPAMD
134 struct spam_spamd spamd;
135 #endif
136 #ifdef HAVE_SPAM_FILTER
137 struct spam_filter filter;
138 #endif
139 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
140 struct spam_cf cf;
141 #endif
142 } vc_t;
143 char const *vc_esep; /* Error separator for progress mode */
146 /* Indices according to enum spam_action */
147 static char const _spam_cmds[][16] = {
148 "spamrate", "spamham", "spamspam", "spamforget"
151 /* Shared action setup */
152 static bool_t _spam_action(enum spam_action sa, int *ip);
154 /* *spam-interface*=spamc: initialize, communicate */
155 #ifdef HAVE_SPAM_SPAMC
156 static bool_t _spamc_setup(struct spam_vc *vcp);
157 static bool_t _spamc_interact(struct spam_vc *vcp);
158 static void _spamc_dtor(struct spam_vc *vcp);
159 #endif
161 /* *spam-interface*=spamd: initialize, communicate */
162 #ifdef HAVE_SPAM_SPAMD
163 static bool_t _spamd_setup(struct spam_vc *vcp);
164 static bool_t _spamd_interact(struct spam_vc *vcp);
165 #endif
167 /* *spam-interface*=filter: initialize, communicate */
168 #ifdef HAVE_SPAM_FILTER
169 static bool_t _spamfilter_setup(struct spam_vc *vcp);
170 static bool_t _spamfilter_interact(struct spam_vc *vcp);
171 static void _spamfilter_dtor(struct spam_vc *vcp);
172 #endif
174 /* *spam-interface*=(spamc|filter): create child + communication */
175 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
176 static void _spam_cf_setup(struct spam_vc *vcp, bool_t useshell);
177 static bool_t _spam_cf_interact(struct spam_vc *vcp);
178 #endif
180 /* Convert a floating-point spam rate into message.m_spamscore */
181 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_SPAMD ||\
182 (defined HAVE_SPAM_FILTER && defined HAVE_REGEX)
183 static void _spam_rate2score(struct spam_vc *vcp, char *buf);
184 #endif
186 static bool_t
187 _spam_action(enum spam_action sa, int *ip)
189 struct spam_vc vc;
190 size_t maxsize, skipped, cnt, curr;
191 char const *cp;
192 bool_t ok = FAL0;
193 NYD_ENTER;
195 memset(&vc, 0, sizeof vc);
196 vc.vc_action = sa;
197 vc.vc_verbose = ((n_poption & n_PO_VERB) != 0);
198 vc.vc_progress = (!vc.vc_verbose && ((n_psonce & n_PSO_INTERACTIVE) != 0));
199 vc.vc_esep = vc.vc_progress ? "\n" : n_empty;
201 /* Check and setup the desired spam interface */
202 if ((cp = ok_vlook(spam_interface)) == NULL) {
203 n_err(_("`%s': no *spam-interface* set\n"), _spam_cmds[sa]);
204 goto jleave;
205 #ifdef HAVE_SPAM_SPAMC
206 } else if (!asccasecmp(cp, "spamc")) {
207 if (!_spamc_setup(&vc))
208 goto jleave;
209 #endif
210 #ifdef HAVE_SPAM_SPAMD
211 } else if (!asccasecmp(cp, "spamd")) { /* TODO v15: remove */
212 n_OBSOLETE(_("*spam-interface*=spamd is obsolete, please use =spamc"));
213 if (!_spamd_setup(&vc))
214 goto jleave;
215 #endif
216 #ifdef HAVE_SPAM_FILTER
217 } else if (!asccasecmp(cp, "filter")) {
218 if (!_spamfilter_setup(&vc))
219 goto jleave;
220 #endif
221 } else {
222 n_err(_("`%s': unknown / unsupported *spam-interface*: %s\n"),
223 _spam_cmds[sa], cp);
224 goto jleave;
227 /* *spam-maxsize* we do handle ourselfs instead */
228 if ((cp = ok_vlook(spam_maxsize)) == NULL ||
229 (n_idec_ui32_cp(&maxsize, cp, 0, NULL), maxsize) == 0)
230 maxsize = SPAM_MAXSIZE;
232 /* Finally get an I/O buffer */
233 vc.vc_buffer = salloc(BUFFER_SIZE);
235 skipped = cnt = 0;
236 if (vc.vc_progress) {
237 while (ip[cnt] != 0)
238 ++cnt;
240 for (curr = 0, ok = TRU1; *ip != 0; --cnt, ++curr, ++ip) {
241 vc.vc_mno = (size_t)*ip;
242 vc.vc_mp = message + vc.vc_mno - 1;
243 if (sa == _SPAM_RATE)
244 vc.vc_mp->m_spamscore = 0;
246 if (vc.vc_mp->m_size > maxsize) {
247 if (vc.vc_verbose)
248 n_err(_("`%s': message %lu exceeds maxsize (%lu > %lu), skip\n"),
249 _spam_cmds[sa], (ul_i)vc.vc_mno, (ul_i)(size_t)vc.vc_mp->m_size,
250 (ul_i)maxsize);
251 else if (vc.vc_progress) {
252 fprintf(n_stdout, "\r%s: !%-6" PRIuZ " %6" PRIuZ "/%-6" PRIuZ,
253 _spam_cmds[sa], vc.vc_mno, cnt, curr);
254 fflush(n_stdout);
256 ++skipped;
257 } else {
258 if (vc.vc_verbose)
259 n_err(_("`%s': message %lu\n"), _spam_cmds[sa], (ul_i)vc.vc_mno);
260 else if (vc.vc_progress) {
261 fprintf(n_stdout, "\r%s: .%-6" PRIuZ " %6" PRIuZ "/%-6" PRIuZ,
262 _spam_cmds[sa], vc.vc_mno, cnt, curr);
263 fflush(n_stdout);
266 setdot(vc.vc_mp);
267 if ((vc.vc_ifp = setinput(&mb, vc.vc_mp, NEED_BODY)) == NULL) {
268 n_err(_("%s`%s': cannot load message %lu: %s\n"),
269 vc.vc_esep, _spam_cmds[sa], (ul_i)vc.vc_mno,
270 n_err_to_doc(n_err_no));
271 ok = FAL0;
272 break;
275 if (!(ok = (*vc.vc_act)(&vc)))
276 break;
279 if (vc.vc_progress) {
280 if (curr > 0)
281 fprintf(n_stdout, _(" %s (%" PRIuZ "/%" PRIuZ " all/skipped)\n"),
282 (ok ? _("done") : V_(n_error)), curr, skipped);
283 fflush(n_stdout);
286 if (vc.vc_dtor != NULL)
287 (*vc.vc_dtor)(&vc);
288 jleave:
289 NYD_LEAVE;
290 return !ok;
293 #ifdef HAVE_SPAM_SPAMC
294 static bool_t
295 _spamc_setup(struct spam_vc *vcp)
297 struct spam_spamc *sscp;
298 struct str str;
299 char const **args, *cp;
300 bool_t rv = FAL0;
301 NYD2_ENTER;
303 sscp = &vcp->vc_t.spamc;
304 args = sscp->c_cmd_arr;
306 if ((cp = ok_vlook(spamc_command)) == NULL) {
307 # ifdef SPAM_SPAMC_PATH
308 cp = SPAM_SPAMC_PATH;
309 # else
310 n_err(_("`%s': *spamc-command* is not set\n"),
311 _spam_cmds[vcp->vc_action]);
312 goto jleave;
313 # endif
315 *args++ = cp;
317 switch (vcp->vc_action) {
318 case _SPAM_RATE:
319 *args = "-c";
320 break;
321 case _SPAM_HAM:
322 args[1] = "ham";
323 goto jlearn;
324 case _SPAM_SPAM:
325 args[1] = "spam";
326 goto jlearn;
327 case _SPAM_FORGET:
328 args[1] = "forget";
329 jlearn:
330 *args = "-L";
331 ++args;
332 break;
334 ++args;
336 *args++ = "-l"; /* --log-to-stderr */
337 *args++ = "-x"; /* No "safe callback", we need to react on errors! */
339 if ((cp = ok_vlook(spamc_arguments)) != NULL)
340 *args++ = cp;
342 if ((cp = ok_vlook(spamc_user)) != NULL) {
343 if (*cp == '\0')
344 cp = ok_vlook(LOGNAME);
345 *args++ = "-u";
346 *args++ = cp;
348 assert(PTR2SIZE(args - sscp->c_cmd_arr) <= n_NELEM(sscp->c_cmd_arr));
350 *args = NULL;
351 sscp->c_super.cf_cmd = str_concat_cpa(&str, sscp->c_cmd_arr, " ")->s;
352 if (vcp->vc_verbose)
353 n_err(_("spamc(1) via %s\n"),
354 n_shexp_quote_cp(sscp->c_super.cf_cmd, FAL0));
356 _spam_cf_setup(vcp, FAL0);
358 vcp->vc_act = &_spamc_interact;
359 vcp->vc_dtor = &_spamc_dtor;
360 rv = TRU1;
361 # ifndef SPAM_SPAMC_PATH
362 jleave:
363 # endif
364 NYD2_LEAVE;
365 return rv;
368 static bool_t
369 _spamc_interact(struct spam_vc *vcp)
371 bool_t rv;
372 NYD2_ENTER;
374 if (!(rv = _spam_cf_interact(vcp)))
375 goto jleave;
377 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
378 if (vcp->vc_action != _SPAM_RATE) {
379 if (vcp->vc_action == _SPAM_SPAM)
380 vcp->vc_mp->m_flag |= MSPAM;
381 } else {
382 char *buf, *cp;
384 switch (WEXITSTATUS(vcp->vc_t.spamc.c_super.cf_waitstat)) {
385 case 1:
386 vcp->vc_mp->m_flag |= MSPAM;
387 /* FALLTHRU */
388 case 0:
389 break;
390 default:
391 rv = FAL0;
392 goto jleave;
395 if ((cp = strchr(buf = vcp->vc_t.spamc.c_super.cf_result, '/')) != NULL)
396 buf[PTR2SIZE(cp - buf)] = '\0';
397 _spam_rate2score(vcp, buf);
399 jleave:
400 NYD2_LEAVE;
401 return rv;
404 static void
405 _spamc_dtor(struct spam_vc *vcp)
407 NYD2_ENTER;
408 if (vcp->vc_t.spamc.c_super.cf_result != NULL)
409 free(vcp->vc_t.spamc.c_super.cf_result);
410 NYD2_LEAVE;
412 #endif /* HAVE_SPAM_SPAMC */
414 #ifdef HAVE_SPAM_SPAMD
415 static bool_t
416 _spamd_setup(struct spam_vc *vcp)
418 struct spam_spamd *ssdp;
419 char const *cp;
420 size_t l;
421 bool_t rv = FAL0;
422 NYD2_ENTER;
424 ssdp = &vcp->vc_t.spamd;
426 if ((cp = ok_vlook(spamd_user)) != NULL) {
427 if (*cp == '\0')
428 cp = ok_vlook(LOGNAME);
429 ssdp->d_user.l = strlen(ssdp->d_user.s = n_UNCONST(cp));
432 if ((cp = ok_vlook(spamd_socket)) == NULL) {
433 n_err(_("`%s': required *spamd-socket* is not set\n"),
434 _spam_cmds[vcp->vc_action]);
435 goto jleave;
437 if ((l = strlen(cp) +1) >= sizeof(ssdp->d_sun.sun_path)) {
438 n_err(_("`%s': *spamd-socket* too long: %s\n"),
439 _spam_cmds[vcp->vc_action], n_shexp_quote_cp(cp, FAL0));
440 goto jleave;
442 ssdp->d_sun.sun_family = AF_UNIX;
443 memcpy(ssdp->d_sun.sun_path, cp, l);
445 vcp->vc_act = &_spamd_interact;
446 rv = TRU1;
447 jleave:
448 NYD2_LEAVE;
449 return rv;
452 static sigjmp_buf __spamd_actjmp; /* TODO oneday, we won't need it no more */
453 static int volatile __spamd_sig; /* TODO oneday, we won't need it no more */
454 static void
455 __spamd_onsig(int sig) /* TODO someday, we won't need it no more */
457 NYD_X; /* Signal handler */
458 __spamd_sig = sig;
459 siglongjmp(__spamd_actjmp, 1);
462 static bool_t
463 _spamd_interact(struct spam_vc *vcp)
465 struct spam_spamd *ssdp;
466 size_t size, i;
467 char *lp, *cp, * volatile headbuf = NULL;
468 int volatile dsfd = -1;
469 bool_t volatile rv = FAL0;
470 NYD2_ENTER;
472 ssdp = &vcp->vc_t.spamd;
474 __spamd_sig = 0;
475 hold_sigs();
476 ssdp->d_otstp = safe_signal(SIGTSTP, SIG_DFL);
477 ssdp->d_ottin = safe_signal(SIGTTIN, SIG_DFL);
478 ssdp->d_ottou = safe_signal(SIGTTOU, SIG_DFL);
479 ssdp->d_opipe = safe_signal(SIGPIPE, SIG_IGN);
480 ssdp->d_ohup = safe_signal(SIGHUP, &__spamd_onsig);
481 ssdp->d_oint = safe_signal(SIGINT, &__spamd_onsig);
482 ssdp->d_oquit = safe_signal(SIGQUIT, &__spamd_onsig);
483 if (sigsetjmp(__spamd_actjmp, 1)) {
484 if (*vcp->vc_esep != '\0')
485 n_err(vcp->vc_esep);
486 goto jleave;
488 rele_sigs();
490 if ((dsfd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
491 n_err(_("%s`%s': can't create unix(4) socket: %s\n"),
492 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
493 goto jleave;
496 if (connect(dsfd, (struct sockaddr*)&ssdp->d_sun, SUN_LEN(&ssdp->d_sun)) ==
497 -1) {
498 n_err(_("%s`%s': can't connect to *spam-socket*: %s\n"),
499 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
500 close(dsfd);
501 goto jleave;
504 /* The command header, finalized with an empty line.
505 * This needs to be written in a single write(2)! */
506 # undef _X
507 # define _X(X) do {memcpy(lp, X, sizeof(X) -1); lp += sizeof(X) -1;} while (0)
509 i = ((cp = ssdp->d_user.s) != NULL) ? ssdp->d_user.l : 0;
510 lp = headbuf = ac_alloc(
511 sizeof(NETLINE("A_VERY_LONG_COMMAND " SPAMD_IDENT)) +
512 sizeof(NETLINE("Content-length: 9223372036854775807")) +
513 ((cp != NULL) ? sizeof("User: ") + i + sizeof(NETNL) : 0) +
514 sizeof(NETLINE("Message-class: spam")) +
515 sizeof(NETLINE("Set: local")) +
516 sizeof(NETLINE("Remove: local")) +
517 sizeof(NETNL) /*+1*/);
519 switch (vcp->vc_action) {
520 case _SPAM_RATE:
521 _X(NETLINE("CHECK " SPAMD_IDENT));
522 break;
523 case _SPAM_HAM:
524 case _SPAM_SPAM:
525 case _SPAM_FORGET:
526 _X(NETLINE("TELL " SPAMD_IDENT));
527 break;
530 lp += snprintf(lp, 0x7FFF, NETLINE("Content-length: %" PRIuZ),
531 (size_t)vcp->vc_mp->m_size);
533 if (cp != NULL) {
534 _X("User: ");
535 memcpy(lp, cp, i);
536 lp += i;
537 _X(NETNL);
540 switch (vcp->vc_action) {
541 case _SPAM_RATE:
542 _X(NETNL);
543 break;
544 case _SPAM_HAM:
545 _X(NETLINE("Message-class: ham")
546 NETLINE("Set: local")
547 NETNL);
548 break;
549 case _SPAM_SPAM:
550 _X(NETLINE("Message-class: spam")
551 NETLINE("Set: local")
552 NETNL);
553 break;
554 case _SPAM_FORGET:
555 if (vcp->vc_mp->m_flag & MSPAM)
556 _X(NETLINE("Message-class: spam"));
557 else
558 _X(NETLINE("Message-class: ham"));
559 _X(NETLINE("Remove: local")
560 NETNL);
561 break;
563 # undef _X
565 i = PTR2SIZE(lp - headbuf);
566 if (n_poption & n_PO_VERBVERB)
567 n_err(">>> %.*s <<<\n", (int)i, headbuf);
568 if (i != (size_t)write(dsfd, headbuf, i))
569 goto jeso;
571 /* Then simply pass through the message "as-is" */
572 for (size = vcp->vc_mp->m_size; size > 0;) {
573 i = fread(vcp->vc_buffer, sizeof *vcp->vc_buffer,
574 n_MIN(size, BUFFER_SIZE), vcp->vc_ifp);
575 if (i == 0) {
576 if (ferror(vcp->vc_ifp))
577 goto jeso;
578 break;
580 size -= i;
582 if (i != (size_t)write(dsfd, vcp->vc_buffer, i)) {
583 jeso:
584 n_err(_("%s`%s': I/O on *spamd-socket* failed: %s\n"),
585 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
586 goto jleave;
590 /* We are finished, say so */
591 shutdown(dsfd, SHUT_WR);
593 /* Be aware on goto: i will be a line counter after this loop! */
594 for (size = 0, i = BUFFER_SIZE -1;;) {
595 ssize_t j = read(dsfd, vcp->vc_buffer + size, i);
596 if (j == -1)
597 goto jeso;
598 if (j == 0)
599 break;
600 size += j;
601 i -= j;
602 /* For the current way of doing things a single read will suffice.
603 * Note we'll be "penaltized" when awaiting EOF on the socket, at least
604 * in blocking mode, so do avoid that and break off */
605 break;
607 i = 0;
608 vcp->vc_buffer[size] = '\0';
610 if (size == 0 || size == BUFFER_SIZE) {
611 jebogus:
612 n_err(_("%s`%s': bogus spamd(1) I/O interaction (%lu)\n"),
613 vcp->vc_esep, _spam_cmds[vcp->vc_action], (ul_i)i);
614 # ifdef HAVE_DEVEL
615 if (n_poption & n_PO_VERBVERB)
616 n_err(">>> BUFFER: %s <<<\n", vcp->vc_buffer);
617 # endif
618 goto jleave;
621 /* From the response, read those lines that interest us */
622 for (lp = vcp->vc_buffer; size > 0; ++i) {
623 cp = lp;
624 lp = strchr(lp, NETNL[0]);
625 if (lp == NULL)
626 goto jebogus;
627 lp[0] = '\0';
628 if (lp[1] != NETNL[1])
629 goto jebogus;
630 lp += 2;
631 size -= PTR2SIZE(lp - cp);
633 if (i == 0) {
634 if (!strncmp(cp, "SPAMD/1.1 0 EX_OK", sizeof("SPAMD/1.1 0 EX_OK") -1))
635 continue;
636 if (vcp->vc_action != _SPAM_RATE ||
637 strstr(cp, "Service Unavailable") == NULL)
638 goto jebogus;
639 else {
640 /* Unfortunately a missing --allow-tell drops connection.. */
641 n_err(_("%s`%s': service not available in spamd(1) instance\n"),
642 vcp->vc_esep, _spam_cmds[vcp->vc_action]);
643 goto jleave;
645 } else if (i == 1) {
646 switch (vcp->vc_action) {
647 case _SPAM_RATE:
648 if (strncmp(cp, "Spam: ", sizeof("Spam: ") -1))
649 goto jebogus;
650 cp += sizeof("Spam: ") -1;
652 if (!strncmp(cp, "False", sizeof("False") -1)) {
653 cp += sizeof("False") -1;
654 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
655 } else if (!strncmp(cp, "True", sizeof("True") -1)) {
656 cp += sizeof("True") -1;
657 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
658 vcp->vc_mp->m_flag |= MSPAM;
659 } else
660 goto jebogus;
662 while (blankspacechar(*cp))
663 ++cp;
665 if (*cp++ != ';')
666 goto jebogus;
667 else {
668 char *xcp = strchr(cp, '/');
669 if (xcp != NULL) {
670 size = PTR2SIZE(xcp - cp);
671 cp[size] = '\0';
673 _spam_rate2score(vcp, cp);
675 goto jdone;
677 case _SPAM_HAM:
678 case _SPAM_SPAM:
679 /* Empty response means ok but "did nothing" */
680 if (*cp != '\0' &&
681 strncmp(cp, "DidSet: local", sizeof("DidSet: local") -1))
682 goto jebogus;
683 if (*cp == '\0' && vcp->vc_verbose)
684 n_err(_("\tBut spamd(1) \"did nothing\" for message\n"));
685 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
686 if (vcp->vc_action == _SPAM_SPAM)
687 vcp->vc_mp->m_flag |= MSPAM;
688 goto jdone;
690 case _SPAM_FORGET:
691 if (*cp != '\0' &&
692 strncmp(cp, "DidRemove: local", sizeof("DidSet: local") -1))
693 goto jebogus;
694 if (*cp == '\0' && vcp->vc_verbose)
695 n_err(_("\tBut spamd(1) \"did nothing\" for message\n"));
696 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
697 goto jdone;
702 jdone:
703 rv = TRU1;
704 jleave:
705 if (headbuf != NULL)
706 ac_free(headbuf);
707 if (dsfd >= 0)
708 close(dsfd);
710 safe_signal(SIGQUIT, ssdp->d_oquit);
711 safe_signal(SIGINT, ssdp->d_oint);
712 safe_signal(SIGHUP, ssdp->d_ohup);
713 safe_signal(SIGPIPE, ssdp->d_opipe);
714 safe_signal(SIGTSTP, ssdp->d_otstp);
715 safe_signal(SIGTTIN, ssdp->d_ottin);
716 safe_signal(SIGTTOU, ssdp->d_ottou);
718 NYD2_LEAVE;
719 if (__spamd_sig != 0) {
720 sigset_t cset;
721 sigemptyset(&cset);
722 sigaddset(&cset, __spamd_sig);
723 sigprocmask(SIG_UNBLOCK, &cset, NULL);
724 n_raise(__spamd_sig);
725 assert(rv == FAL0);
727 return rv;
729 #endif /* HAVE_SPAM_SPAMD */
731 #ifdef HAVE_SPAM_FILTER
732 static bool_t
733 _spamfilter_setup(struct spam_vc *vcp)
735 struct spam_filter *sfp;
736 char const *cp, *var;
737 bool_t rv = FAL0;
738 NYD2_ENTER;
740 sfp = &vcp->vc_t.filter;
742 switch (vcp->vc_action) {
743 case _SPAM_RATE:
744 cp = ok_vlook(spamfilter_rate);
745 var = "spam-filter-rate";
746 goto jonecmd;
747 case _SPAM_HAM:
748 cp = ok_vlook(spamfilter_ham);
749 var = "spam-filter-ham";
750 goto jonecmd;
751 case _SPAM_SPAM:
752 cp = ok_vlook(spamfilter_spam);
753 var = "spam-filter-spam";
754 jonecmd:
755 if (cp == NULL) {
756 jecmd:
757 n_err(_("`%s': *%s* is not set\n"), _spam_cmds[vcp->vc_action], var);
758 goto jleave;
760 sfp->f_super.cf_cmd = savestr(cp);
761 break;
762 case _SPAM_FORGET:
763 var = "spam-filter-nospam";
764 if ((cp = ok_vlook(spamfilter_nospam)) == NULL)
765 goto jecmd;
766 sfp->f_cmd_nospam = savestr(cp);
767 if ((cp = ok_vlook(spamfilter_noham)) == NULL)
768 goto jecmd;
769 sfp->f_cmd_noham = savestr(cp);
770 break;
773 # ifdef HAVE_REGEX
774 if (vcp->vc_action == _SPAM_RATE &&
775 (cp = ok_vlook(spamfilter_rate_scanscore)) != NULL) {
776 int s;
777 char const *bp;
779 var = strchr(cp, ';');
780 if (var == NULL) {
781 n_err(_("`%s': *spamfilter-rate-scanscore*: missing semicolon;: %s\n"),
782 _spam_cmds[vcp->vc_action], cp);
783 goto jleave;
785 bp = &var[1];
787 if((n_idec_buf(&sfp->f_score_grpno, cp, PTR2SIZE(var - cp), 0,
788 n_IDEC_MODE_LIMIT_32BIT, NULL
789 ) & (n_IDEC_STATE_EMASK | n_IDEC_STATE_CONSUMED)
790 ) != n_IDEC_STATE_CONSUMED){
791 n_err(_("`%s': *spamfilter-rate-scanscore*: bad group: %s\n"),
792 _spam_cmds[vcp->vc_action], cp);
793 goto jleave;
795 if (sfp->f_score_grpno >= SPAM_FILTER_MATCHES) {
796 n_err(_("`%s': *spamfilter-rate-scanscore*: "
797 "group %u excesses limit %u\n"),
798 _spam_cmds[vcp->vc_action], sfp->f_score_grpno,
799 SPAM_FILTER_MATCHES);
800 goto jleave;
803 if ((s = regcomp(&sfp->f_score_regex, bp, REG_EXTENDED | REG_ICASE))
804 != 0) {
805 n_err(_("`%s': invalid *spamfilter-rate-scanscore* regex: %s: %s\n"),
806 _spam_cmds[vcp->vc_action], n_shexp_quote_cp(cp, FAL0),
807 n_regex_err_to_doc(&sfp->f_score_regex, s));
808 goto jleave;
810 if (sfp->f_score_grpno > sfp->f_score_regex.re_nsub) {
811 regfree(&sfp->f_score_regex);
812 n_err(_("`%s': *spamfilter-rate-scanscore*: no group %u: %s\n"),
813 _spam_cmds[vcp->vc_action], sfp->f_score_grpno, cp);
814 goto jleave;
817 # endif /* HAVE_REGEX */
819 _spam_cf_setup(vcp, TRU1);
821 vcp->vc_act = &_spamfilter_interact;
822 vcp->vc_dtor = &_spamfilter_dtor;
823 rv = TRU1;
824 jleave:
825 NYD2_LEAVE;
826 return rv;
829 static bool_t
830 _spamfilter_interact(struct spam_vc *vcp)
832 # ifdef HAVE_REGEX
833 regmatch_t rem[SPAM_FILTER_MATCHES], *remp;
834 struct spam_filter *sfp;
835 char *cp;
836 # endif
837 bool_t rv;
838 NYD2_ENTER;
840 if (vcp->vc_action == _SPAM_FORGET)
841 vcp->vc_t.cf.cf_cmd = (vcp->vc_mp->m_flag & MSPAM)
842 ? vcp->vc_t.filter.f_cmd_nospam : vcp->vc_t.filter.f_cmd_noham;
844 if (!(rv = _spam_cf_interact(vcp)))
845 goto jleave;
847 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
848 if (vcp->vc_action != _SPAM_RATE) {
849 if (vcp->vc_action == _SPAM_SPAM)
850 vcp->vc_mp->m_flag |= MSPAM;
851 goto jleave;
852 } else switch (WEXITSTATUS(vcp->vc_t.filter.f_super.cf_waitstat)) {
853 case 2:
854 vcp->vc_mp->m_flag |= MSPAMUNSURE;
855 /* FALLTHRU */
856 case 1:
857 break;
858 case 0:
859 vcp->vc_mp->m_flag |= MSPAM;
860 break;
861 default:
862 rv = FAL0;
863 goto jleave;
866 # ifdef HAVE_REGEX
867 sfp = &vcp->vc_t.filter;
869 if (sfp->f_score_grpno == 0)
870 goto jleave;
872 assert(sfp->f_super.cf_result != NULL);
873 remp = rem + sfp->f_score_grpno;
875 if (regexec(&sfp->f_score_regex, sfp->f_super.cf_result, n_NELEM(rem), rem,
876 0) == REG_NOMATCH || (remp->rm_so | remp->rm_eo) < 0) {
877 n_err(_("`%s': *spamfilter-rate-scanscore* "
878 "doesn't match filter output!\n"),
879 _spam_cmds[vcp->vc_action]);
880 sfp->f_score_grpno = 0;
881 goto jleave;
884 cp = sfp->f_super.cf_result;
885 cp[remp->rm_eo] = '\0';
886 cp += remp->rm_so;
887 _spam_rate2score(vcp, cp);
888 # endif /* HAVE_REGEX */
890 jleave:
891 NYD2_LEAVE;
892 return rv;
895 static void
896 _spamfilter_dtor(struct spam_vc *vcp)
898 struct spam_filter *sfp;
899 NYD2_ENTER;
901 sfp = &vcp->vc_t.filter;
903 if (sfp->f_super.cf_result != NULL)
904 free(sfp->f_super.cf_result);
905 # ifdef HAVE_REGEX
906 if (sfp->f_score_grpno > 0)
907 regfree(&sfp->f_score_regex);
908 # endif
909 NYD2_LEAVE;
911 #endif /* HAVE_SPAM_FILTER */
913 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
914 static void
915 _spam_cf_setup(struct spam_vc *vcp, bool_t useshell)
917 struct str s;
918 char const *cp;
919 struct spam_cf *scfp;
920 NYD2_ENTER;
921 n_LCTA(2 < n_NELEM(scfp->cf_env), "Preallocated buffer too small");
923 scfp = &vcp->vc_t.cf;
925 if ((scfp->cf_useshell = useshell)) {
926 scfp->cf_acmd = ok_vlook(SHELL);
927 scfp->cf_a0 = "-c";
930 /* MAILX_FILENAME_GENERATED *//* TODO pathconf NAME_MAX; but user can create
931 * TODO a file wherever he wants! *Do* create a zero-size temporary file
932 * TODO and give *that* path as MAILX_FILENAME_TEMPORARY, clean it up once
933 * TODO the pipe returns? Like this we *can* verify path/name issues! */
934 cp = n_random_create_cp(n_MIN(NAME_MAX / 4, 16));
935 scfp->cf_env[0] = str_concat_csvl(&s,
936 n_PIPEENV_FILENAME_GENERATED, "=", cp, NULL)->s;
937 /* v15 compat NAIL_ environments vanish! */
938 scfp->cf_env[1] = str_concat_csvl(&s,
939 "NAIL_FILENAME_GENERATED", "=", cp, NULL)->s;
940 scfp->cf_env[2] = NULL;
941 NYD2_LEAVE;
944 static sigjmp_buf __spam_cf_actjmp; /* TODO someday, we won't need it */
945 static int volatile __spam_cf_sig; /* TODO someday, we won't need it */
946 static void
947 __spam_cf_onsig(int sig) /* TODO someday, we won't need it no more */
949 NYD_X; /* Signal handler */
950 __spam_cf_sig = sig;
951 siglongjmp(__spam_cf_actjmp, 1);
954 static bool_t
955 _spam_cf_interact(struct spam_vc *vcp)
957 struct spam_cf *scfp;
958 int p2c[2], c2p[2];
959 sigset_t cset;
960 char const *cp;
961 size_t size;
962 pid_t volatile pid;
963 enum {
964 _NONE = 0,
965 _SIGHOLD = 1<<0,
966 _P2C_0 = 1<<1,
967 _P2C_1 = 1<<2,
968 _P2C = _P2C_0 | _P2C_1,
969 _C2P_0 = 1<<3,
970 _C2P_1 = 1<<4,
971 _C2P = _C2P_0 | _C2P_1,
972 _JUMPED = 1<<5,
973 _RUNNING = 1<<6,
974 _GOODRUN = 1<<7,
975 _ERRORS = 1<<8
976 } volatile state = _NONE;
977 NYD2_ENTER;
979 scfp = &vcp->vc_t.cf;
980 if (scfp->cf_result != NULL) {
981 free(scfp->cf_result);
982 scfp->cf_result = NULL;
985 /* TODO Avoid that we jump away; yet necessary signal mess */
986 /*__spam_cf_sig = 0;*/
987 hold_sigs();
988 state |= _SIGHOLD;
989 scfp->cf_otstp = safe_signal(SIGTSTP, SIG_DFL);
990 scfp->cf_ottin = safe_signal(SIGTTIN, SIG_DFL);
991 scfp->cf_ottou = safe_signal(SIGTTOU, SIG_DFL);
992 scfp->cf_opipe = safe_signal(SIGPIPE, SIG_IGN);
993 scfp->cf_ohup = safe_signal(SIGHUP, &__spam_cf_onsig);
994 scfp->cf_oint = safe_signal(SIGINT, &__spam_cf_onsig);
995 scfp->cf_oquit = safe_signal(SIGQUIT, &__spam_cf_onsig);
996 /* Keep sigs blocked */
997 pid = 0; /* cc uninit */
999 if (!pipe_cloexec(p2c)) {
1000 n_err(_("%s`%s': cannot create parent communication pipe: %s\n"),
1001 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
1002 goto jtail;
1004 state |= _P2C;
1006 if (!pipe_cloexec(c2p)) {
1007 n_err(_("%s`%s': cannot create child pipe: %s\n"),
1008 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
1009 goto jtail;
1011 state |= _C2P;
1013 if (sigsetjmp(__spam_cf_actjmp, 1)) {
1014 if (*vcp->vc_esep != '\0')
1015 n_err(vcp->vc_esep);
1016 state |= _JUMPED;
1017 goto jtail;
1019 rele_sigs();
1020 state &= ~_SIGHOLD;
1022 /* Start our command as requested */
1023 sigemptyset(&cset);
1024 if ((pid = n_child_start(
1025 (scfp->cf_acmd != NULL ? scfp->cf_acmd : scfp->cf_cmd),
1026 &cset, p2c[0], c2p[1],
1027 scfp->cf_a0, (scfp->cf_acmd != NULL ? scfp->cf_cmd : NULL), NULL,
1028 scfp->cf_env)) < 0) {
1029 state |= _ERRORS;
1030 goto jtail;
1032 state |= _RUNNING;
1033 close(p2c[0]);
1034 state &= ~_P2C_0;
1036 /* Yes, we could sendmp(SEND_MBOX), but simply passing through the MBOX
1037 * content does the same in effect, however much more efficiently.
1038 * XXX NOTE: this may mean we pass a message without From_ line! */
1039 for (size = vcp->vc_mp->m_size; size > 0;) {
1040 size_t i;
1042 i = fread(vcp->vc_buffer, 1, n_MIN(size, BUFFER_SIZE), vcp->vc_ifp);
1043 if (i == 0) {
1044 if (ferror(vcp->vc_ifp))
1045 state |= _ERRORS;
1046 break;
1048 size -= i;
1049 if (i != (size_t)write(p2c[1], vcp->vc_buffer, i)) {
1050 state |= _ERRORS;
1051 break;
1055 jtail:
1056 /* TODO Quite racy -- block anything for a while? */
1057 if (state & _SIGHOLD) {
1058 state &= ~_SIGHOLD;
1059 rele_sigs();
1062 if (state & _P2C_0) {
1063 state &= ~_P2C_0;
1064 close(p2c[0]);
1066 if (state & _C2P_1) {
1067 state &= ~_C2P_1;
1068 close(c2p[1]);
1070 /* And cause EOF for the reader */
1071 if (state & _P2C_1) {
1072 state &= ~_P2C_1;
1073 close(p2c[1]);
1076 if (state & _RUNNING) {
1077 if (!(state & _ERRORS) &&
1078 vcp->vc_action == _SPAM_RATE && !(state & (_JUMPED | _ERRORS))) {
1079 ssize_t i = read(c2p[0], vcp->vc_buffer, BUFFER_SIZE - 1);
1080 if (i > 0) {
1081 vcp->vc_buffer[i] = '\0';
1082 if ((cp = strchr(vcp->vc_buffer, NETNL[0])) == NULL &&
1083 (cp = strchr(vcp->vc_buffer, NETNL[1])) == NULL) {
1084 n_err(_("%s`%s': program generates too much output: %s\n"),
1085 vcp->vc_esep, _spam_cmds[vcp->vc_action],
1086 n_shexp_quote_cp(scfp->cf_cmd, FAL0));
1087 state |= _ERRORS;
1088 } else {
1089 scfp->cf_result = sbufdup(vcp->vc_buffer,
1090 PTR2SIZE(cp - vcp->vc_buffer));
1091 /* FIXME consume child output until EOF??? */
1093 } else if (i != 0)
1094 state |= _ERRORS;
1097 state &= ~_RUNNING;
1098 n_child_wait(pid, &scfp->cf_waitstat);
1099 if (WIFEXITED(scfp->cf_waitstat))
1100 state |= _GOODRUN;
1103 if (state & _C2P_0) {
1104 state &= ~_C2P_0;
1105 close(c2p[0]);
1108 safe_signal(SIGQUIT, scfp->cf_oquit);
1109 safe_signal(SIGINT, scfp->cf_oint);
1110 safe_signal(SIGHUP, scfp->cf_ohup);
1111 safe_signal(SIGPIPE, scfp->cf_opipe);
1112 safe_signal(SIGTSTP, scfp->cf_otstp);
1113 safe_signal(SIGTTIN, scfp->cf_ottin);
1114 safe_signal(SIGTTOU, scfp->cf_ottou);
1116 NYD2_LEAVE;
1117 if (state & _JUMPED) {
1118 assert(vcp->vc_dtor != NULL);
1119 (*vcp->vc_dtor)(vcp);
1121 sigemptyset(&cset);
1122 sigaddset(&cset, __spam_cf_sig);
1123 sigprocmask(SIG_UNBLOCK, &cset, NULL);
1124 n_raise(__spam_cf_sig);
1126 return !(state & (_JUMPED | _ERRORS));
1128 #endif /* HAVE_SPAM_SPAMC || HAVE_SPAM_FILTER */
1130 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_SPAMD ||\
1131 (defined HAVE_SPAM_FILTER && defined HAVE_REGEX)
1132 static void
1133 _spam_rate2score(struct spam_vc *vcp, char *buf){
1134 ui32_t m, s;
1135 enum n_idec_state ids;
1136 NYD2_ENTER;
1138 /* C99 */{ /* Overcome ISO C / compiler weirdness */
1139 char const *cp;
1141 cp = buf;
1142 ids = n_idec_ui32_cp(&m, buf, 10, &cp);
1143 if((ids & n_IDEC_STATE_EMASK) & ~n_IDEC_STATE_EBASE)
1144 goto jleave;
1145 buf = n_UNCONST(cp);
1148 s = 0;
1149 if(!(ids & n_IDEC_STATE_CONSUMED)){
1150 /* Floating-point rounding for non-mathematicians */
1151 char c1, c2, c3;
1153 ++buf; /* '.' */
1154 if((c1 = buf[0]) != '\0' && (c2 = buf[1]) != '\0' &&
1155 (c3 = buf[2]) != '\0'){
1156 buf[2] = '\0';
1157 if(c3 >= '5'){
1158 if(c2 == '9'){
1159 if(c1 == '9'){
1160 ++m;
1161 goto jscore_ok;
1162 }else
1163 buf[0] = ++c1;
1164 c2 = '0';
1165 }else
1166 ++c2;
1167 buf[1] = c2;
1171 ids = n_idec_ui32_cp(&s, buf, 10, NULL);
1172 if((ids & (n_IDEC_STATE_EMASK | n_IDEC_STATE_CONSUMED)
1173 ) != n_IDEC_STATE_CONSUMED)
1174 goto jleave;
1177 jscore_ok:
1178 vcp->vc_mp->m_spamscore = (m << 8) | s;
1179 jleave:
1180 NYD2_LEAVE;
1182 #endif /* _SPAM_SPAMC || _SPAM_SPAMD || (_SPAM_FILTER && HAVE_REGEX) */
1184 FL int
1185 c_spam_clear(void *v)
1187 int *ip;
1188 NYD_ENTER;
1190 for (ip = v; *ip != 0; ++ip)
1191 message[(size_t)*ip - 1].m_flag &= ~(MSPAM | MSPAMUNSURE);
1192 NYD_LEAVE;
1193 return 0;
1196 FL int
1197 c_spam_set(void *v)
1199 int *ip;
1200 NYD_ENTER;
1202 for (ip = v; *ip != 0; ++ip) {
1203 message[(size_t)*ip - 1].m_flag &= ~(MSPAM | MSPAMUNSURE);
1204 message[(size_t)*ip - 1].m_flag |= MSPAM;
1206 NYD_LEAVE;
1207 return 0;
1210 FL int
1211 c_spam_forget(void *v)
1213 int rv;
1214 NYD_ENTER;
1216 rv = _spam_action(_SPAM_FORGET, (int*)v) ? OKAY : STOP;
1217 NYD_LEAVE;
1218 return rv;
1221 FL int
1222 c_spam_ham(void *v)
1224 int rv;
1225 NYD_ENTER;
1227 rv = _spam_action(_SPAM_HAM, (int*)v) ? OKAY : STOP;
1228 NYD_LEAVE;
1229 return rv;
1232 FL int
1233 c_spam_rate(void *v)
1235 int rv;
1236 NYD_ENTER;
1238 rv = _spam_action(_SPAM_RATE, (int*)v) ? OKAY : STOP;
1239 NYD_LEAVE;
1240 return rv;
1243 FL int
1244 c_spam_spam(void *v)
1246 int rv;
1247 NYD_ENTER;
1249 rv = _spam_action(_SPAM_SPAM, (int*)v) ? OKAY : STOP;
1250 NYD_LEAVE;
1251 return rv;
1253 #endif /* HAVE_SPAM */
1255 /* s-it-mode */