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