mime-types.c: (just) (satisfy) scan-build
[s-mailx.git] / spam.c
blob1c6da251fd54dd713d28526b1779177dc4a237f2
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
28 #ifdef HAVE_SPAM_SPAMD
29 # include <sys/socket.h>
30 # include <sys/un.h>
31 #endif
33 /* This is chosen rather arbitrarily.
34 * It must be able to swallow the first line of a rate response,
35 * and an entire CHECK/TELL spamd(1) response */
36 #if BUFFER_SIZE < 1024
37 # error *spam-interface* BUFFER_SIZE constraints are not matched
38 #endif
40 #ifdef HAVE_SPAM_SPAMD
41 # define SPAMD_IDENT "SPAMC/1.5"
42 # ifndef SUN_LEN
43 # define SUN_LEN(SUP) \
44 (sizeof(*(SUP)) - sizeof((SUP)->sun_path) + strlen((SUP)->sun_path))
45 # endif
46 #endif
48 #ifdef HAVE_SPAM_FILTER
49 /* n_NELEM() of regmatch_t groups */
50 # define SPAM_FILTER_MATCHES 32u
51 #endif
53 enum spam_action {
54 _SPAM_RATE,
55 _SPAM_HAM,
56 _SPAM_SPAM,
57 _SPAM_FORGET
60 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
61 struct spam_cf {
62 char const *cf_cmd;
63 char *cf_result; /* _SPAM_RATE: first response line */
64 int cf_waitstat;
65 ui8_t __pad[3];
66 bool_t cf_useshell;
67 /* .cf_cmd may be adjusted for each call (`spamforget')... */
68 char const *cf_acmd;
69 char const *cf_a0;
70 char const *cf_env[4];
71 sighandler_type cf_otstp;
72 sighandler_type cf_ottin;
73 sighandler_type cf_ottou;
74 sighandler_type cf_ohup;
75 sighandler_type cf_opipe;
76 sighandler_type cf_oint;
77 sighandler_type cf_oquit;
79 #endif
81 #ifdef HAVE_SPAM_SPAMC
82 struct spam_spamc {
83 struct spam_cf c_super;
84 char const *c_cmd_arr[8];
86 #endif
88 #ifdef HAVE_SPAM_SPAMD
89 struct spam_spamd {
90 struct str d_user;
91 sighandler_type d_otstp;
92 sighandler_type d_ottin;
93 sighandler_type d_ottou;
94 sighandler_type d_ohup;
95 sighandler_type d_opipe;
96 sighandler_type d_oint;
97 sighandler_type d_oquit;
98 struct sockaddr_un d_sun;
100 #endif
102 #ifdef HAVE_SPAM_FILTER
103 struct spam_filter {
104 struct spam_cf f_super;
105 char const *f_cmd_nospam; /* Working relative to current message.. */
106 char const *f_cmd_noham;
107 # ifdef HAVE_REGEX
108 ui8_t __pad[4];
109 ui32_t f_score_grpno; /* 0 for not set */
110 regex_t f_score_regex;
111 # endif
113 #endif
115 struct spam_vc {
116 enum spam_action vc_action;
117 bool_t vc_verbose; /* Verbose output */
118 bool_t vc_progress; /* "Progress meter" (mutual verbose) */
119 ui8_t __pad[2];
120 bool_t (*vc_act)(struct spam_vc *);
121 void (*vc_dtor)(struct spam_vc *);
122 char *vc_buffer; /* I/O buffer, BUFFER_SIZE bytes */
123 size_t vc_mno; /* Current message number */
124 struct message *vc_mp; /* Current message */
125 FILE *vc_ifp; /* Input stream on .vc_mp */
126 union {
127 #ifdef HAVE_SPAM_SPAMC
128 struct spam_spamc spamc;
129 #endif
130 #ifdef HAVE_SPAM_SPAMD
131 struct spam_spamd spamd;
132 #endif
133 #ifdef HAVE_SPAM_FILTER
134 struct spam_filter filter;
135 #endif
136 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
137 struct spam_cf cf;
138 #endif
139 } vc_t;
140 char const *vc_esep; /* Error separator for progress mode */
143 /* Indices according to enum spam_action */
144 static char const _spam_cmds[][16] = {
145 "spamrate", "spamham", "spamspam", "spamforget"
148 /* Shared action setup */
149 static bool_t _spam_action(enum spam_action sa, int *ip);
151 /* *spam-interface*=spamc: initialize, communicate */
152 #ifdef HAVE_SPAM_SPAMC
153 static bool_t _spamc_setup(struct spam_vc *vcp);
154 static bool_t _spamc_interact(struct spam_vc *vcp);
155 static void _spamc_dtor(struct spam_vc *vcp);
156 #endif
158 /* *spam-interface*=spamd: initialize, communicate */
159 #ifdef HAVE_SPAM_SPAMD
160 static bool_t _spamd_setup(struct spam_vc *vcp);
161 static bool_t _spamd_interact(struct spam_vc *vcp);
162 #endif
164 /* *spam-interface*=filter: initialize, communicate */
165 #ifdef HAVE_SPAM_FILTER
166 static bool_t _spamfilter_setup(struct spam_vc *vcp);
167 static bool_t _spamfilter_interact(struct spam_vc *vcp);
168 static void _spamfilter_dtor(struct spam_vc *vcp);
169 #endif
171 /* *spam-interface*=(spamc|filter): create child + communication */
172 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
173 static void _spam_cf_setup(struct spam_vc *vcp, bool_t useshell);
174 static bool_t _spam_cf_interact(struct spam_vc *vcp);
175 #endif
177 /* Convert a floating-point spam rate into message.m_spamscore */
178 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_SPAMD ||\
179 (defined HAVE_SPAM_FILTER && defined HAVE_REGEX)
180 static void _spam_rate2score(struct spam_vc *vcp, char *buf);
181 #endif
183 static bool_t
184 _spam_action(enum spam_action sa, int *ip)
186 struct spam_vc vc;
187 size_t maxsize, skipped, cnt, curr;
188 char const *cp;
189 bool_t ok = FAL0;
190 NYD_ENTER;
192 memset(&vc, 0, sizeof vc);
193 vc.vc_action = sa;
194 vc.vc_verbose = ((n_poption & n_PO_VERB) != 0);
195 vc.vc_progress = (!vc.vc_verbose && ((n_psonce & n_PSO_INTERACTIVE) != 0));
196 vc.vc_esep = vc.vc_progress ? "\n" : n_empty;
198 /* Check and setup the desired spam interface */
199 if ((cp = ok_vlook(spam_interface)) == NULL) {
200 n_err(_("`%s': no *spam-interface* set\n"), _spam_cmds[sa]);
201 goto jleave;
202 #ifdef HAVE_SPAM_SPAMC
203 } else if (!asccasecmp(cp, "spamc")) {
204 if (!_spamc_setup(&vc))
205 goto jleave;
206 #endif
207 #ifdef HAVE_SPAM_SPAMD
208 } else if (!asccasecmp(cp, "spamd")) { /* TODO v15: remove */
209 n_OBSOLETE(_("*spam-interface*=spamd is obsolete, please use =spamc"));
210 if (!_spamd_setup(&vc))
211 goto jleave;
212 #endif
213 #ifdef HAVE_SPAM_FILTER
214 } else if (!asccasecmp(cp, "filter")) {
215 if (!_spamfilter_setup(&vc))
216 goto jleave;
217 #endif
218 } else {
219 n_err(_("`%s': unknown / unsupported *spam-interface*: %s\n"),
220 _spam_cmds[sa], cp);
221 goto jleave;
224 /* *spam-maxsize* we do handle ourselfs instead */
225 if ((cp = ok_vlook(spam_maxsize)) == NULL ||
226 (n_idec_ui32_cp(&maxsize, cp, 0, NULL), maxsize) == 0)
227 maxsize = SPAM_MAXSIZE;
229 /* Finally get an I/O buffer */
230 vc.vc_buffer = salloc(BUFFER_SIZE);
232 skipped = cnt = 0;
233 if (vc.vc_progress) {
234 while (ip[cnt] != 0)
235 ++cnt;
237 for (curr = 0, ok = TRU1; *ip != 0; --cnt, ++curr, ++ip) {
238 vc.vc_mno = (size_t)*ip;
239 vc.vc_mp = message + vc.vc_mno - 1;
240 if (sa == _SPAM_RATE)
241 vc.vc_mp->m_spamscore = 0;
243 if (vc.vc_mp->m_size > maxsize) {
244 if (vc.vc_verbose)
245 n_err(_("`%s': message %lu exceeds maxsize (%lu > %lu), skip\n"),
246 _spam_cmds[sa], (ul_i)vc.vc_mno, (ul_i)(size_t)vc.vc_mp->m_size,
247 (ul_i)maxsize);
248 else if (vc.vc_progress) {
249 fprintf(n_stdout, "\r%s: !%-6" PRIuZ " %6" PRIuZ "/%-6" PRIuZ,
250 _spam_cmds[sa], vc.vc_mno, cnt, curr);
251 fflush(n_stdout);
253 ++skipped;
254 } else {
255 if (vc.vc_verbose)
256 n_err(_("`%s': message %lu\n"), _spam_cmds[sa], (ul_i)vc.vc_mno);
257 else if (vc.vc_progress) {
258 fprintf(n_stdout, "\r%s: .%-6" PRIuZ " %6" PRIuZ "/%-6" PRIuZ,
259 _spam_cmds[sa], vc.vc_mno, cnt, curr);
260 fflush(n_stdout);
263 setdot(vc.vc_mp);
264 if ((vc.vc_ifp = setinput(&mb, vc.vc_mp, NEED_BODY)) == NULL) {
265 n_err(_("%s`%s': cannot load message %lu: %s\n"),
266 vc.vc_esep, _spam_cmds[sa], (ul_i)vc.vc_mno,
267 n_err_to_doc(n_err_no));
268 ok = FAL0;
269 break;
272 if (!(ok = (*vc.vc_act)(&vc)))
273 break;
276 if (vc.vc_progress) {
277 if (curr > 0)
278 fprintf(n_stdout, _(" %s (%" PRIuZ "/%" PRIuZ " all/skipped)\n"),
279 (ok ? _("done") : V_(n_error)), curr, skipped);
280 fflush(n_stdout);
283 if (vc.vc_dtor != NULL)
284 (*vc.vc_dtor)(&vc);
285 jleave:
286 NYD_LEAVE;
287 return !ok;
290 #ifdef HAVE_SPAM_SPAMC
291 static bool_t
292 _spamc_setup(struct spam_vc *vcp)
294 struct spam_spamc *sscp;
295 struct str str;
296 char const **args, *cp;
297 bool_t rv = FAL0;
298 NYD2_ENTER;
300 sscp = &vcp->vc_t.spamc;
301 args = sscp->c_cmd_arr;
303 if ((cp = ok_vlook(spamc_command)) == NULL) {
304 # ifdef SPAM_SPAMC_PATH
305 cp = SPAM_SPAMC_PATH;
306 # else
307 n_err(_("`%s': *spamc-command* is not set\n"),
308 _spam_cmds[vcp->vc_action]);
309 goto jleave;
310 # endif
312 *args++ = cp;
314 switch (vcp->vc_action) {
315 case _SPAM_RATE:
316 *args = "-c";
317 break;
318 case _SPAM_HAM:
319 args[1] = "ham";
320 goto jlearn;
321 case _SPAM_SPAM:
322 args[1] = "spam";
323 goto jlearn;
324 case _SPAM_FORGET:
325 args[1] = "forget";
326 jlearn:
327 *args = "-L";
328 ++args;
329 break;
331 ++args;
333 *args++ = "-l"; /* --log-to-stderr */
334 *args++ = "-x"; /* No "safe callback", we need to react on errors! */
336 if ((cp = ok_vlook(spamc_arguments)) != NULL)
337 *args++ = cp;
339 if ((cp = ok_vlook(spamc_user)) != NULL) {
340 if (*cp == '\0')
341 cp = ok_vlook(LOGNAME);
342 *args++ = "-u";
343 *args++ = cp;
345 assert(PTR2SIZE(args - sscp->c_cmd_arr) <= n_NELEM(sscp->c_cmd_arr));
347 *args = NULL;
348 sscp->c_super.cf_cmd = str_concat_cpa(&str, sscp->c_cmd_arr, " ")->s;
349 if (vcp->vc_verbose)
350 n_err(_("spamc(1) via %s\n"),
351 n_shexp_quote_cp(sscp->c_super.cf_cmd, FAL0));
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 = ok_vlook(LOGNAME);
426 ssdp->d_user.l = strlen(ssdp->d_user.s = n_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], n_shexp_quote_cp(cp, FAL0));
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], n_err_to_doc(n_err_no));
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], n_err_to_doc(n_err_no));
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 size = sizeof(NETLINE("A_VERY_LONG_COMMAND " SPAMD_IDENT)) +
508 sizeof(NETLINE("Content-length: 9223372036854775807")) +
509 ((cp != NULL) ? sizeof("User: ") + i + sizeof(NETNL) : 0) +
510 sizeof(NETLINE("Message-class: spam")) +
511 sizeof(NETLINE("Set: local")) +
512 sizeof(NETLINE("Remove: local")) +
513 sizeof(NETNL) /*+1*/;
514 lp = headbuf = n_lofi_alloc(size);
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, size, 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 (n_poption & n_PO_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 n_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], n_err_to_doc(n_err_no));
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 /* For the current way of doing things a single read will suffice.
599 * Note we'll be "penaltized" when awaiting EOF on the socket, at least
600 * in blocking mode, so do avoid that and break off */
601 break;
603 i = 0;
604 vcp->vc_buffer[size] = '\0';
606 if (size == 0 || size == BUFFER_SIZE) {
607 jebogus:
608 n_err(_("%s`%s': bogus spamd(1) I/O interaction (%lu)\n"),
609 vcp->vc_esep, _spam_cmds[vcp->vc_action], (ul_i)i);
610 # ifdef HAVE_DEVEL
611 if (n_poption & n_PO_VERBVERB)
612 n_err(">>> BUFFER: %s <<<\n", vcp->vc_buffer);
613 # endif
614 goto jleave;
617 /* From the response, read those lines that interest us */
618 for (lp = vcp->vc_buffer; size > 0; ++i) {
619 cp = lp;
620 lp = strchr(lp, NETNL[0]);
621 if (lp == NULL)
622 goto jebogus;
623 lp[0] = '\0';
624 if (lp[1] != NETNL[1])
625 goto jebogus;
626 lp += 2;
627 size -= PTR2SIZE(lp - cp);
629 if (i == 0) {
630 if (!strncmp(cp, "SPAMD/1.1 0 EX_OK", sizeof("SPAMD/1.1 0 EX_OK") -1))
631 continue;
632 if (vcp->vc_action != _SPAM_RATE ||
633 strstr(cp, "Service Unavailable") == NULL)
634 goto jebogus;
635 else {
636 /* Unfortunately a missing --allow-tell drops connection.. */
637 n_err(_("%s`%s': service not available in spamd(1) instance\n"),
638 vcp->vc_esep, _spam_cmds[vcp->vc_action]);
639 goto jleave;
641 } else if (i == 1) {
642 switch (vcp->vc_action) {
643 case _SPAM_RATE:
644 if (strncmp(cp, "Spam: ", sizeof("Spam: ") -1))
645 goto jebogus;
646 cp += sizeof("Spam: ") -1;
648 if (!strncmp(cp, "False", sizeof("False") -1)) {
649 cp += sizeof("False") -1;
650 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
651 } else if (!strncmp(cp, "True", sizeof("True") -1)) {
652 cp += sizeof("True") -1;
653 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
654 vcp->vc_mp->m_flag |= MSPAM;
655 } else
656 goto jebogus;
658 while (blankspacechar(*cp))
659 ++cp;
661 if (*cp++ != ';')
662 goto jebogus;
663 else {
664 char *xcp = strchr(cp, '/');
665 if (xcp != NULL) {
666 size = PTR2SIZE(xcp - cp);
667 cp[size] = '\0';
669 _spam_rate2score(vcp, cp);
671 goto jdone;
673 case _SPAM_HAM:
674 case _SPAM_SPAM:
675 /* Empty response means ok but "did nothing" */
676 if (*cp != '\0' &&
677 strncmp(cp, "DidSet: local", sizeof("DidSet: local") -1))
678 goto jebogus;
679 if (*cp == '\0' && vcp->vc_verbose)
680 n_err(_("\tBut spamd(1) \"did nothing\" for message\n"));
681 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
682 if (vcp->vc_action == _SPAM_SPAM)
683 vcp->vc_mp->m_flag |= MSPAM;
684 goto jdone;
686 case _SPAM_FORGET:
687 if (*cp != '\0' &&
688 strncmp(cp, "DidRemove: local", sizeof("DidSet: local") -1))
689 goto jebogus;
690 if (*cp == '\0' && vcp->vc_verbose)
691 n_err(_("\tBut spamd(1) \"did nothing\" for message\n"));
692 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
693 goto jdone;
698 jdone:
699 rv = TRU1;
700 jleave:
701 if (headbuf != NULL)
702 ac_free(headbuf);
703 if (dsfd >= 0)
704 close(dsfd);
706 safe_signal(SIGQUIT, ssdp->d_oquit);
707 safe_signal(SIGINT, ssdp->d_oint);
708 safe_signal(SIGHUP, ssdp->d_ohup);
709 safe_signal(SIGPIPE, ssdp->d_opipe);
710 safe_signal(SIGTSTP, ssdp->d_otstp);
711 safe_signal(SIGTTIN, ssdp->d_ottin);
712 safe_signal(SIGTTOU, ssdp->d_ottou);
714 NYD2_LEAVE;
715 if (__spamd_sig != 0) {
716 sigset_t cset;
717 sigemptyset(&cset);
718 sigaddset(&cset, __spamd_sig);
719 sigprocmask(SIG_UNBLOCK, &cset, NULL);
720 n_raise(__spamd_sig);
721 assert(rv == FAL0);
723 return rv;
725 #endif /* HAVE_SPAM_SPAMD */
727 #ifdef HAVE_SPAM_FILTER
728 static bool_t
729 _spamfilter_setup(struct spam_vc *vcp)
731 struct spam_filter *sfp;
732 char const *cp, *var;
733 bool_t rv = FAL0;
734 NYD2_ENTER;
736 sfp = &vcp->vc_t.filter;
738 switch (vcp->vc_action) {
739 case _SPAM_RATE:
740 cp = ok_vlook(spamfilter_rate);
741 var = "spam-filter-rate";
742 goto jonecmd;
743 case _SPAM_HAM:
744 cp = ok_vlook(spamfilter_ham);
745 var = "spam-filter-ham";
746 goto jonecmd;
747 case _SPAM_SPAM:
748 cp = ok_vlook(spamfilter_spam);
749 var = "spam-filter-spam";
750 jonecmd:
751 if (cp == NULL) {
752 jecmd:
753 n_err(_("`%s': *%s* is not set\n"), _spam_cmds[vcp->vc_action], var);
754 goto jleave;
756 sfp->f_super.cf_cmd = savestr(cp);
757 break;
758 case _SPAM_FORGET:
759 var = "spam-filter-nospam";
760 if ((cp = ok_vlook(spamfilter_nospam)) == NULL)
761 goto jecmd;
762 sfp->f_cmd_nospam = savestr(cp);
763 if ((cp = ok_vlook(spamfilter_noham)) == NULL)
764 goto jecmd;
765 sfp->f_cmd_noham = savestr(cp);
766 break;
769 # ifdef HAVE_REGEX
770 if (vcp->vc_action == _SPAM_RATE &&
771 (cp = ok_vlook(spamfilter_rate_scanscore)) != NULL) {
772 int s;
773 char const *bp;
775 var = strchr(cp, ';');
776 if (var == NULL) {
777 n_err(_("`%s': *spamfilter-rate-scanscore*: missing semicolon;: %s\n"),
778 _spam_cmds[vcp->vc_action], cp);
779 goto jleave;
781 bp = &var[1];
783 if((n_idec_buf(&sfp->f_score_grpno, cp, PTR2SIZE(var - cp), 0,
784 n_IDEC_MODE_LIMIT_32BIT, NULL
785 ) & (n_IDEC_STATE_EMASK | n_IDEC_STATE_CONSUMED)
786 ) != n_IDEC_STATE_CONSUMED){
787 n_err(_("`%s': *spamfilter-rate-scanscore*: bad group: %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 ((s = regcomp(&sfp->f_score_regex, bp, REG_EXTENDED | REG_ICASE))
800 != 0) {
801 n_err(_("`%s': invalid *spamfilter-rate-scanscore* regex: %s: %s\n"),
802 _spam_cmds[vcp->vc_action], n_shexp_quote_cp(cp, FAL0),
803 n_regex_err_to_doc(&sfp->f_score_regex, s));
804 goto jleave;
806 if (sfp->f_score_grpno > sfp->f_score_regex.re_nsub) {
807 regfree(&sfp->f_score_regex);
808 n_err(_("`%s': *spamfilter-rate-scanscore*: no group %u: %s\n"),
809 _spam_cmds[vcp->vc_action], sfp->f_score_grpno, cp);
810 goto jleave;
813 # endif /* HAVE_REGEX */
815 _spam_cf_setup(vcp, TRU1);
817 vcp->vc_act = &_spamfilter_interact;
818 vcp->vc_dtor = &_spamfilter_dtor;
819 rv = TRU1;
820 jleave:
821 NYD2_LEAVE;
822 return rv;
825 static bool_t
826 _spamfilter_interact(struct spam_vc *vcp)
828 # ifdef HAVE_REGEX
829 regmatch_t rem[SPAM_FILTER_MATCHES], *remp;
830 struct spam_filter *sfp;
831 char *cp;
832 # endif
833 bool_t rv;
834 NYD2_ENTER;
836 if (vcp->vc_action == _SPAM_FORGET)
837 vcp->vc_t.cf.cf_cmd = (vcp->vc_mp->m_flag & MSPAM)
838 ? vcp->vc_t.filter.f_cmd_nospam : vcp->vc_t.filter.f_cmd_noham;
840 if (!(rv = _spam_cf_interact(vcp)))
841 goto jleave;
843 vcp->vc_mp->m_flag &= ~(MSPAM | MSPAMUNSURE);
844 if (vcp->vc_action != _SPAM_RATE) {
845 if (vcp->vc_action == _SPAM_SPAM)
846 vcp->vc_mp->m_flag |= MSPAM;
847 goto jleave;
848 } else switch (WEXITSTATUS(vcp->vc_t.filter.f_super.cf_waitstat)) {
849 case 2:
850 vcp->vc_mp->m_flag |= MSPAMUNSURE;
851 /* FALLTHRU */
852 case 1:
853 break;
854 case 0:
855 vcp->vc_mp->m_flag |= MSPAM;
856 break;
857 default:
858 rv = FAL0;
859 goto jleave;
862 # ifdef HAVE_REGEX
863 sfp = &vcp->vc_t.filter;
865 if (sfp->f_score_grpno == 0)
866 goto jleave;
868 assert(sfp->f_super.cf_result != NULL);
869 remp = rem + sfp->f_score_grpno;
871 if (regexec(&sfp->f_score_regex, sfp->f_super.cf_result, n_NELEM(rem), rem,
872 0) == REG_NOMATCH || (remp->rm_so | remp->rm_eo) < 0) {
873 n_err(_("`%s': *spamfilter-rate-scanscore* "
874 "doesn't match filter output!\n"),
875 _spam_cmds[vcp->vc_action]);
876 sfp->f_score_grpno = 0;
877 goto jleave;
880 cp = sfp->f_super.cf_result;
881 cp[remp->rm_eo] = '\0';
882 cp += remp->rm_so;
883 _spam_rate2score(vcp, cp);
884 # endif /* HAVE_REGEX */
886 jleave:
887 NYD2_LEAVE;
888 return rv;
891 static void
892 _spamfilter_dtor(struct spam_vc *vcp)
894 struct spam_filter *sfp;
895 NYD2_ENTER;
897 sfp = &vcp->vc_t.filter;
899 if (sfp->f_super.cf_result != NULL)
900 free(sfp->f_super.cf_result);
901 # ifdef HAVE_REGEX
902 if (sfp->f_score_grpno > 0)
903 regfree(&sfp->f_score_regex);
904 # endif
905 NYD2_LEAVE;
907 #endif /* HAVE_SPAM_FILTER */
909 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_FILTER
910 static void
911 _spam_cf_setup(struct spam_vc *vcp, bool_t useshell)
913 struct str s;
914 char const *cp;
915 struct spam_cf *scfp;
916 NYD2_ENTER;
917 n_LCTA(2 < n_NELEM(scfp->cf_env), "Preallocated buffer too small");
919 scfp = &vcp->vc_t.cf;
921 if ((scfp->cf_useshell = useshell)) {
922 scfp->cf_acmd = ok_vlook(SHELL);
923 scfp->cf_a0 = "-c";
926 /* MAILX_FILENAME_GENERATED *//* TODO pathconf NAME_MAX; but user can create
927 * TODO a file wherever he wants! *Do* create a zero-size temporary file
928 * TODO and give *that* path as MAILX_FILENAME_TEMPORARY, clean it up once
929 * TODO the pipe returns? Like this we *can* verify path/name issues! */
930 cp = n_random_create_cp(n_MIN(NAME_MAX / 4, 16), NULL);
931 scfp->cf_env[0] = str_concat_csvl(&s,
932 n_PIPEENV_FILENAME_GENERATED, "=", cp, NULL)->s;
933 /* v15 compat NAIL_ environments vanish! */
934 scfp->cf_env[1] = str_concat_csvl(&s,
935 "NAIL_FILENAME_GENERATED", "=", cp, NULL)->s;
936 scfp->cf_env[2] = NULL;
937 NYD2_LEAVE;
940 static sigjmp_buf __spam_cf_actjmp; /* TODO someday, we won't need it */
941 static int volatile __spam_cf_sig; /* TODO someday, we won't need it */
942 static void
943 __spam_cf_onsig(int sig) /* TODO someday, we won't need it no more */
945 NYD_X; /* Signal handler */
946 __spam_cf_sig = sig;
947 siglongjmp(__spam_cf_actjmp, 1);
950 static bool_t
951 _spam_cf_interact(struct spam_vc *vcp)
953 struct spam_cf *scfp;
954 int p2c[2], c2p[2];
955 sigset_t cset;
956 char const *cp;
957 size_t size;
958 pid_t volatile pid;
959 enum {
960 _NONE = 0,
961 _SIGHOLD = 1<<0,
962 _P2C_0 = 1<<1,
963 _P2C_1 = 1<<2,
964 _P2C = _P2C_0 | _P2C_1,
965 _C2P_0 = 1<<3,
966 _C2P_1 = 1<<4,
967 _C2P = _C2P_0 | _C2P_1,
968 _JUMPED = 1<<5,
969 _RUNNING = 1<<6,
970 _GOODRUN = 1<<7,
971 _ERRORS = 1<<8
972 } volatile state = _NONE;
973 NYD2_ENTER;
975 scfp = &vcp->vc_t.cf;
976 if (scfp->cf_result != NULL) {
977 free(scfp->cf_result);
978 scfp->cf_result = NULL;
981 /* TODO Avoid that we jump away; yet necessary signal mess */
982 /*__spam_cf_sig = 0;*/
983 hold_sigs();
984 state |= _SIGHOLD;
985 scfp->cf_otstp = safe_signal(SIGTSTP, SIG_DFL);
986 scfp->cf_ottin = safe_signal(SIGTTIN, SIG_DFL);
987 scfp->cf_ottou = safe_signal(SIGTTOU, SIG_DFL);
988 scfp->cf_opipe = safe_signal(SIGPIPE, SIG_IGN);
989 scfp->cf_ohup = safe_signal(SIGHUP, &__spam_cf_onsig);
990 scfp->cf_oint = safe_signal(SIGINT, &__spam_cf_onsig);
991 scfp->cf_oquit = safe_signal(SIGQUIT, &__spam_cf_onsig);
992 /* Keep sigs blocked */
993 pid = 0; /* cc uninit */
995 if (!pipe_cloexec(p2c)) {
996 n_err(_("%s`%s': cannot create parent communication pipe: %s\n"),
997 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
998 goto jtail;
1000 state |= _P2C;
1002 if (!pipe_cloexec(c2p)) {
1003 n_err(_("%s`%s': cannot create child pipe: %s\n"),
1004 vcp->vc_esep, _spam_cmds[vcp->vc_action], n_err_to_doc(n_err_no));
1005 goto jtail;
1007 state |= _C2P;
1009 if (sigsetjmp(__spam_cf_actjmp, 1)) {
1010 if (*vcp->vc_esep != '\0')
1011 n_err(vcp->vc_esep);
1012 state |= _JUMPED;
1013 goto jtail;
1015 rele_sigs();
1016 state &= ~_SIGHOLD;
1018 /* Start our command as requested */
1019 sigemptyset(&cset);
1020 if ((pid = n_child_start(
1021 (scfp->cf_acmd != NULL ? scfp->cf_acmd : scfp->cf_cmd),
1022 &cset, p2c[0], c2p[1],
1023 scfp->cf_a0, (scfp->cf_acmd != NULL ? scfp->cf_cmd : NULL), NULL,
1024 scfp->cf_env)) < 0) {
1025 state |= _ERRORS;
1026 goto jtail;
1028 state |= _RUNNING;
1029 close(p2c[0]);
1030 state &= ~_P2C_0;
1032 /* Yes, we could sendmp(SEND_MBOX), but simply passing through the MBOX
1033 * content does the same in effect, however much more efficiently.
1034 * XXX NOTE: this may mean we pass a message without From_ line! */
1035 for (size = vcp->vc_mp->m_size; size > 0;) {
1036 size_t i;
1038 i = fread(vcp->vc_buffer, 1, n_MIN(size, BUFFER_SIZE), vcp->vc_ifp);
1039 if (i == 0) {
1040 if (ferror(vcp->vc_ifp))
1041 state |= _ERRORS;
1042 break;
1044 size -= i;
1045 if (i != (size_t)write(p2c[1], vcp->vc_buffer, i)) {
1046 state |= _ERRORS;
1047 break;
1051 jtail:
1052 /* TODO Quite racy -- block anything for a while? */
1053 if (state & _SIGHOLD) {
1054 state &= ~_SIGHOLD;
1055 rele_sigs();
1058 if (state & _P2C_0) {
1059 state &= ~_P2C_0;
1060 close(p2c[0]);
1062 if (state & _C2P_1) {
1063 state &= ~_C2P_1;
1064 close(c2p[1]);
1066 /* And cause EOF for the reader */
1067 if (state & _P2C_1) {
1068 state &= ~_P2C_1;
1069 close(p2c[1]);
1072 if (state & _RUNNING) {
1073 if (!(state & _ERRORS) &&
1074 vcp->vc_action == _SPAM_RATE && !(state & (_JUMPED | _ERRORS))) {
1075 ssize_t i = read(c2p[0], vcp->vc_buffer, BUFFER_SIZE - 1);
1076 if (i > 0) {
1077 vcp->vc_buffer[i] = '\0';
1078 if ((cp = strchr(vcp->vc_buffer, NETNL[0])) == NULL &&
1079 (cp = strchr(vcp->vc_buffer, NETNL[1])) == NULL) {
1080 n_err(_("%s`%s': program generates too much output: %s\n"),
1081 vcp->vc_esep, _spam_cmds[vcp->vc_action],
1082 n_shexp_quote_cp(scfp->cf_cmd, FAL0));
1083 state |= _ERRORS;
1084 } else {
1085 scfp->cf_result = sbufdup(vcp->vc_buffer,
1086 PTR2SIZE(cp - vcp->vc_buffer));
1087 /* FIXME consume child output until EOF??? */
1089 } else if (i != 0)
1090 state |= _ERRORS;
1093 state &= ~_RUNNING;
1094 n_child_wait(pid, &scfp->cf_waitstat);
1095 if (WIFEXITED(scfp->cf_waitstat))
1096 state |= _GOODRUN;
1099 if (state & _C2P_0) {
1100 state &= ~_C2P_0;
1101 close(c2p[0]);
1104 safe_signal(SIGQUIT, scfp->cf_oquit);
1105 safe_signal(SIGINT, scfp->cf_oint);
1106 safe_signal(SIGHUP, scfp->cf_ohup);
1107 safe_signal(SIGPIPE, scfp->cf_opipe);
1108 safe_signal(SIGTSTP, scfp->cf_otstp);
1109 safe_signal(SIGTTIN, scfp->cf_ottin);
1110 safe_signal(SIGTTOU, scfp->cf_ottou);
1112 NYD2_LEAVE;
1113 if (state & _JUMPED) {
1114 assert(vcp->vc_dtor != NULL);
1115 (*vcp->vc_dtor)(vcp);
1117 sigemptyset(&cset);
1118 sigaddset(&cset, __spam_cf_sig);
1119 sigprocmask(SIG_UNBLOCK, &cset, NULL);
1120 n_raise(__spam_cf_sig);
1122 return !(state & (_JUMPED | _ERRORS));
1124 #endif /* HAVE_SPAM_SPAMC || HAVE_SPAM_FILTER */
1126 #if defined HAVE_SPAM_SPAMC || defined HAVE_SPAM_SPAMD ||\
1127 (defined HAVE_SPAM_FILTER && defined HAVE_REGEX)
1128 static void
1129 _spam_rate2score(struct spam_vc *vcp, char *buf){
1130 ui32_t m, s;
1131 enum n_idec_state ids;
1132 NYD2_ENTER;
1134 /* C99 */{ /* Overcome ISO C / compiler weirdness */
1135 char const *cp;
1137 cp = buf;
1138 ids = n_idec_ui32_cp(&m, buf, 10, &cp);
1139 if((ids & n_IDEC_STATE_EMASK) & ~n_IDEC_STATE_EBASE)
1140 goto jleave;
1141 buf = n_UNCONST(cp);
1144 s = 0;
1145 if(!(ids & n_IDEC_STATE_CONSUMED)){
1146 /* Floating-point rounding for non-mathematicians */
1147 char c1, c2, c3;
1149 ++buf; /* '.' */
1150 if((c1 = buf[0]) != '\0' && (c2 = buf[1]) != '\0' &&
1151 (c3 = buf[2]) != '\0'){
1152 buf[2] = '\0';
1153 if(c3 >= '5'){
1154 if(c2 == '9'){
1155 if(c1 == '9'){
1156 ++m;
1157 goto jscore_ok;
1158 }else
1159 buf[0] = ++c1;
1160 c2 = '0';
1161 }else
1162 ++c2;
1163 buf[1] = c2;
1167 ids = n_idec_ui32_cp(&s, buf, 10, NULL);
1168 if((ids & (n_IDEC_STATE_EMASK | n_IDEC_STATE_CONSUMED)
1169 ) != n_IDEC_STATE_CONSUMED)
1170 goto jleave;
1173 jscore_ok:
1174 vcp->vc_mp->m_spamscore = (m << 8) | s;
1175 jleave:
1176 NYD2_LEAVE;
1178 #endif /* _SPAM_SPAMC || _SPAM_SPAMD || (_SPAM_FILTER && HAVE_REGEX) */
1180 FL int
1181 c_spam_clear(void *v)
1183 int *ip;
1184 NYD_ENTER;
1186 for (ip = v; *ip != 0; ++ip)
1187 message[(size_t)*ip - 1].m_flag &= ~(MSPAM | MSPAMUNSURE);
1188 NYD_LEAVE;
1189 return 0;
1192 FL int
1193 c_spam_set(void *v)
1195 int *ip;
1196 NYD_ENTER;
1198 for (ip = v; *ip != 0; ++ip) {
1199 message[(size_t)*ip - 1].m_flag &= ~(MSPAM | MSPAMUNSURE);
1200 message[(size_t)*ip - 1].m_flag |= MSPAM;
1202 NYD_LEAVE;
1203 return 0;
1206 FL int
1207 c_spam_forget(void *v)
1209 int rv;
1210 NYD_ENTER;
1212 rv = _spam_action(_SPAM_FORGET, (int*)v) ? OKAY : STOP;
1213 NYD_LEAVE;
1214 return rv;
1217 FL int
1218 c_spam_ham(void *v)
1220 int rv;
1221 NYD_ENTER;
1223 rv = _spam_action(_SPAM_HAM, (int*)v) ? OKAY : STOP;
1224 NYD_LEAVE;
1225 return rv;
1228 FL int
1229 c_spam_rate(void *v)
1231 int rv;
1232 NYD_ENTER;
1234 rv = _spam_action(_SPAM_RATE, (int*)v) ? OKAY : STOP;
1235 NYD_LEAVE;
1236 return rv;
1239 FL int
1240 c_spam_spam(void *v)
1242 int rv;
1243 NYD_ENTER;
1245 rv = _spam_action(_SPAM_SPAM, (int*)v) ? OKAY : STOP;
1246 NYD_LEAVE;
1247 return rv;
1249 #endif /* HAVE_SPAM */
1251 /* s-it-mode */