2 * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2006 Hiroyuki Yamamoto and the Sylpheed-Claws Team
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
26 #include <sys/types.h>
30 #include <glib/gi18n.h>
36 #include "common/sylpheed.h"
37 #include "common/version.h"
39 #include "common/utils.h"
44 #include "prefs_gtk.h"
46 #include "bogofilter.h"
49 #include "prefs_common.h"
50 #include "alertpanel.h"
52 #ifdef HAVE_SYSEXITS_H
58 #ifdef HAVE_SYS_ERRNO_H
59 #include <sys/errno.h>
64 #ifdef HAVE_SYS_TIME_H
74 #define MAILS_PER_BATCH 20
76 static guint hook_id = -1;
77 static MessageCallback message_callback;
79 static BogofilterConfig config;
81 static PrefParam param[] = {
82 {"process_emails", "TRUE", &config.process_emails, P_BOOL,
84 {"receive_spam", "TRUE", &config.receive_spam, P_BOOL,
86 {"save_folder", NULL, &config.save_folder, P_STRING,
88 {"max_size", "250", &config.max_size, P_INT,
90 {"bogopath", "bogofilter", &config.bogopath, P_STRING,
93 {NULL, NULL, NULL, P_OTHER, NULL, NULL, NULL}
97 * Helper function for spawn_with_input() - write an entire
107 gssize count = write (fd, buf, to_write);
123 static gboolean mail_filtering_hook(gpointer source, gpointer data)
125 MailFilteringData *mail_filtering_data = (MailFilteringData *) source;
126 MsgInfo *msginfo = mail_filtering_data->msginfo;
127 GSList *msglist = mail_filtering_data->msglist;
129 static gboolean warned_error = FALSE;
130 gchar *file = NULL, *cmd = NULL;
132 gchar *bogo_exec = (config.bogopath && *config.bogopath) ? config.bogopath:"bogofilter";
133 int total = 0, curnum = 0;
134 GSList *spams = NULL;
139 gint bogo_stdin, bogo_stdout;
140 GError *error = NULL;
141 gboolean bogo_forked;
143 if (!config.process_emails) {
147 if (msglist == NULL && msginfo != NULL) {
148 g_warning("wrong call to bogofilter mail_filtering_hook");
152 total = g_slist_length(msglist);
153 if (message_callback != NULL)
154 message_callback(_("Bogofilter: filtering messages..."), total, 0);
156 cmd = g_strdup_printf("%s -T -b", bogo_exec);
158 bogo_args[0] = bogo_exec;
163 bogo_forked = g_spawn_async_with_pipes(
164 NULL, bogo_args,NULL, G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD,
165 NULL, NULL, &bogo_pid, &bogo_stdin,
166 &bogo_stdout, NULL, &error);
168 if (bogo_forked == FALSE) {
169 g_warning("%s\n", error ? error->message:"ERROR???");
174 for (cur = msglist; cur; cur = cur->next) {
175 msginfo = (MsgInfo *)cur->data;
176 debug_print("Filtering message %d (%d/%d)\n", msginfo->msgnum, curnum, total);
178 if (message_callback != NULL)
179 message_callback(NULL, total, curnum++);
181 file = procmsg_get_message_file(msginfo);
184 gchar *tmp = g_strdup_printf("%s\n",file);
185 write_all(bogo_stdin, tmp, strlen(tmp));
187 memset(buf, 0, sizeof(buf));
188 if (read(bogo_stdout, buf, sizeof(buf)-1) < 0) {
191 gchar **parts = NULL;
192 if (strchr(buf, '/')) {
193 tmp = strrchr(buf, '/')+1;
197 parts = g_strsplit(tmp, " ", 0);
198 debug_print("read %s\n", buf);
199 if (parts && parts[0] && parts[1] && *parts[1] == 'S') {
200 debug_print("message %d is spam\n", msginfo->msgnum);
201 procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
202 if (config.receive_spam) {
203 procmsg_msginfo_unset_flags(msginfo, ~0, 0);
204 procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
205 spams = g_slist_prepend(spams, msginfo);
207 folder_item_remove_msg(msginfo->folder, msginfo->msgnum);
209 mail_filtering_data->filtered = g_slist_prepend(
210 mail_filtering_data->filtered, msginfo);
212 debug_print("message %d is ham\n", msginfo->msgnum);
213 procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
214 mail_filtering_data->unfiltered = g_slist_prepend(
215 mail_filtering_data->unfiltered, msginfo);
221 mail_filtering_data->unfiltered = g_slist_prepend(
222 mail_filtering_data->unfiltered, msginfo);
230 waitpid(bogo_pid, &status, 0);
231 if (!WIFEXITED(status))
234 status = WEXITSTATUS(status);
237 if (status < 0 || status > 2) { /* I/O or other errors */
241 msg = g_strdup_printf(_("The Bogofilter plugin couldn't filter "
242 "a message. The probable cause of the "
243 "error is that it didn't learn from any mail.\n"
244 "Use \"/Mark/Mark as spam\" and \"/Mark/Mark as "
245 "ham\" to train Bogofilter with a few hundred "
246 "spam and ham messages."));
248 msg = g_strdup_printf(_("The Bogofilter plugin couldn't filter "
249 "a message. the command `%s` couldn't be run."), cmd);
250 if (!prefs_common.no_recv_err_panel) {
252 alertpanel_error(msg);
256 gchar *tmp = g_strdup_printf("%s\n", msg);
262 if (status < 0 || status > 2) {
263 g_slist_free(mail_filtering_data->filtered);
264 g_slist_free(mail_filtering_data->unfiltered);
266 mail_filtering_data->filtered = NULL;
267 mail_filtering_data->unfiltered = NULL;
268 } else if (config.receive_spam && spams) {
269 FolderItem *save_folder;
271 if ((!config.save_folder) ||
272 (config.save_folder[0] == '\0') ||
273 ((save_folder = folder_find_item_from_identifier(config.save_folder)) == NULL))
274 save_folder = folder_get_default_trash();
276 folder_item_move_msgs(save_folder, spams);
279 if (message_callback != NULL)
280 message_callback(NULL, 0, 0);
281 mail_filtering_data->filtered = g_slist_reverse(
282 mail_filtering_data->filtered);
283 mail_filtering_data->unfiltered = g_slist_reverse(
284 mail_filtering_data->unfiltered);
289 BogofilterConfig *bogofilter_get_config(void)
294 int bogofilter_learn(MsgInfo *msginfo, GSList *msglist, gboolean spam)
298 const gchar *bogo_exec = (config.bogopath && *config.bogopath) ? config.bogopath:"bogofilter";
300 if (msginfo == NULL && msglist == NULL) {
305 file = procmsg_get_message_file(msginfo);
309 if (message_callback != NULL)
310 message_callback(_("Bogofilter: learning from message..."), 0, 0);
313 cmd = g_strdup_printf("%s -s -I '%s'", bogo_exec, file);
314 else if (MSG_IS_SPAM(msginfo->flags))
315 /* correct bogofilter, this wasn't spam */
316 cmd = g_strdup_printf("%s -Sn -I '%s'", bogo_exec, file);
319 cmd = g_strdup_printf("%s -n -I '%s'", bogo_exec, file);
320 if ((status = execute_command_line(cmd, FALSE)) != 0)
321 alertpanel_error(_("Learning failed; `%s` returned with status %d."),
325 if (message_callback != NULL)
326 message_callback(NULL, 0, 0);
331 GSList *cur = msglist;
333 int total = g_slist_length(msglist);
335 gboolean some_correction = FALSE, some_no_correction = FALSE;
337 if (message_callback != NULL)
338 message_callback(_("Bogofilter: learning from messages..."), total, 0);
340 for (cur = msglist; cur && status == 0; cur = cur->next) {
341 info = (MsgInfo *)cur->data;
343 some_no_correction = TRUE;
344 else if (MSG_IS_SPAM(info->flags))
345 /* correct bogofilter, this wasn't spam */
346 some_correction = TRUE;
348 some_no_correction = TRUE;
352 if (some_correction && some_no_correction) {
353 /* we potentially have to do different stuff for every mail */
354 for (cur = msglist; cur && status == 0; cur = cur->next) {
355 info = (MsgInfo *)cur->data;
356 file = procmsg_get_message_file(info);
360 cmd = g_strdup_printf("%s -s -I '%s'", bogo_exec, file);
361 else if (MSG_IS_SPAM(info->flags))
362 /* correct bogofilter, this wasn't spam */
363 cmd = g_strdup_printf("%s -Sn -I '%s'", bogo_exec, file);
366 cmd = g_strdup_printf("%s -n -I '%s'", bogo_exec, file);
368 if ((status = execute_command_line(cmd, FALSE)) != 0)
369 alertpanel_error(_("Learning failed; `%s` returned with status %d."),
375 if (message_callback != NULL)
376 message_callback(NULL, total, done);
378 } else if (some_correction || some_no_correction) {
384 GError *error = NULL;
385 gboolean bogo_forked;
387 bogo_args[0] = (gchar *)bogo_exec;
388 if (some_correction && !some_no_correction)
389 bogo_args[1] = "-Sn";
390 else if (some_no_correction && !some_correction)
391 bogo_args[1] = spam ? "-s":"-n";
395 bogo_forked = g_spawn_async_with_pipes(
396 NULL, bogo_args,NULL, G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD,
397 NULL, NULL, &bogo_pid, &bogo_stdin,
400 while (bogo_forked && cur) {
402 info = (MsgInfo *)cur->data;
403 file = procmsg_get_message_file(info);
405 tmp = g_strdup_printf("%s\n",
407 write_all(bogo_stdin, tmp, strlen(tmp));
412 if (message_callback != NULL)
413 message_callback(NULL, total, done);
418 waitpid(bogo_pid, &status, 0);
419 if (!WIFEXITED(status))
422 status = WEXITSTATUS(status);
424 if (!bogo_forked || status != 0) {
425 alertpanel_error(_("Learning failed; `%s` returned with error:\n%s"),
426 cmd, error ? error->message:_("Unknown error"));
433 if (message_callback != NULL)
434 message_callback(NULL, 0, 0);
440 void bogofilter_save_config(void)
445 debug_print("Saving Bogofilter Page\n");
447 rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
448 pfile = prefs_write_open(rcpath);
450 if (!pfile || (prefs_set_block_label(pfile, "Bogofilter") < 0))
453 if (prefs_write_param(param, pfile->fp) < 0) {
454 g_warning("Failed to write Bogofilter configuration to file\n");
455 prefs_file_close_revert(pfile);
458 fprintf(pfile->fp, "\n");
460 prefs_file_close(pfile);
463 void bogofilter_set_message_callback(MessageCallback callback)
465 message_callback = callback;
468 gint plugin_init(gchar **error)
474 if ((sylpheed_get_version() > VERSION_NUMERIC)) {
475 *error = g_strdup("Your version of Sylpheed-Claws is newer than the version the Bogofilter plugin was built with");
479 if ((sylpheed_get_version() < MAKE_NUMERIC_VERSION(0, 9, 3, 86))) {
480 *error = g_strdup("Your version of Sylpheed-Claws is too old for the Bogofilter plugin");
484 prefs_set_default(param);
485 rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
486 prefs_read_config(param, "Bogofilter", rcpath, NULL);
489 bogofilter_gtk_init();
491 debug_print("Bogofilter plugin loaded\n");
493 if (config.process_emails) {
494 bogofilter_register_hook();
497 procmsg_register_spam_learner(bogofilter_learn);
498 procmsg_spam_set_folder(config.save_folder);
504 void plugin_done(void)
507 bogofilter_unregister_hook();
509 g_free(config.save_folder);
510 bogofilter_gtk_done();
511 procmsg_unregister_spam_learner(bogofilter_learn);
512 procmsg_spam_set_folder(NULL);
513 debug_print("Bogofilter plugin unloaded\n");
516 const gchar *plugin_name(void)
518 return _("Bogofilter");
521 const gchar *plugin_desc(void)
523 return _("This plugin can check all messages that are received from an "
524 "IMAP, LOCAL or POP account for spam using Bogofilter. "
525 "You will need Bogofilter installed locally.\n "
527 "Before Bogofilter can recognize spam messages, you have to "
528 "train it by marking a few hundred spam and ham messages. "
529 "Use \"/Mark/Mark as Spam\" and \"/Mark/Mark as ham\" to "
530 "train Bogofilter.\n"
532 "When a message is identified as spam it can be deleted or "
533 "saved in a specially designated folder.\n"
535 "Options can be found in /Configuration/Preferences/Plugins/Bogofilter");
538 const gchar *plugin_type(void)
543 const gchar *plugin_licence(void)
548 const gchar *plugin_version(void)
553 struct PluginFeature *plugin_provides(void)
555 static struct PluginFeature features[] =
556 { {PLUGIN_FILTERING, N_("Spam detection")},
557 {PLUGIN_FILTERING, N_("Spam learning")},
558 {PLUGIN_NOTHING, NULL}};
562 void bogofilter_register_hook(void)
564 hook_id = hooks_register_hook(MAIL_LISTFILTERING_HOOKLIST, mail_filtering_hook, NULL);
566 g_warning("Failed to register mail filtering hook");
567 config.process_emails = FALSE;
571 void bogofilter_unregister_hook(void)
574 hooks_unregister_hook(MAIL_LISTFILTERING_HOOKLIST, hook_id);