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