e659d51f4418547ccbb7facfb42b419feb2db8af
[claws.git] / src / plugins / bogofilter / bogofilter.c
1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2007 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 #endif
24
25 #include "defs.h"
26
27 #include <sys/types.h>
28 #include <sys/wait.h>
29 #include <errno.h>
30
31 #include <glib.h>
32 #include <glib/gi18n.h>
33
34 #if HAVE_LOCALE_H
35 #  include <locale.h>
36 #endif
37
38 #include "common/claws.h"
39 #include "common/version.h"
40 #include "plugin.h"
41 #include "common/utils.h"
42 #include "hooks.h"
43 #include "procmsg.h"
44 #include "folder.h"
45 #include "prefs.h"
46 #include "prefs_gtk.h"
47
48 #include "bogofilter.h"
49 #include "inc.h"
50 #include "log.h"
51 #include "prefs_common.h"
52 #include "alertpanel.h"
53 #include "addr_compl.h"
54
55 #ifdef HAVE_SYSEXITS_H
56 #include <sysexits.h>
57 #endif
58 #ifdef HAVE_ERRNO_H
59 #include <errno.h>
60 #endif
61 #ifdef HAVE_SYS_ERRNO_H
62 #include <sys/errno.h>
63 #endif
64 #ifdef HAVE_TIME_H
65 #include <time.h>
66 #endif
67 #ifdef HAVE_SYS_TIME_H
68 #include <sys/time.h>
69 #endif
70 #ifdef HAVE_SIGNAL_H
71 #include <signal.h>
72 #endif
73 #ifdef HAVE_PWD_H
74 #include <pwd.h>
75 #endif
76
77 #define PLUGIN_NAME (_("Bogofilter"))
78
79 static guint hook_id = -1;
80 static MessageCallback message_callback;
81
82 static BogofilterConfig config;
83
84 static PrefParam param[] = {
85         {"process_emails", "TRUE", &config.process_emails, P_BOOL,
86          NULL, NULL, NULL},
87         {"receive_spam", "TRUE", &config.receive_spam, P_BOOL,
88          NULL, NULL, NULL},
89         {"save_folder", NULL, &config.save_folder, P_STRING,
90          NULL, NULL, NULL},
91         {"save_unsure", "FALSE", &config.save_unsure, P_BOOL,
92          NULL, NULL, NULL},
93         {"save_unsure_folder", NULL, &config.save_unsure_folder, P_STRING,
94          NULL, NULL, NULL},
95         {"max_size", "250", &config.max_size, P_INT,
96          NULL, NULL, NULL},
97         {"bogopath", "bogofilter", &config.bogopath, P_STRING,
98          NULL, NULL, NULL},
99         {"insert_header", "FALSE", &config.insert_header, P_BOOL,
100          NULL, NULL, NULL},
101         {"whitelist_ab", "FALSE", &config.whitelist_ab, P_BOOL,
102          NULL, NULL, NULL},
103         {"whitelist_ab_folder", N_("Any"), &config.whitelist_ab_folder, P_STRING,
104          NULL, NULL, NULL},
105         {"learn_from_whitelist", "FALSE", &config.learn_from_whitelist, P_BOOL,
106          NULL, NULL, NULL},
107         {"mark_as_read", "TRUE", &config.mark_as_read, P_BOOL,
108          NULL, NULL, NULL},
109
110         {NULL, NULL, NULL, P_OTHER, NULL, NULL, NULL}
111 };
112
113 /*
114  * Helper function for spawn_with_input() - write an entire
115  * string to a fd.
116  */
117 static gboolean
118 write_all (int         fd,
119            const char *buf,
120            gsize       to_write)
121 {
122   while (to_write > 0)
123     {
124       gssize count = write (fd, buf, to_write);
125       if (count < 0)
126         {
127           if (errno != EINTR)
128             return FALSE;
129         }
130       else
131         {
132           to_write -= count;
133           buf += count;
134         }
135     }
136
137   return TRUE;
138 }
139
140 typedef struct _BogoFilterData {
141         MailFilteringData *mail_filtering_data;
142         gchar **bogo_args;
143         GSList *msglist;
144         GSList *new_hams;
145         GSList *new_unsure;
146         GSList *new_spams;
147         GSList *whitelisted_new_spams;
148         gboolean done;
149         int status;
150         gboolean in_thread;
151 } BogoFilterData;
152
153 static BogoFilterData *to_filter_data = NULL;
154 #ifdef USE_PTHREAD
155 static gboolean filter_th_done = FALSE;
156 static pthread_mutex_t list_mutex = PTHREAD_MUTEX_INITIALIZER;
157 static pthread_mutex_t wait_mutex = PTHREAD_MUTEX_INITIALIZER; 
158 static pthread_cond_t wait_cond = PTHREAD_COND_INITIALIZER; 
159 #endif
160
161 static gboolean found_in_addressbook(const gchar *address)
162 {
163         gchar *addr = NULL;
164         gboolean found = FALSE;
165         gint num_addr = 0;
166         
167         if (!address)
168                 return FALSE;
169         
170         addr = g_strdup(address);
171         extract_address(addr);
172         num_addr = complete_address(addr);
173         if (num_addr > 1) {
174                 /* skip first item (this is the search string itself) */
175                 int i = 1;
176                 for (; i < num_addr && !found; i++) {
177                         gchar *caddr = get_complete_address(i);
178                         extract_address(caddr);
179                         if (strcasecmp(caddr, addr) == 0)
180                                 found = TRUE;
181                         g_free(caddr);
182                 }
183         }
184         g_free(addr);
185         return found;
186 }
187
188 static void bogofilter_do_filter(BogoFilterData *data)
189 {
190         GPid bogo_pid;
191         gint bogo_stdin, bogo_stdout;
192         GError *error = NULL;
193         gboolean bogo_forked;
194         int status = 0;
195         MsgInfo *msginfo;
196         GSList *cur = NULL;
197         int total = 0, curnum = 0;
198         gchar *file = NULL;
199         gchar buf[BUFSIZ];
200
201         total = g_slist_length(data->msglist);
202
203         bogo_forked = g_spawn_async_with_pipes(
204                         NULL, data->bogo_args,NULL, G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD,
205                         NULL, NULL, &bogo_pid, &bogo_stdin,
206                         &bogo_stdout, NULL, &error);
207                 
208         if (bogo_forked == FALSE) {
209                 g_warning("%s\n", error ? error->message:"ERROR???");
210                 g_error_free(error);
211                 error = NULL;
212                 status = -1;
213         } else {
214         
215                 if (config.whitelist_ab) {
216                         gchar *ab_folderpath;
217
218                         if (*config.whitelist_ab_folder == '\0' ||
219                                 strcasecmp(config.whitelist_ab_folder, _("Any")) == 0) {
220                                 /* match the whole addressbook */
221                                 ab_folderpath = NULL;
222                         } else {
223                                 /* match the specific book/folder of the addressbook */
224                                 ab_folderpath = config.whitelist_ab_folder;
225                         }
226
227                         start_address_completion(ab_folderpath);
228                 }
229
230                 for (cur = data->msglist; cur; cur = cur->next) {
231                         gboolean whitelisted = FALSE;
232                         msginfo = (MsgInfo *)cur->data;
233                         debug_print("Filtering message %d (%d/%d)\n", msginfo->msgnum, curnum, total);
234
235                         if (message_callback != NULL)
236                                 message_callback(NULL, total, curnum++, data->in_thread);
237
238                         if (config.whitelist_ab && msginfo->from && 
239                             found_in_addressbook(msginfo->from))
240                                 whitelisted = TRUE;
241
242                         /* can set flags (SCANNED, ATTACHMENT) but that's ok 
243                          * as GUI updates are hooked not direct */
244
245                         file = procmsg_get_message_file(msginfo);
246
247                         if (file) {
248                                 gchar *tmp = g_strdup_printf("%s\n",file);
249                                 /* send filename to bogofilter */
250                                 write_all(bogo_stdin, tmp, strlen(tmp));
251                                 g_free(tmp);
252                                 memset(buf, 0, sizeof(buf));
253                                 /* get the result */
254                                 if (read(bogo_stdout, buf, sizeof(buf)-1) < 0) {
255                                         g_warning("bogofilter short read\n");
256                                         debug_print("message %d is ham\n", msginfo->msgnum);
257                                         data->mail_filtering_data->unfiltered = g_slist_prepend(
258                                                 data->mail_filtering_data->unfiltered, msginfo);
259                                         data->new_hams = g_slist_prepend(data->new_hams, msginfo);
260                                 } else {
261                                         gchar **parts = NULL;
262                                         if (strchr(buf, '/')) {
263                                                 tmp = strrchr(buf, '/')+1;
264                                         } else {
265                                                 tmp = buf;
266                                         }
267                                         parts = g_strsplit(tmp, " ", 0);
268                                         debug_print("read %s\n", buf);
269                                         
270                                         /* note the result if the header if needed */
271                                         if (parts && parts[0] && parts[1] && parts[2] && 
272                                             FOLDER_TYPE(msginfo->folder->folder) == F_MH &&
273                                             config.insert_header) {
274                                                 gchar *tmpfile = get_tmp_file();
275                                                 FILE *input = fopen(file, "r");
276                                                 FILE *output = fopen(tmpfile, "w");
277                                                 if (strstr(parts[2], "\n"))
278                                                         *(strstr(parts[2], "\n")) = '\0';
279                                                 if (input && !output) 
280                                                         fclose (input);
281                                                 else if (!input && output)
282                                                         fclose (output);
283                                                 else {
284                                                         gchar tmpbuf[BUFFSIZE];
285                                                         gboolean err = FALSE;
286                                                         const gchar *bogosity = *parts[1] == 'S' ? "Spam":
287                                                                                  (*parts[1] == 'H' ? "Ham":"Unsure");
288                                                         gchar *tmpstr = g_strdup_printf(
289                                                                         "X-Claws-Bogosity: %s, spamicity=%s%s\n",
290                                                                         bogosity, parts[2],
291                                                                         whitelisted?" [whitelisted]":"");
292                                                         if (fwrite(tmpstr, 1, strlen(tmpstr), output) < strlen(tmpstr)) {
293                                                                 err = TRUE;
294                                                         } else {
295                                                                 while (fgets(tmpbuf, sizeof(buf), input)) {
296                                                                         if (fputs(tmpbuf, output) == EOF) {
297                                                                                 err = TRUE;
298                                                                                 break;
299                                                                         }
300                                                                 }
301                                                         }
302                                                         fclose(input);
303                                                         if (fclose(output) == EOF)
304                                                                 err = TRUE;
305                                                         if (!err)
306                                                                 move_file(tmpfile, file, TRUE);
307                                                         g_free(tmpstr);
308                                                 }
309                                                 g_free(tmpfile);
310                                         }
311
312                                         /* file the mail */
313                                         if (!whitelisted && parts && parts[0] && parts[1] && *parts[1] == 'S') {
314
315                                                 debug_print("message %d is spam\n", msginfo->msgnum);
316                                                 /* Spam will be filtered away */
317                                                 data->mail_filtering_data->filtered = g_slist_prepend(
318                                                         data->mail_filtering_data->filtered, msginfo);
319                                                 data->new_spams = g_slist_prepend(data->new_spams, msginfo);
320
321                                         } else if (whitelisted && parts && parts[0] && parts[1] && 
322                                                         (*parts[1] == 'S' || *parts[1] == 'U')) {
323
324                                                 debug_print("message %d is whitelisted %s\n", msginfo->msgnum,
325                                                         *parts[1] == 'S' ? "spam":"unsure");
326                                                 /* Whitelisted spam will *not* be filtered away, but continue
327                                                  * their trip through filtering as if it was ham. */
328                                                 data->mail_filtering_data->unfiltered = g_slist_prepend(
329                                                         data->mail_filtering_data->unfiltered, msginfo);
330                                                 /* But it gets put in a different list, so that we 
331                                                  * can still flag it and inform the user that it is
332                                                  * considered a spam (so that he can teach bogo that 
333                                                  * it was not). */
334                                                 data->whitelisted_new_spams = g_slist_prepend(data->whitelisted_new_spams, msginfo);
335
336                                         } else if (config.save_unsure && parts && parts[0] && parts[1] && *parts[1] == 'U') {
337                                                 
338                                                 debug_print("message %d is unsure\n", msginfo->msgnum);
339                                                 /* Spam will be filtered away */
340                                                 data->mail_filtering_data->filtered = g_slist_prepend(
341                                                         data->mail_filtering_data->filtered, msginfo);
342                                                 data->new_unsure = g_slist_prepend(data->new_unsure, msginfo);
343
344                                         } else {
345                                                 
346                                                 debug_print("message %d is ham\n", msginfo->msgnum);
347                                                 data->mail_filtering_data->unfiltered = g_slist_prepend(
348                                                         data->mail_filtering_data->unfiltered, msginfo);
349                                                 data->new_hams = g_slist_prepend(data->new_hams, msginfo);
350
351                                         }
352                                         g_strfreev(parts);
353                                 }
354                                 g_free(file);
355                         } else {
356                                 data->mail_filtering_data->unfiltered = g_slist_prepend(
357                                         data->mail_filtering_data->unfiltered, msginfo);
358                                 data->new_hams = g_slist_prepend(data->new_hams, msginfo);
359                         }
360                 }
361                 if (config.whitelist_ab)
362                         end_address_completion();
363         }
364         if (status != -1) {
365                 close(bogo_stdout);
366                 close(bogo_stdin);
367                 waitpid(bogo_pid, &status, 0);
368                 if (!WIFEXITED(status))
369                         status = -1;
370                 else
371                         status = WEXITSTATUS(status);
372         }
373
374         to_filter_data->status = status; 
375 }
376
377 #ifdef USE_PTHREAD
378 static void *bogofilter_filtering_thread(void *data) 
379 {
380         while (!filter_th_done) {
381                 pthread_mutex_lock(&list_mutex);
382                 if (to_filter_data == NULL || to_filter_data->done == TRUE) {
383                         pthread_mutex_unlock(&list_mutex);
384                         debug_print("thread is waiting for something to filter\n");
385                         pthread_mutex_lock(&wait_mutex);
386                         pthread_cond_wait(&wait_cond, &wait_mutex);
387                         pthread_mutex_unlock(&wait_mutex);
388                 } else {
389                         debug_print("thread awaken with something to filter\n");
390                         to_filter_data->done = FALSE;
391                         bogofilter_do_filter(to_filter_data);
392                         pthread_mutex_unlock(&list_mutex);
393                         to_filter_data->done = TRUE;
394                         usleep(100);
395                 }
396         }
397         return NULL;
398 }
399
400 static pthread_t filter_th = 0;
401
402 static void bogofilter_start_thread(void)
403 {
404         filter_th_done = FALSE;
405         if (filter_th != 0 || 1)
406                 return;
407         if (pthread_create(&filter_th, 0, 
408                         bogofilter_filtering_thread, 
409                         NULL) != 0) {
410                 filter_th = 0;
411                 return;
412         }
413         debug_print("thread created\n");
414 }
415
416 static void bogofilter_stop_thread(void)
417 {
418         void *res;
419         while (pthread_mutex_trylock(&list_mutex) != 0) {
420                 GTK_EVENTS_FLUSH();
421                 usleep(100);
422         }
423         if (filter_th != 0) {
424                 filter_th_done = TRUE;
425                 debug_print("waking thread up\n");
426                 pthread_mutex_lock(&wait_mutex);
427                 pthread_cond_broadcast(&wait_cond);
428                 pthread_mutex_unlock(&wait_mutex);
429                 pthread_join(filter_th, &res);
430                 filter_th = 0;
431         }
432         pthread_mutex_unlock(&list_mutex);
433         debug_print("thread done\n");
434 }
435 #endif
436
437 static gboolean mail_filtering_hook(gpointer source, gpointer data)
438 {
439         MailFilteringData *mail_filtering_data = (MailFilteringData *) source;
440         MsgInfo *msginfo = mail_filtering_data->msginfo;
441         GSList *msglist = mail_filtering_data->msglist;
442         GSList *cur = NULL;
443         static gboolean warned_error = FALSE;
444         int status = 0;
445         int total = 0, curnum = 0;
446         GSList *new_hams = NULL, *new_spams = NULL;
447         GSList *new_unsure, *whitelisted_new_spams = NULL;
448         gchar *bogo_exec = (config.bogopath && *config.bogopath) ? config.bogopath:"bogofilter";
449         gchar *bogo_args[4];
450         gboolean ok_to_thread = TRUE;
451
452         bogo_args[0] = bogo_exec;
453         bogo_args[1] = "-T";
454         bogo_args[2] = "-b";
455         bogo_args[3] = NULL;
456         
457         if (!config.process_emails) {
458                 return FALSE;
459         }
460         
461         if (msglist == NULL && msginfo != NULL) {
462                 g_warning("wrong call to bogofilter mail_filtering_hook");
463                 return FALSE;
464         }
465         
466         total = g_slist_length(msglist);
467         
468         /* we have to make sure the mails are cached - or it'll break on IMAP */
469         if (message_callback != NULL)
470                 message_callback(_("Bogofilter: fetching bodies..."), total, 0, FALSE);
471         for (cur = msglist; cur; cur = cur->next) {
472                 gchar *file = procmsg_get_message_file((MsgInfo *)cur->data);
473                 if (file == NULL)
474                         ok_to_thread = FALSE;
475                 if (message_callback != NULL)
476                         message_callback(NULL, total, curnum++, FALSE);
477                 g_free(file);
478         }
479         if (message_callback != NULL)
480                 message_callback(NULL, 0, 0, FALSE);
481
482         if (message_callback != NULL)
483                 message_callback(_("Bogofilter: filtering messages..."), total, 0, FALSE);
484
485 #ifdef USE_PTHREAD
486         while (pthread_mutex_trylock(&list_mutex) != 0) {
487                 GTK_EVENTS_FLUSH();
488                 usleep(100);
489         }
490 #endif
491         to_filter_data = g_new0(BogoFilterData, 1);
492         to_filter_data->msglist = msglist;
493         to_filter_data->mail_filtering_data = mail_filtering_data;
494         to_filter_data->new_hams = NULL;
495         to_filter_data->new_unsure = NULL;
496         to_filter_data->new_spams = NULL;
497         to_filter_data->whitelisted_new_spams = NULL;
498         to_filter_data->done = FALSE;
499         to_filter_data->status = -1;
500         to_filter_data->bogo_args = bogo_args;
501 #ifdef USE_PTHREAD
502         to_filter_data->in_thread = (filter_th != 0 && ok_to_thread);
503 #else
504         to_filter_data->in_thread = FALSE;
505 #endif
506
507 #ifdef USE_PTHREAD
508         pthread_mutex_unlock(&list_mutex);
509         
510         if (filter_th != 0 && ok_to_thread) {
511                 debug_print("waking thread to let it filter things\n");
512                 pthread_mutex_lock(&wait_mutex);
513                 pthread_cond_broadcast(&wait_cond);
514                 pthread_mutex_unlock(&wait_mutex);
515
516                 while (!to_filter_data->done) {
517                         GTK_EVENTS_FLUSH();
518                         usleep(100);
519                 }
520         }
521
522         while (pthread_mutex_trylock(&list_mutex) != 0) {
523                 GTK_EVENTS_FLUSH();
524                 usleep(100);
525
526         }
527         if (filter_th == 0 || !ok_to_thread)
528                 bogofilter_do_filter(to_filter_data);
529 #else
530         bogofilter_do_filter(to_filter_data);   
531 #endif
532
533         new_hams = to_filter_data->new_hams;
534         new_unsure = to_filter_data->new_unsure;
535         new_spams = to_filter_data->new_spams;
536         whitelisted_new_spams = to_filter_data->whitelisted_new_spams;
537         status = to_filter_data->status;
538         g_free(to_filter_data);
539         to_filter_data = NULL;
540 #ifdef USE_PTHREAD
541         pthread_mutex_unlock(&list_mutex);
542 #endif
543
544
545         /* unflag hams */
546         for (cur = new_hams; cur; cur = cur->next) {
547                 MsgInfo *msginfo = (MsgInfo *)cur->data;
548                 procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
549                 debug_print("unflagging ham: %d\n", msginfo->msgnum);
550         }
551         /* unflag unsure */
552         for (cur = new_unsure; cur; cur = cur->next) {
553                 MsgInfo *msginfo = (MsgInfo *)cur->data;
554                 procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
555                 debug_print("unflagging unsure: %d\n", msginfo->msgnum);
556         }
557         if (config.learn_from_whitelist && whitelisted_new_spams) {
558                 /* flag whitelisted spams */
559                 for (cur = whitelisted_new_spams; cur; cur = cur->next) {
560                         MsgInfo *msginfo = (MsgInfo *)cur->data;
561                         procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
562                         debug_print("flagging whitelisted non-ham: %d\n", msginfo->msgnum);
563                 }
564                 /* correct bogo */
565                 bogofilter_learn(NULL, whitelisted_new_spams, FALSE);
566
567                 /* unflag them */
568                 for (cur = whitelisted_new_spams; cur; cur = cur->next) {
569                         MsgInfo *msginfo = (MsgInfo *)cur->data;
570                         procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
571                         debug_print("unflagging whitelisted non-ham: %d\n", msginfo->msgnum);
572                 }
573         } else {
574                 for (cur = whitelisted_new_spams; cur; cur = cur->next) {
575                         MsgInfo *msginfo = (MsgInfo *)cur->data;
576                         procmsg_msginfo_unset_flags(msginfo, MSG_SPAM, 0);
577                         debug_print("not flagging whitelisted non-ham: %d\n", msginfo->msgnum);
578                 }
579         }
580
581         /* flag spams and delete them if !config.receive_spam 
582          * (if config.receive_spam is set, we'll move them later) */
583         for (cur = new_spams; cur; cur = cur->next) {
584                 MsgInfo *msginfo = (MsgInfo *)cur->data;
585                 if (config.receive_spam) {
586                         if (config.mark_as_read)
587                                 procmsg_msginfo_unset_flags(msginfo, ~0, 0);
588                         procmsg_msginfo_set_flags(msginfo, MSG_SPAM, 0);
589                 } else {
590                         folder_item_remove_msg(msginfo->folder, msginfo->msgnum);
591                 }
592         }
593         
594         if (status < 0 || status > 2) { /* I/O or other errors */
595                 gchar *msg = NULL;
596                 
597                 if (status == 3)
598                         msg =  g_strdup_printf(_("The Bogofilter plugin couldn't filter "
599                                            "a message. The probable cause of the "
600                                            "error is that it didn't learn from any mail.\n"
601                                            "Use \"/Mark/Mark as spam\" and \"/Mark/Mark as "
602                                            "ham\" to train Bogofilter with a few hundred "
603                                            "spam and ham messages."));
604                 else
605                         msg =  g_strdup_printf(_("The Bogofilter plugin couldn't filter "
606                                            "a message. The command `%s %s %s` couldn't be run."), 
607                                            bogo_args[0], bogo_args[1], bogo_args[2]);
608                 if (!prefs_common.no_recv_err_panel) {
609                         if (!warned_error) {
610                                 alertpanel_error("%s", msg);
611                         }
612                         warned_error = TRUE;
613                 } else {
614                         log_error(LOG_PROTOCOL, "%s\n", msg);
615                 }
616                 g_free(msg);
617         }
618         if (status < 0 || status > 2) {
619                 g_slist_free(mail_filtering_data->filtered);
620                 g_slist_free(mail_filtering_data->unfiltered);
621                 mail_filtering_data->filtered = NULL;
622                 mail_filtering_data->unfiltered = NULL;
623         } else {
624                 if (config.receive_spam && new_spams) {
625                         FolderItem *save_folder = NULL;
626
627                         if ((!config.save_folder) ||
628                             (config.save_folder[0] == '\0') ||
629                             ((save_folder = folder_find_item_from_identifier(config.save_folder)) == NULL)) {
630                                 if (mail_filtering_data->account && mail_filtering_data->account->set_trash_folder) {
631                                         save_folder = folder_find_item_from_identifier(
632                                                 mail_filtering_data->account->trash_folder);
633                                         if (save_folder)
634                                                 debug_print("found trash folder from account's advanced settings\n");
635                                 }
636                                 if (save_folder == NULL && mail_filtering_data->account &&
637                                     mail_filtering_data->account->folder) {
638                                         save_folder = mail_filtering_data->account->folder->trash;
639                                         if (save_folder)
640                                                 debug_print("found trash folder from account's trash\n");
641                                 }
642                                 if (save_folder == NULL && mail_filtering_data->account &&
643                                     !mail_filtering_data->account->folder)  {
644                                         if (mail_filtering_data->account->inbox) {
645                                                 FolderItem *item = folder_find_item_from_identifier(
646                                                         mail_filtering_data->account->inbox);
647                                                 if (item && item->folder->trash) {
648                                                         save_folder = item->folder->trash;
649                                                         debug_print("found trash folder from account's inbox\n");
650                                                 }
651                                         } 
652                                         if (!save_folder && mail_filtering_data->account->local_inbox) {
653                                                 FolderItem *item = folder_find_item_from_identifier(
654                                                         mail_filtering_data->account->local_inbox);
655                                                 if (item && item->folder->trash) {
656                                                         save_folder = item->folder->trash;
657                                                         debug_print("found trash folder from account's local_inbox\n");
658                                                 }
659                                         }
660                                 }
661                                 if (save_folder == NULL) {
662                                         debug_print("using default trash folder\n");
663                                         save_folder = folder_get_default_trash();
664                                 }
665                         }
666                         if (save_folder) {
667                                 for (cur = new_spams; cur; cur = cur->next) {
668                                         msginfo = (MsgInfo *)cur->data;
669                                         msginfo->filter_op = IS_MOVE;
670                                         msginfo->to_filter_folder = save_folder;
671                                 }
672                         }
673                 }
674                 if (config.save_unsure && new_unsure) {
675                         FolderItem *save_unsure_folder = NULL;
676
677                         if ((!config.save_unsure_folder) ||
678                             (config.save_unsure_folder[0] == '\0') ||
679                             ((save_unsure_folder = folder_find_item_from_identifier(config.save_unsure_folder)) == NULL)) {
680                                 if (mail_filtering_data->account)
681                                         save_unsure_folder = folder_find_item_from_identifier(
682                                                 mail_filtering_data->account->inbox);
683                                 if (save_unsure_folder == NULL && mail_filtering_data->account &&
684                                     mail_filtering_data->account->folder)
685                                         save_unsure_folder = mail_filtering_data->account->folder->inbox;
686                                 if (save_unsure_folder == NULL && mail_filtering_data->account &&
687                                     !mail_filtering_data->account->folder)  {
688                                         if (mail_filtering_data->account->inbox) {
689                                                 FolderItem *item = folder_find_item_from_identifier(
690                                                         mail_filtering_data->account->inbox);
691                                                 if (item) {
692                                                         save_unsure_folder = item;
693                                                 }
694                                         } 
695                                         if (!save_unsure_folder && mail_filtering_data->account->local_inbox) {
696                                                 FolderItem *item = folder_find_item_from_identifier(
697                                                         mail_filtering_data->account->local_inbox);
698                                                 if (item) {
699                                                         save_unsure_folder = item;
700                                                 }
701                                         }
702                                 }
703                                 if (save_unsure_folder == NULL)
704                                         save_unsure_folder = folder_get_default_inbox();
705                         }
706                         if (save_unsure_folder) {
707                                 for (cur = new_unsure; cur; cur = cur->next) {
708                                         msginfo = (MsgInfo *)cur->data;
709                                         msginfo->filter_op = IS_MOVE;
710                                         msginfo->to_filter_folder = save_unsure_folder;
711                                 }
712                         }
713                 }
714         } 
715         g_slist_free(new_hams);
716         g_slist_free(new_unsure);
717         g_slist_free(new_spams);
718         g_slist_free(whitelisted_new_spams);
719
720         if (message_callback != NULL)
721                 message_callback(NULL, 0, 0, FALSE);
722         mail_filtering_data->filtered   = g_slist_reverse(
723                 mail_filtering_data->filtered);
724         mail_filtering_data->unfiltered = g_slist_reverse(
725                 mail_filtering_data->unfiltered);
726         
727         return FALSE;
728 }
729
730 BogofilterConfig *bogofilter_get_config(void)
731 {
732         return &config;
733 }
734
735 int bogofilter_learn(MsgInfo *msginfo, GSList *msglist, gboolean spam)
736 {
737         gchar *cmd = NULL;
738         gchar *file = NULL;
739         const gchar *bogo_exec = (config.bogopath && *config.bogopath) ? config.bogopath:"bogofilter";
740         gint status = 0;
741         if (msginfo == NULL && msglist == NULL) {
742                 return -1;
743         }
744
745         if (msginfo) {
746                 file = procmsg_get_message_file(msginfo);
747                 if (file == NULL) {
748                         return -1;
749                 } else {
750                         if (message_callback != NULL)
751                                 message_callback(_("Bogofilter: learning from message..."), 0, 0, FALSE);
752                         if (spam)
753                                 /* learn as spam */
754                                 cmd = g_strdup_printf("%s -s -I '%s'", bogo_exec, file);
755                         else if (MSG_IS_SPAM(msginfo->flags))
756                                 /* correct bogofilter, this wasn't spam */
757                                 cmd = g_strdup_printf("%s -Sn -I '%s'", bogo_exec, file);
758                         else 
759                                 /* learn as ham */
760                                 cmd = g_strdup_printf("%s -n -I '%s'", bogo_exec, file);
761                                 
762                         debug_print("%s\n", cmd);
763                         if ((status = execute_command_line(cmd, FALSE)) != 0)
764                                 log_error(LOG_PROTOCOL, _("Learning failed; `%s` returned with status %d."),
765                                                 cmd, status);
766                         g_free(cmd);
767                         g_free(file);
768                         if (message_callback != NULL)
769                                 message_callback(NULL, 0, 0, FALSE);
770                         return 0;
771                 }
772         }
773         if (msglist) {
774                 GSList *cur = msglist;
775                 MsgInfo *info;
776                 int total = g_slist_length(msglist);
777                 int done = 0;
778                 gboolean some_correction = FALSE, some_no_correction = FALSE;
779         
780                 if (message_callback != NULL)
781                         message_callback(_("Bogofilter: learning from messages..."), total, 0, FALSE);
782                 
783                 for (cur = msglist; cur && status == 0; cur = cur->next) {
784                         info = (MsgInfo *)cur->data;
785                         if (spam)
786                                 some_no_correction = TRUE;
787                         else if (MSG_IS_SPAM(info->flags))
788                                 /* correct bogofilter, this wasn't spam */
789                                 some_correction = TRUE;
790                         else 
791                                 some_no_correction = TRUE;
792                         
793                 }
794                 
795                 if (some_correction && some_no_correction) {
796                         /* we potentially have to do different stuff for every mail */
797                         for (cur = msglist; cur && status == 0; cur = cur->next) {
798                                 info = (MsgInfo *)cur->data;
799                                 file = procmsg_get_message_file(info);
800
801                                 if (spam)
802                                         /* learn as spam */
803                                         cmd = g_strdup_printf("%s -s -I '%s'", bogo_exec, file);
804                                 else if (MSG_IS_SPAM(info->flags))
805                                         /* correct bogofilter, this wasn't spam */
806                                         cmd = g_strdup_printf("%s -Sn -I '%s'", bogo_exec, file);
807                                 else 
808                                         /* learn as ham */
809                                         cmd = g_strdup_printf("%s -n -I '%s'", bogo_exec, file);
810                                 
811                                 debug_print("%s\n", cmd);
812                                 if ((status = execute_command_line(cmd, FALSE)) != 0)
813                                         log_error(LOG_PROTOCOL, _("Learning failed; `%s` returned with status %d."),
814                                                         cmd, status);
815
816                                 g_free(cmd);
817                                 g_free(file);
818                                 done++;
819                                 if (message_callback != NULL)
820                                         message_callback(NULL, total, done, FALSE);
821                         }
822                 } else if (some_correction || some_no_correction) {
823                         cur = msglist;
824                         
825                         gchar *bogo_args[4];
826                         GPid bogo_pid;
827                         gint bogo_stdin;
828                         GError *error = NULL;
829                         gboolean bogo_forked;
830
831                         bogo_args[0] = (gchar *)bogo_exec;
832                         if (some_correction && !some_no_correction)
833                                 bogo_args[1] = "-Sn";
834                         else if (some_no_correction && !some_correction)
835                                 bogo_args[1] = spam ? "-s":"-n";
836                         bogo_args[2] = "-b";
837                         bogo_args[3] = NULL;
838                         debug_print("|%s %s %s ...\n", bogo_args[0], bogo_args[1], bogo_args[2]);
839                         bogo_forked = g_spawn_async_with_pipes(
840                                         NULL, bogo_args,NULL, G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD,
841                                         NULL, NULL, &bogo_pid, &bogo_stdin,
842                                         NULL, NULL, &error);
843
844                         while (bogo_forked && cur) {
845                                 gchar *tmp = NULL;
846                                 info = (MsgInfo *)cur->data;
847                                 file = procmsg_get_message_file(info);
848                                 if (file) {
849                                         tmp = g_strdup_printf("%s\n", 
850                                                 file);
851                                         write_all(bogo_stdin, tmp, strlen(tmp));
852                                         g_free(tmp);
853                                 }
854                                 g_free(file);
855                                 done++;
856                                 if (message_callback != NULL)
857                                         message_callback(NULL, total, done, FALSE);
858                                 cur = cur->next;
859                         }
860                         if (bogo_forked) {
861                                 close(bogo_stdin);
862                                 waitpid(bogo_pid, &status, 0);
863                                 if (!WIFEXITED(status))
864                                         status = -1;
865                                 else
866                                         status = WEXITSTATUS(status);
867                         }
868                         if (!bogo_forked || status != 0) {
869                                 log_error(LOG_PROTOCOL, _("Learning failed; `%s %s %s` returned with error:\n%s"),
870                                                 bogo_args[0], bogo_args[1], bogo_args[2], 
871                                                 error ? error->message:_("Unknown error"));
872                                 if (error)
873                                         g_error_free(error);
874                         }
875
876                 }
877
878                 if (message_callback != NULL)
879                         message_callback(NULL, 0, 0, FALSE);
880                 return 0;
881         }
882         return -1;
883 }
884
885 void bogofilter_save_config(void)
886 {
887         PrefFile *pfile;
888         gchar *rcpath;
889
890         debug_print("Saving Bogofilter Page\n");
891
892         rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
893         pfile = prefs_write_open(rcpath);
894         g_free(rcpath);
895         if (!pfile || (prefs_set_block_label(pfile, "Bogofilter") < 0))
896                 return;
897
898         if (prefs_write_param(param, pfile->fp) < 0) {
899                 g_warning("Failed to write Bogofilter configuration to file\n");
900                 prefs_file_close_revert(pfile);
901                 return;
902         }
903         if (fprintf(pfile->fp, "\n") < 0) {
904                 FILE_OP_ERROR(rcpath, "fprintf");
905                 prefs_file_close_revert(pfile);
906         } else
907                 prefs_file_close(pfile);
908 }
909
910 void bogofilter_set_message_callback(MessageCallback callback)
911 {
912         message_callback = callback;
913 }
914
915 gint plugin_init(gchar **error)
916 {
917         gchar *rcpath;
918
919         hook_id = -1;
920
921         if (!check_plugin_version(MAKE_NUMERIC_VERSION(2,9,2,72),
922                                 VERSION_NUMERIC, PLUGIN_NAME, error))
923                 return -1;
924
925         prefs_set_default(param);
926         rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
927         prefs_read_config(param, "Bogofilter", rcpath, NULL);
928         g_free(rcpath);
929
930         bogofilter_gtk_init();
931                 
932         debug_print("Bogofilter plugin loaded\n");
933
934 #ifdef USE_PTHREAD
935         bogofilter_start_thread();
936 #endif
937
938         if (config.process_emails) {
939                 bogofilter_register_hook();
940         }
941
942         procmsg_register_spam_learner(bogofilter_learn);
943         procmsg_spam_set_folder(config.save_folder, bogofilter_get_spam_folder);
944
945         return 0;
946         
947 }
948
949 FolderItem *bogofilter_get_spam_folder(MsgInfo *msginfo)
950 {
951         FolderItem *item = folder_find_item_from_identifier(config.save_folder);
952
953         if (item || msginfo == NULL || msginfo->folder == NULL)
954                 return item;
955
956         if (msginfo->folder->folder &&
957             msginfo->folder->folder->account && 
958             msginfo->folder->folder->account->set_trash_folder) {
959                 item = folder_find_item_from_identifier(
960                         msginfo->folder->folder->account->trash_folder);
961         }
962
963         if (item == NULL && 
964             msginfo->folder->folder &&
965             msginfo->folder->folder->trash)
966                 item = msginfo->folder->folder->trash;
967                 
968         if (item == NULL)
969                 item = folder_get_default_trash();
970                 
971         debug_print("bogo spam dir: %s\n", folder_item_get_path(item));
972         return item;
973 }
974
975 gboolean plugin_done(void)
976 {
977         if (hook_id != -1) {
978                 bogofilter_unregister_hook();
979         }
980 #ifdef USE_PTHREAD
981         bogofilter_stop_thread();
982 #endif
983         g_free(config.save_folder);
984         bogofilter_gtk_done();
985         procmsg_unregister_spam_learner(bogofilter_learn);
986         procmsg_spam_set_folder(NULL, NULL);
987         debug_print("Bogofilter plugin unloaded\n");
988         return TRUE;
989 }
990
991 const gchar *plugin_name(void)
992 {
993         return PLUGIN_NAME;
994 }
995
996 const gchar *plugin_desc(void)
997 {
998         return _("This plugin can check all messages that are received from an "
999                  "IMAP, LOCAL or POP account for spam using Bogofilter. "
1000                  "You will need Bogofilter installed locally.\n"
1001                  "\n"
1002                  "Before Bogofilter can recognize spam messages, you have to "
1003                  "train it by marking a few hundred spam and ham messages "
1004                  "with the use of \"/Mark/Mark as spam\" and \"/Mark/Mark as "
1005                  "ham\".\n"
1006                  "\n"
1007                  "When a message is identified as spam it can be deleted or "
1008                  "saved in a specially designated folder.\n"
1009                  "\n"
1010                  "Options can be found in /Configuration/Preferences/Plugins/Bogofilter");
1011 }
1012
1013 const gchar *plugin_type(void)
1014 {
1015         return "GTK2";
1016 }
1017
1018 const gchar *plugin_licence(void)
1019 {
1020         return "GPL3+";
1021 }
1022
1023 const gchar *plugin_version(void)
1024 {
1025         return VERSION;
1026 }
1027
1028 struct PluginFeature *plugin_provides(void)
1029 {
1030         static struct PluginFeature features[] = 
1031                 { {PLUGIN_FILTERING, N_("Spam detection")},
1032                   {PLUGIN_FILTERING, N_("Spam learning")},
1033                   {PLUGIN_NOTHING, NULL}};
1034         return features;
1035 }
1036
1037 void bogofilter_register_hook(void)
1038 {
1039         if (hook_id == -1)
1040                 hook_id = hooks_register_hook(MAIL_LISTFILTERING_HOOKLIST, mail_filtering_hook, NULL);
1041         if (hook_id == -1) {
1042                 g_warning("Failed to register mail filtering hook");
1043                 config.process_emails = FALSE;
1044         }
1045 }
1046
1047 void bogofilter_unregister_hook(void)
1048 {
1049         if (hook_id != -1) {
1050                 hooks_unregister_hook(MAIL_LISTFILTERING_HOOKLIST, hook_id);
1051         }
1052         hook_id = -1;
1053 }