1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2009 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 <glib.h>
27 #include <glib/gi18n.h>
28 
29 #include "defs.h"
30 
31 #include <sys/types.h>
32 #if HAVE_SYS_WAIT_H
33 #include <sys/wait.h>
34 #endif
35 #ifdef USE_PTHREAD
36 #include <pthread.h>
37 #endif
38 #include <errno.h>
39 
40 #include <glib.h>
41 
42 #include "common/claws.h"
43 #include "common/version.h"
44 #include "plugin.h"
45 #include "common/utils.h"
46 #include "hooks.h"
47 #include "procmsg.h"
48 #include "folder.h"
49 #include "prefs.h"
50 #include "prefs_gtk.h"
51 #include "utils.h"
52 
53 #include "bsfilter.h"
54 #include "inc.h"
55 #include "log.h"
56 #include "prefs_common.h"
57 #include "alertpanel.h"
58 #include "addr_compl.h"
59 
60 #ifdef HAVE_SYSEXITS_H
61 #include <sysexits.h>
62 #endif
63 #ifdef HAVE_ERRNO_H
64 #include <errno.h>
65 #endif
66 #ifdef HAVE_SYS_ERRNO_H
67 #include <sys/errno.h>
68 #endif
69 #ifdef HAVE_TIME_H
70 #include <time.h>
71 #endif
72 #ifdef HAVE_SYS_TIME_H
73 #include <sys/time.h>
74 #endif
75 #ifdef HAVE_SIGNAL_H
76 #include <signal.h>
77 #endif
78 #ifdef HAVE_PWD_H
79 #include <pwd.h>
80 #endif
81 
82 #define PLUGIN_NAME (_("Bsfilter"))
83 
84 static gulong hook_id = HOOK_NONE;
85 static MessageCallback message_callback;
86 
87 static BsfilterConfig config;
88 
89 static PrefParam param[] = {
90 	{"process_emails", "TRUE", &config.process_emails, P_BOOL,
91 	 NULL, NULL, NULL},
92 	{"receive_spam", "TRUE", &config.receive_spam, P_BOOL,
93 	 NULL, NULL, NULL},
94 	{"save_folder", NULL, &config.save_folder, P_STRING,
95 	 NULL, NULL, NULL},
96 	{"max_size", "250", &config.max_size, P_INT,
97 	 NULL, NULL, NULL},
98 #ifndef G_OS_WIN32
99 	{"bspath", "bsfilter", &config.bspath, P_STRING,
100 	 NULL, NULL, NULL},
101 #else
102 	{"bspath", "bsfilterw.exe", &config.bspath, P_STRING,
103 	 NULL, NULL, NULL},
104 #endif
105 	{"whitelist_ab", "FALSE", &config.whitelist_ab, P_BOOL,
106 	 NULL, NULL, NULL},
107 	{"whitelist_ab_folder", N_("Any"), &config.whitelist_ab_folder, P_STRING,
108 	 NULL, NULL, NULL},
109 	{"learn_from_whitelist", "FALSE", &config.learn_from_whitelist, P_BOOL,
110 	 NULL, NULL, NULL},
111 	{"mark_as_read", "TRUE", &config.mark_as_read, P_BOOL,
112 	 NULL, NULL, NULL},
113 
114 	{NULL, NULL, NULL, P_OTHER, NULL, NULL, NULL}
115 };
116 
117 typedef struct _BsFilterData {
118 	MailFilteringData *mail_filtering_data;
119 	gchar **bs_args;
120 	MsgInfo *msginfo;
121 	gboolean done;
122 	int status;
123 	int whitelisted;
124 	gboolean in_thread;
125 } BsFilterData;
126 
127 static BsFilterData *to_filter_data = NULL;
128 #ifdef USE_PTHREAD
129 static gboolean filter_th_done = FALSE;
130 static pthread_mutex_t list_mutex = PTHREAD_MUTEX_INITIALIZER;
131 static pthread_mutex_t wait_mutex = PTHREAD_MUTEX_INITIALIZER;
132 static pthread_cond_t wait_cond = PTHREAD_COND_INITIALIZER;
133 #endif
134 
bsfilter_do_filter(BsFilterData * data)135 static void bsfilter_do_filter(BsFilterData *data)
136 {
137 	int status = 0;
138 	gchar *file = NULL;
139 	gboolean whitelisted = FALSE;
140 	MsgInfo *msginfo = to_filter_data->msginfo;
141 
142 	if (config.whitelist_ab) {
143 		gchar *ab_folderpath;
144 
145 		if (*config.whitelist_ab_folder == '\0' ||
146 			strcasecmp(config.whitelist_ab_folder, "Any") == 0) {
147 			/* match the whole addressbook */
148 			ab_folderpath = NULL;
149 		} else {
150 			/* match the specific book/folder of the addressbook */
151 			ab_folderpath = config.whitelist_ab_folder;
152 		}
153 
154 		start_address_completion(ab_folderpath);
155 	}
156 
157 	debug_print("Filtering message %d\n", msginfo->msgnum);
158 
159 	if (config.whitelist_ab && msginfo->from &&
160 	    found_in_addressbook(msginfo->from))
161 		whitelisted = TRUE;
162 
163 	/* can set flags (SCANNED, ATTACHMENT) but that's ok
164 	 * as GUI updates are hooked not direct */
165 
166 	file = procmsg_get_message_file(msginfo);
167 
168 	if (file) {
169 #ifndef G_OS_WIN32
170 		gchar *classify = g_strconcat((config.bspath && *config.bspath) ? config.bspath:"bsfilter",
171 			" --homedir '",get_rc_dir(),"' '", file, "'", NULL);
172 #else
173 		gchar *classify = g_strconcat((config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe",
174 			" --homedir '",get_rc_dir(),"' '", file, "'", NULL);
175 #endif
176 		status = execute_command_line(classify, FALSE,
177 				claws_get_startup_dir());
178 	}
179 
180 	if (config.whitelist_ab)
181 		end_address_completion();
182 
183 	to_filter_data->status = status;
184 	to_filter_data->whitelisted = whitelisted;
185 }
186 
187 #ifdef USE_PTHREAD
bsfilter_filtering_thread(void * data)188 static void *bsfilter_filtering_thread(void *data)
189 {
190 	while (!filter_th_done) {
191 		pthread_mutex_lock(&list_mutex);
192 		if (to_filter_data == NULL || to_filter_data->done == TRUE) {
193 			pthread_mutex_unlock(&list_mutex);
194 			debug_print("thread is waiting for something to filter\n");
195 			pthread_mutex_lock(&wait_mutex);
196 			pthread_cond_wait(&wait_cond, &wait_mutex);
197 			pthread_mutex_unlock(&wait_mutex);
198 		} else {
199 			debug_print("thread awaken with something to filter\n");
200 			to_filter_data->done = FALSE;
201 			bsfilter_do_filter(to_filter_data);
202 			pthread_mutex_unlock(&list_mutex);
203 			to_filter_data->done = TRUE;
204 			g_usleep(100);
205 		}
206 	}
207 	return NULL;
208 }
209 
210 static pthread_t filter_th;
211 static int filter_th_started = 0;
212 
bsfilter_start_thread(void)213 static void bsfilter_start_thread(void)
214 {
215 	filter_th_done = FALSE;
216 	if (filter_th_started != 0)
217 		return;
218 	if (pthread_create(&filter_th, NULL,
219 			bsfilter_filtering_thread,
220 			NULL) != 0) {
221 		filter_th_started = 0;
222 		return;
223 	}
224 	debug_print("thread created\n");
225 	filter_th_started = 1;
226 }
227 
bsfilter_stop_thread(void)228 static void bsfilter_stop_thread(void)
229 {
230 	void *res;
231 	while (pthread_mutex_trylock(&list_mutex) != 0) {
232 		GTK_EVENTS_FLUSH();
233 		g_usleep(100);
234 	}
235 	if (filter_th_started != 0) {
236 		filter_th_done = TRUE;
237 		debug_print("waking thread up\n");
238 		pthread_mutex_lock(&wait_mutex);
239 		pthread_cond_broadcast(&wait_cond);
240 		pthread_mutex_unlock(&wait_mutex);
241 		pthread_join(filter_th, &res);
242 		filter_th_started = 0;
243 	}
244 	pthread_mutex_unlock(&list_mutex);
245 	debug_print("thread done\n");
246 }
247 #endif
248 
mail_filtering_hook(gpointer source,gpointer data)249 static gboolean mail_filtering_hook(gpointer source, gpointer data)
250 {
251 	MailFilteringData *mail_filtering_data = (MailFilteringData *) source;
252 	MsgInfo *msginfo = mail_filtering_data->msginfo;
253 	static gboolean warned_error = FALSE;
254 	int status = 0, whitelisted = 0;
255 #ifndef G_OS_WIN32
256 	gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilter";
257 #else
258 	gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe";
259 #endif
260 	gboolean filtered = FALSE;
261 
262 	if (!config.process_emails) {
263 		return filtered;
264 	}
265 
266 	if (msginfo == NULL) {
267 		g_warning("wrong call to bsfilter mail_filtering_hook");
268 		return filtered;
269 	}
270 
271 	/* we have to make sure the mails are cached - or it'll break on IMAP */
272 	if (message_callback != NULL)
273 		message_callback(_("Bsfilter: fetching body..."), 0, 0, FALSE);
274 	if (msginfo) {
275 		gchar *file = procmsg_get_message_file(msginfo);
276 		g_free(file);
277 	}
278 	if (message_callback != NULL)
279 		message_callback(NULL, 0, 0, FALSE);
280 
281 	if (message_callback != NULL)
282 		message_callback(_("Bsfilter: filtering message..."), 0, 0, FALSE);
283 
284 #ifdef USE_PTHREAD
285 	while (pthread_mutex_trylock(&list_mutex) != 0) {
286 		GTK_EVENTS_FLUSH();
287 		g_usleep(100);
288 	}
289 #endif
290 
291 	to_filter_data = g_new0(BsFilterData, 1);
292 	to_filter_data->msginfo = msginfo;
293 	to_filter_data->mail_filtering_data = mail_filtering_data;
294 	to_filter_data->done = FALSE;
295 	to_filter_data->status = -1;
296 	to_filter_data->whitelisted = 0;
297 #ifdef USE_PTHREAD
298 	to_filter_data->in_thread = (filter_th_started != 0);
299 #else
300 	to_filter_data->in_thread = FALSE;
301 #endif
302 
303 #ifdef USE_PTHREAD
304 	pthread_mutex_unlock(&list_mutex);
305 
306 	if (filter_th_started != 0) {
307 		debug_print("waking thread to let it filter things\n");
308 		pthread_mutex_lock(&wait_mutex);
309 		pthread_cond_broadcast(&wait_cond);
310 		pthread_mutex_unlock(&wait_mutex);
311 
312 		while (!to_filter_data->done) {
313 			GTK_EVENTS_FLUSH();
314 			g_usleep(100);
315 		}
316 	}
317 
318 	while (pthread_mutex_trylock(&list_mutex) != 0) {
319 		GTK_EVENTS_FLUSH();
320 		g_usleep(100);
321 
322 	}
323 	if (filter_th_started == 0)
324 		bsfilter_do_filter(to_filter_data);
325 #else
326 	bsfilter_do_filter(to_filter_data);
327 #endif
328 
329 	status = to_filter_data->status;
330 	whitelisted = to_filter_data->whitelisted;
331 
332 	g_free(to_filter_data);
333 	to_filter_data = NULL;
334 #ifdef USE_PTHREAD
335 	pthread_mutex_unlock(&list_mutex);
336 #endif
337 
338 	if (status == 1) {
339 		procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
340 		debug_print("unflagging ham: %d\n", msginfo->msgnum);
341 		filtered = FALSE;
342 	} else {
343 		if (!whitelisted || (whitelisted && !config.learn_from_whitelist)) {
344 			procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
345 			debug_print("flagging spam: %d\n", msginfo->msgnum);
346 			filtered = TRUE;
347 		}
348 		if (whitelisted && config.learn_from_whitelist) {
349 			bsfilter_learn(msginfo, NULL, FALSE);
350 			procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
351 			debug_print("unflagging ham: %d\n", msginfo->msgnum);
352 			filtered = FALSE;
353 		}
354 		if (MSG_IS_SPAM(msginfo->flags) && config.receive_spam) {
355 			if (config.receive_spam && config.mark_as_read)
356 				procmsg_msginfo_unset_flags(msginfo, (MSG_NEW|MSG_UNREAD), 0);
357 			if (!config.receive_spam)
358 				folder_item_remove_msg(msginfo->folder, msginfo->msgnum);
359 			filtered = TRUE;
360 		}
361 	}
362 
363 	if (status < 0 || status > 2) { /* I/O or other errors */
364 		gchar *msg = NULL;
365 
366 		if (status == 3)
367 			msg =  g_strdup_printf(_("The Bsfilter plugin couldn't filter "
368 					   "a message. The probable cause of the "
369 					   "error is that it didn't learn from any mail.\n"
370 					   "Use \"/Mark/Mark as spam\" and \"/Mark/Mark as "
371 					   "ham\" to train Bsfilter with a few hundred "
372 					   "spam and ham messages."));
373 		else
374 			msg =  g_strdup_printf(_("The Bsfilter plugin couldn't filter "
375 					   "a message. The command `%s` couldn't be run."),
376 					   bs_exec);
377 		if (!prefs_common_get_prefs()->no_recv_err_panel) {
378 			if (!warned_error) {
379 				alertpanel_error("%s", msg);
380 			}
381 			warned_error = TRUE;
382 		} else {
383 			log_error(LOG_PROTOCOL, "%s\n", msg);
384 		}
385 		g_free(msg);
386 	}
387 
388 	if (status == 0) {
389 		if (config.receive_spam && MSG_IS_SPAM(msginfo->flags)) {
390 			FolderItem *save_folder = NULL;
391 
392 			if ((!config.save_folder) ||
393 			    (config.save_folder[0] == '\0') ||
394 			    ((save_folder = folder_find_item_from_identifier(config.save_folder)) == NULL)) {
395 			 	if (mail_filtering_data->account && mail_filtering_data->account->set_trash_folder) {
396 					save_folder = folder_find_item_from_identifier(
397 						mail_filtering_data->account->trash_folder);
398 					if (save_folder)
399 						debug_print("found trash folder from account's advanced settings\n");
400 				}
401 				if (save_folder == NULL && mail_filtering_data->account &&
402 				    mail_filtering_data->account->folder) {
403 				    	save_folder = mail_filtering_data->account->folder->trash;
404 					if (save_folder)
405 						debug_print("found trash folder from account's trash\n");
406 				}
407 				if (save_folder == NULL && mail_filtering_data->account &&
408 				    !mail_filtering_data->account->folder)  {
409 					if (mail_filtering_data->account->inbox) {
410 						FolderItem *item = folder_find_item_from_identifier(
411 							mail_filtering_data->account->inbox);
412 						if (item && item->folder->trash) {
413 							save_folder = item->folder->trash;
414 							debug_print("found trash folder from account's inbox\n");
415 						}
416 					}
417 					if (!save_folder && mail_filtering_data->account->local_inbox) {
418 						FolderItem *item = folder_find_item_from_identifier(
419 							mail_filtering_data->account->local_inbox);
420 						if (item && item->folder->trash) {
421 							save_folder = item->folder->trash;
422 							debug_print("found trash folder from account's local_inbox\n");
423 						}
424 					}
425 				}
426 				if (save_folder == NULL) {
427 					debug_print("using default trash folder\n");
428 					save_folder = folder_get_default_trash();
429 				}
430 			}
431 			if (save_folder) {
432 				msginfo->filter_op = IS_MOVE;
433 				msginfo->to_filter_folder = save_folder;
434 			}
435 		}
436 	}
437 	if (message_callback != NULL)
438 		message_callback(NULL, 0, 0, FALSE);
439 
440 	return filtered;
441 }
442 
bsfilter_get_config(void)443 BsfilterConfig *bsfilter_get_config(void)
444 {
445 	return &config;
446 }
447 
bsfilter_learn(MsgInfo * msginfo,GSList * msglist,gboolean spam)448 int bsfilter_learn(MsgInfo *msginfo, GSList *msglist, gboolean spam)
449 {
450 	gchar *cmd = NULL;
451 	gchar *file = NULL;
452 #ifndef G_OS_WIN32
453 	gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilter";
454 #else
455 	gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe";
456 #endif
457 	gint status = 0;
458 	gboolean free_list = FALSE;
459 	GSList *cur = NULL;
460 
461 	if (msginfo == NULL && msglist == NULL) {
462 		return -1;
463 	}
464 	if (msginfo != NULL && msglist == NULL) {
465 		msglist = g_slist_append(NULL, msginfo);
466 		free_list = TRUE;
467 	}
468 	for (cur = msglist; cur; cur = cur->next) {
469 		msginfo = (MsgInfo *)cur->data;
470 		file = procmsg_get_message_file(msginfo);
471 		if (file == NULL) {
472 			return -1;
473 		} else {
474 			if (message_callback != NULL)
475 				message_callback(_("Bsfilter: learning from message..."), 0, 0, FALSE);
476 			if (spam)
477 				/* learn as spam */
478 				cmd = g_strdup_printf("%s --homedir '%s' -su '%s'", bs_exec, get_rc_dir(), file);
479 			else
480 				/* learn as ham */
481 				cmd = g_strdup_printf("%s --homedir '%s' -cu '%s'", bs_exec, get_rc_dir(), file);
482 
483 			debug_print("%s\n", cmd);
484 			if ((status = execute_command_line(cmd, FALSE,
485 							claws_get_startup_dir())) != 0)
486 				log_error(LOG_PROTOCOL, _("Learning failed; `%s` returned with status %d."),
487 						cmd, status);
488 			g_free(cmd);
489 			g_free(file);
490 			if (message_callback != NULL)
491 				message_callback(NULL, 0, 0, FALSE);
492 		}
493 	}
494 	if (free_list)
495 		g_slist_free(msglist);
496 
497 	return 0;
498 }
499 
bsfilter_save_config(void)500 void bsfilter_save_config(void)
501 {
502 	PrefFile *pfile;
503 	gchar *rcpath;
504 
505 	debug_print("Saving Bsfilter Page\n");
506 
507 	rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
508 	pfile = prefs_write_open(rcpath);
509 	g_free(rcpath);
510 	if (!pfile || (prefs_set_block_label(pfile, "Bsfilter") < 0))
511 		return;
512 
513 	if (prefs_write_param(param, pfile->fp) < 0) {
514 		g_warning("Failed to write Bsfilter configuration to file");
515 		prefs_file_close_revert(pfile);
516 		return;
517 	}
518         if (fprintf(pfile->fp, "\n") < 0) {
519 		FILE_OP_ERROR(rcpath, "fprintf");
520 		prefs_file_close_revert(pfile);
521 	} else
522 	        prefs_file_close(pfile);
523 }
524 
bsfilter_set_message_callback(MessageCallback callback)525 void bsfilter_set_message_callback(MessageCallback callback)
526 {
527 	message_callback = callback;
528 }
529 
plugin_init(gchar ** error)530 gint plugin_init(gchar **error)
531 {
532 	gchar *rcpath;
533 	hook_id = HOOK_NONE;
534 
535 	if (!check_plugin_version(MAKE_NUMERIC_VERSION(2,9,2,72),
536 				VERSION_NUMERIC, PLUGIN_NAME, error))
537 		return -1;
538 
539 	prefs_set_default(param);
540 	rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
541 	prefs_read_config(param, "Bsfilter", rcpath, NULL);
542 	g_free(rcpath);
543 
544 	bsfilter_gtk_init();
545 
546 	debug_print("Bsfilter plugin loaded\n");
547 
548 #ifdef USE_PTHREAD
549 	bsfilter_start_thread();
550 #endif
551 
552 	if (config.process_emails) {
553 		bsfilter_register_hook();
554 	}
555 
556 	procmsg_register_spam_learner(bsfilter_learn);
557 	procmsg_spam_set_folder(config.save_folder, bsfilter_get_spam_folder);
558 
559 	return 0;
560 
561 }
562 
bsfilter_get_spam_folder(MsgInfo * msginfo)563 FolderItem *bsfilter_get_spam_folder(MsgInfo *msginfo)
564 {
565 	FolderItem *item = NULL;
566 
567 	if (config.save_folder != NULL) {
568 		item = folder_find_item_from_identifier(config.save_folder);
569 	}
570 
571 	if (item || msginfo == NULL || msginfo->folder == NULL)
572 		return item;
573 
574 	if (msginfo->folder->folder &&
575 	    msginfo->folder->folder->account &&
576 	    msginfo->folder->folder->account->set_trash_folder) {
577 		item = folder_find_item_from_identifier(
578 			msginfo->folder->folder->account->trash_folder);
579 	}
580 
581 	if (item == NULL &&
582 	    msginfo->folder->folder &&
583 	    msginfo->folder->folder->trash)
584 		item = msginfo->folder->folder->trash;
585 
586 	if (item == NULL)
587 		item = folder_get_default_trash();
588 
589 	debug_print("bs spam dir: %s\n", folder_item_get_path(item));
590 	return item;
591 }
592 
plugin_done(void)593 gboolean plugin_done(void)
594 {
595 	if (hook_id != HOOK_NONE) {
596 		bsfilter_unregister_hook();
597 	}
598 #ifdef USE_PTHREAD
599 	bsfilter_stop_thread();
600 #endif
601 	g_free(config.save_folder);
602 	bsfilter_gtk_done();
603 	procmsg_unregister_spam_learner(bsfilter_learn);
604 	procmsg_spam_set_folder(NULL, NULL);
605 	debug_print("Bsfilter plugin unloaded\n");
606 	return TRUE;
607 }
608 
plugin_name(void)609 const gchar *plugin_name(void)
610 {
611 	return PLUGIN_NAME;
612 }
613 
plugin_desc(void)614 const gchar *plugin_desc(void)
615 {
616 	return _("This plugin can check all messages that are received from an "
617 	         "IMAP, LOCAL or POP account for spam using Bsfilter. "
618 		 "You will need Bsfilter installed locally.\n"
619 	         "\n"
620 		 "Before Bsfilter can recognize spam messages, you have to "
621 		 "train it by marking a few hundred spam and ham messages "
622 		 "with the use of \"/Mark/Mark as spam\" and \"/Mark/Mark as "
623 		 "ham\".\n"
624 	         "\n"
625 	         "When a message is identified as spam it can be deleted or "
626 	         "saved in a specially designated folder.\n"
627 	         "\n"
628 		 "Options can be found in /Configuration/Preferences/Plugins/Bsfilter");
629 }
630 
plugin_type(void)631 const gchar *plugin_type(void)
632 {
633 	return "GTK2";
634 }
635 
plugin_licence(void)636 const gchar *plugin_licence(void)
637 {
638 	return "GPL3+";
639 }
640 
plugin_version(void)641 const gchar *plugin_version(void)
642 {
643 	return VERSION;
644 }
645 
plugin_provides(void)646 struct PluginFeature *plugin_provides(void)
647 {
648 	static struct PluginFeature features[] =
649 		{ {PLUGIN_FILTERING, N_("Spam detection")},
650 		  {PLUGIN_FILTERING, N_("Spam learning")},
651 		  {PLUGIN_NOTHING, NULL}};
652 	return features;
653 }
654 
bsfilter_register_hook(void)655 void bsfilter_register_hook(void)
656 {
657 	if (hook_id == HOOK_NONE)
658 		hook_id = hooks_register_hook(MAIL_FILTERING_HOOKLIST, mail_filtering_hook, NULL);
659 	if (hook_id == HOOK_NONE) {
660 		g_warning("Failed to register mail filtering hook");
661 		config.process_emails = FALSE;
662 	}
663 }
664 
bsfilter_unregister_hook(void)665 void bsfilter_unregister_hook(void)
666 {
667 	if (hook_id != HOOK_NONE) {
668 		hooks_unregister_hook(MAIL_FILTERING_HOOKLIST, hook_id);
669 	}
670 	hook_id = HOOK_NONE;
671 }
672