1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2012 Colin Leroy <colin@colino.net> and
4  * the Claws Mail team
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program. If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 #ifdef HAVE_CONFIG_H
22 #  include "config.h"
23 #include "claws-features.h"
24 #endif
25 
26 #include "defs.h"
27 
28 #include <sys/types.h>
29 #include <sys/wait.h>
30 #include <errno.h>
31 
32 #include <glib.h>
33 #include <glib/gi18n.h>
34 
35 #if HAVE_LOCALE_H
36 #  include <locale.h>
37 #endif
38 
39 #include "common/claws.h"
40 #include "common/version.h"
41 #include "plugin.h"
42 #include "common/utils.h"
43 #include "hooks.h"
44 #include "procmsg.h"
45 #include "folder.h"
46 #include "prefs.h"
47 #include "prefs_gtk.h"
48 
49 #include "bogofilter.h"
50 #include "inc.h"
51 #include "log.h"
52 #include "prefs_common.h"
53 #include "alertpanel.h"
54 #include "addr_compl.h"
55 #include "file-utils.h"
56 
57 #ifdef HAVE_SYSEXITS_H
58 #include <sysexits.h>
59 #endif
60 #ifdef HAVE_ERRNO_H
61 #include <errno.h>
62 #endif
63 #ifdef HAVE_SYS_ERRNO_H
64 #include <sys/errno.h>
65 #endif
66 #ifdef HAVE_TIME_H
67 #include <time.h>
68 #endif
69 #ifdef HAVE_SYS_TIME_H
70 #include <sys/time.h>
71 #endif
72 #ifdef HAVE_SIGNAL_H
73 #include <signal.h>
74 #endif
75 #ifdef HAVE_PWD_H
76 #include <pwd.h>
77 #endif
78 #ifdef USE_PTHREAD
79 #include <pthread.h>
80 #endif
81 
82 #define PLUGIN_NAME (_("Bogofilter"))
83 
84 static gulong hook_id = HOOK_NONE;
85 static MessageCallback message_callback;
86 
87 static BogofilterConfig config;
88 
89 static PrefParam param[] = {
90 	{"process_emails", "TRUE", &config.process_emails, P_BOOL,
91 	 NULL, NULL, NULL},
92 	{"receive_spam", "1", &config.receive_spam, P_INT,
93 	 NULL, NULL, NULL},
94 	{"save_folder", NULL, &config.save_folder, P_STRING,
95 	 NULL, NULL, NULL},
96 	{"save_unsure", "FALSE", &config.save_unsure, P_BOOL,
97 	 NULL, NULL, NULL},
98 	{"save_unsure_folder", NULL, &config.save_unsure_folder, P_STRING,
99 	 NULL, NULL, NULL},
100 	{"max_size", "250", &config.max_size, P_INT,
101 	 NULL, NULL, NULL},
102 	{"bogopath", "bogofilter", &config.bogopath, P_STRING,
103 	 NULL, NULL, NULL},
104 	{"insert_header", "FALSE", &config.insert_header, P_BOOL,
105 	 NULL, NULL, NULL},
106 	{"whitelist_ab", "FALSE", &config.whitelist_ab, P_BOOL,
107 	 NULL, NULL, NULL},
108 	{"whitelist_ab_folder", N_("Any"), &config.whitelist_ab_folder, P_STRING,
109 	 NULL, NULL, NULL},
110 	{"learn_from_whitelist", "FALSE", &config.learn_from_whitelist, P_BOOL,
111 	 NULL, NULL, NULL},
112 	{"mark_as_read", "TRUE", &config.mark_as_read, P_BOOL,
113 	 NULL, NULL, NULL},
114 
115 	{NULL, NULL, NULL, P_OTHER, NULL, NULL, NULL}
116 };
117 
118 /*
119  * Helper function for spawn_with_input() - write an entire
120  * string to a fd.
121  */
122 static gboolean
write_all(int fd,const char * buf,gsize to_write)123 write_all (int         fd,
124 	   const char *buf,
125 	   gsize       to_write)
126 {
127   while (to_write > 0)
128     {
129       gssize count = write (fd, buf, to_write);
130       if (count < 0)
131 	{
132 	  if (errno != EINTR)
133 	    return FALSE;
134 	}
135       else
136 	{
137 	  to_write -= count;
138 	  buf += count;
139 	}
140     }
141 
142   return TRUE;
143 }
144 
145 typedef struct _BogoFilterData {
146 	MailFilteringData *mail_filtering_data;
147 	gchar **bogo_args;
148 	GSList *msglist;
149 	GSList *new_hams;
150 	GSList *new_unsure;
151 	GSList *new_spams;
152 	GSList *whitelisted_new_spams;
153 	gboolean done;
154 	int status;
155 	gboolean in_thread;
156 } BogoFilterData;
157 
158 static BogoFilterData *to_filter_data = NULL;
159 #ifdef USE_PTHREAD
160 static gboolean filter_th_done = FALSE;
161 static pthread_mutex_t list_mutex = PTHREAD_MUTEX_INITIALIZER;
162 static pthread_mutex_t wait_mutex = PTHREAD_MUTEX_INITIALIZER;
163 static pthread_cond_t wait_cond = PTHREAD_COND_INITIALIZER;
164 #endif
165 
bogofilter_do_filter(BogoFilterData * data)166 static void bogofilter_do_filter(BogoFilterData *data)
167 {
168 	GPid bogo_pid;
169 	gint bogo_stdin, bogo_stdout;
170 	GError *error = NULL;
171 	gboolean bogo_forked;
172 	int status = 0;
173 	MsgInfo *msginfo;
174 	GSList *cur = NULL;
175 	int total = 0, curnum = 1;
176 	gchar *file = NULL;
177 	gchar buf[BUFSIZ];
178 
179 	total = g_slist_length(data->msglist);
180 
181 	bogo_forked = g_spawn_async_with_pipes(
182 			NULL, data->bogo_args,NULL, G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD,
183 			NULL, NULL, &bogo_pid, &bogo_stdin,
184 			&bogo_stdout, NULL, &error);
185 
186 	if (bogo_forked == FALSE) {
187 		g_warning("%s", error ? error->message:"ERROR???");
188 		g_error_free(error);
189 		error = NULL;
190 		status = -1;
191 	} else {
192 
193 		if (config.whitelist_ab) {
194 			gchar *ab_folderpath;
195 
196 			if (*config.whitelist_ab_folder == '\0' ||
197 				strcasecmp(config.whitelist_ab_folder, "Any") == 0) {
198 				/* match the whole addressbook */
199 				ab_folderpath = NULL;
200 			} else {
201 				/* match the specific book/folder of the addressbook */
202 				ab_folderpath = config.whitelist_ab_folder;
203 			}
204 
205 			start_address_completion(ab_folderpath);
206 		}
207 
208 		for (cur = data->msglist; cur; cur = cur->next) {
209 			gboolean whitelisted = FALSE;
210 			msginfo = (MsgInfo *)cur->data;
211 			debug_print("Filtering message %d (%d/%d)\n", msginfo->msgnum, curnum, total);
212 
213 			if (message_callback != NULL)
214 				message_callback(NULL, total, curnum++, data->in_thread);
215 
216 			if (config.whitelist_ab && msginfo->from &&
217 			    found_in_addressbook(msginfo->from))
218 				whitelisted = TRUE;
219 
220 			/* can set flags (SCANNED, ATTACHMENT) but that's ok
221 			 * as GUI updates are hooked not direct */
222 
223 			file = procmsg_get_message_file(msginfo);
224 
225 			if (file) {
226 				gchar *tmp = g_strdup_printf("%s\n",file);
227 				/* send filename to bogofilter */
228 				write_all(bogo_stdin, tmp, strlen(tmp));
229 				g_free(tmp);
230 				memset(buf, 0, sizeof(buf));
231 				/* get the result */
232 				if (read(bogo_stdout, buf, sizeof(buf)-1) < 0) {
233 					g_warning("bogofilter short read");
234 					debug_print("message %d is ham\n", msginfo->msgnum);
235 					data->mail_filtering_data->unfiltered = g_slist_prepend(
236 						data->mail_filtering_data->unfiltered, msginfo);
237 					data->new_hams = g_slist_prepend(data->new_hams, msginfo);
238 				} else {
239 					gchar **parts = NULL;
240 
241 					buf[sizeof(buf) - 1] = '\0';
242 					if (strchr(buf, '/')) {
243 						tmp = strrchr(buf, '/')+1;
244 					} else {
245 						tmp = buf;
246 					}
247 					parts = g_strsplit(tmp, " ", 0);
248 					debug_print("read %s\n", buf);
249 
250 					/* note the result if the header if needed */
251 					if (parts && parts[0] && parts[1] && parts[2] &&
252 					    FOLDER_TYPE(msginfo->folder->folder) == F_MH &&
253 					    config.insert_header) {
254 						gchar *tmpfile = get_tmp_file();
255 						FILE *input = claws_fopen(file, "r");
256 						FILE *output = claws_fopen(tmpfile, "w");
257 						if (strstr(parts[2], "\n"))
258 							*(strstr(parts[2], "\n")) = '\0';
259 						if (input && !output)
260 							claws_fclose (input);
261 						else if (!input && output)
262 							claws_fclose (output);
263 						else if (input && output) {
264 							gchar tmpbuf[BUFFSIZE];
265 							gboolean err = FALSE;
266 							const gchar *bogosity = *parts[1] == 'S' ? "Spam":
267 										 (*parts[1] == 'H' ? "Ham":"Unsure");
268 							gchar *tmpstr = g_strdup_printf(
269 									"X-Bogosity: %s, spamicity=%s%s\n",
270 									bogosity, parts[2],
271 									whitelisted?" [whitelisted]":"");
272 							if (claws_fwrite(tmpstr, 1, strlen(tmpstr), output) < strlen(tmpstr)) {
273 								err = TRUE;
274 							} else {
275 								while (claws_fgets(tmpbuf, sizeof(buf), input)) {
276 									if (claws_fputs(tmpbuf, output) == EOF) {
277 										err = TRUE;
278 										break;
279 									}
280 								}
281 							}
282 							claws_fclose(input);
283 							if (claws_safe_fclose(output) == EOF)
284 								err = TRUE;
285 							if (!err)
286 								move_file(tmpfile, file, TRUE);
287 							g_free(tmpstr);
288 						}
289 						g_free(tmpfile);
290 					}
291 
292 					/* file the mail */
293 					if (!whitelisted && parts && parts[0] && parts[1] && *parts[1] == 'S') {
294 
295 						debug_print("message %d is spam\n", msginfo->msgnum);
296 						/* Spam will be filtered away, unless we want "mark only".
297 						 * In that case, we want it among unfiltered messages, so
298 						 * it gets processed further. */
299 						if (config.receive_spam == SPAM_MARK_ONLY) {
300 							data->mail_filtering_data->unfiltered = g_slist_prepend(
301 								data->mail_filtering_data->unfiltered, msginfo);
302 						} else {
303 							data->mail_filtering_data->filtered = g_slist_prepend(
304 								data->mail_filtering_data->filtered, msginfo);
305 						}
306 						data->new_spams = g_slist_prepend(data->new_spams, msginfo);
307 
308 					} else if (whitelisted && parts && parts[0] && parts[1] &&
309 							(*parts[1] == 'S' || *parts[1] == 'U')) {
310 
311 						debug_print("message %d is whitelisted %s\n", msginfo->msgnum,
312 							*parts[1] == 'S' ? "spam":"unsure");
313 						/* Whitelisted spam will *not* be filtered away, but continue
314 						 * their trip through filtering as if it was ham. */
315 						data->mail_filtering_data->unfiltered = g_slist_prepend(
316 							data->mail_filtering_data->unfiltered, msginfo);
317 						/* But it gets put in a different list, so that we
318 						 * can still flag it and inform the user that it is
319 						 * considered a spam (so that he can teach bogo that
320 						 * it was not). */
321 						data->whitelisted_new_spams = g_slist_prepend(data->whitelisted_new_spams, msginfo);
322 
323 					} else if (config.save_unsure && parts && parts[0] && parts[1] && *parts[1] == 'U') {
324 
325 						debug_print("message %d is unsure\n", msginfo->msgnum);
326 						/* Spam will be filtered away */
327 						data->mail_filtering_data->filtered = g_slist_prepend(
328 							data->mail_filtering_data->filtered, msginfo);
329 						data->new_unsure = g_slist_prepend(data->new_unsure, msginfo);
330 
331 					} else {
332 
333 						debug_print("message %d is ham\n", msginfo->msgnum);
334 						data->mail_filtering_data->unfiltered = g_slist_prepend(
335 							data->mail_filtering_data->unfiltered, msginfo);
336 						data->new_hams = g_slist_prepend(data->new_hams, msginfo);
337 
338 					}
339 					g_strfreev(parts);
340 				}
341 				g_free(file);
342 			} else {
343 				data->mail_filtering_data->unfiltered = g_slist_prepend(
344 					data->mail_filtering_data->unfiltered, msginfo);
345 				data->new_hams = g_slist_prepend(data->new_hams, msginfo);
346 			}
347 		}
348 		if (config.whitelist_ab)
349 			end_address_completion();
350 	}
351 	if (status != -1) {
352 		close(bogo_stdout);
353 		close(bogo_stdin);
354 		waitpid(bogo_pid, &status, 0);
355 		if (!WIFEXITED(status))
356 			status = -1;
357 		else
358 			status = WEXITSTATUS(status);
359 	}
360 
361 	to_filter_data->status = status;
362 }
363 
364 #ifdef USE_PTHREAD
bogofilter_filtering_thread(void * data)365 static void *bogofilter_filtering_thread(void *data)
366 {
367 	while (!filter_th_done) {
368 		pthread_mutex_lock(&list_mutex);
369 		if (to_filter_data == NULL || to_filter_data->done == TRUE) {
370 			pthread_mutex_unlock(&list_mutex);
371 			debug_print("thread is waiting for something to filter\n");
372 			pthread_mutex_lock(&wait_mutex);
373 			pthread_cond_wait(&wait_cond, &wait_mutex);
374 			pthread_mutex_unlock(&wait_mutex);
375 		} else {
376 			debug_print("thread awaken with something to filter\n");
377 			to_filter_data->done = FALSE;
378 			bogofilter_do_filter(to_filter_data);
379 			pthread_mutex_unlock(&list_mutex);
380 			to_filter_data->done = TRUE;
381 			usleep(100);
382 		}
383 	}
384 	return NULL;
385 }
386 
387 static pthread_t filter_th = 0;
388 
bogofilter_start_thread(void)389 static void bogofilter_start_thread(void)
390 {
391 	filter_th_done = FALSE;
392 	if (filter_th != 0 || 1)
393 		return;
394 	if (pthread_create(&filter_th, NULL,
395 			bogofilter_filtering_thread,
396 			NULL) != 0) {
397 		filter_th = 0;
398 		return;
399 	}
400 	debug_print("thread created\n");
401 }
402 
bogofilter_stop_thread(void)403 static void bogofilter_stop_thread(void)
404 {
405 	void *res;
406 	while (pthread_mutex_trylock(&list_mutex) != 0) {
407 		GTK_EVENTS_FLUSH();
408 		usleep(100);
409 	}
410 	if (filter_th != 0) {
411 		filter_th_done = TRUE;
412 		debug_print("waking thread up\n");
413 		pthread_mutex_lock(&wait_mutex);
414 		pthread_cond_broadcast(&wait_cond);
415 		pthread_mutex_unlock(&wait_mutex);
416 		pthread_join(filter_th, &res);
417 		filter_th = 0;
418 	}
419 	pthread_mutex_unlock(&list_mutex);
420 	debug_print("thread done\n");
421 }
422 #endif
423 
mail_filtering_hook(gpointer source,gpointer data)424 static gboolean mail_filtering_hook(gpointer source, gpointer data)
425 {
426 	MailFilteringData *mail_filtering_data = (MailFilteringData *) source;
427 	MsgInfo *msginfo = mail_filtering_data->msginfo;
428 	GSList *msglist = mail_filtering_data->msglist;
429 	GSList *cur = NULL;
430 	static gboolean warned_error = FALSE;
431 	int status = 0;
432 	int total = 0, curnum = 0;
433 	GSList *new_hams = NULL, *new_spams = NULL;
434 	GSList *new_unsure, *whitelisted_new_spams = NULL;
435 	gchar *bogo_exec = (config.bogopath && *config.bogopath) ? config.bogopath:"bogofilter";
436 	gchar *bogo_args[4];
437 	gboolean ok_to_thread = TRUE;
438 
439 	bogo_args[0] = bogo_exec;
440 	bogo_args[1] = "-T";
441 	bogo_args[2] = "-b";
442 	bogo_args[3] = NULL;
443 
444 	if (!config.process_emails) {
445 		return FALSE;
446 	}
447 
448 	if (msglist == NULL && msginfo != NULL) {
449 		g_warning("wrong call to bogofilter mail_filtering_hook");
450 		return FALSE;
451 	}
452 
453 	total = g_slist_length(msglist);
454 
455 	/* we have to make sure the mails are cached - or it'll break on IMAP */
456 	if (message_callback != NULL)
457 		message_callback(_("Bogofilter: fetching bodies..."), total, 0, FALSE);
458 	for (cur = msglist; cur; cur = cur->next) {
459 		gchar *file = procmsg_get_message_file((MsgInfo *)cur->data);
460 		if (file == NULL)
461 			ok_to_thread = FALSE;
462 		if (message_callback != NULL)
463 			message_callback(NULL, total, curnum++, FALSE);
464 		g_free(file);
465 	}
466 	if (message_callback != NULL)
467 		message_callback(NULL, 0, 0, FALSE);
468 
469 	if (message_callback != NULL)
470 		message_callback(_("Bogofilter: filtering messages..."), total, 0, FALSE);
471 
472 #ifdef USE_PTHREAD
473 	while (pthread_mutex_trylock(&list_mutex) != 0) {
474 		GTK_EVENTS_FLUSH();
475 		usleep(100);
476 	}
477 #endif
478 	to_filter_data = g_new0(BogoFilterData, 1);
479 	to_filter_data->msglist = msglist;
480 	to_filter_data->mail_filtering_data = mail_filtering_data;
481 	to_filter_data->new_hams = NULL;
482 	to_filter_data->new_unsure = NULL;
483 	to_filter_data->new_spams = NULL;
484 	to_filter_data->whitelisted_new_spams = NULL;
485 	to_filter_data->done = FALSE;
486 	to_filter_data->status = -1;
487 	to_filter_data->bogo_args = bogo_args;
488 #ifdef USE_PTHREAD
489 	to_filter_data->in_thread = (filter_th != 0 && ok_to_thread);
490 #else
491 	to_filter_data->in_thread = FALSE;
492 #endif
493 
494 #ifdef USE_PTHREAD
495 	pthread_mutex_unlock(&list_mutex);
496 
497 	if (filter_th != 0 && ok_to_thread) {
498 		debug_print("waking thread to let it filter things\n");
499 		pthread_mutex_lock(&wait_mutex);
500 		pthread_cond_broadcast(&wait_cond);
501 		pthread_mutex_unlock(&wait_mutex);
502 
503 		while (!to_filter_data->done) {
504 			GTK_EVENTS_FLUSH();
505 			usleep(100);
506 		}
507 	}
508 
509 	while (pthread_mutex_trylock(&list_mutex) != 0) {
510 		GTK_EVENTS_FLUSH();
511 		usleep(100);
512 
513 	}
514 	if (filter_th == 0 || !ok_to_thread)
515 		bogofilter_do_filter(to_filter_data);
516 #else
517 	bogofilter_do_filter(to_filter_data);
518 #endif
519 
520 	new_hams = to_filter_data->new_hams;
521 	new_unsure = to_filter_data->new_unsure;
522 	new_spams = to_filter_data->new_spams;
523 	whitelisted_new_spams = to_filter_data->whitelisted_new_spams;
524 	status = to_filter_data->status;
525 	g_free(to_filter_data);
526 	to_filter_data = NULL;
527 #ifdef USE_PTHREAD
528 	pthread_mutex_unlock(&list_mutex);
529 #endif
530 
531 
532 	/* unflag hams */
533 	for (cur = new_hams; cur; cur = cur->next) {
534 		MsgInfo *msginfo = (MsgInfo *)cur->data;
535 		procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
536 		debug_print("unflagging ham: %d\n", msginfo->msgnum);
537 	}
538 	/* unflag unsure */
539 	for (cur = new_unsure; cur; cur = cur->next) {
540 		MsgInfo *msginfo = (MsgInfo *)cur->data;
541 		procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
542 		debug_print("unflagging unsure: %d\n", msginfo->msgnum);
543 	}
544 	if (config.learn_from_whitelist && whitelisted_new_spams) {
545 		/* flag whitelisted spams */
546 		for (cur = whitelisted_new_spams; cur; cur = cur->next) {
547 			MsgInfo *msginfo = (MsgInfo *)cur->data;
548 			procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
549 			debug_print("flagging whitelisted non-ham: %d\n", msginfo->msgnum);
550 		}
551 		/* correct bogo */
552 		bogofilter_learn(NULL, whitelisted_new_spams, FALSE);
553 
554 		/* unflag them */
555 		for (cur = whitelisted_new_spams; cur; cur = cur->next) {
556 			MsgInfo *msginfo = (MsgInfo *)cur->data;
557 			procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
558 			debug_print("unflagging whitelisted non-ham: %d\n", msginfo->msgnum);
559 		}
560 	} else {
561 		for (cur = whitelisted_new_spams; cur; cur = cur->next) {
562 			MsgInfo *msginfo = (MsgInfo *)cur->data;
563 			procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
564 			debug_print("not flagging whitelisted non-ham: %d\n", msginfo->msgnum);
565 		}
566 	}
567 
568 	/* flag spams and delete them if config.receive_spam == 0
569 	 * (if config.receive_spam is set to 1, we'll move them later,
570 	 * mark as spam only if set to 2) */
571 	for (cur = new_spams; cur; cur = cur->next) {
572 		MsgInfo *msginfo = (MsgInfo *)cur->data;
573 		if (config.receive_spam != SPAM_DELETE) {
574 			if (config.mark_as_read)
575 				procmsg_msginfo_unset_flags(msginfo, ~0, 0);
576 			procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
577 		} else {
578 			folder_item_remove_msg(msginfo->folder, msginfo->msgnum);
579 		}
580 	}
581 
582 	if (status < 0 || status > 2) { /* I/O or other errors */
583 		gchar *msg = NULL;
584 
585 		if (status == 3)
586 			msg =  g_strdup_printf(_("The Bogofilter plugin couldn't filter "
587 					   "a message. The probable cause of the "
588 					   "error is that it didn't learn from any mail.\n"
589 					   "Use \"/Mark/Mark as spam\" and \"/Mark/Mark as "
590 					   "ham\" to train Bogofilter with a few hundred "
591 					   "spam and ham messages."));
592 		else
593 			msg =  g_strdup_printf(_("The Bogofilter plugin couldn't filter "
594 					   "a message. The command `%s %s %s` couldn't be run."),
595 					   bogo_args[0], bogo_args[1], bogo_args[2]);
596 		if (!prefs_common_get_prefs()->no_recv_err_panel) {
597 			if (!warned_error) {
598 				alertpanel_error("%s", msg);
599 			}
600 			warned_error = TRUE;
601 		} else {
602 			log_error(LOG_PROTOCOL, "%s\n", msg);
603 		}
604 		g_free(msg);
605 	}
606 	if (status < 0 || status > 2) {
607 		g_slist_free(mail_filtering_data->filtered);
608 		g_slist_free(mail_filtering_data->unfiltered);
609 		mail_filtering_data->filtered = NULL;
610 		mail_filtering_data->unfiltered = NULL;
611 	} else {
612 		if (config.receive_spam == SPAM_MARK_AND_SAVE && new_spams) {
613 			FolderItem *save_folder = NULL;
614 
615 			if ((!config.save_folder) ||
616 			    (config.save_folder[0] == '\0') ||
617 			    ((save_folder = folder_find_item_from_identifier(config.save_folder)) == NULL)) {
618 			 	if (mail_filtering_data->account && mail_filtering_data->account->set_trash_folder) {
619 					save_folder = folder_find_item_from_identifier(
620 						mail_filtering_data->account->trash_folder);
621 					if (save_folder)
622 						debug_print("found trash folder from account's advanced settings\n");
623 				}
624 				if (save_folder == NULL && mail_filtering_data->account &&
625 				    mail_filtering_data->account->folder) {
626 				    	save_folder = mail_filtering_data->account->folder->trash;
627 					if (save_folder)
628 						debug_print("found trash folder from account's trash\n");
629 				}
630 				if (save_folder == NULL && mail_filtering_data->account &&
631 				    !mail_filtering_data->account->folder)  {
632 					if (mail_filtering_data->account->inbox) {
633 						FolderItem *item = folder_find_item_from_identifier(
634 							mail_filtering_data->account->inbox);
635 						if (item && item->folder->trash) {
636 							save_folder = item->folder->trash;
637 							debug_print("found trash folder from account's inbox\n");
638 						}
639 					}
640 					if (!save_folder && mail_filtering_data->account->local_inbox) {
641 						FolderItem *item = folder_find_item_from_identifier(
642 							mail_filtering_data->account->local_inbox);
643 						if (item && item->folder->trash) {
644 							save_folder = item->folder->trash;
645 							debug_print("found trash folder from account's local_inbox\n");
646 						}
647 					}
648 				}
649 				if (save_folder == NULL) {
650 					debug_print("using default trash folder\n");
651 					save_folder = folder_get_default_trash();
652 				}
653 			}
654 			if (save_folder) {
655 				for (cur = new_spams; cur; cur = cur->next) {
656 					msginfo = (MsgInfo *)cur->data;
657 					msginfo->filter_op = IS_MOVE;
658 					msginfo->to_filter_folder = save_folder;
659 				}
660 			}
661 		}
662 		if (config.save_unsure && new_unsure) {
663 			FolderItem *save_unsure_folder = NULL;
664 
665 			if ((!config.save_unsure_folder) ||
666 			    (config.save_unsure_folder[0] == '\0') ||
667 			    ((save_unsure_folder = folder_find_item_from_identifier(config.save_unsure_folder)) == NULL)) {
668 			 	if (mail_filtering_data->account)
669 					save_unsure_folder = folder_find_item_from_identifier(
670 						mail_filtering_data->account->inbox);
671 				if (save_unsure_folder == NULL && mail_filtering_data->account &&
672 				    mail_filtering_data->account->folder)
673 				    	save_unsure_folder = mail_filtering_data->account->folder->inbox;
674 				if (save_unsure_folder == NULL && mail_filtering_data->account &&
675 				    !mail_filtering_data->account->folder)  {
676 					if (mail_filtering_data->account->inbox) {
677 						FolderItem *item = folder_find_item_from_identifier(
678 							mail_filtering_data->account->inbox);
679 						if (item) {
680 							save_unsure_folder = item;
681 						}
682 					}
683 					if (!save_unsure_folder && mail_filtering_data->account->local_inbox) {
684 						FolderItem *item = folder_find_item_from_identifier(
685 							mail_filtering_data->account->local_inbox);
686 						if (item) {
687 							save_unsure_folder = item;
688 						}
689 					}
690 				}
691 				if (save_unsure_folder == NULL)
692 					save_unsure_folder = folder_get_default_inbox();
693 			}
694 			if (save_unsure_folder) {
695 				for (cur = new_unsure; cur; cur = cur->next) {
696 					msginfo = (MsgInfo *)cur->data;
697 					msginfo->filter_op = IS_MOVE;
698 					msginfo->to_filter_folder = save_unsure_folder;
699 				}
700 			}
701 		}
702 	}
703 	g_slist_free(new_hams);
704 	g_slist_free(new_unsure);
705 	g_slist_free(new_spams);
706 	g_slist_free(whitelisted_new_spams);
707 
708 	if (message_callback != NULL)
709 		message_callback(NULL, 0, 0, FALSE);
710 	mail_filtering_data->filtered   = g_slist_reverse(
711 		mail_filtering_data->filtered);
712 	mail_filtering_data->unfiltered = g_slist_reverse(
713 		mail_filtering_data->unfiltered);
714 
715 	return FALSE;
716 }
717 
bogofilter_get_config(void)718 BogofilterConfig *bogofilter_get_config(void)
719 {
720 	return &config;
721 }
722 
bogofilter_learn(MsgInfo * msginfo,GSList * msglist,gboolean spam)723 int bogofilter_learn(MsgInfo *msginfo, GSList *msglist, gboolean spam)
724 {
725 	gchar *cmd = NULL;
726 	gchar *file = NULL;
727 	const gchar *bogo_exec = (config.bogopath && *config.bogopath) ? config.bogopath:"bogofilter";
728 	gint status = 0;
729 
730 	if (msginfo == NULL && msglist == NULL) {
731 		return -1;
732 	}
733 
734 	if (msginfo) {
735 		file = procmsg_get_message_file(msginfo);
736 		if (file == NULL) {
737 			return -1;
738 		} else {
739 			if (message_callback != NULL)
740 				message_callback(_("Bogofilter: learning from message..."), 0, 0, FALSE);
741 			if (spam)
742 				/* learn as spam */
743 				cmd = g_strdup_printf("%s -s -I '%s'", bogo_exec, file);
744 			else if (MSG_IS_SPAM(msginfo->flags))
745 				/* correct bogofilter, this wasn't spam */
746 				cmd = g_strdup_printf("%s -Sn -I '%s'", bogo_exec, file);
747 			else
748 				/* learn as ham */
749 				cmd = g_strdup_printf("%s -n -I '%s'", bogo_exec, file);
750 
751 			debug_print("%s\n", cmd);
752 			if ((status = execute_command_line(cmd, FALSE, NULL)) != 0)
753 				log_error(LOG_PROTOCOL, _("Learning failed; `%s` returned with status %d."),
754 						cmd, status);
755 			g_free(cmd);
756 			g_free(file);
757 			if (message_callback != NULL)
758 				message_callback(NULL, 0, 0, FALSE);
759 		}
760 	} else if (msglist) {
761 		GSList *cur = msglist;
762 		MsgInfo *info;
763 		int total = g_slist_length(msglist);
764 		int done = 0;
765 		gboolean some_correction = FALSE, some_no_correction = FALSE;
766 
767 		if (message_callback != NULL)
768 			message_callback(_("Bogofilter: learning from messages..."), total, 0, FALSE);
769 
770 		for (cur = msglist; cur && status == 0; cur = cur->next) {
771 			info = (MsgInfo *)cur->data;
772 			if (spam)
773 				some_no_correction = TRUE;
774 			else if (MSG_IS_SPAM(info->flags))
775 				/* correct bogofilter, this wasn't spam */
776 				some_correction = TRUE;
777 			else
778 				some_no_correction = TRUE;
779 
780 		}
781 
782 		if (some_correction && some_no_correction) {
783 			/* we potentially have to do different stuff for every mail */
784 			for (cur = msglist; cur && status == 0; cur = cur->next) {
785 				info = (MsgInfo *)cur->data;
786 				file = procmsg_get_message_file(info);
787 
788 				if (spam)
789 					/* learn as spam */
790 					cmd = g_strdup_printf("%s -s -I '%s'", bogo_exec, file);
791 				else if (MSG_IS_SPAM(info->flags))
792 					/* correct bogofilter, this wasn't spam */
793 					cmd = g_strdup_printf("%s -Sn -I '%s'", bogo_exec, file);
794 				else
795 					/* learn as ham */
796 					cmd = g_strdup_printf("%s -n -I '%s'", bogo_exec, file);
797 
798 				debug_print("%s\n", cmd);
799 				if ((status = execute_command_line(cmd, FALSE, NULL)) != 0)
800 					log_error(LOG_PROTOCOL, _("Learning failed; `%s` returned with status %d."),
801 							cmd, status);
802 
803 				g_free(cmd);
804 				g_free(file);
805 				done++;
806 				if (message_callback != NULL)
807 					message_callback(NULL, total, done, FALSE);
808 			}
809 		} else if (some_correction || some_no_correction) {
810 			cur = msglist;
811 
812 			gchar *bogo_args[4];
813 			GPid bogo_pid;
814 			gint bogo_stdin;
815 			GError *error = NULL;
816 			gboolean bogo_forked;
817 
818 			bogo_args[0] = (gchar *)bogo_exec;
819 			if (some_correction && !some_no_correction)
820 				bogo_args[1] = "-Sn";
821 			else if (some_no_correction && !some_correction)
822 				bogo_args[1] = spam ? "-s":"-n";
823 			bogo_args[2] = "-b";
824 			bogo_args[3] = NULL;
825 			debug_print("|%s %s %s ...\n", bogo_args[0], bogo_args[1], bogo_args[2]);
826 			bogo_forked = g_spawn_async_with_pipes(
827 					NULL, bogo_args,NULL, G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD,
828 					NULL, NULL, &bogo_pid, &bogo_stdin,
829 					NULL, NULL, &error);
830 
831 			while (bogo_forked && cur) {
832 				gchar *tmp = NULL;
833 				info = (MsgInfo *)cur->data;
834 				file = procmsg_get_message_file(info);
835 				if (file) {
836 					tmp = g_strdup_printf("%s\n",
837 						file);
838 					write_all(bogo_stdin, tmp, strlen(tmp));
839 					g_free(tmp);
840 				}
841 				g_free(file);
842 				done++;
843 				if (message_callback != NULL)
844 					message_callback(NULL, total, done, FALSE);
845 				cur = cur->next;
846 			}
847 			if (bogo_forked) {
848 				close(bogo_stdin);
849 				waitpid(bogo_pid, &status, 0);
850 				if (!WIFEXITED(status))
851 					status = -1;
852 				else
853 					status = WEXITSTATUS(status);
854 			}
855 			if (!bogo_forked || status != 0) {
856 				log_error(LOG_PROTOCOL, _("Learning failed; `%s %s %s` returned with error:\n%s"),
857 						bogo_args[0], bogo_args[1], bogo_args[2],
858 						error ? error->message:_("Unknown error"));
859 				if (error)
860 					g_error_free(error);
861 			}
862 
863 		}
864 
865 		if (message_callback != NULL)
866 			message_callback(NULL, 0, 0, FALSE);
867 	}
868 	return 0;
869 }
870 
bogofilter_save_config(void)871 void bogofilter_save_config(void)
872 {
873 	PrefFile *pfile;
874 	gchar *rcpath;
875 
876 	debug_print("Saving Bogofilter Page\n");
877 
878 	rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
879 	pfile = prefs_write_open(rcpath);
880 	g_free(rcpath);
881 	if (!pfile || (prefs_set_block_label(pfile, "Bogofilter") < 0))
882 		return;
883 
884 	if (prefs_write_param(param, pfile->fp) < 0) {
885 		g_warning("Failed to write Bogofilter configuration to file");
886 		prefs_file_close_revert(pfile);
887 		return;
888 	}
889         if (fprintf(pfile->fp, "\n") < 0) {
890 		FILE_OP_ERROR(rcpath, "fprintf");
891 		prefs_file_close_revert(pfile);
892 	} else
893 	        prefs_file_close(pfile);
894 }
895 
bogofilter_set_message_callback(MessageCallback callback)896 void bogofilter_set_message_callback(MessageCallback callback)
897 {
898 	message_callback = callback;
899 }
900 
plugin_init(gchar ** error)901 gint plugin_init(gchar **error)
902 {
903 	gchar *rcpath;
904 
905 	hook_id = HOOK_NONE;
906 
907 	if (!check_plugin_version(MAKE_NUMERIC_VERSION(2,9,2,72),
908 				VERSION_NUMERIC, PLUGIN_NAME, error))
909 		return -1;
910 
911 	prefs_set_default(param);
912 	rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
913 	prefs_read_config(param, "Bogofilter", rcpath, NULL);
914 	g_free(rcpath);
915 
916 	bogofilter_gtk_init();
917 
918 	debug_print("Bogofilter plugin loaded\n");
919 
920 #ifdef USE_PTHREAD
921 	bogofilter_start_thread();
922 #endif
923 
924 	if (config.process_emails) {
925 		bogofilter_register_hook();
926 	}
927 
928 	procmsg_register_spam_learner(bogofilter_learn);
929 	procmsg_spam_set_folder(config.save_folder, bogofilter_get_spam_folder);
930 
931 	return 0;
932 
933 }
934 
bogofilter_get_spam_folder(MsgInfo * msginfo)935 FolderItem *bogofilter_get_spam_folder(MsgInfo *msginfo)
936 {
937 	FolderItem *item = folder_find_item_from_identifier(config.save_folder);
938 
939 	if (item || msginfo == NULL || msginfo->folder == NULL)
940 		return item;
941 
942 	if (msginfo->folder->folder &&
943 	    msginfo->folder->folder->account &&
944 	    msginfo->folder->folder->account->set_trash_folder) {
945 		item = folder_find_item_from_identifier(
946 			msginfo->folder->folder->account->trash_folder);
947 	}
948 
949 	if (item == NULL &&
950 	    msginfo->folder->folder &&
951 	    msginfo->folder->folder->trash)
952 		item = msginfo->folder->folder->trash;
953 
954 	if (item == NULL)
955 		item = folder_get_default_trash();
956 
957 	debug_print("bogo spam dir: %s\n", folder_item_get_path(item));
958 	return item;
959 }
960 
plugin_done(void)961 gboolean plugin_done(void)
962 {
963 	if (hook_id != HOOK_NONE) {
964 		bogofilter_unregister_hook();
965 	}
966 #ifdef USE_PTHREAD
967 	bogofilter_stop_thread();
968 #endif
969 	g_free(config.save_folder);
970 	bogofilter_gtk_done();
971 	procmsg_unregister_spam_learner(bogofilter_learn);
972 	procmsg_spam_set_folder(NULL, NULL);
973 	debug_print("Bogofilter plugin unloaded\n");
974 	return TRUE;
975 }
976 
plugin_name(void)977 const gchar *plugin_name(void)
978 {
979 	return PLUGIN_NAME;
980 }
981 
plugin_desc(void)982 const gchar *plugin_desc(void)
983 {
984 	return _("This plugin can check all messages that are received from an "
985 	         "IMAP, LOCAL or POP account for spam using Bogofilter. "
986 		 "You will need Bogofilter installed locally.\n"
987 	         "\n"
988 		 "Before Bogofilter can recognize spam messages, you have to "
989 		 "train it by marking a few hundred spam and ham messages "
990 		 "with the use of \"/Mark/Mark as spam\" and \"/Mark/Mark as "
991 		 "ham\".\n"
992 	         "\n"
993 	         "When a message is identified as spam it can be deleted or "
994 	         "saved in a specially designated folder.\n"
995 	         "\n"
996 		 "Options can be found in /Configuration/Preferences/Plugins/Bogofilter");
997 }
998 
plugin_type(void)999 const gchar *plugin_type(void)
1000 {
1001 	return "GTK2";
1002 }
1003 
plugin_licence(void)1004 const gchar *plugin_licence(void)
1005 {
1006 	return "GPL3+";
1007 }
1008 
plugin_version(void)1009 const gchar *plugin_version(void)
1010 {
1011 	return VERSION;
1012 }
1013 
plugin_provides(void)1014 struct PluginFeature *plugin_provides(void)
1015 {
1016 	static struct PluginFeature features[] =
1017 		{ {PLUGIN_FILTERING, N_("Spam detection")},
1018 		  {PLUGIN_FILTERING, N_("Spam learning")},
1019 		  {PLUGIN_NOTHING, NULL}};
1020 	return features;
1021 }
1022 
bogofilter_register_hook(void)1023 void bogofilter_register_hook(void)
1024 {
1025 	if (hook_id == HOOK_NONE)
1026 		hook_id = hooks_register_hook(MAIL_LISTFILTERING_HOOKLIST, mail_filtering_hook, NULL);
1027 	if (hook_id == HOOK_NONE) {
1028 		g_warning("Failed to register mail filtering hook");
1029 		config.process_emails = FALSE;
1030 	}
1031 }
1032 
bogofilter_unregister_hook(void)1033 void bogofilter_unregister_hook(void)
1034 {
1035 	if (hook_id != HOOK_NONE) {
1036 		hooks_unregister_hook(MAIL_LISTFILTERING_HOOKLIST, hook_id);
1037 	}
1038 	hook_id = HOOK_NONE;
1039 }
1040