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