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