2011-10-23 [colin] 3.7.10cvs44
[claws.git] / src / filtering.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2011 Hiroyuki Yamamoto & The Claws Mail Team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  * 
18  */
19
20 #include "defs.h"
21 #include <glib.h>
22 #include <glib/gi18n.h>
23 #include <ctype.h>
24 #include <string.h>
25 #include <stdlib.h>
26 #include <errno.h>
27 #include <gtk/gtk.h>
28 #include <stdio.h>
29
30 #include "utils.h"
31 #include "procheader.h"
32 #include "matcher.h"
33 #include "filtering.h"
34 #include "prefs_gtk.h"
35 #include "compose.h"
36 #include "prefs_common.h"
37 #include "addrbook.h"
38 #include "addr_compl.h"
39 #include "tags.h"
40 #include "log.h"
41
42 GSList * pre_global_processing = NULL;
43 GSList * post_global_processing = NULL;
44 GSList * filtering_rules = NULL;
45
46 gboolean debug_filtering_session = FALSE;
47
48 static gboolean filtering_is_final_action(FilteringAction *filtering_action);
49
50 #define STRLEN_WITH_CHECK(expr) \
51         strlen_with_check(#expr, __LINE__, expr)
52                 
53 static inline gint strlen_with_check(const gchar *expr, gint fline, const gchar *str)
54 {
55         if (str) 
56                 return strlen(str);
57         else {
58                 debug_print("%s(%d) - invalid string %s\n", __FILE__, fline, expr?expr:"(null)");
59                 return 0;
60         }
61 }
62
63 FilteringAction * filteringaction_new(int type, int account_id,
64                                       gchar * destination,
65                                       gint labelcolor, gint score, gchar * header)
66 {
67         FilteringAction * action;
68
69         action = g_new0(FilteringAction, 1);
70
71         action->type = type;
72         action->account_id = account_id;
73         if (destination) {
74                 action->destination       = g_strdup(destination);
75         } else {
76                 action->destination       = NULL;
77         }
78         if (header) {
79                 action->header    = g_strdup(header);
80         } else {
81                 action->header       = NULL;
82         }
83         action->labelcolor = labelcolor;        
84         action->score = score;
85         return action;
86 }
87
88 void filteringaction_free(FilteringAction * action)
89 {
90         cm_return_if_fail(action);
91         g_free(action->header);
92         g_free(action->destination);
93         g_free(action);
94 }
95
96 FilteringProp * filteringprop_new(gboolean enabled,
97                                   const gchar *name,
98                                   gint account_id,
99                                   MatcherList * matchers,
100                                   GSList * action_list)
101 {
102         FilteringProp * filtering;
103
104         filtering = g_new0(FilteringProp, 1);
105         filtering->enabled = enabled;
106         filtering->name = name ? g_strdup(name): NULL;
107         filtering->account_id = account_id;
108         filtering->matchers = matchers;
109         filtering->action_list = action_list;
110
111         return filtering;
112 }
113
114 static FilteringAction * filteringaction_copy(FilteringAction * src)
115 {
116         FilteringAction * new;
117         
118         new = g_new0(FilteringAction, 1);
119         
120         new->type = src->type;
121         new->account_id = src->account_id;
122         if (src->destination)
123                 new->destination = g_strdup(src->destination);
124         else 
125                 new->destination = NULL;
126         new->labelcolor = src->labelcolor;
127         new->score = src->score;
128
129         return new;
130 }
131
132 FilteringProp * filteringprop_copy(FilteringProp *src)
133 {
134         FilteringProp * new;
135         GSList *tmp;
136         
137         new = g_new0(FilteringProp, 1);
138         new->matchers = g_new0(MatcherList, 1);
139
140         for (tmp = src->matchers->matchers; tmp != NULL && tmp->data != NULL;) {
141                 MatcherProp *matcher = (MatcherProp *)tmp->data;
142                 
143                 new->matchers->matchers = g_slist_append(new->matchers->matchers,
144                                                    matcherprop_copy(matcher));
145                 tmp = tmp->next;
146         }
147
148         new->matchers->bool_and = src->matchers->bool_and;
149
150         new->action_list = NULL;
151
152         for (tmp = src->action_list ; tmp != NULL ; tmp = tmp->next) {
153                 FilteringAction *filtering_action;
154                 
155                 filtering_action = tmp->data;
156                 
157                 new->action_list = g_slist_append(new->action_list,
158                     filteringaction_copy(filtering_action));
159         }
160
161         new->enabled = src->enabled;
162         new->name = g_strdup(src->name);
163
164         return new;
165 }
166
167 void filteringprop_free(FilteringProp * prop)
168 {
169         GSList * tmp;
170
171         cm_return_if_fail(prop);
172         matcherlist_free(prop->matchers);
173         
174         for (tmp = prop->action_list ; tmp != NULL ; tmp = tmp->next) {
175                 filteringaction_free(tmp->data);
176         }
177         g_slist_free(prop->action_list);
178         g_free(prop->name);
179         g_free(prop);
180 }
181
182 /* move and copy messages by batches to be faster on IMAP */
183 void filtering_move_and_copy_msgs(GSList *msgs)
184 {
185         GSList *messages = g_slist_copy(msgs);
186         FolderItem *last_item = NULL;
187         FiltOp cur_op = IS_NOTHING;
188
189         debug_print("checking %d messages\n", g_slist_length(msgs));
190         while (messages) {
191                 GSList *batch = NULL, *cur;
192                 gint found = 0;
193                 for (cur = messages; cur; cur = cur->next) {
194                         MsgInfo *info = (MsgInfo *)cur->data;
195                         if (last_item == NULL) {
196                                 if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE)
197                                         last_item = info->to_filter_folder;
198                                 else if (info->filter_op == IS_DELE)
199                                         last_item = info->folder;
200                         }
201                         if (last_item == NULL)
202                                 continue;
203                         if (cur_op == IS_NOTHING) {
204                                 if (info->filter_op == IS_COPY)
205                                         cur_op = IS_COPY;
206                                 else if (info->filter_op == IS_MOVE)
207                                         cur_op = IS_MOVE;
208                                 else if (info->filter_op == IS_DELE)
209                                         cur_op = IS_DELE;
210                         }
211                         if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE) {
212                                 if (info->to_filter_folder == last_item 
213                                 &&  cur_op == info->filter_op) {
214                                         found++;
215                                         batch = g_slist_prepend(batch, info);
216                                 }
217                         } else if (info->filter_op == IS_DELE) {
218                                 if (info->folder == last_item 
219                                 &&  cur_op == info->filter_op) {
220                                         found++;
221                                         batch = g_slist_prepend(batch, info);
222                                 }
223                         }
224                 }
225                 if (found == 0) {
226                         debug_print("no more messages to move/copy/del\n");
227                         break;
228                 } else {
229                         debug_print("%d messages to %s in %s\n", found,
230                                 cur_op==IS_COPY ? "copy":(cur_op==IS_DELE ?"delete":"move"), 
231                                 last_item?(last_item->name ? last_item->name:"(noname)"):"nowhere");
232                 }
233                 for (cur = batch; cur; cur = cur->next) {
234                         MsgInfo *info = (MsgInfo *)cur->data;
235                         messages = g_slist_remove(messages, info);
236                         info->to_filter_folder = NULL;
237                         info->filter_op = IS_NOTHING;
238                 }
239                 batch = g_slist_reverse(batch);
240                 if (g_slist_length(batch)) {
241                         MsgInfo *info = (MsgInfo *)batch->data;
242                         if (cur_op == IS_COPY && last_item != info->folder) {
243                                 folder_item_copy_msgs(last_item, batch);
244                         } else if (cur_op == IS_MOVE && last_item != info->folder) {
245                                 if (folder_item_move_msgs(last_item, batch) < 0)
246                                         folder_item_move_msgs(
247                                                 folder_get_default_inbox(), 
248                                                 batch);
249                         } else if (cur_op == IS_DELE && last_item == info->folder) {
250                                 folder_item_remove_msgs(last_item, batch);
251                         }
252                         /* we don't reference the msginfos, because caller will do */
253                         if (prefs_common.real_time_sync)
254                                 folder_item_synchronise(last_item);
255                         g_slist_free(batch);
256                         batch = NULL;
257                         GTK_EVENTS_FLUSH();
258                 }
259                 last_item = NULL;
260                 cur_op = IS_NOTHING;
261         }
262         /* we don't reference the msginfos, because caller will do */
263         g_slist_free(messages);
264 }
265
266 /*
267   fitleringaction_apply
268   runs the action on one MsgInfo
269   return value : return TRUE if the action could be applied
270 */
271
272 #define FLUSH_COPY_IF_NEEDED(info) {                                    \
273         if (info->filter_op == IS_COPY && info->to_filter_folder) {     \
274                 debug_print("must debatch pending copy\n");             \
275                 folder_item_copy_msg(info->to_filter_folder, info);     \
276                 info->filter_op = IS_NOTHING;                           \
277         }                                                               \
278 }
279
280 static gboolean filteringaction_apply(FilteringAction * action, MsgInfo * info)
281 {
282         FolderItem * dest_folder;
283         gint val;
284         Compose * compose;
285         PrefsAccount * account;
286         gchar * cmd;
287
288         switch(action->type) {
289         case MATCHACTION_MOVE:
290                 dest_folder =
291                         folder_find_item_from_identifier(action->destination);
292                 if (!dest_folder) {
293                         debug_print("*** folder not found '%s'\n",
294                                 action->destination ?action->destination :"(null)");
295                         return FALSE;
296                 }
297                 
298                 FLUSH_COPY_IF_NEEDED(info);
299                 /* mark message to be moved */          
300                 info->filter_op = IS_MOVE;
301                 info->to_filter_folder = dest_folder;
302                 return TRUE;
303
304         case MATCHACTION_COPY:
305                 dest_folder =
306                         folder_find_item_from_identifier(action->destination);
307
308                 if (!dest_folder) {
309                         debug_print("*** folder not found '%s'\n",
310                                 action->destination ?action->destination :"(null)");
311                         return FALSE;
312                 }
313
314                 FLUSH_COPY_IF_NEEDED(info);
315                 /* mark message to be copied */         
316                 info->filter_op = IS_COPY;
317                 info->to_filter_folder = dest_folder;
318                 return TRUE;
319
320         case MATCHACTION_SET_TAG:
321         case MATCHACTION_UNSET_TAG:
322                 val = tags_get_id_for_str(action->destination);
323                 if (val == -1) {
324                         debug_print("*** tag '%s' not found\n",
325                                 action->destination ?action->destination :"(null)");
326                         return FALSE;
327                 }
328                 FLUSH_COPY_IF_NEEDED(info);
329                 procmsg_msginfo_update_tags(info, (action->type == MATCHACTION_SET_TAG), val);
330                 return TRUE;
331
332         case MATCHACTION_CLEAR_TAGS:
333                 FLUSH_COPY_IF_NEEDED(info);
334                 procmsg_msginfo_clear_tags(info);
335                 return TRUE;
336
337         case MATCHACTION_DELETE:
338                 FLUSH_COPY_IF_NEEDED(info);
339                 info->filter_op = IS_DELE;
340                 return TRUE;
341
342         case MATCHACTION_MARK:
343                 FLUSH_COPY_IF_NEEDED(info);
344                 procmsg_msginfo_set_flags(info, MSG_MARKED, 0);
345                 return TRUE;
346
347         case MATCHACTION_UNMARK:
348                 FLUSH_COPY_IF_NEEDED(info);
349                 procmsg_msginfo_unset_flags(info, MSG_MARKED, 0);
350                 return TRUE;
351
352         case MATCHACTION_LOCK:
353                 FLUSH_COPY_IF_NEEDED(info);
354                 procmsg_msginfo_set_flags(info, MSG_LOCKED, 0);
355                 return TRUE;
356
357         case MATCHACTION_UNLOCK:
358                 FLUSH_COPY_IF_NEEDED(info);
359                 procmsg_msginfo_unset_flags(info, MSG_LOCKED, 0);       
360                 return TRUE;
361                 
362         case MATCHACTION_MARK_AS_READ:
363                 FLUSH_COPY_IF_NEEDED(info);
364                 procmsg_msginfo_unset_flags(info, MSG_UNREAD | MSG_NEW, 0);
365                 return TRUE;
366
367         case MATCHACTION_MARK_AS_UNREAD:
368                 FLUSH_COPY_IF_NEEDED(info);
369                 procmsg_msginfo_set_flags(info, MSG_UNREAD, 0);
370                 return TRUE;
371         
372         case MATCHACTION_MARK_AS_SPAM:
373                 FLUSH_COPY_IF_NEEDED(info);
374                 procmsg_spam_learner_learn(info, NULL, TRUE);
375                 procmsg_msginfo_change_flags(info, MSG_SPAM, 0, MSG_NEW|MSG_UNREAD, 0);
376                 if (procmsg_spam_get_folder(info)) {
377                         info->filter_op = IS_MOVE;
378                         info->to_filter_folder = procmsg_spam_get_folder(info);
379                 }
380                 return TRUE;
381
382         case MATCHACTION_MARK_AS_HAM:
383                 FLUSH_COPY_IF_NEEDED(info);
384                 procmsg_spam_learner_learn(info, NULL, FALSE);
385                 procmsg_msginfo_unset_flags(info, MSG_SPAM, 0);
386                 return TRUE;
387         
388         case MATCHACTION_COLOR:
389                 FLUSH_COPY_IF_NEEDED(info);
390                 procmsg_msginfo_unset_flags(info, MSG_CLABEL_FLAG_MASK, 0); 
391                 procmsg_msginfo_set_flags(info, MSG_COLORLABEL_TO_FLAGS(action->labelcolor), 0);
392                 return TRUE;
393
394         case MATCHACTION_FORWARD:
395         case MATCHACTION_FORWARD_AS_ATTACHMENT:
396                 account = account_find_from_id(action->account_id);
397                 compose = compose_forward(account, info,
398                         action->type == MATCHACTION_FORWARD ? FALSE : TRUE,
399                         NULL, TRUE, TRUE);
400                 compose_entry_append(compose, action->destination,
401                                      compose->account->protocol == A_NNTP
402                                             ? COMPOSE_NEWSGROUPS
403                                             : COMPOSE_TO, PREF_NONE);
404
405                 val = compose_send(compose);
406
407                 return val == 0 ? TRUE : FALSE;
408
409         case MATCHACTION_REDIRECT:
410                 account = account_find_from_id(action->account_id);
411                 compose = compose_redirect(account, info, TRUE);
412                 if (compose->account->protocol == A_NNTP)
413                         break;
414                 else
415                         compose_entry_append(compose, action->destination,
416                                              COMPOSE_TO, PREF_NONE);
417
418                 val = compose_send(compose);
419                 
420                 return val == 0 ? TRUE : FALSE;
421
422         case MATCHACTION_EXECUTE:
423                 cmd = matching_build_command(action->destination, info);
424                 if (cmd == NULL)
425                         return FALSE;
426                 else {
427                         if (system(cmd) == -1)
428                                 g_warning("couldn't run %s", cmd);
429                         g_free(cmd);
430                 }
431                 return TRUE;
432
433         case MATCHACTION_SET_SCORE:
434                 FLUSH_COPY_IF_NEEDED(info);
435                 info->score = action->score;
436                 return TRUE;
437
438         case MATCHACTION_CHANGE_SCORE:
439                 FLUSH_COPY_IF_NEEDED(info);
440                 info->score += action->score;
441                 return TRUE;
442
443         case MATCHACTION_STOP:
444                 return FALSE;
445
446         case MATCHACTION_HIDE:
447                 FLUSH_COPY_IF_NEEDED(info);
448                 info->hidden = TRUE;
449                 return TRUE;
450
451         case MATCHACTION_IGNORE:
452                 FLUSH_COPY_IF_NEEDED(info);
453                 procmsg_msginfo_set_flags(info, MSG_IGNORE_THREAD, 0);
454                 return TRUE;
455
456         case MATCHACTION_WATCH:
457                 FLUSH_COPY_IF_NEEDED(info);
458                 procmsg_msginfo_set_flags(info, MSG_WATCH_THREAD, 0);
459                 return TRUE;
460
461         case MATCHACTION_ADD_TO_ADDRESSBOOK:
462                 {
463                         AddressDataSource *book = NULL;
464                         AddressBookFile *abf = NULL;
465                         ItemFolder *folder = NULL;
466                         gchar buf[BUFFSIZE];
467                         Header *header;
468                         gint errors = 0;
469
470                         if (!addressbook_peek_folder_exists(action->destination, &book, &folder)) {
471                                 g_warning("addressbook folder not found '%s'\n", action->destination?action->destination:"(null)");
472                                 return FALSE;
473                         }
474                         if (!book) {
475                                 g_warning("addressbook_peek_folder_exists returned NULL book\n");
476                                 return FALSE;
477                         }
478
479                         abf = book->rawDataSource;
480
481                         /* get the header */
482                         procheader_get_header_from_msginfo(info, buf, sizeof(buf), action->header);
483                         header = procheader_parse_header(buf);
484
485                         /* add all addresses that are not already in */
486                         if (header && *header->body && (*header->body != '\0')) {
487                                 GSList *address_list = NULL;
488                                 GSList *walk = NULL;
489                                 gchar *path = NULL;
490
491                                 if (action->destination == NULL ||
492                                                 strcasecmp(action->destination, "Any") == 0 ||
493                                                 *(action->destination) == '\0')
494                                         path = NULL;
495                                 else
496                                         path = action->destination;
497                                 start_address_completion(path);
498
499                                 address_list = address_list_append(address_list, header->body);
500                                 for (walk = address_list; walk != NULL; walk = walk->next) {
501                                         gchar *stripped_addr = g_strdup(walk->data);
502                                         extract_address(stripped_addr);
503
504                                         if (complete_matches_found(walk->data) == 0) {
505                                                 debug_print("adding address '%s' to addressbook '%s'\n",
506                                                                 stripped_addr, action->destination);
507                                                 if (!addrbook_add_contact(abf, folder, stripped_addr, stripped_addr, NULL)) {
508                                                         g_warning("contact could not been added\n");
509                                                         errors++;
510                                                 }
511                                         } else {
512                                                 debug_print("address '%s' already found in addressbook '%s', skipping\n",
513                                                                 stripped_addr, action->destination);
514                                         }
515                                         g_free(stripped_addr);
516                                 }
517
518                                 g_slist_free(address_list);
519                                 end_address_completion();
520                         } else {
521                                 g_warning("header '%s' not set or empty\n", action->header?action->header:"(null)");
522                         }
523                         return (errors == 0);
524                 }
525
526         default:
527                 break;
528         }
529         return FALSE;
530 }
531
532 gboolean filteringaction_apply_action_list(GSList *action_list, MsgInfo *info)
533 {
534         GSList *p;
535         cm_return_val_if_fail(action_list, FALSE);
536         cm_return_val_if_fail(info, FALSE);
537         for (p = action_list; p && p->data; p = g_slist_next(p)) {
538                 FilteringAction *a = (FilteringAction *) p->data;
539                 if (filteringaction_apply(a, info)) {
540                         if (filtering_is_final_action(a))
541                                 break;
542                 } else
543                         return FALSE;
544                 
545         }
546         return TRUE;
547 }
548
549 static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info,
550                                                         PrefsAccount *ac_prefs)
551
552 /* this function returns true if a filtering rule applies regarding to its account
553    data and if it does, if the conditions list match.
554
555    per-account data of a filtering rule is either matched against current account
556    when filtering is done manually, or against the account currently used for
557    retrieving messages when it's an manual or automatic fetching of messages.
558    per-account data match doesn't apply to pre-/post-/folder-processing rules.
559
560    when filtering messages manually:
561     - either the filtering rule is not account-based and it will be processed
562         - or it's per-account and we check if we HAVE TO match it against the current
563           account (according to user-land settings, per-account rules might have to
564           be skipped, or only the rules that match the current account have to be
565           applied, or all rules will have to be applied regardless to if they are
566           account-based or not)
567
568    notes about debugging output in that function:
569    when not matching, log_status_skip() is used, otherwise log_status_ok() is used
570    no debug output is done when filtering_debug_level is low
571 */
572 {
573         gboolean matches = FALSE;
574
575         if (ac_prefs != NULL) {
576                 matches = ((filtering->account_id == 0)
577                                         || (filtering->account_id == ac_prefs->account_id));
578
579                 /* debug output */
580                 if (debug_filtering_session) {
581                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
582                                 if (filtering->account_id == 0) {
583                                         log_status_ok(LOG_DEBUG_FILTERING,
584                                                         _("rule is not account-based\n"));
585                                 } else {
586                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
587                                                 log_status_ok(LOG_DEBUG_FILTERING,
588                                                                 _("rule is account-based [id=%d, name='%s'], "
589                                                                 "matching the account currently used to retrieve messages\n"),
590                                                                 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
591                                         }
592                                 }
593                         }
594                         else
595                         if (!matches) {
596                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
597                                         log_status_skip(LOG_DEBUG_FILTERING,
598                                                         _("rule is account-based, "
599                                                         "not matching the account currently used to retrieve messages\n"));
600                                 } else {
601                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
602                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
603
604                                                 log_status_skip(LOG_DEBUG_FILTERING,
605                                                                 _("rule is account-based [id=%d, name='%s'], "
606                                                                 "not matching the account currently used to retrieve messages [id=%d, name='%s']\n"),
607                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
608                                                                 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
609                                         }
610                                 }
611                         }
612                 }
613         } else {
614                 switch (prefs_common.apply_per_account_filtering_rules) {
615                 case FILTERING_ACCOUNT_RULES_FORCE:
616                         /* apply filtering rules regardless to the account info */
617                         matches = TRUE;
618
619                         /* debug output */
620                         if (debug_filtering_session) {
621                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
622                                         if (filtering->account_id == 0) {
623                                                 log_status_ok(LOG_DEBUG_FILTERING,
624                                                                 _("rule is not account-based, "
625                                                                 "all rules are applied on user request anyway\n"));
626                                         } else {
627                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
628
629                                                 log_status_ok(LOG_DEBUG_FILTERING,
630                                                                 _("rule is account-based [id=%d, name='%s'], "
631                                                                 "but all rules are applied on user request\n"),
632                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
633                                         }
634                                 }
635                         }
636                         break;
637                 case FILTERING_ACCOUNT_RULES_SKIP:
638                         /* don't apply filtering rules that belong to an account */
639                         matches = (filtering->account_id == 0);
640
641                         /* debug output */
642                         if (debug_filtering_session) {
643                                 if (!matches) {
644                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
645                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
646
647                                                 log_status_skip(LOG_DEBUG_FILTERING,
648                                                                 _("rule is account-based [id=%d, name='%s'], "
649                                                                 "skipped on user request\n"),
650                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
651                                         } else {
652                                                 log_status_skip(LOG_DEBUG_FILTERING,
653                                                                 _("rule is account-based, "
654                                                                 "skipped on user request\n"));
655                                         }
656                                 } else {
657                                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
658                                                 log_status_ok(LOG_DEBUG_FILTERING,
659                                                                 _("rule is not account-based\n"));
660                                         }
661                                 }
662                         }
663                         break;
664                 case FILTERING_ACCOUNT_RULES_USE_CURRENT:
665                         matches = ((filtering->account_id == 0)
666                                         || (filtering->account_id == cur_account->account_id));
667
668                         /* debug output */
669                         if (debug_filtering_session) {
670                                 if (!matches) {
671                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
672                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
673
674                                                 log_status_skip(LOG_DEBUG_FILTERING,
675                                                                 _("rule is account-based [id=%d, name='%s'], "
676                                                                 "not matching current account [id=%d, name='%s']\n"),
677                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
678                                                                 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
679                                         } else {
680                                                 log_status_skip(LOG_DEBUG_FILTERING,
681                                                                 _("rule is account-based, "
682                                                                 "not matching current account\n"));
683                                         }
684                                 } else {
685                                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
686                                                 if (filtering->account_id == 0) {
687                                                         log_status_ok(LOG_DEBUG_FILTERING,
688                                                                         _("rule is not account-based\n"));
689                                                 } else {
690                                                         PrefsAccount *account = account_find_from_id(filtering->account_id);
691
692                                                         log_status_ok(LOG_DEBUG_FILTERING,
693                                                                         _("rule is account-based [id=%d, name='%s'], "
694                                                                         "current account [id=%d, name='%s']\n"),
695                                                                         account->account_id, account?account->account_name:_("NON_EXISTENT"),
696                                                                         cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
697                                                 }
698                                         }
699                                 }
700                         }
701                         break;
702                 }
703         }
704
705         return matches && matcherlist_match(filtering->matchers, info);
706 }
707
708 /*!
709  *\brief        Apply a rule on message.
710  *
711  *\param        filtering List of filtering rules.
712  *\param        info Message to apply rules on.
713  *\param        final Variable returning TRUE or FALSE if one of the
714  *              encountered actions was final. 
715  *              See also \ref filtering_is_final_action.
716  *
717  *\return       gboolean TRUE to continue applying rules.
718  */
719 static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info,
720     gboolean * final)
721 {
722         gboolean result = TRUE;
723         gchar    *buf;
724         GSList * tmp;
725         
726         * final = FALSE;
727         for (tmp = filtering->action_list ; tmp != NULL ; tmp = tmp->next) {
728                 FilteringAction * action;
729
730                 action = tmp->data;
731                 buf = filteringaction_to_string(action);
732                 if (debug_filtering_session)
733                         log_print(LOG_DEBUG_FILTERING, _("applying action [ %s ]\n"), buf);
734
735                 if (FALSE == (result = filteringaction_apply(action, info))) {
736                                         if (debug_filtering_session) {
737                                                 if (action->type != MATCHACTION_STOP)
738                                                         log_warning(LOG_DEBUG_FILTERING, _("action could not apply\n"));
739                                                 log_print(LOG_DEBUG_FILTERING,
740                                                                 _("no further processing after action [ %s ]\n"), buf);
741                                         }
742                                         debug_print("No further processing after rule %s\n", buf);
743                 }
744                 g_free(buf);
745                 if (filtering_is_final_action(action)) {
746                         * final = TRUE;
747                         break;
748                 }
749                 
750         }
751         return result;
752 }
753
754 /*!
755  *\brief        Check if an action is "final", i.e. should break further
756  *              processing.
757  *
758  *\param        filtering_action Action to check.
759  *
760  *\return       gboolean TRUE if \a filtering_action is final.  
761  */
762 static gboolean filtering_is_final_action(FilteringAction *filtering_action)
763 {
764         switch(filtering_action->type) {
765         case MATCHACTION_MOVE:
766         case MATCHACTION_DELETE:
767         case MATCHACTION_STOP:
768         case MATCHACTION_MARK_AS_SPAM:
769                 return TRUE; /* MsgInfo invalid for message */
770         default:
771                 return FALSE;
772         }
773 }
774
775 static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
776 {
777         GSList  *l;
778         gboolean final;
779         gboolean apply_next;
780         
781         cm_return_val_if_fail(info != NULL, TRUE);
782         
783         for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
784                 FilteringProp * filtering = (FilteringProp *) l->data;
785
786                 if (filtering->enabled) {
787                         if (debug_filtering_session) {
788                                 gchar *buf = filteringprop_to_string(filtering);
789                                 if (filtering->name && *filtering->name != '\0') {
790                                         log_print(LOG_DEBUG_FILTERING,
791                                                 _("processing rule '%s' [ %s ]\n"),
792                                                 filtering->name, buf);
793                                 } else {
794                                         log_print(LOG_DEBUG_FILTERING,
795                                                 _("processing rule <unnamed> [ %s ]\n"),
796                                                 buf);
797                                 }
798                                 g_free(buf);
799                         }
800
801                         if (filtering_match_condition(filtering, info, ac_prefs)) {
802                                 apply_next = filtering_apply_rule(filtering, info, &final);
803                                 if (final)
804                                         break;
805                         }
806
807                 } else {
808                         if (debug_filtering_session) {
809                                 gchar *buf = filteringprop_to_string(filtering);
810                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
811                                         if (filtering->name && *filtering->name != '\0') {
812                                                 log_status_skip(LOG_DEBUG_FILTERING,
813                                                                 _("disabled rule '%s' [ %s ]\n"),
814                                                                 filtering->name, buf);
815                                         } else {
816                                                 log_status_skip(LOG_DEBUG_FILTERING,
817                                                                 _("disabled rule <unnamed> [ %s ]\n"),
818                                                                 buf);
819                                         }
820                                 }
821                                 g_free(buf);
822                         }
823                 }
824         }
825
826     /* put in inbox if the last rule was not a final one, or
827      * a final rule could not be applied.
828      * Either of these cases is likely. */
829     if (!final || !apply_next) {
830                 return FALSE;
831         }
832
833         return TRUE;
834 }
835
836 /*!
837  *\brief        Filter a message against a list of rules.
838  *
839  *\param        flist List of filter rules.
840  *\param        info Message.
841  *
842  *\return       gboolean TRUE if filter rules handled the message.
843  *
844  *\note         Returning FALSE means the message was not handled,
845  *              and that the calling code should do the default
846  *              processing. E.g. \ref inc.c::inc_start moves the 
847  *              message to the inbox.   
848  */
849 gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
850                                                                    FilteringInvocationType context, gchar *extra_info)
851 {
852         gboolean ret;
853
854         if (prefs_common.enable_filtering_debug) {
855                 gchar *tmp = _("undetermined");
856 #ifndef G_OS_WIN32
857                 switch (context) {
858                 case FILTERING_INCORPORATION:
859                         tmp = _("incorporation");
860                         debug_filtering_session = prefs_common.enable_filtering_debug_inc;
861                         break;
862                 case FILTERING_MANUALLY:
863                         tmp = _("manually");
864                         debug_filtering_session = prefs_common.enable_filtering_debug_manual;
865                         break;
866                 case FILTERING_FOLDER_PROCESSING:
867                         tmp = _("folder processing");
868                         debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
869                         break;
870                 case FILTERING_PRE_PROCESSING:
871                         tmp = _("pre-processing");
872                         debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
873                         break;
874                 case FILTERING_POST_PROCESSING:
875                         tmp = _("post-processing");
876                         debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
877                         break;
878                 default:
879                         debug_filtering_session = FALSE;
880                         break;
881                 }
882 #else
883                 debug_filtering_session = FALSE;
884 #endif
885                 if (debug_filtering_session) {
886                         gchar *file = procmsg_get_message_file_path(info);
887                         gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');
888
889                         /* show context info and essential info about the message */
890                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
891                                 log_print(LOG_DEBUG_FILTERING,
892                                                 _("filtering message (%s%s%s)\n"
893                                                 "%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
894                                                 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
895                                                 spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
896                                                 spc, prefs_common_translated_header_name("From:"), info->from,
897                                                 spc, prefs_common_translated_header_name("To:"), info->to,
898                                                 spc, prefs_common_translated_header_name("Subject:"), info->subject);
899                         } else {
900                                 log_print(LOG_DEBUG_FILTERING,
901                                                 _("filtering message (%s%s%s)\n"
902                                                 "%smessage file: %s\n"),
903                                                 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
904                                                 spc, file);
905                         }
906                         g_free(file);
907                         g_free(spc);
908                 }
909         } else
910                 debug_filtering_session = FALSE;
911
912         ret = filter_msginfo(flist, info, ac_prefs);
913         debug_filtering_session = FALSE;
914         return ret;
915 }
916
917 gchar *filteringaction_to_string(FilteringAction *action)
918 {
919         const gchar *command_str;
920         gchar * quoted_dest;
921         gchar * quoted_header;
922         GString *dest = g_string_new("");
923         gchar *deststr = NULL;
924
925         command_str = get_matchparser_tab_str(action->type);
926
927         if (command_str == NULL)
928                 return NULL;
929
930         switch(action->type) {
931         case MATCHACTION_MOVE:
932         case MATCHACTION_COPY:
933         case MATCHACTION_EXECUTE:
934         case MATCHACTION_SET_TAG:
935         case MATCHACTION_UNSET_TAG:
936                 quoted_dest = matcher_quote_str(action->destination);
937                 g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
938                 g_free(quoted_dest);
939                 break;
940
941         case MATCHACTION_DELETE:
942         case MATCHACTION_MARK:
943         case MATCHACTION_UNMARK:
944         case MATCHACTION_LOCK:
945         case MATCHACTION_UNLOCK:
946         case MATCHACTION_MARK_AS_READ:
947         case MATCHACTION_MARK_AS_UNREAD:
948         case MATCHACTION_MARK_AS_SPAM:
949         case MATCHACTION_MARK_AS_HAM:
950         case MATCHACTION_STOP:
951         case MATCHACTION_HIDE:
952         case MATCHACTION_IGNORE:
953         case MATCHACTION_WATCH:
954         case MATCHACTION_CLEAR_TAGS:
955                 g_string_append_printf(dest, "%s", command_str);
956                 break;
957
958         case MATCHACTION_REDIRECT:
959         case MATCHACTION_FORWARD:
960         case MATCHACTION_FORWARD_AS_ATTACHMENT:
961                 quoted_dest = matcher_quote_str(action->destination);
962                 g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
963                 g_free(quoted_dest);
964                 break;
965
966         case MATCHACTION_COLOR:
967                 g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
968                 break;
969
970         case MATCHACTION_CHANGE_SCORE:
971         case MATCHACTION_SET_SCORE:
972                 g_string_append_printf(dest, "%s %d", command_str, action->score);
973                 break;
974
975         case MATCHACTION_ADD_TO_ADDRESSBOOK:
976                 quoted_header = matcher_quote_str(action->header);
977                 quoted_dest = matcher_quote_str(action->destination);
978                 g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
979                 g_free(quoted_dest);
980                 g_free(quoted_header);
981                 break;
982
983         default:
984                 return NULL;
985         }
986         deststr = dest->str;
987         g_string_free(dest, FALSE);
988         return deststr;
989 }
990
991 gchar * filteringaction_list_to_string(GSList * action_list)
992 {
993         gchar *action_list_str;
994         GSList * tmp;
995         gchar *list_str;
996
997         action_list_str = NULL;
998         for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
999                 gchar *action_str;
1000                 FilteringAction * action;
1001                 
1002                 action = tmp->data;
1003                 
1004                 action_str = filteringaction_to_string(action);
1005                 
1006                 if (action_list_str != NULL) {
1007                         list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1008                         g_free(action_list_str);
1009                 }
1010                 else {
1011                         list_str = g_strdup(action_str);
1012                 }
1013                 g_free(action_str);
1014                 action_list_str = list_str;
1015         }
1016
1017         return action_list_str;
1018 }
1019
1020 gchar * filteringprop_to_string(FilteringProp * prop)
1021 {
1022         gchar *list_str;
1023         gchar *action_list_str;
1024         gchar *filtering_str;
1025
1026         if (prop == NULL)
1027                 return NULL;
1028
1029         action_list_str = filteringaction_list_to_string(prop->action_list);
1030
1031         if (action_list_str == NULL)
1032                 return NULL;
1033
1034         list_str = matcherlist_to_string(prop->matchers);
1035
1036         if (list_str == NULL) {
1037                 g_free(action_list_str);
1038                 return NULL;
1039         }
1040
1041         filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1042         g_free(action_list_str);
1043         g_free(list_str);
1044
1045         return filtering_str;
1046 }
1047
1048 static void prefs_filtering_free(GSList * prefs_filtering)
1049 {
1050         while (prefs_filtering != NULL) {
1051                 FilteringProp * filtering = (FilteringProp *)
1052                         prefs_filtering->data;
1053                 filteringprop_free(filtering);
1054                 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1055         }
1056 }
1057
1058 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1059 {
1060         FolderItem *item = node->data;
1061
1062         cm_return_val_if_fail(item, FALSE);
1063         cm_return_val_if_fail(item->prefs, FALSE);
1064
1065         prefs_filtering_free(item->prefs->processing);
1066         item->prefs->processing = NULL;
1067
1068         return FALSE;
1069 }
1070
1071 void prefs_filtering_clear(void)
1072 {
1073         GList * cur;
1074
1075         for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1076                 Folder *folder;
1077
1078                 folder = (Folder *) cur->data;
1079                 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1080                                 prefs_filtering_free_func, NULL);
1081         }
1082
1083         prefs_filtering_free(filtering_rules);
1084         filtering_rules = NULL;
1085         prefs_filtering_free(pre_global_processing);
1086         pre_global_processing = NULL;
1087         prefs_filtering_free(post_global_processing);
1088         post_global_processing = NULL;
1089 }
1090
1091 void prefs_filtering_clear_folder(Folder *folder)
1092 {
1093         cm_return_if_fail(folder);
1094         cm_return_if_fail(folder->node);
1095
1096         g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1097                         prefs_filtering_free_func, NULL);
1098         /* FIXME: Note folder settings were changed, where the updates? */
1099 }
1100
1101 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1102 /* return TRUE if there's at least one per-account filtering rule */
1103 {
1104         GSList *l;
1105
1106         for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1107                 FilteringProp * filtering = (FilteringProp *) l->data;
1108
1109                 if (filtering->enabled && (filtering->account_id != 0)) {
1110                         return TRUE;
1111                 }               
1112         }
1113
1114         return FALSE;
1115 }