Move c_stouch(), c_mboxit() to cmd2.c: they belong
[s-mailx.git] / spam.c
blob71e67fc5d452c3c25efe2d68078f2d407f382827
1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
2 *@ Spam related facilities.
4 * Copyright (c) 2013 - 2015 Steffen (Daode) Nurpmeso <sdaoden@users.sf.net>.
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 /* 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 = ((options & OPT_VERB) != 0);
198 vc.vc_progress = (!vc.vc_verbose && ((options & OPT_INTERACTIVE) != 0));
199 vc.vc_esep = vc.vc_progress ? "\n" : "";
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")) {
212 if (!_spamd_setup(&vc))
213 goto jleave;
214 #endif
215 #ifdef HAVE_SPAM_FILTER
216 } else if (!asccasecmp(cp, "filter")) {
217 if (!_spamfilter_setup(&vc))
218 goto jleave;
219 #endif
220 } else {
221 n_err(_("`%s': unknown / unsupported *spam-interface*: \"%s\"\n"),
222 _spam_cmds[sa], cp);
223 goto jleave;
226 /* *spam-maxsize* we do handle ourselfs instead */
227 if ((cp = ok_vlook(spam_maxsize)) == NULL ||
228 (maxsize = (size_t)strtoul(cp, NULL, 0)) == 0)
229 maxsize = SPAM_MAXSIZE;
231 /* Finally get an I/O buffer */
232 vc.vc_buffer = salloc(BUFFER_SIZE);
234 skipped = cnt = 0;
235 if (vc.vc_progress) {
236 while (ip[cnt] != 0)
237 ++cnt;
239 for (curr = 0, ok = TRU1; *ip != 0; --cnt, ++curr, ++ip) {
240 vc.vc_mno = (size_t)*ip;
241 vc.vc_mp = message + vc.vc_mno - 1;
242 if (sa == _SPAM_RATE)
243 vc.vc_mp->m_spamscore = 0;
245 if (vc.vc_mp->m_size > maxsize) {
246 if (vc.vc_verbose)
247 n_err(_("`%s': message %lu exceeds maxsize (%lu > %lu), skip\n"),
248 _spam_cmds[sa], (ul_i)vc.vc_mno, (ul_i)(size_t)vc.vc_mp->m_size,
249 (ul_i)maxsize);
250 else if (vc.vc_progress) {
251 fprintf(stdout, "\r%s: !%-6" PRIuZ " %6" PRIuZ "/%-6" PRIuZ,
252 _spam_cmds[sa], vc.vc_mno, cnt, curr);
253 fflush(stdout);
255 ++skipped;
256 } else {
257 if (vc.vc_verbose)
258 n_err(_("`%s': message %lu\n"), _spam_cmds[sa], (ul_i)vc.vc_mno);
259 else if (vc.vc_progress) {
260 fprintf(stdout, "\r%s: .%-6" PRIuZ " %6" PRIuZ "/%-6" PRIuZ,
261 _spam_cmds[sa], vc.vc_mno, cnt, curr);
262 fflush(stdout);
265 setdot(vc.vc_mp);
266 if ((vc.vc_ifp = setinput(&mb, vc.vc_mp, NEED_BODY)) == NULL) {
267 n_err(_("%s`%s': cannot load message %lu: %s\n"),
268 vc.vc_esep, _spam_cmds[sa], (ul_i)vc.vc_mno, strerror(errno));
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(stdout, _(" %s (%" PRIuZ "/%" PRIuZ " all/skipped)\n"),
280 (ok ? "done" : "ERROR"), curr, skipped);
281 fflush(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 = myname;
343 *args++ = "-u";
344 *args++ = cp;
346 assert(PTR2SIZE(args - sscp->c_cmd_arr) <= 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"), sscp->c_super.cf_cmd);
353 _spam_cf_setup(vcp, FAL0);
355 vcp->vc_act = &_spamc_interact;
356 vcp->vc_dtor = &_spamc_dtor;
357 rv = TRU1;
358 # ifndef SPAM_SPAMC_PATH
359 jleave:
360 # endif
361 NYD2_LEAVE;
362 return rv;
365 static bool_t
366 _spamc_interact(struct spam_vc *vcp)
368 bool_t rv;
369 NYD2_ENTER;
371 if (!(rv = _spam_cf_interact(vcp)))
372 goto jleave;
374 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
375 if (vcp->vc_action != _SPAM_RATE) {
376 if (vcp->vc_action == _SPAM_SPAM)
377 vcp->vc_mp->m_flag |= MSPAM;
378 } else {
379 char *buf, *cp;
381 switch (WEXITSTATUS(vcp->vc_t.spamc.c_super.cf_waitstat)) {
382 case 1:
383 vcp->vc_mp->m_flag |= MSPAM;
384 /* FALLTHRU */
385 case 0:
386 break;
387 default:
388 rv = FAL0;
389 goto jleave;
392 if ((cp = strchr(buf = vcp->vc_t.spamc.c_super.cf_result, '/')) != NULL)
393 buf[PTR2SIZE(cp - buf)] = '\0';
394 _spam_rate2score(vcp, buf);
396 jleave:
397 NYD2_LEAVE;
398 return rv;
401 static void
402 _spamc_dtor(struct spam_vc *vcp)
404 NYD2_ENTER;
405 if (vcp->vc_t.spamc.c_super.cf_result != NULL)
406 free(vcp->vc_t.spamc.c_super.cf_result);
407 NYD2_LEAVE;
409 #endif /* HAVE_SPAM_SPAMC */
411 #ifdef HAVE_SPAM_SPAMD
412 static bool_t
413 _spamd_setup(struct spam_vc *vcp)
415 struct spam_spamd *ssdp;
416 char const *cp;
417 size_t l;
418 bool_t rv = FAL0;
419 NYD2_ENTER;
421 ssdp = &vcp->vc_t.spamd;
423 if ((cp = ok_vlook(spamd_user)) != NULL) {
424 if (*cp == '\0')
425 cp = myname;
426 ssdp->d_user.l = strlen(ssdp->d_user.s = UNCONST(cp));
429 if ((cp = ok_vlook(spamd_socket)) == NULL) {
430 n_err(_("`%s': required *spamd-socket* is not set\n"),
431 _spam_cmds[vcp->vc_action]);
432 goto jleave;
434 if ((l = strlen(cp) +1) >= sizeof(ssdp->d_sun.sun_path)) {
435 n_err(_("`%s': *spamd-socket* too long: \"%s\"\n"),
436 _spam_cmds[vcp->vc_action], cp);
437 goto jleave;
439 ssdp->d_sun.sun_family = AF_UNIX;
440 memcpy(ssdp->d_sun.sun_path, cp, l);
442 vcp->vc_act = &_spamd_interact;
443 rv = TRU1;
444 jleave:
445 NYD2_LEAVE;
446 return rv;
449 static sigjmp_buf __spamd_actjmp; /* TODO oneday, we won't need it no more */
450 static int volatile __spamd_sig; /* TODO oneday, we won't need it no more */
451 static void
452 __spamd_onsig(int sig) /* TODO someday, we won't need it no more */
454 NYD_X; /* Signal handler */
455 __spamd_sig = sig;
456 siglongjmp(__spamd_actjmp, 1);
459 static bool_t
460 _spamd_interact(struct spam_vc *vcp)
462 struct spam_spamd *ssdp;
463 size_t size, i;
464 char *lp, *cp, * volatile headbuf = NULL;
465 int volatile dsfd = -1;
466 bool_t volatile rv = FAL0;
467 NYD2_ENTER;
469 ssdp = &vcp->vc_t.spamd;
471 __spamd_sig = 0;
472 hold_sigs();
473 ssdp->d_otstp = safe_signal(SIGTSTP, SIG_DFL);
474 ssdp->d_ottin = safe_signal(SIGTTIN, SIG_DFL);
475 ssdp->d_ottou = safe_signal(SIGTTOU, SIG_DFL);
476 ssdp->d_opipe = safe_signal(SIGPIPE, SIG_IGN);
477 ssdp->d_ohup = safe_signal(SIGHUP, &__spamd_onsig);
478 ssdp->d_oint = safe_signal(SIGINT, &__spamd_onsig);
479 ssdp->d_oquit = safe_signal(SIGQUIT, &__spamd_onsig);
480 if (sigsetjmp(__spamd_actjmp, 1)) {
481 if (*vcp->vc_esep != '\0')
482 n_err(vcp->vc_esep);
483 goto jleave;
485 rele_sigs();
487 if ((dsfd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
488 n_err(_("%s`%s': can't create unix(4) socket: %s\n"),
489 vcp->vc_esep, _spam_cmds[vcp->vc_action], strerror(errno));
490 goto jleave;
493 if (connect(dsfd, (struct sockaddr*)&ssdp->d_sun, SUN_LEN(&ssdp->d_sun)) ==
494 -1) {
495 n_err(_("%s`%s': can't connect to *spam-socket*: %s\n"),
496 vcp->vc_esep, _spam_cmds[vcp->vc_action], strerror(errno));
497 close(dsfd);
498 goto jleave;
501 /* The command header, finalized with an empty line.
502 * This needs to be written in a single write(2)! */
503 # undef _X
504 # define _X(X) do {memcpy(lp, X, sizeof(X) -1); lp += sizeof(X) -1;} while (0)
506 i = ((cp = ssdp->d_user.s) != NULL) ? ssdp->d_user.l : 0;
507 lp = headbuf = ac_alloc(
508 sizeof(NETLINE("A_VERY_LONG_COMMAND " SPAMD_IDENT)) +
509 sizeof(NETLINE("Content-length: 9223372036854775807")) +
510 ((cp != NULL) ? sizeof("User: ") + i + sizeof(NETNL) : 0) +
511 sizeof(NETLINE("Message-class: spam")) +
512 sizeof(NETLINE("Set: local")) +
513 sizeof(NETLINE("Remove: local")) +
514 sizeof(NETNL) /*+1*/);
516 switch (vcp->vc_action) {
517 case _SPAM_RATE:
518 _X(NETLINE("CHECK " SPAMD_IDENT));
519 break;
520 case _SPAM_HAM:
521 case _SPAM_SPAM:
522 case _SPAM_FORGET:
523 _X(NETLINE("TELL " SPAMD_IDENT));
524 break;
527 lp += snprintf(lp, 0x7FFF, NETLINE("Content-length: %" PRIuZ),
528 (size_t)vcp->vc_mp->m_size);
530 if (cp != NULL) {
531 _X("User: ");
532 memcpy(lp, cp, i);
533 lp += i;
534 _X(NETNL);
537 switch (vcp->vc_action) {
538 case _SPAM_RATE:
539 _X(NETNL);
540 break;
541 case _SPAM_HAM:
542 _X(NETLINE("Message-class: ham")
543 NETLINE("Set: local")
544 NETNL);
545 break;
546 case _SPAM_SPAM:
547 _X(NETLINE("Message-class: spam")
548 NETLINE("Set: local")
549 NETNL);
550 break;
551 case _SPAM_FORGET:
552 if (vcp->vc_mp->m_flag & MSPAM)
553 _X(NETLINE("Message-class: spam"));
554 else
555 _X(NETLINE("Message-class: ham"));
556 _X(NETLINE("Remove: local")
557 NETNL);
558 break;
560 # undef _X
562 i = PTR2SIZE(lp - headbuf);
563 if (options & OPT_VERBVERB)
564 n_err(">>> %.*s <<<\n", (int)i, headbuf);
565 if (i != (size_t)write(dsfd, headbuf, i))
566 goto jeso;
568 /* Then simply pass through the message "as-is" */
569 for (size = vcp->vc_mp->m_size; size > 0;) {
570 i = fread(vcp->vc_buffer, sizeof *vcp->vc_buffer,
571 MIN(size, BUFFER_SIZE), vcp->vc_ifp);
572 if (i == 0) {
573 if (ferror(vcp->vc_ifp))
574 goto jeso;
575 break;
577 size -= i;
579 if (i != (size_t)write(dsfd, vcp->vc_buffer, i)) {
580 jeso:
581 n_err(_("%s`%s': I/O on *spamd-socket* failed: %s\n"),
582 vcp->vc_esep, _spam_cmds[vcp->vc_action], strerror(errno));
583 goto jleave;
587 /* We are finished, say so */
588 shutdown(dsfd, SHUT_WR);
590 /* Be aware on goto: i will be a line counter after this loop! */
591 for (size = 0, i = BUFFER_SIZE -1;;) {
592 ssize_t j = read(dsfd, vcp->vc_buffer + size, i);
593 if (j == -1)
594 goto jeso;
595 if (j == 0)
596 break;
597 size += j;
598 i -= j;
599 /* For the current way of doing things a single read will suffice.
600 * Note we'll be "penaltized" when awaiting EOF on the socket, at least
601 * in blocking mode, so do avoid that and break off */
602 break;
604 i = 0;
605 vcp->vc_buffer[size] = '\0';
607 if (size == 0 || size == BUFFER_SIZE) {
608 jebogus:
609 n_err(_("%s`%s': bogus spamd(1) I/O interaction (%lu)\n"),
610 vcp->vc_esep, _spam_cmds[vcp->vc_action], (ul_i)i);
611 # ifdef HAVE_DEVEL
612 if (options & OPT_VERBVERB)
613 n_err(">>> BUFFER: %s <<<\n", vcp->vc_buffer);
614 # endif
615 goto jleave;
618 /* From the response, read those lines that interest us */
619 for (lp = vcp->vc_buffer; size > 0; ++i) {
620 cp = lp;
621 lp = strchr(lp, NETNL[0]);
622 if (lp == NULL)
623 goto jebogus;
624 lp[0] = '\0';
625 if (lp[1] != NETNL[1])
626 goto jebogus;
627 lp += 2;
628 size -= PTR2SIZE(lp - cp);
630 if (i == 0) {
631 if (!strncmp(cp, "SPAMD/1.1 0 EX_OK", sizeof("SPAMD/1.1 0 EX_OK") -1))
632 continue;
633 if (vcp->vc_action != _SPAM_RATE ||
634 strstr(cp, "Service Unavailable") == NULL)
635 goto jebogus;
636 else {
637 /* Unfortunately a missing --allow-tell drops connection.. */
638 n_err(_("%s`%s': service not available in spamd(1) instance\n"),
639 vcp->vc_esep, _spam_cmds[vcp->vc_action]);
640 goto jleave;
642 } else if (i == 1) {
643 switch (vcp->vc_action) {
644 case _SPAM_RATE:
645 if (strncmp(cp, "Spam: ", sizeof("Spam: ") -1))
646 goto jebogus;
647 cp += sizeof("Spam: ") -1;
649 if (!strncmp(cp, "False", sizeof("False") -1)) {
650 cp += sizeof("False") -1;
651 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
652 } else if (!strncmp(cp, "True", sizeof("True") -1)) {
653 cp += sizeof("True") -1;
654 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
655 vcp->vc_mp->m_flag |= MSPAM;
656 } else
657 goto jebogus;
659 while (blankspacechar(*cp))
660 ++cp;
662 if (*cp++ != ';')
663 goto jebogus;
664 else {
665 char *xcp = strchr(cp, '/');
666 if (xcp != NULL) {
667 size = PTR2SIZE(xcp - cp);
668 cp[size] = '\0';
670 _spam_rate2score(vcp, cp);
672 goto jdone;
674 case _SPAM_HAM:
675 case _SPAM_SPAM:
676 /* Empty response means ok but "did nothing" */
677 if (*cp != '\0' &&
678 strncmp(cp, "DidSet: local", sizeof("DidSet: local") -1))
679 goto jebogus;
680 if (*cp == '\0' && vcp->vc_verbose)
681 n_err(_("\tBut spamd(1) \"did nothing\" for message\n"));
682 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
683 if (vcp->vc_action == _SPAM_SPAM)
684 vcp->vc_mp->m_flag |= MSPAM;
685 goto jdone;
687 case _SPAM_FORGET:
688 if (*cp != '\0' &&
689 strncmp(cp, "DidRemove: local", sizeof("DidSet: local") -1))
690 goto jebogus;
691 if (*cp == '\0' && vcp->vc_verbose)
692 n_err(_("\tBut spamd(1) \"did nothing\" for message\n"));
693 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
694 goto jdone;
699 jdone:
700 rv = TRU1;
701 jleave:
702 if (headbuf != NULL)
703 ac_free(headbuf);
704 if (dsfd >= 0)
705 close(dsfd);
707 safe_signal(SIGQUIT, ssdp->d_oquit);
708 safe_signal(SIGINT, ssdp->d_oint);
709 safe_signal(SIGHUP, ssdp->d_ohup);
710 safe_signal(SIGPIPE, ssdp->d_opipe);
711 safe_signal(SIGTSTP, ssdp->d_otstp);
712 safe_signal(SIGTTIN, ssdp->d_ottin);
713 safe_signal(SIGTTOU, ssdp->d_ottou);
715 NYD2_LEAVE;
716 if (__spamd_sig != 0) {
717 sigset_t cset;
718 sigemptyset(&cset);
719 sigaddset(&cset, __spamd_sig);
720 sigprocmask(SIG_UNBLOCK, &cset, NULL);
721 n_raise(__spamd_sig);
722 assert(rv == FAL0);
724 return rv;
726 #endif /* HAVE_SPAM_SPAMD */
728 #ifdef HAVE_SPAM_FILTER
729 static bool_t
730 _spamfilter_setup(struct spam_vc *vcp)
732 struct spam_filter *sfp;
733 char const *cp, *var;
734 bool_t rv = FAL0;
735 NYD2_ENTER;
737 sfp = &vcp->vc_t.filter;
739 switch (vcp->vc_action) {
740 case _SPAM_RATE:
741 cp = ok_vlook(spamfilter_rate);
742 var = "spam-filter-rate";
743 goto jonecmd;
744 case _SPAM_HAM:
745 cp = ok_vlook(spamfilter_ham);
746 var = "spam-filter-ham";
747 goto jonecmd;
748 case _SPAM_SPAM:
749 cp = ok_vlook(spamfilter_spam);
750 var = "spam-filter-spam";
751 jonecmd:
752 if (cp == NULL) {
753 jecmd:
754 n_err(_("`%s': *%s* is not set\n"), _spam_cmds[vcp->vc_action], var);
755 goto jleave;
757 sfp->f_super.cf_cmd = savestr(cp);
758 break;
759 case _SPAM_FORGET:
760 var = "spam-filter-nospam";
761 if ((cp = ok_vlook(spamfilter_nospam)) == NULL)
762 goto jecmd;
763 sfp->f_cmd_nospam = savestr(cp);
764 if ((cp = ok_vlook(spamfilter_noham)) == NULL)
765 goto jecmd;
766 sfp->f_cmd_noham = savestr(cp);
767 break;
770 # ifdef HAVE_REGEX
771 if (vcp->vc_action == _SPAM_RATE &&
772 (cp = ok_vlook(spamfilter_rate_scanscore)) != NULL) {
773 char const *bp;
774 char *ep;
776 var = strchr(cp, ';');
777 if (var == NULL) {
778 n_err(_("`%s': *spamfilter-rate-scanscore*: no `;' in \"%s\"\n"),
779 _spam_cmds[vcp->vc_action], cp);
780 goto jleave;
782 bp = var + 1;
784 var = savestrbuf(cp, PTR2SIZE(var - cp));
785 sfp->f_score_grpno = (ui32_t)strtoul(var, &ep, 0);
786 if (var == ep || *ep != '\0') {
787 n_err(_("`%s': *spamfilter-rate-scanscore*: bad group in \"%s\"\n"),
788 _spam_cmds[vcp->vc_action], cp);
789 goto jleave;
791 if (sfp->f_score_grpno >= SPAM_FILTER_MATCHES) {
792 n_err(_("`%s': *spamfilter-rate-scanscore*: "
793 "group %u excesses limit %u\n"),
794 _spam_cmds[vcp->vc_action], sfp->f_score_grpno,
795 SPAM_FILTER_MATCHES);
796 goto jleave;
799 if (regcomp(&sfp->f_score_regex, bp, REG_EXTENDED | REG_ICASE)) {
800 n_err(_("`%s': invalid *spamfilter-rate-scanscore* regex: \"%s\"\n"),
801 _spam_cmds[vcp->vc_action], cp);
802 goto jleave;
804 if (sfp->f_score_grpno > sfp->f_score_regex.re_nsub) {
805 regfree(&sfp->f_score_regex);
806 n_err(_("`%s': *spamfilter-rate-scanscore*: "
807 "no group %u in \"%s\"\n"),
808 _spam_cmds[vcp->vc_action], sfp->f_score_grpno, cp);
809 goto jleave;
812 # endif /* HAVE_REGEX */
814 _spam_cf_setup(vcp, TRU1);
816 vcp->vc_act = &_spamfilter_interact;
817 vcp->vc_dtor = &_spamfilter_dtor;
818 rv = TRU1;
819 jleave:
820 NYD2_LEAVE;
821 return rv;
824 static bool_t
825 _spamfilter_interact(struct spam_vc *vcp)
827 # ifdef HAVE_REGEX
828 regmatch_t rem[SPAM_FILTER_MATCHES], *remp;
829 struct spam_filter *sfp;
830 char *cp;
831 # endif
832 bool_t rv;
833 NYD2_ENTER;
835 if (vcp->vc_action == _SPAM_FORGET)
836 vcp->vc_t.cf.cf_cmd = (vcp->vc_mp->m_flag & MSPAM)
837 ? vcp->vc_t.filter.f_cmd_nospam : vcp->vc_t.filter.f_cmd_noham;
839 if (!(rv = _spam_cf_interact(vcp)))
840 goto jleave;
842 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
843 if (vcp->vc_action != _SPAM_RATE) {
844 if (vcp->vc_action == _SPAM_SPAM)
845 vcp->vc_mp->m_flag |= MSPAM;
846 goto jleave;
847 } else switch (WEXITSTATUS(vcp->vc_t.filter.f_super.cf_waitstat)) {
848 case 2:
849 vcp->vc_mp->m_flag |= MSPAMUNSURE;
850 /* FALLTHRU */
851 case 1:
852 break;
853 case 0:
854 vcp->vc_mp->m_flag |= MSPAM;
855 break;
856 default:
857 rv = FAL0;
858 goto jleave;
861 # ifdef HAVE_REGEX
862 sfp = &vcp->vc_t.filter;
864 if (sfp->f_score_grpno == 0)
865 goto jleave;
867 assert(sfp->f_super.cf_result != NULL);
868 remp = rem + sfp->f_score_grpno;
870 if (regexec(&sfp->f_score_regex, sfp->f_super.cf_result, NELEM(rem), rem,
871 0) == REG_NOMATCH || (remp->rm_so | remp->rm_eo) < 0) {
872 n_err(_("`%s': *spamfilter-rate-scanscore* "
873 "doesn't match filter output!\n"),
874 _spam_cmds[vcp->vc_action]);
875 sfp->f_score_grpno = 0;
876 goto jleave;
879 cp = sfp->f_super.cf_result;
880 cp[remp->rm_eo] = '\0';
881 cp += remp->rm_so;
882 _spam_rate2score(vcp, cp);
883 # endif /* HAVE_REGEX */
885 jleave:
886 NYD2_LEAVE;
887 return rv;
890 static void
891 _spamfilter_dtor(struct spam_vc *vcp)
893 struct spam_filter *sfp;
894 NYD2_ENTER;
896 sfp = &vcp->vc_t.filter;
898 if (sfp->f_super.cf_result != NULL)
899 free(sfp->f_super.cf_result);
900 # ifdef HAVE_REGEX
901 if (sfp->f_score_grpno > 0)
902 regfree(&sfp->f_score_regex);
903 # endif
904 NYD2_LEAVE;
906 #endif /* HAVE_SPAM_FILTER */
908 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
909 static void
910 _spam_cf_setup(struct spam_vc *vcp, bool_t useshell)
912 struct str s;
913 struct spam_cf *scfp;
914 NYD2_ENTER;
915 LCTA(3 < NELEM(scfp->cf_env));
917 scfp = &vcp->vc_t.cf;
919 if ((scfp->cf_useshell = useshell)) {
920 scfp->cf_acmd = ok_vlook(SHELL);
921 scfp->cf_a0 = "-c";
924 /* NAIL_FILENAME_GENERATED *//* TODO pathconf NAME_MAX; but user can create
925 * TODO a file wherever he wants! *Do* create a zero-size temporary file
926 * TODO and give *that* path as NAIL_FILENAME_TEMPORARY, clean it up once
927 * TODO the pipe returns? Like this we *can* verify path/name issues! */
928 scfp->cf_env[0] = str_concat_csvl(&s, NAILENV_FILENAME_GENERATED, "=",
929 getrandstring(MIN(NAME_MAX / 4, 16)), NULL)->s;
931 scfp->cf_env[1] = str_concat_csvl(&s, NAILENV_TMPDIR, "=", tempdir, NULL)->s;
932 scfp->cf_env[2] = str_concat_csvl(&s, "TMPDIR", "=", tempdir, NULL)->s;
933 scfp->cf_env[3] = NULL;
934 NYD2_LEAVE;
937 static sigjmp_buf __spam_cf_actjmp; /* TODO someday, we won't need it */
938 static int volatile __spam_cf_sig; /* TODO someday, we won't need it */
939 static void
940 __spam_cf_onsig(int sig) /* TODO someday, we won't need it no more */
942 NYD_X; /* Signal handler */
943 __spam_cf_sig = sig;
944 siglongjmp(__spam_cf_actjmp, 1);
947 static bool_t
948 _spam_cf_interact(struct spam_vc *vcp)
950 struct spam_cf *scfp;
951 int p2c[2], c2p[2];
952 sigset_t cset;
953 char const *cp;
954 size_t size;
955 pid_t volatile pid;
956 enum {
957 _NONE = 0,
958 _SIGHOLD = 1<<0,
959 _P2C_0 = 1<<1,
960 _P2C_1 = 1<<2,
961 _P2C = _P2C_0 | _P2C_1,
962 _C2P_0 = 1<<3,
963 _C2P_1 = 1<<4,
964 _C2P = _C2P_0 | _C2P_1,
965 _JUMPED = 1<<5,
966 _RUNNING = 1<<6,
967 _GOODRUN = 1<<7,
968 _ERRORS = 1<<8
969 } volatile state = _NONE;
970 NYD2_ENTER;
972 scfp = &vcp->vc_t.cf;
973 if (scfp->cf_result != NULL) {
974 free(scfp->cf_result);
975 scfp->cf_result = NULL;
978 /* TODO Avoid that we jump away; yet necessary signal mess */
979 /*__spam_cf_sig = 0;*/
980 hold_sigs();
981 state |= _SIGHOLD;
982 scfp->cf_otstp = safe_signal(SIGTSTP, SIG_DFL);
983 scfp->cf_ottin = safe_signal(SIGTTIN, SIG_DFL);
984 scfp->cf_ottou = safe_signal(SIGTTOU, SIG_DFL);
985 scfp->cf_opipe = safe_signal(SIGPIPE, SIG_IGN);
986 scfp->cf_ohup = safe_signal(SIGHUP, &__spam_cf_onsig);
987 scfp->cf_oint = safe_signal(SIGINT, &__spam_cf_onsig);
988 scfp->cf_oquit = safe_signal(SIGQUIT, &__spam_cf_onsig);
989 /* Keep sigs blocked */
990 pid = 0; /* cc uninit */
992 if (!pipe_cloexec(p2c)) {
993 n_err(_("%s`%s': cannot create parent pipe: %s\n"),
994 vcp->vc_esep, _spam_cmds[vcp->vc_action], strerror(errno));
995 goto jtail;
997 state |= _P2C;
999 if (!pipe_cloexec(c2p)) {
1000 n_err(_("%s`%s': cannot create child pipe: %s\n"),
1001 vcp->vc_esep, _spam_cmds[vcp->vc_action], strerror(errno));
1002 goto jtail;
1004 state |= _C2P;
1006 if (sigsetjmp(__spam_cf_actjmp, 1)) {
1007 if (*vcp->vc_esep != '\0')
1008 n_err(vcp->vc_esep);
1009 state |= _JUMPED;
1010 goto jtail;
1012 rele_sigs();
1013 state &= ~_SIGHOLD;
1015 /* Start our command as requested */
1016 sigemptyset(&cset);
1017 if ((pid = start_command(
1018 (scfp->cf_acmd != NULL ? scfp->cf_acmd : scfp->cf_cmd),
1019 &cset, p2c[0], c2p[1],
1020 scfp->cf_a0, (scfp->cf_acmd != NULL ? scfp->cf_cmd : NULL), NULL,
1021 scfp->cf_env)) < 0) {
1022 state |= _ERRORS;
1023 goto jtail;
1025 state |= _RUNNING;
1026 close(p2c[0]);
1027 state &= ~_P2C_0;
1029 /* Yes, we could sendmp(SEND_MBOX), but simply passing through the MBOX
1030 * content does the same in effect, however much more efficiently.
1031 * XXX NOTE: this may mean we pass a message without From_ line! */
1032 for (size = vcp->vc_mp->m_size; size > 0;) {
1033 size_t i = fread(vcp->vc_buffer, 1, MIN(size, BUFFER_SIZE), vcp->vc_ifp);
1034 if (i == 0) {
1035 if (ferror(vcp->vc_ifp))
1036 state |= _ERRORS;
1037 break;
1039 size -= i;
1040 if (i != (size_t)write(p2c[1], vcp->vc_buffer, i)) {
1041 state |= _ERRORS;
1042 break;
1046 jtail:
1047 /* TODO Quite racy -- block anything for a while? */
1048 if (state & _SIGHOLD) {
1049 state &= ~_SIGHOLD;
1050 rele_sigs();
1053 if (state & _P2C_0) {
1054 state &= ~_P2C_0;
1055 close(p2c[0]);
1057 if (state & _C2P_1) {
1058 state &= ~_C2P_1;
1059 close(c2p[1]);
1061 /* And cause EOF for the reader */
1062 if (state & _P2C_1) {
1063 state &= ~_P2C_1;
1064 close(p2c[1]);
1067 if (state & _RUNNING) {
1068 if (!(state & _ERRORS) &&
1069 vcp->vc_action == _SPAM_RATE && !(state & (_JUMPED | _ERRORS))) {
1070 ssize_t i = read(c2p[0], vcp->vc_buffer, BUFFER_SIZE - 1);
1071 if (i > 0) {
1072 vcp->vc_buffer[i] = '\0';
1073 if ((cp = strchr(vcp->vc_buffer, NETNL[0])) == NULL &&
1074 (cp = strchr(vcp->vc_buffer, NETNL[1])) == NULL) {
1075 n_err(_("%s`%s': program generates too much output: \"%s\"\n"),
1076 vcp->vc_esep, _spam_cmds[vcp->vc_action], scfp->cf_cmd);
1077 state |= _ERRORS;
1078 } else {
1079 scfp->cf_result = sbufdup(vcp->vc_buffer,
1080 PTR2SIZE(cp - vcp->vc_buffer));
1081 /* FIXME consume child output until EOF??? */
1083 } else if (i != 0)
1084 state |= _ERRORS;
1087 state &= ~_RUNNING;
1088 wait_child(pid, &scfp->cf_waitstat);
1089 if (WIFEXITED(scfp->cf_waitstat))
1090 state |= _GOODRUN;
1093 if (state & _C2P_0) {
1094 state &= ~_C2P_0;
1095 close(c2p[0]);
1098 safe_signal(SIGQUIT, scfp->cf_oquit);
1099 safe_signal(SIGINT, scfp->cf_oint);
1100 safe_signal(SIGHUP, scfp->cf_ohup);
1101 safe_signal(SIGPIPE, scfp->cf_opipe);
1102 safe_signal(SIGTSTP, scfp->cf_otstp);
1103 safe_signal(SIGTTIN, scfp->cf_ottin);
1104 safe_signal(SIGTTOU, scfp->cf_ottou);
1106 NYD2_LEAVE;
1107 if (state & _JUMPED) {
1108 assert(vcp->vc_dtor != NULL);
1109 (*vcp->vc_dtor)(vcp);
1111 sigemptyset(&cset);
1112 sigaddset(&cset, __spam_cf_sig);
1113 sigprocmask(SIG_UNBLOCK, &cset, NULL);
1114 n_raise(__spam_cf_sig);
1116 return !(state & (_JUMPED | _ERRORS));
1118 #endif /* HAVE_SPAM_SPAMC || HAVE_SPAM_FILTER */
1120 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_SPAMD ||\
1121 (defined HAVE_SPAM_FILTER && defined HAVE_REGEX)
1122 static void
1123 _spam_rate2score(struct spam_vc *vcp, char *buf)
1125 char *cp;
1126 ui32_t m, s;
1127 NYD2_ENTER;
1129 m = (ui32_t)strtol(buf, &cp, 10);
1130 if (cp == buf)
1131 goto jleave;
1133 s = 0;
1134 if (*cp++ != '\0') {
1135 /* Floating-point rounding for non-mathematicians */
1136 char c1, c2, c3;
1137 if ((c1 = cp[0]) != '\0' && (c2 = cp[1]) != '\0' &&
1138 (c3 = cp[2]) != '\0') {
1139 cp[2] = '\0';
1140 if (c3 >= '5') {
1141 if (c2 == '9') {
1142 if (c1 == '9') {
1143 ++m;
1144 goto jscore_ok;
1145 } else
1146 cp[0] = ++c1;
1147 c2 = '0';
1148 } else
1149 ++c2;
1150 cp[1] = c2;
1153 s = (ui32_t)strtol(cp, NULL, 10);
1156 jscore_ok:
1157 vcp->vc_mp->m_spamscore = (m << 8) | s;
1158 jleave:
1159 NYD2_LEAVE;
1161 #endif /* _SPAM_SPAMC || _SPAM_SPAMD || (_SPAM_FILTER && HAVE_REGEX) */
1163 FL int
1164 c_spam_clear(void *v)
1166 int *ip;
1167 NYD_ENTER;
1169 for (ip = v; *ip != 0; ++ip)
1170 message[(size_t)*ip - 1].m_flag &= ~(MSPAM | MSPAMUNSURE);
1171 NYD_LEAVE;
1172 return 0;
1175 FL int
1176 c_spam_set(void *v)
1178 int *ip;
1179 NYD_ENTER;
1181 for (ip = v; *ip != 0; ++ip) {
1182 message[(size_t)*ip - 1].m_flag &= ~(MSPAM | MSPAMUNSURE);
1183 message[(size_t)*ip - 1].m_flag |= MSPAM;
1185 NYD_LEAVE;
1186 return 0;
1189 FL int
1190 c_spam_forget(void *v)
1192 int rv;
1193 NYD_ENTER;
1195 rv = _spam_action(_SPAM_FORGET, (int*)v) ? OKAY : STOP;
1196 NYD_LEAVE;
1197 return rv;
1200 FL int
1201 c_spam_ham(void *v)
1203 int rv;
1204 NYD_ENTER;
1206 rv = _spam_action(_SPAM_HAM, (int*)v) ? OKAY : STOP;
1207 NYD_LEAVE;
1208 return rv;
1211 FL int
1212 c_spam_rate(void *v)
1214 int rv;
1215 NYD_ENTER;
1217 rv = _spam_action(_SPAM_RATE, (int*)v) ? OKAY : STOP;
1218 NYD_LEAVE;
1219 return rv;
1222 FL int
1223 c_spam_spam(void *v)
1225 int rv;
1226 NYD_ENTER;
1228 rv = _spam_action(_SPAM_SPAM, (int*)v) ? OKAY : STOP;
1229 NYD_LEAVE;
1230 return rv;
1232 #endif /* HAVE_SPAM */
1234 /* s-it-mode */