2 * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2009 Colin Leroy <colin@colino.net> and
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.
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.
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/>.
23 #include "claws-features.h"
27 #include <glib/gi18n.h>
31 #include <sys/types.h>
42 #include "common/claws.h"
43 #include "common/version.h"
45 #include "common/utils.h"
50 #include "prefs_gtk.h"
56 #include "prefs_common.h"
57 #include "alertpanel.h"
58 #include "addr_compl.h"
60 #ifdef HAVE_SYSEXITS_H
66 #ifdef HAVE_SYS_ERRNO_H
67 #include <sys/errno.h>
72 #ifdef HAVE_SYS_TIME_H
82 #define PLUGIN_NAME (_("Bsfilter"))
84 static guint hook_id = -1;
85 static MessageCallback message_callback;
87 static BsfilterConfig config;
89 static PrefParam param[] = {
90 {"process_emails", "TRUE", &config.process_emails, P_BOOL,
92 {"receive_spam", "TRUE", &config.receive_spam, P_BOOL,
94 {"save_folder", NULL, &config.save_folder, P_STRING,
96 {"max_size", "250", &config.max_size, P_INT,
99 {"bspath", "bsfilter", &config.bspath, P_STRING,
102 {"bspath", "bsfilterw.exe", &config.bspath, P_STRING,
105 {"whitelist_ab", "FALSE", &config.whitelist_ab, P_BOOL,
107 {"whitelist_ab_folder", N_("Any"), &config.whitelist_ab_folder, P_STRING,
109 {"learn_from_whitelist", "FALSE", &config.learn_from_whitelist, P_BOOL,
111 {"mark_as_read", "TRUE", &config.mark_as_read, P_BOOL,
114 {NULL, NULL, NULL, P_OTHER, NULL, NULL, NULL}
117 typedef struct _BsFilterData {
118 MailFilteringData *mail_filtering_data;
127 static BsFilterData *to_filter_data = NULL;
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;
135 static gboolean found_in_addressbook(const gchar *address)
138 gboolean found = FALSE;
144 addr = g_strdup(address);
145 extract_address(addr);
146 num_addr = complete_address(addr);
148 /* skip first item (this is the search string itself) */
150 for (; i < num_addr && !found; i++) {
151 gchar *caddr = get_complete_address(i);
152 extract_address(caddr);
153 if (strcasecmp(caddr, addr) == 0)
162 static void bsfilter_do_filter(BsFilterData *data)
166 gboolean whitelisted = FALSE;
167 MsgInfo *msginfo = to_filter_data->msginfo;
169 if (config.whitelist_ab) {
170 gchar *ab_folderpath;
172 if (*config.whitelist_ab_folder == '\0' ||
173 strcasecmp(config.whitelist_ab_folder, "Any") == 0) {
174 /* match the whole addressbook */
175 ab_folderpath = NULL;
177 /* match the specific book/folder of the addressbook */
178 ab_folderpath = config.whitelist_ab_folder;
181 start_address_completion(ab_folderpath);
184 debug_print("Filtering message %d\n", msginfo->msgnum);
186 if (config.whitelist_ab && msginfo->from &&
187 found_in_addressbook(msginfo->from))
190 /* can set flags (SCANNED, ATTACHMENT) but that's ok
191 * as GUI updates are hooked not direct */
193 file = procmsg_get_message_file(msginfo);
197 gchar *classify = g_strconcat((config.bspath && *config.bspath) ? config.bspath:"bsfilter",
198 " --homedir '",get_rc_dir(),"' '", file, "'", NULL);
200 gchar *classify = g_strconcat((config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe",
201 " --homedir '",get_rc_dir(),"' '", file, "'", NULL);
203 status = execute_command_line(classify, FALSE);
206 if (config.whitelist_ab)
207 end_address_completion();
209 to_filter_data->status = status;
210 to_filter_data->whitelisted = whitelisted;
214 static void *bsfilter_filtering_thread(void *data)
216 while (!filter_th_done) {
217 pthread_mutex_lock(&list_mutex);
218 if (to_filter_data == NULL || to_filter_data->done == TRUE) {
219 pthread_mutex_unlock(&list_mutex);
220 debug_print("thread is waiting for something to filter\n");
221 pthread_mutex_lock(&wait_mutex);
222 pthread_cond_wait(&wait_cond, &wait_mutex);
223 pthread_mutex_unlock(&wait_mutex);
225 debug_print("thread awaken with something to filter\n");
226 to_filter_data->done = FALSE;
227 bsfilter_do_filter(to_filter_data);
228 pthread_mutex_unlock(&list_mutex);
229 to_filter_data->done = TRUE;
236 static pthread_t filter_th;
237 static int filter_th_started = 0;
239 static void bsfilter_start_thread(void)
241 filter_th_done = FALSE;
242 if (filter_th_started != 0)
244 if (pthread_create(&filter_th, 0,
245 bsfilter_filtering_thread,
247 filter_th_started = 0;
250 debug_print("thread created\n");
251 filter_th_started = 1;
254 static void bsfilter_stop_thread(void)
257 while (pthread_mutex_trylock(&list_mutex) != 0) {
261 if (filter_th_started != 0) {
262 filter_th_done = TRUE;
263 debug_print("waking thread up\n");
264 pthread_mutex_lock(&wait_mutex);
265 pthread_cond_broadcast(&wait_cond);
266 pthread_mutex_unlock(&wait_mutex);
267 pthread_join(filter_th, &res);
268 filter_th_started = 0;
270 pthread_mutex_unlock(&list_mutex);
271 debug_print("thread done\n");
275 static gboolean mail_filtering_hook(gpointer source, gpointer data)
277 MailFilteringData *mail_filtering_data = (MailFilteringData *) source;
278 MsgInfo *msginfo = mail_filtering_data->msginfo;
279 static gboolean warned_error = FALSE;
280 int status = 0, whitelisted = 0;
282 gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilter";
284 gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe";
286 gboolean filtered = FALSE;
288 if (!config.process_emails) {
292 if (msginfo == NULL) {
293 g_warning("wrong call to bsfilter mail_filtering_hook");
297 /* we have to make sure the mails are cached - or it'll break on IMAP */
298 if (message_callback != NULL)
299 message_callback(_("Bsfilter: fetching body..."), 0, 0, FALSE);
301 gchar *file = procmsg_get_message_file(msginfo);
304 if (message_callback != NULL)
305 message_callback(NULL, 0, 0, FALSE);
307 if (message_callback != NULL)
308 message_callback(_("Bsfilter: filtering message..."), 0, 0, FALSE);
311 while (pthread_mutex_trylock(&list_mutex) != 0) {
317 to_filter_data = g_new0(BsFilterData, 1);
318 to_filter_data->msginfo = msginfo;
319 to_filter_data->mail_filtering_data = mail_filtering_data;
320 to_filter_data->done = FALSE;
321 to_filter_data->status = -1;
322 to_filter_data->whitelisted = 0;
324 to_filter_data->in_thread = (filter_th_started != 0);
326 to_filter_data->in_thread = FALSE;
330 pthread_mutex_unlock(&list_mutex);
332 if (filter_th_started != 0) {
333 debug_print("waking thread to let it filter things\n");
334 pthread_mutex_lock(&wait_mutex);
335 pthread_cond_broadcast(&wait_cond);
336 pthread_mutex_unlock(&wait_mutex);
338 while (!to_filter_data->done) {
344 while (pthread_mutex_trylock(&list_mutex) != 0) {
349 if (filter_th_started == 0)
350 bsfilter_do_filter(to_filter_data);
352 bsfilter_do_filter(to_filter_data);
355 status = to_filter_data->status;
356 whitelisted = to_filter_data->whitelisted;
358 g_free(to_filter_data);
359 to_filter_data = NULL;
361 pthread_mutex_unlock(&list_mutex);
365 procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
366 debug_print("unflagging ham: %d\n", msginfo->msgnum);
369 if (!whitelisted || (whitelisted && !config.learn_from_whitelist)) {
370 procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
371 debug_print("flagging spam: %d\n", msginfo->msgnum);
374 if (whitelisted && config.learn_from_whitelist) {
375 bsfilter_learn(msginfo, NULL, FALSE);
376 procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
377 debug_print("unflagging ham: %d\n", msginfo->msgnum);
380 if (MSG_IS_SPAM(msginfo->flags) && config.receive_spam) {
381 if (config.receive_spam && config.mark_as_read)
382 procmsg_msginfo_unset_flags(msginfo, (MSG_NEW|MSG_UNREAD), 0);
383 if (!config.receive_spam)
384 folder_item_remove_msg(msginfo->folder, msginfo->msgnum);
389 if (status < 0 || status > 2) { /* I/O or other errors */
393 msg = g_strdup_printf(_("The Bsfilter plugin couldn't filter "
394 "a message. The probable cause of the "
395 "error is that it didn't learn from any mail.\n"
396 "Use \"/Mark/Mark as spam\" and \"/Mark/Mark as "
397 "ham\" to train Bsfilter with a few hundred "
398 "spam and ham messages."));
400 msg = g_strdup_printf(_("The Bsfilter plugin couldn't filter "
401 "a message. The command `%s` couldn't be run."),
403 if (!prefs_common.no_recv_err_panel) {
405 alertpanel_error("%s", msg);
409 log_error(LOG_PROTOCOL, "%s\n", msg);
415 if (config.receive_spam && MSG_IS_SPAM(msginfo->flags)) {
416 FolderItem *save_folder = NULL;
418 if ((!config.save_folder) ||
419 (config.save_folder[0] == '\0') ||
420 ((save_folder = folder_find_item_from_identifier(config.save_folder)) == NULL)) {
421 if (mail_filtering_data->account && mail_filtering_data->account->set_trash_folder) {
422 save_folder = folder_find_item_from_identifier(
423 mail_filtering_data->account->trash_folder);
425 debug_print("found trash folder from account's advanced settings\n");
427 if (save_folder == NULL && mail_filtering_data->account &&
428 mail_filtering_data->account->folder) {
429 save_folder = mail_filtering_data->account->folder->trash;
431 debug_print("found trash folder from account's trash\n");
433 if (save_folder == NULL && mail_filtering_data->account &&
434 !mail_filtering_data->account->folder) {
435 if (mail_filtering_data->account->inbox) {
436 FolderItem *item = folder_find_item_from_identifier(
437 mail_filtering_data->account->inbox);
438 if (item && item->folder->trash) {
439 save_folder = item->folder->trash;
440 debug_print("found trash folder from account's inbox\n");
443 if (!save_folder && mail_filtering_data->account->local_inbox) {
444 FolderItem *item = folder_find_item_from_identifier(
445 mail_filtering_data->account->local_inbox);
446 if (item && item->folder->trash) {
447 save_folder = item->folder->trash;
448 debug_print("found trash folder from account's local_inbox\n");
452 if (save_folder == NULL) {
453 debug_print("using default trash folder\n");
454 save_folder = folder_get_default_trash();
458 msginfo->filter_op = IS_MOVE;
459 msginfo->to_filter_folder = save_folder;
463 if (message_callback != NULL)
464 message_callback(NULL, 0, 0, FALSE);
469 BsfilterConfig *bsfilter_get_config(void)
474 int bsfilter_learn(MsgInfo *msginfo, GSList *msglist, gboolean spam)
479 gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilter";
481 gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe";
484 gboolean free_list = FALSE;
487 if (msginfo == NULL && msglist == NULL) {
490 if (msginfo != NULL && msglist == NULL) {
491 msglist = g_slist_append(NULL, msginfo);
494 for (cur = msglist; cur; cur = cur->next) {
495 msginfo = (MsgInfo *)cur->data;
496 file = procmsg_get_message_file(msginfo);
500 if (message_callback != NULL)
501 message_callback(_("Bsfilter: learning from message..."), 0, 0, FALSE);
504 cmd = g_strdup_printf("%s --homedir '%s' -su '%s'", bs_exec, get_rc_dir(), file);
507 cmd = g_strdup_printf("%s --homedir '%s' -cu '%s'", bs_exec, get_rc_dir(), file);
509 debug_print("%s\n", cmd);
510 if ((status = execute_command_line(cmd, FALSE)) != 0)
511 log_error(LOG_PROTOCOL, _("Learning failed; `%s` returned with status %d."),
515 if (message_callback != NULL)
516 message_callback(NULL, 0, 0, FALSE);
520 g_slist_free(msglist);
525 void bsfilter_save_config(void)
530 debug_print("Saving Bsfilter Page\n");
532 rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
533 pfile = prefs_write_open(rcpath);
535 if (!pfile || (prefs_set_block_label(pfile, "Bsfilter") < 0))
538 if (prefs_write_param(param, pfile->fp) < 0) {
539 g_warning("Failed to write Bsfilter configuration to file\n");
540 prefs_file_close_revert(pfile);
543 if (fprintf(pfile->fp, "\n") < 0) {
544 FILE_OP_ERROR(rcpath, "fprintf");
545 prefs_file_close_revert(pfile);
547 prefs_file_close(pfile);
550 void bsfilter_set_message_callback(MessageCallback callback)
552 message_callback = callback;
555 gint plugin_init(gchar **error)
560 if (!check_plugin_version(MAKE_NUMERIC_VERSION(2,9,2,72),
561 VERSION_NUMERIC, PLUGIN_NAME, error))
564 prefs_set_default(param);
565 rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
566 prefs_read_config(param, "Bsfilter", rcpath, NULL);
571 debug_print("Bsfilter plugin loaded\n");
574 bsfilter_start_thread();
577 if (config.process_emails) {
578 bsfilter_register_hook();
581 procmsg_register_spam_learner(bsfilter_learn);
582 procmsg_spam_set_folder(config.save_folder, bsfilter_get_spam_folder);
588 FolderItem *bsfilter_get_spam_folder(MsgInfo *msginfo)
590 FolderItem *item = NULL;
592 if (config.save_folder != NULL) {
593 item = folder_find_item_from_identifier(config.save_folder);
596 if (item || msginfo == NULL || msginfo->folder == NULL)
599 if (msginfo->folder->folder &&
600 msginfo->folder->folder->account &&
601 msginfo->folder->folder->account->set_trash_folder) {
602 item = folder_find_item_from_identifier(
603 msginfo->folder->folder->account->trash_folder);
607 msginfo->folder->folder &&
608 msginfo->folder->folder->trash)
609 item = msginfo->folder->folder->trash;
612 item = folder_get_default_trash();
614 debug_print("bs spam dir: %s\n", folder_item_get_path(item));
618 gboolean plugin_done(void)
621 bsfilter_unregister_hook();
624 bsfilter_stop_thread();
626 g_free(config.save_folder);
628 procmsg_unregister_spam_learner(bsfilter_learn);
629 procmsg_spam_set_folder(NULL, NULL);
630 debug_print("Bsfilter plugin unloaded\n");
634 const gchar *plugin_name(void)
639 const gchar *plugin_desc(void)
641 return _("This plugin can check all messages that are received from an "
642 "IMAP, LOCAL or POP account for spam using Bsfilter. "
643 "You will need Bsfilter installed locally.\n"
645 "Before Bsfilter can recognize spam messages, you have to "
646 "train it by marking a few hundred spam and ham messages "
647 "with the use of \"/Mark/Mark as spam\" and \"/Mark/Mark as "
650 "When a message is identified as spam it can be deleted or "
651 "saved in a specially designated folder.\n"
653 "Options can be found in /Configuration/Preferences/Plugins/Bsfilter");
656 const gchar *plugin_type(void)
661 const gchar *plugin_licence(void)
666 const gchar *plugin_version(void)
671 struct PluginFeature *plugin_provides(void)
673 static struct PluginFeature features[] =
674 { {PLUGIN_FILTERING, N_("Spam detection")},
675 {PLUGIN_FILTERING, N_("Spam learning")},
676 {PLUGIN_NOTHING, NULL}};
680 void bsfilter_register_hook(void)
683 hook_id = hooks_register_hook(MAIL_FILTERING_HOOKLIST, mail_filtering_hook, NULL);
685 g_warning("Failed to register mail filtering hook");
686 config.process_emails = FALSE;
690 void bsfilter_unregister_hook(void)
693 hooks_unregister_hook(MAIL_FILTERING_HOOKLIST, hook_id);