Improve some sieve-related translations
[claws.git] / src / plugins / bsfilter / bsfilter.c
blobb4d59cb1114d9e2e4c3095ba15cd0e04c63d188b
1 /*
2 * Claws Mail -- a GTK based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2021 the Claws Mail team and
4 * Colin Leroy <colin@colino.net>
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/>.
21 #ifdef HAVE_CONFIG_H
22 # include "config.h"
23 #include "claws-features.h"
24 #endif
26 #include <glib.h>
27 #include <glib/gi18n.h>
29 #include "defs.h"
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
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 #include "utils.h"
50 #include "bsfilter.h"
51 #include "inc.h"
52 #include "log.h"
53 #include "prefs_common.h"
54 #include "alertpanel.h"
55 #include "addr_compl.h"
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
79 #define PLUGIN_NAME (_("Bsfilter"))
81 static gulong hook_id = HOOK_NONE;
82 static MessageCallback message_callback;
84 static BsfilterConfig config;
86 static PrefParam param[] = {
87 {"process_emails", "TRUE", &config.process_emails, P_BOOL,
88 NULL, NULL, NULL},
89 {"receive_spam", "TRUE", &config.receive_spam, P_BOOL,
90 NULL, NULL, NULL},
91 {"save_folder", NULL, &config.save_folder, P_STRING,
92 NULL, NULL, NULL},
93 {"max_size", "250", &config.max_size, P_INT,
94 NULL, NULL, NULL},
95 #ifndef G_OS_WIN32
96 {"bspath", "bsfilter", &config.bspath, P_STRING,
97 NULL, NULL, NULL},
98 #else
99 {"bspath", "bsfilterw.exe", &config.bspath, P_STRING,
100 NULL, NULL, NULL},
101 #endif
102 {"whitelist_ab", "FALSE", &config.whitelist_ab, P_BOOL,
103 NULL, NULL, NULL},
104 {"whitelist_ab_folder", N_("Any"), &config.whitelist_ab_folder, P_STRING,
105 NULL, NULL, NULL},
106 {"learn_from_whitelist", "FALSE", &config.learn_from_whitelist, P_BOOL,
107 NULL, NULL, NULL},
108 {"mark_as_read", "TRUE", &config.mark_as_read, P_BOOL,
109 NULL, NULL, NULL},
111 {NULL, NULL, NULL, P_OTHER, NULL, NULL, NULL}
114 typedef struct _BsFilterData {
115 MailFilteringData *mail_filtering_data;
116 gchar **bs_args;
117 MsgInfo *msginfo;
118 gboolean done;
119 int status;
120 int whitelisted;
121 gboolean in_thread;
122 } BsFilterData;
124 static BsFilterData *to_filter_data = NULL;
125 #ifdef USE_PTHREAD
126 static gboolean filter_th_done = FALSE;
127 static pthread_mutex_t list_mutex = PTHREAD_MUTEX_INITIALIZER;
128 static pthread_mutex_t wait_mutex = PTHREAD_MUTEX_INITIALIZER;
129 static pthread_cond_t wait_cond = PTHREAD_COND_INITIALIZER;
130 #endif
132 static void bsfilter_do_filter(BsFilterData *data)
134 int status = 0;
135 gchar *file = NULL;
136 gboolean whitelisted = FALSE;
137 MsgInfo *msginfo = to_filter_data->msginfo;
139 if (config.whitelist_ab) {
140 gchar *ab_folderpath;
142 if (*config.whitelist_ab_folder == '\0' ||
143 strcasecmp(config.whitelist_ab_folder, "Any") == 0) {
144 /* match the whole addressbook */
145 ab_folderpath = NULL;
146 } else {
147 /* match the specific book/folder of the addressbook */
148 ab_folderpath = config.whitelist_ab_folder;
151 start_address_completion(ab_folderpath);
154 debug_print("Filtering message %d\n", msginfo->msgnum);
156 if (config.whitelist_ab && msginfo->from &&
157 found_in_addressbook(msginfo->from))
158 whitelisted = TRUE;
160 /* can set flags (SCANNED, ATTACHMENT) but that's ok
161 * as GUI updates are hooked not direct */
163 file = procmsg_get_message_file(msginfo);
165 if (file) {
166 #ifndef G_OS_WIN32
167 gchar *classify = g_strconcat((config.bspath && *config.bspath) ? config.bspath:"bsfilter",
168 " --homedir '",get_rc_dir(),"' '", file, "'", NULL);
169 #else
170 gchar *classify = g_strconcat((config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe",
171 " --homedir '",get_rc_dir(),"' '", file, "'", NULL);
172 #endif
173 status = execute_command_line(classify, FALSE,
174 claws_get_startup_dir());
175 g_free(classify);
178 if (config.whitelist_ab)
179 end_address_completion();
181 to_filter_data->status = status;
182 to_filter_data->whitelisted = whitelisted;
185 #ifdef USE_PTHREAD
186 static void *bsfilter_filtering_thread(void *data)
188 while (!filter_th_done) {
189 pthread_mutex_lock(&list_mutex);
190 if (to_filter_data == NULL || to_filter_data->done == TRUE) {
191 pthread_mutex_unlock(&list_mutex);
192 debug_print("thread is waiting for something to filter\n");
193 pthread_mutex_lock(&wait_mutex);
194 pthread_cond_wait(&wait_cond, &wait_mutex);
195 pthread_mutex_unlock(&wait_mutex);
196 } else {
197 debug_print("thread awaken with something to filter\n");
198 to_filter_data->done = FALSE;
199 bsfilter_do_filter(to_filter_data);
200 pthread_mutex_unlock(&list_mutex);
201 to_filter_data->done = TRUE;
202 g_usleep(100);
205 return NULL;
208 static pthread_t filter_th;
209 static int filter_th_started = 0;
211 static void bsfilter_start_thread(void)
213 filter_th_done = FALSE;
214 if (filter_th_started != 0)
215 return;
216 if (pthread_create(&filter_th, NULL,
217 bsfilter_filtering_thread,
218 NULL) != 0) {
219 filter_th_started = 0;
220 return;
222 debug_print("thread created\n");
223 filter_th_started = 1;
226 static void bsfilter_stop_thread(void)
228 void *res;
229 while (pthread_mutex_trylock(&list_mutex) != 0) {
230 GTK_EVENTS_FLUSH();
231 g_usleep(100);
233 if (filter_th_started != 0) {
234 filter_th_done = TRUE;
235 debug_print("waking thread up\n");
236 pthread_mutex_lock(&wait_mutex);
237 pthread_cond_broadcast(&wait_cond);
238 pthread_mutex_unlock(&wait_mutex);
239 pthread_join(filter_th, &res);
240 filter_th_started = 0;
242 pthread_mutex_unlock(&list_mutex);
243 debug_print("thread done\n");
245 #endif
247 static gboolean mail_filtering_hook(gpointer source, gpointer data)
249 MailFilteringData *mail_filtering_data = (MailFilteringData *) source;
250 MsgInfo *msginfo = mail_filtering_data->msginfo;
251 static gboolean warned_error = FALSE;
252 int status = 0, whitelisted = 0;
253 #ifndef G_OS_WIN32
254 gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilter";
255 #else
256 gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe";
257 #endif
258 gboolean filtered = FALSE;
260 if (!config.process_emails) {
261 return filtered;
264 if (msginfo == NULL) {
265 g_warning("wrong call to bsfilter mail_filtering_hook");
266 return filtered;
269 /* we have to make sure the mails are cached - or it'll break on IMAP */
270 if (message_callback != NULL)
271 message_callback(_("Bsfilter: fetching body..."), 0, 0, FALSE);
272 if (msginfo) {
273 gchar *file = procmsg_get_message_file(msginfo);
274 g_free(file);
276 if (message_callback != NULL)
277 message_callback(NULL, 0, 0, FALSE);
279 if (message_callback != NULL)
280 message_callback(_("Bsfilter: filtering message..."), 0, 0, FALSE);
282 #ifdef USE_PTHREAD
283 while (pthread_mutex_trylock(&list_mutex) != 0) {
284 GTK_EVENTS_FLUSH();
285 g_usleep(100);
287 #endif
289 to_filter_data = g_new0(BsFilterData, 1);
290 to_filter_data->msginfo = msginfo;
291 to_filter_data->mail_filtering_data = mail_filtering_data;
292 to_filter_data->done = FALSE;
293 to_filter_data->status = -1;
294 to_filter_data->whitelisted = 0;
295 #ifdef USE_PTHREAD
296 to_filter_data->in_thread = (filter_th_started != 0);
297 #else
298 to_filter_data->in_thread = FALSE;
299 #endif
301 #ifdef USE_PTHREAD
302 pthread_mutex_unlock(&list_mutex);
304 if (filter_th_started != 0) {
305 debug_print("waking thread to let it filter things\n");
306 pthread_mutex_lock(&wait_mutex);
307 pthread_cond_broadcast(&wait_cond);
308 pthread_mutex_unlock(&wait_mutex);
310 while (!to_filter_data->done) {
311 GTK_EVENTS_FLUSH();
312 g_usleep(100);
316 while (pthread_mutex_trylock(&list_mutex) != 0) {
317 GTK_EVENTS_FLUSH();
318 g_usleep(100);
321 if (filter_th_started == 0)
322 bsfilter_do_filter(to_filter_data);
323 #else
324 bsfilter_do_filter(to_filter_data);
325 #endif
327 status = to_filter_data->status;
328 whitelisted = to_filter_data->whitelisted;
330 g_free(to_filter_data);
331 to_filter_data = NULL;
332 #ifdef USE_PTHREAD
333 pthread_mutex_unlock(&list_mutex);
334 #endif
336 if (status == 1) {
337 procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
338 debug_print("unflagging ham: %d\n", msginfo->msgnum);
339 filtered = FALSE;
340 } else {
341 if (!whitelisted || (whitelisted && !config.learn_from_whitelist)) {
342 procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
343 debug_print("flagging spam: %d\n", msginfo->msgnum);
344 filtered = TRUE;
346 if (whitelisted && config.learn_from_whitelist) {
347 bsfilter_learn(msginfo, NULL, FALSE);
348 procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
349 debug_print("unflagging ham: %d\n", msginfo->msgnum);
350 filtered = FALSE;
352 if (MSG_IS_SPAM(msginfo->flags) && config.receive_spam) {
353 if (config.receive_spam && config.mark_as_read)
354 procmsg_msginfo_unset_flags(msginfo, (MSG_NEW|MSG_UNREAD), 0);
355 if (!config.receive_spam)
356 folder_item_remove_msg(msginfo->folder, msginfo->msgnum);
357 filtered = TRUE;
361 if (status < 0 || status > 2) { /* I/O or other errors */
362 gchar *msg = NULL;
364 if (status == 3)
365 msg = g_strdup_printf(_("The Bsfilter plugin couldn't filter "
366 "a message. The probable cause of the "
367 "error is that it didn't learn from any mail.\n"
368 "Use \"/Mark/Mark as spam\" and \"/Mark/Mark as "
369 "ham\" to train Bsfilter with a few hundred "
370 "spam and ham messages."));
371 else
372 msg = g_strdup_printf(_("The Bsfilter plugin couldn't filter "
373 "a message. The command `%s` couldn't be run."),
374 bs_exec);
375 if (!prefs_common_get_prefs()->no_recv_err_panel) {
376 if (!warned_error) {
377 alertpanel_error("%s", msg);
379 warned_error = TRUE;
380 } else {
381 log_error(LOG_PROTOCOL, "%s\n", msg);
383 g_free(msg);
386 if (status == 0) {
387 if (config.receive_spam && MSG_IS_SPAM(msginfo->flags)) {
388 FolderItem *save_folder = NULL;
390 if ((!config.save_folder) ||
391 (config.save_folder[0] == '\0') ||
392 ((save_folder = folder_find_item_from_identifier(config.save_folder)) == NULL)) {
393 if (mail_filtering_data->account && mail_filtering_data->account->set_trash_folder) {
394 save_folder = folder_find_item_from_identifier(
395 mail_filtering_data->account->trash_folder);
396 if (save_folder)
397 debug_print("found trash folder from account's advanced settings\n");
399 if (save_folder == NULL && mail_filtering_data->account &&
400 mail_filtering_data->account->folder) {
401 save_folder = mail_filtering_data->account->folder->trash;
402 if (save_folder)
403 debug_print("found trash folder from account's trash\n");
405 if (save_folder == NULL && mail_filtering_data->account &&
406 !mail_filtering_data->account->folder) {
407 if (mail_filtering_data->account->inbox) {
408 FolderItem *item = folder_find_item_from_identifier(
409 mail_filtering_data->account->inbox);
410 if (item && item->folder->trash) {
411 save_folder = item->folder->trash;
412 debug_print("found trash folder from account's inbox\n");
415 if (!save_folder && mail_filtering_data->account->local_inbox) {
416 FolderItem *item = folder_find_item_from_identifier(
417 mail_filtering_data->account->local_inbox);
418 if (item && item->folder->trash) {
419 save_folder = item->folder->trash;
420 debug_print("found trash folder from account's local_inbox\n");
424 if (save_folder == NULL) {
425 debug_print("using default trash folder\n");
426 save_folder = folder_get_default_trash();
429 if (save_folder) {
430 msginfo->filter_op = IS_MOVE;
431 msginfo->to_filter_folder = save_folder;
435 if (message_callback != NULL)
436 message_callback(NULL, 0, 0, FALSE);
438 return filtered;
441 BsfilterConfig *bsfilter_get_config(void)
443 return &config;
446 int bsfilter_learn(MsgInfo *msginfo, GSList *msglist, gboolean spam)
448 gchar *cmd = NULL;
449 gchar *file = NULL;
450 #ifndef G_OS_WIN32
451 gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilter";
452 #else
453 gchar *bs_exec = (config.bspath && *config.bspath) ? config.bspath:"bsfilterw.exe";
454 #endif
455 gint status = 0;
456 gboolean free_list = FALSE;
457 GSList *cur = NULL;
459 if (msginfo == NULL && msglist == NULL) {
460 return -1;
462 /* process *either* a msginfo or a msglist */
463 if (msginfo != NULL && msglist == NULL) {
464 msglist = g_slist_append(NULL, msginfo);
465 free_list = TRUE;
467 for (cur = msglist; cur; cur = cur->next) {
468 msginfo = (MsgInfo *)cur->data;
469 file = procmsg_get_message_file(msginfo);
470 if (file == NULL) {
471 return -1;
472 } else {
473 if (message_callback != NULL)
474 message_callback(_("Bsfilter: learning from message..."), 0, 0, FALSE);
475 if (spam)
476 /* learn as spam */
477 cmd = g_strdup_printf("%s --homedir '%s' -su '%s'", bs_exec, get_rc_dir(), file);
478 else
479 /* learn as ham */
480 cmd = g_strdup_printf("%s --homedir '%s' -cu '%s'", bs_exec, get_rc_dir(), file);
482 debug_print("%s\n", cmd);
483 if ((status = execute_command_line(cmd, FALSE,
484 claws_get_startup_dir())) != 0)
485 log_error(LOG_PROTOCOL, _("Learning failed; `%s` returned with status %d."),
486 cmd, status);
487 g_free(cmd);
488 g_free(file);
489 if (message_callback != NULL)
490 message_callback(NULL, 0, 0, FALSE);
493 if (free_list)
494 g_slist_free(msglist);
496 return 0;
499 void bsfilter_save_config(void)
501 PrefFile *pfile;
502 gchar *rcpath;
504 debug_print("Saving Bsfilter Page\n");
506 rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
507 pfile = prefs_write_open(rcpath);
508 g_free(rcpath);
509 if (!pfile || (prefs_set_block_label(pfile, "Bsfilter") < 0))
510 return;
512 if (prefs_write_param(param, pfile->fp) < 0) {
513 g_warning("failed to write Bsfilter configuration to file");
514 prefs_file_close_revert(pfile);
515 return;
517 if (fprintf(pfile->fp, "\n") < 0) {
518 FILE_OP_ERROR(rcpath, "fprintf");
519 prefs_file_close_revert(pfile);
520 } else
521 prefs_file_close(pfile);
524 void bsfilter_set_message_callback(MessageCallback callback)
526 message_callback = callback;
529 gint plugin_init(gchar **error)
531 gchar *rcpath;
532 hook_id = HOOK_NONE;
534 if (!check_plugin_version(MAKE_NUMERIC_VERSION(2,9,2,72),
535 VERSION_NUMERIC, PLUGIN_NAME, error))
536 return -1;
538 prefs_set_default(param);
539 rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
540 prefs_read_config(param, "Bsfilter", rcpath, NULL);
541 g_free(rcpath);
543 bsfilter_gtk_init();
545 debug_print("Bsfilter plugin loaded\n");
547 #ifdef USE_PTHREAD
548 bsfilter_start_thread();
549 #endif
551 if (config.process_emails) {
552 bsfilter_register_hook();
555 procmsg_register_spam_learner(bsfilter_learn);
556 procmsg_spam_set_folder(config.save_folder, bsfilter_get_spam_folder);
558 return 0;
562 FolderItem *bsfilter_get_spam_folder(MsgInfo *msginfo)
564 FolderItem *item = NULL;
566 if (config.save_folder != NULL) {
567 item = folder_find_item_from_identifier(config.save_folder);
570 if (item || msginfo == NULL || msginfo->folder == NULL)
571 return item;
573 if (msginfo->folder->folder &&
574 msginfo->folder->folder->account &&
575 msginfo->folder->folder->account->set_trash_folder) {
576 item = folder_find_item_from_identifier(
577 msginfo->folder->folder->account->trash_folder);
580 if (item == NULL &&
581 msginfo->folder->folder &&
582 msginfo->folder->folder->trash)
583 item = msginfo->folder->folder->trash;
585 if (item == NULL)
586 item = folder_get_default_trash();
588 debug_print("bs spam dir: %s\n", folder_item_get_path(item));
589 return item;
592 gboolean plugin_done(void)
594 if (hook_id != HOOK_NONE) {
595 bsfilter_unregister_hook();
597 #ifdef USE_PTHREAD
598 bsfilter_stop_thread();
599 #endif
600 g_free(config.save_folder);
601 bsfilter_gtk_done();
602 procmsg_unregister_spam_learner(bsfilter_learn);
603 procmsg_spam_set_folder(NULL, NULL);
604 debug_print("Bsfilter plugin unloaded\n");
605 return TRUE;
608 const gchar *plugin_name(void)
610 return PLUGIN_NAME;
613 const gchar *plugin_desc(void)
615 return _("This plugin can check all messages that are received from an "
616 "IMAP, LOCAL or POP account for spam using Bsfilter. "
617 "You will need Bsfilter installed locally.\n"
618 "\n"
619 "Before Bsfilter can recognize spam messages, you have to "
620 "train it by marking a few hundred spam and ham messages "
621 "with the use of \"/Mark/Mark as spam\" and \"/Mark/Mark as "
622 "ham\".\n"
623 "\n"
624 "When a message is identified as spam it can be deleted or "
625 "saved in a specially designated folder.\n"
626 "\n"
627 "Options can be found in /Configuration/Preferences/Plugins/Bsfilter");
630 const gchar *plugin_type(void)
632 return "GTK3";
635 const gchar *plugin_licence(void)
637 return "GPL3+";
640 const gchar *plugin_version(void)
642 return VERSION;
645 struct PluginFeature *plugin_provides(void)
647 static struct PluginFeature features[] =
648 { {PLUGIN_FILTERING, N_("Spam detection")},
649 {PLUGIN_FILTERING, N_("Spam learning")},
650 {PLUGIN_NOTHING, NULL}};
651 return features;
654 void bsfilter_register_hook(void)
656 if (hook_id == HOOK_NONE)
657 hook_id = hooks_register_hook(MAIL_FILTERING_HOOKLIST, mail_filtering_hook, NULL);
658 if (hook_id == HOOK_NONE) {
659 g_warning("failed to register mail filtering hook");
660 config.process_emails = FALSE;
664 void bsfilter_unregister_hook(void)
666 if (hook_id != HOOK_NONE) {
667 hooks_unregister_hook(MAIL_FILTERING_HOOKLIST, hook_id);
669 hook_id = HOOK_NONE;