Fix *newmail* (oops, ",$s//" no-no-no, "%s//" yes!)..
[s-mailx.git] / spam.c
blob83bc7dca1ee71b682f1adc202ad826755a198181
1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
2 *@ Spam related facilities.
4 * Copyright (c) 2013 - 2014 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.
19 #ifndef HAVE_AMALGAMATION
20 # include "nail.h"
21 #endif
23 EMPTY_FILE(spam)
24 #ifdef HAVE_SPAM
26 * TODO - We cannot use the spamc library because of our jumping behaviour.
27 * TODO We could nonetheless if we'd start a fork(2)ed child which would
28 * TODO use the spamc library.
29 * TODO -- In fact using a child process that is immune from the terrible
30 * TODO signal and jumping mess, and that controls further childs, and
31 * TODO gains file descriptors via sendmsg(2), and is started once it is
32 * TODO needed first, i have in mind for quite some time, for the transition
33 * TODO to a select(2) based implementation: we could slowly convert SMTP,
34 * TODO etc., finally IMAP, at which case we could rejoin back into a single
35 * TODO process program (unless we want to isolate the UI at that time, to
36 * TODO allow for some xUI protocol).
37 * TODO :: That is to say -- it's a horrible signal and jump mess ::
38 * TODO - We do not yet handle direct communication with spamd(1).
39 * TODO I.e., this could be a lean alternative to the first item;
40 * TODO the protocol is easy and we could support ALL operations easily.
41 * TODO - We do not yet update mails in place, i.e., replace the original
42 * TODO message with the updated one; we could easily do it as if `edit' has
43 * TODO been used, but the nail codebase doesn't truly support that for IMAP.
44 * TODO (And it seems a bit grazy to download a message, update it and upload
45 * TODO it again.)
48 enum spam_action {
49 _SPAM_RATE,
50 _SPAM_HAM,
51 _SPAM_SPAM,
52 _SPAM_FORGET
55 struct spam_vc {
56 struct message *mp;
57 size_t mno;
58 int action;
59 int __dummy;
60 char *comm_s;
61 char *buffer;
62 /* TODO This codebase jumps around and uses "stacks" of signal handling;
63 * TODO until some later time we have to play the same game */
64 sighandler_type otstp;
65 sighandler_type ottin;
66 sighandler_type ottou;
67 sighandler_type ohup;
68 sighandler_type opipe;
69 sighandler_type oint;
70 char const *comm_a[16];
73 /* Indices according to enum spam_action */
74 static char const _spam_comms[][16] = {
75 "spamrate", "spamham", "spamspam", "spamforget"
78 /* Shared action setup */
79 static bool_t _spam_action(enum spam_action sa, int *ip);
81 /* Convert a 2[.x]/whatever spam rate into message.m_spamscore */
82 static void _spam_rate2score(struct spam_vc *vc);
84 /* Interact with spamc(1) */
85 static bool_t _spam_interact(struct spam_vc *vc);
87 static bool_t
88 _spam_action(enum spam_action sa, int *ip)
90 struct spam_vc vc;
91 struct str str;
92 size_t maxsize;
93 char const *cp, **args;
94 bool_t ok = FAL0;
95 NYD_ENTER;
97 vc.action = sa;
99 if ((cp = ok_vlook(spam_command)) == NULL) {
100 #ifdef SPAMC_PATH
101 cp = SPAMC_PATH;
102 #else
103 fprintf(stderr, _("`%s': *spam-command* is not set\n"), _spam_comms[sa]);
104 goto jleave;
105 #endif
108 /* Prepare the spamc(1) command line */
109 args = vc.comm_a;
110 *args++ = cp;
112 switch (sa) {
113 case _SPAM_RATE:
114 *args = "-c";
115 break;
116 case _SPAM_HAM:
117 args[1] = "ham";
118 goto jlearn;
119 case _SPAM_SPAM:
120 args[1] = "spam";
121 goto jlearn;
122 case _SPAM_FORGET:
123 args[1] = "forget";
124 jlearn:
125 *args = "-L";
126 ++args;
127 break;
129 ++args;
131 if ((cp = ok_vlook(spam_socket)) != NULL) {
132 *args++ = "-U";
133 *args++ = cp;
134 } else {
135 if ((cp = ok_vlook(spam_host)) != NULL) {
136 *args++ = "-d";
137 *args++ = cp;
139 if ((cp = ok_vlook(spam_port)) != NULL) {
140 *args++ = "-p";
141 *args++ = cp;
145 *args++ = "-l"; /* --log-to-stderr */
147 if ((cp = ok_vlook(spam_user)) != NULL) {
148 *args++ = "-u";
149 *args++ = cp;
152 *args = NULL;
153 vc.comm_s = str_concat_cpa(&str, vc.comm_a, " ")->s;
154 if (options & OPT_DEBUG)
155 fprintf(stderr, "spamc(1) via <%s>\n", vc.comm_s);
157 /* *spam-maxsize* we do handle ourselfs instead */
158 maxsize = 0;
159 if ((cp = ok_vlook(spam_maxsize)) != NULL)
160 maxsize = (size_t)strtol(cp, NULL, 10);
161 if (maxsize <= 0)
162 maxsize = SPAM_MAXSIZE;
164 /* Finally get an I/O buffer */
165 vc.buffer = salloc(BUFFER_SIZE);
167 for (ok = TRU1; *ip != 0; ++ip) {
168 vc.mno = (size_t)*ip - 1;
169 vc.mp = message + vc.mno;
170 if (sa == _SPAM_RATE)
171 vc.mp->m_spamscore = 0;
172 if (vc.mp->m_size > maxsize) {
173 if (options & OPT_VERB)
174 fprintf(stderr,
175 _("`%s': message %lu exceeds maxsize (%lu > %lu), skip\n"),
176 _spam_comms[sa], (ul_it)vc.mno + 1,
177 (ul_it)vc.mp->m_size, (ul_it)maxsize);
178 continue;
180 if ((ok = _spam_interact(&vc)) == FAL0)
181 break;
183 #ifndef SPAMC_PATH
184 jleave:
185 #endif
186 NYD_LEAVE;
187 return !ok;
190 static void
191 _spam_rate2score(struct spam_vc *vc)
193 char *cp;
194 size_t size;
195 ui32_t m, s;
196 NYD_ENTER;
198 cp = strchr(vc->buffer, '/');
199 if (cp == NULL)
200 goto jleave;
201 size = PTR2SIZE(cp - vc->buffer);
202 vc->buffer[size] = '\0';
204 m = (ui32_t)strtol(vc->buffer, &cp, 10);
205 if (cp == vc->buffer)
206 goto jleave;
208 s = (*cp == '\0') ? 0 : (ui32_t)strtol(++cp, NULL, 10);
210 vc->mp->m_spamscore = (m << 8) | (s & 0xFF);
211 jleave:
212 NYD_LEAVE;
215 static sigjmp_buf __spam_actjmp; /* TODO someday, we won't need it no more */
216 static int __spam_sig; /* TODO someday, we won't need it no more */
217 static void
218 __spam_onsig(int sig) /* TODO someday, we won't need it no more */
220 NYD_X; /* Signal handler */
221 __spam_sig = sig;
222 siglongjmp(__spam_actjmp, 1);
225 static bool_t
226 _spam_interact(struct spam_vc *vc)
228 int p2c[2], c2p[2];
229 sigset_t cset;
230 size_t size;
231 pid_t volatile pid;
232 FILE *ibuf;
233 enum {
234 _NONE = 0,
235 _SIGHOLD = 1<<0,
236 _P2C_0 = 1<<1,
237 _P2C_1 = 1<<2,
238 _P2C = _P2C_0 | _P2C_1,
239 _C2P_0 = 1<<3,
240 _C2P_1 = 1<<4,
241 _C2P = _C2P_0 | _C2P_1,
242 _JUMPED = 1<<5,
243 _RUNNING = 1<<6,
244 _GOODRUN = 1<<7,
245 _ERRORS = 1<<8
246 } state = _NONE;
247 NYD_ENTER;
249 setdot(vc->mp);
250 if ((ibuf = setinput(&mb, vc->mp, NEED_BODY)) == NULL) {
251 perror("setinput"); /* XXX tr() */
252 goto j_leave;
255 /* TODO Avoid that we jump away; yet necessary signal mess */
256 vc->otstp = safe_signal(SIGTSTP, SIG_DFL);
257 vc->ottin = safe_signal(SIGTTIN, SIG_DFL);
258 vc->ottou = safe_signal(SIGTTOU, SIG_DFL);
259 vc->opipe = safe_signal(SIGPIPE, SIG_IGN);
260 hold_sigs();
261 state |= _SIGHOLD;
262 vc->ohup = safe_signal(SIGHUP, &__spam_onsig);
263 vc->oint = safe_signal(SIGINT, &__spam_onsig);
264 /* Keep sigs blocked */
265 pid = 0; /* cc uninit */
267 if (!pipe_cloexec(p2c)) {
268 perror("pipe"); /* XXX tr() */
269 goto jleave;
271 state |= _P2C;
273 if (!pipe_cloexec(c2p)) {
274 perror("pipe"); /* XXX tr() */
275 goto jleave;
277 state |= _C2P;
279 if (sigsetjmp(__spam_actjmp, 1)) {
280 state |= _JUMPED;
281 goto jleave;
283 rele_sigs();
284 state &= ~_SIGHOLD;
286 sigemptyset(&cset);
287 pid = start_command(vc->comm_s, &cset, p2c[0], c2p[1], NULL, NULL, NULL,
288 NULL);
289 state |= _RUNNING;
290 close(p2c[0]);
291 state &= ~_P2C_0;
293 /* Yes, we could sendmp(SEND_MBOX), but simply passing through the MBOX
294 * content does the same in effect, but is much more efficient.
295 * NOTE: this may mean we pass a message without From_ line! */
296 for (size = vc->mp->m_size; size > 0;) {
297 size_t i = fread(vc->buffer, 1, MIN(size, BUFFER_SIZE), ibuf);
298 if (i == 0) {
299 if (ferror(ibuf))
300 state |= _ERRORS;
301 break;
303 size -= i;
304 if (i != (size_t)write(p2c[1], vc->buffer, i)) {
305 state |= _ERRORS;
306 break;
310 jleave:
311 /* TODO In what follows you see a lot of races; these can't be helped without
312 * TODO atomic compare-and-swap -- WE COULD ALSO BLOCK ANYTHING FOR A WHILE*/
313 if (state & _SIGHOLD) {
314 state &= ~_SIGHOLD;
315 rele_sigs();
318 if (state & _P2C_0) {
319 state &= ~_P2C_0;
320 close(p2c[0]);
322 if (state & _C2P_1) {
323 state &= ~_C2P_1;
324 close(c2p[1]);
326 /* Close the write end, so that spamc(1) goes */
327 if (state & _P2C_1) {
328 state &= ~_P2C_1;
329 close(p2c[1]);
332 if (state & _RUNNING) {
333 state &= ~_RUNNING;
334 if (wait_child(pid, NULL))
335 state |= _GOODRUN;
338 /* XXX This only works because spamc(1) follows the clear protocol (1) read
339 * XXX everything until EOF on input, then (2) work, then (3) output
340 * XXX a single result line; otherwise we could deadlock here, but since
341 * TODO this is rather intermediate, go with it */
342 if (vc->action == _SPAM_RATE && !(state & (_JUMPED | _ERRORS))) {
343 ssize_t i = read(c2p[0], vc->buffer, BUFFER_SIZE - 1);
344 if (i > 0) {
345 vc->buffer[i] = '\0';
346 _spam_rate2score(vc);
347 } else if (i != 0)
348 state |= _ERRORS;
351 if (state & _C2P_0) {
352 state &= ~_C2P_0;
353 close(c2p[0]);
356 if (vc->action == _SPAM_RATE) {
357 switch (state & (_JUMPED | _GOODRUN | _ERRORS)) {
358 case _GOODRUN:
359 vc->mp->m_flag &= ~MSPAM;
360 break;
361 case 0:
362 vc->mp->m_flag |= MSPAM;
363 default:
364 break;
366 } else {
367 if (state & (_JUMPED | _ERRORS))
368 /* xxx print message? */;
369 else if (vc->action == _SPAM_SPAM)
370 vc->mp->m_flag |= MSPAM;
371 else if (vc->action == _SPAM_HAM)
372 vc->mp->m_flag &= ~MSPAM;
375 safe_signal(SIGINT, vc->oint);
376 safe_signal(SIGHUP, vc->ohup);
377 safe_signal(SIGPIPE, vc->opipe);
378 safe_signal(SIGTSTP, vc->otstp);
379 safe_signal(SIGTTIN, vc->ottin);
380 safe_signal(SIGTTOU, vc->ottou);
382 /* Bounce jumps to the lex.c trampolines
383 * (i'd have never believed i'd ever say or even do something like this) */
384 if (state & _JUMPED) {
385 sigemptyset(&cset);
386 sigaddset(&cset, __spam_sig);
387 sigprocmask(SIG_UNBLOCK, &cset, NULL);
389 j_leave:
390 NYD_LEAVE;
391 if (state & _JUMPED)
392 kill(0, __spam_sig);
393 return !(state & _ERRORS);
396 FL int
397 c_spam_clear(void *v)
399 int *ip;
400 NYD_ENTER;
402 for (ip = v; *ip != 0; ++ip)
403 message[(size_t)*ip - 1].m_flag &= ~MSPAM;
404 NYD_LEAVE;
405 return 0;
408 FL int
409 c_spam_set(void *v)
411 int *ip;
412 NYD_ENTER;
414 for (ip = v; *ip != 0; ++ip)
415 message[(size_t)*ip - 1].m_flag |= MSPAM;
416 NYD_LEAVE;
417 return 0;
420 FL int
421 c_spam_forget(void *v)
423 int rv;
424 NYD_ENTER;
426 rv = _spam_action(_SPAM_FORGET, (int*)v) ? OKAY : STOP;
427 NYD_LEAVE;
428 return rv;
431 FL int
432 c_spam_ham(void *v)
434 int rv;
435 NYD_ENTER;
437 rv = _spam_action(_SPAM_HAM, (int*)v) ? OKAY : STOP;
438 NYD_LEAVE;
439 return rv;
442 FL int
443 c_spam_rate(void *v)
445 int rv;
446 NYD_ENTER;
448 rv = _spam_action(_SPAM_RATE, (int*)v) ? OKAY : STOP;
449 NYD_LEAVE;
450 return rv;
453 FL int
454 c_spam_spam(void *v)
456 int rv;
457 NYD_ENTER;
459 rv = _spam_action(_SPAM_SPAM, (int*)v) ? OKAY : STOP;
460 NYD_LEAVE;
461 return rv;
463 #endif /* HAVE_SPAM */
465 /* s-it-mode */