2009-06-26 [colin] 3.7.1cvs79
[claws.git] / src / filtering.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2009 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);
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);
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                         system(cmd);
428                         g_free(cmd);
429                 }
430                 return TRUE;
431
432         case MATCHACTION_SET_SCORE:
433                 FLUSH_COPY_IF_NEEDED(info);
434                 info->score = action->score;
435                 return TRUE;
436
437         case MATCHACTION_CHANGE_SCORE:
438                 FLUSH_COPY_IF_NEEDED(info);
439                 info->score += action->score;
440                 return TRUE;
441
442         case MATCHACTION_STOP:
443                 return FALSE;
444
445         case MATCHACTION_HIDE:
446                 FLUSH_COPY_IF_NEEDED(info);
447                 info->hidden = TRUE;
448                 return TRUE;
449
450         case MATCHACTION_IGNORE:
451                 FLUSH_COPY_IF_NEEDED(info);
452                 procmsg_msginfo_set_flags(info, MSG_IGNORE_THREAD, 0);
453                 return TRUE;
454
455         case MATCHACTION_WATCH:
456                 FLUSH_COPY_IF_NEEDED(info);
457                 procmsg_msginfo_set_flags(info, MSG_WATCH_THREAD, 0);
458                 return TRUE;
459
460         case MATCHACTION_ADD_TO_ADDRESSBOOK:
461                 {
462                         AddressDataSource *book = NULL;
463                         AddressBookFile *abf = NULL;
464                         ItemFolder *folder = NULL;
465                         gchar buf[BUFFSIZE];
466                         Header *header;
467                         gint errors = 0;
468
469                         if (!addressbook_peek_folder_exists(action->destination, &book, &folder)) {
470                                 g_warning("addressbook folder not found '%s'\n", action->destination?action->destination:"(null)");
471                                 return FALSE;
472                         }
473                         if (!book) {
474                                 g_warning("addressbook_peek_folder_exists returned NULL book\n");
475                                 return FALSE;
476                         }
477
478                         abf = book->rawDataSource;
479
480                         /* get the header */
481                         procheader_get_header_from_msginfo(info, buf, sizeof(buf), action->header);
482                         header = procheader_parse_header(buf);
483
484                         /* add all addresses that are not already in */
485                         if (header && *header->body && (*header->body != '\0')) {
486                                 GSList *address_list = NULL;
487                                 GSList *walk = NULL;
488                                 gchar *path = NULL;
489
490                                 if (action->destination == NULL ||
491                                                 strcasecmp(action->destination, "Any") == 0 ||
492                                                 *(action->destination) == '\0')
493                                         path = NULL;
494                                 else
495                                         path = action->destination;
496                                 start_address_completion(path);
497
498                                 address_list = address_list_append(address_list, header->body);
499                                 for (walk = address_list; walk != NULL; walk = walk->next) {
500                                         gchar *stripped_addr = g_strdup(walk->data);
501                                         extract_address(stripped_addr);
502
503                                         if (complete_matches_found(walk->data) == 0) {
504                                                 debug_print("adding address '%s' to addressbook '%s'\n",
505                                                                 stripped_addr, action->destination);
506                                                 if (!addrbook_add_contact(abf, folder, stripped_addr, stripped_addr, NULL)) {
507                                                         g_warning("contact could not been added\n");
508                                                         errors++;
509                                                 }
510                                         } else {
511                                                 debug_print("address '%s' already found in addressbook '%s', skipping\n",
512                                                                 stripped_addr, action->destination);
513                                         }
514                                         g_free(stripped_addr);
515                                 }
516
517                                 g_slist_free(address_list);
518                                 end_address_completion();
519                         } else {
520                                 g_warning("header '%s' not set or empty\n", action->header?action->header:"(null)");
521                         }
522                         return (errors == 0);
523                 }
524
525         default:
526                 break;
527         }
528         return FALSE;
529 }
530
531 gboolean filteringaction_apply_action_list(GSList *action_list, MsgInfo *info)
532 {
533         GSList *p;
534         cm_return_val_if_fail(action_list, FALSE);
535         cm_return_val_if_fail(info, FALSE);
536         for (p = action_list; p && p->data; p = g_slist_next(p)) {
537                 FilteringAction *a = (FilteringAction *) p->data;
538                 if (filteringaction_apply(a, info)) {
539                         if (filtering_is_final_action(a))
540                                 break;
541                 } else
542                         return FALSE;
543                 
544         }
545         return TRUE;
546 }
547
548 static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info,
549                                                         PrefsAccount *ac_prefs)
550
551 /* this function returns true if a filtering rule applies regarding to its account
552    data and if it does, if the conditions list match.
553
554    per-account data of a filtering rule is either matched against current account
555    when filtering is done manually, or against the account currently used for
556    retrieving messages when it's an manual or automatic fetching of messages.
557    per-account data match doesn't apply to pre-/post-/folder-processing rules.
558
559    when filtering messages manually:
560     - either the filtering rule is not account-based and it will be processed
561         - or it's per-account and we check if we HAVE TO match it against the current
562           account (according to user-land settings, per-account rules might have to
563           be skipped, or only the rules that match the current account have to be
564           applied, or all rules will have to be applied regardless to if they are
565           account-based or not)
566
567    notes about debugging output in that function:
568    when not matching, log_status_skip() is used, otherwise log_status_ok() is used
569    no debug output is done when filtering_debug_level is low
570 */
571 {
572         gboolean matches = FALSE;
573
574         if (ac_prefs != NULL) {
575                 matches = ((filtering->account_id == 0)
576                                         || (filtering->account_id == ac_prefs->account_id));
577
578                 /* debug output */
579                 if (debug_filtering_session) {
580                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
581                                 if (filtering->account_id == 0) {
582                                         log_status_ok(LOG_DEBUG_FILTERING,
583                                                         _("rule is not account-based\n"));
584                                 } else {
585                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
586                                                 log_status_ok(LOG_DEBUG_FILTERING,
587                                                                 _("rule is account-based [id=%d, name='%s'], "
588                                                                 "matching the account currently used to retrieve messages\n"),
589                                                                 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
590                                         }
591                                 }
592                         }
593                         else
594                         if (!matches) {
595                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
596                                         log_status_skip(LOG_DEBUG_FILTERING,
597                                                         _("rule is account-based, "
598                                                         "not matching the account currently used to retrieve messages\n"));
599                                 } else {
600                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
601                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
602
603                                                 log_status_skip(LOG_DEBUG_FILTERING,
604                                                                 _("rule is account-based [id=%d, name='%s'], "
605                                                                 "not matching the account currently used to retrieve messages [id=%d, name='%s']\n"),
606                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
607                                                                 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
608                                         }
609                                 }
610                         }
611                 }
612         } else {
613                 switch (prefs_common.apply_per_account_filtering_rules) {
614                 case FILTERING_ACCOUNT_RULES_FORCE:
615                         /* apply filtering rules regardless to the account info */
616                         matches = TRUE;
617
618                         /* debug output */
619                         if (debug_filtering_session) {
620                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
621                                         if (filtering->account_id == 0) {
622                                                 log_status_ok(LOG_DEBUG_FILTERING,
623                                                                 _("rule is not account-based, "
624                                                                 "all rules are applied on user request anyway\n"));
625                                         } else {
626                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
627
628                                                 log_status_ok(LOG_DEBUG_FILTERING,
629                                                                 _("rule is account-based [id=%d, name='%s'], "
630                                                                 "but all rules are applied on user request\n"),
631                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
632                                         }
633                                 }
634                         }
635                         break;
636                 case FILTERING_ACCOUNT_RULES_SKIP:
637                         /* don't apply filtering rules that belong to an account */
638                         matches = (filtering->account_id == 0);
639
640                         /* debug output */
641                         if (debug_filtering_session) {
642                                 if (!matches) {
643                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
644                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
645
646                                                 log_status_skip(LOG_DEBUG_FILTERING,
647                                                                 _("rule is account-based [id=%d, name='%s'], "
648                                                                 "skipped on user request\n"),
649                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
650                                         } else {
651                                                 log_status_skip(LOG_DEBUG_FILTERING,
652                                                                 _("rule is account-based, "
653                                                                 "skipped on user request\n"));
654                                         }
655                                 } else {
656                                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
657                                                 log_status_ok(LOG_DEBUG_FILTERING,
658                                                                 _("rule is not account-based\n"));
659                                         }
660                                 }
661                         }
662                         break;
663                 case FILTERING_ACCOUNT_RULES_USE_CURRENT:
664                         matches = ((filtering->account_id == 0)
665                                         || (filtering->account_id == cur_account->account_id));
666
667                         /* debug output */
668                         if (debug_filtering_session) {
669                                 if (!matches) {
670                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
671                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
672
673                                                 log_status_skip(LOG_DEBUG_FILTERING,
674                                                                 _("rule is account-based [id=%d, name='%s'], "
675                                                                 "not matching current account [id=%d, name='%s']\n"),
676                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
677                                                                 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
678                                         } else {
679                                                 log_status_skip(LOG_DEBUG_FILTERING,
680                                                                 _("rule is account-based, "
681                                                                 "not matching current account\n"));
682                                         }
683                                 } else {
684                                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
685                                                 if (filtering->account_id == 0) {
686                                                         log_status_ok(LOG_DEBUG_FILTERING,
687                                                                         _("rule is not account-based\n"));
688                                                 } else {
689                                                         PrefsAccount *account = account_find_from_id(filtering->account_id);
690
691                                                         log_status_ok(LOG_DEBUG_FILTERING,
692                                                                         _("rule is account-based [id=%d, name='%s'], "
693                                                                         "current account [id=%d, name='%s']\n"),
694                                                                         account->account_id, account?account->account_name:_("NON_EXISTENT"),
695                                                                         cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
696                                                 }
697                                         }
698                                 }
699                         }
700                         break;
701                 }
702         }
703
704         return matches && matcherlist_match(filtering->matchers, info);
705 }
706
707 /*!
708  *\brief        Apply a rule on message.
709  *
710  *\param        filtering List of filtering rules.
711  *\param        info Message to apply rules on.
712  *\param        final Variable returning TRUE or FALSE if one of the
713  *              encountered actions was final. 
714  *              See also \ref filtering_is_final_action.
715  *
716  *\return       gboolean TRUE to continue applying rules.
717  */
718 static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info,
719     gboolean * final)
720 {
721         gboolean result = TRUE;
722         gchar    buf[256];
723         GSList * tmp;
724         
725         * final = FALSE;
726         for (tmp = filtering->action_list ; tmp != NULL ; tmp = tmp->next) {
727                 FilteringAction * action;
728
729                 action = tmp->data;
730                                 filteringaction_to_string(buf, sizeof buf, action);
731                                 if (debug_filtering_session)
732                                         log_print(LOG_DEBUG_FILTERING, _("applying action [ %s ]\n"), buf);
733
734                 if (FALSE == (result = filteringaction_apply(action, info))) {
735                                         if (debug_filtering_session) {
736                                                 if (action->type != MATCHACTION_STOP)
737                                                         log_warning(LOG_DEBUG_FILTERING, _("action could not apply\n"));
738                                                 log_print(LOG_DEBUG_FILTERING,
739                                                                 _("no further processing after action [ %s ]\n"), buf);
740                                         } else
741                                                 g_warning("No further processing after rule %s\n", buf);
742                 }
743                 
744                 if (filtering_is_final_action(action)) {
745                         * final = TRUE;
746                         break;
747                 }
748         }
749         return result;
750 }
751
752 /*!
753  *\brief        Check if an action is "final", i.e. should break further
754  *              processing.
755  *
756  *\param        filtering_action Action to check.
757  *
758  *\return       gboolean TRUE if \a filtering_action is final.  
759  */
760 static gboolean filtering_is_final_action(FilteringAction *filtering_action)
761 {
762         switch(filtering_action->type) {
763         case MATCHACTION_MOVE:
764         case MATCHACTION_DELETE:
765         case MATCHACTION_STOP:
766         case MATCHACTION_MARK_AS_SPAM:
767                 return TRUE; /* MsgInfo invalid for message */
768         default:
769                 return FALSE;
770         }
771 }
772
773 static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
774 {
775         GSList  *l;
776         gboolean final;
777         gboolean apply_next;
778         
779         cm_return_val_if_fail(info != NULL, TRUE);
780         
781         for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
782                 FilteringProp * filtering = (FilteringProp *) l->data;
783
784                 if (filtering->enabled) {
785                         if (debug_filtering_session) {
786                                 gchar *buf = filteringprop_to_string(filtering);
787                                 if (filtering->name && *filtering->name != '\0') {
788                                         log_print(LOG_DEBUG_FILTERING,
789                                                 _("processing rule '%s' [ %s ]\n"),
790                                                 filtering->name, buf);
791                                 } else {
792                                         log_print(LOG_DEBUG_FILTERING,
793                                                 _("processing rule <unnamed> [ %s ]\n"),
794                                                 buf);
795                                 }
796                                 g_free(buf);
797                         }
798
799                         if (filtering_match_condition(filtering, info, ac_prefs)) {
800                                 apply_next = filtering_apply_rule(filtering, info, &final);
801                                 if (final)
802                                         break;
803                         }
804
805                 } else {
806                         if (debug_filtering_session) {
807                                 gchar *buf = filteringprop_to_string(filtering);
808                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
809                                         if (filtering->name && *filtering->name != '\0') {
810                                                 log_status_skip(LOG_DEBUG_FILTERING,
811                                                                 _("disabled rule '%s' [ %s ]\n"),
812                                                                 filtering->name, buf);
813                                         } else {
814                                                 log_status_skip(LOG_DEBUG_FILTERING,
815                                                                 _("disabled rule <unnamed> [ %s ]\n"),
816                                                                 buf);
817                                         }
818                                 }
819                                 g_free(buf);
820                         }
821                 }
822         }
823
824         /* put in inbox if a final rule could not be applied, or
825          * the last rule was not a final one. */
826         if ((final && !apply_next) || !final) {
827                 return FALSE;
828         }
829
830         return TRUE;
831 }
832
833 /*!
834  *\brief        Filter a message against a list of rules.
835  *
836  *\param        flist List of filter rules.
837  *\param        info Message.
838  *
839  *\return       gboolean TRUE if filter rules handled the message.
840  *
841  *\note         Returning FALSE means the message was not handled,
842  *              and that the calling code should do the default
843  *              processing. E.g. \ref inc.c::inc_start moves the 
844  *              message to the inbox.   
845  */
846 gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
847                                                                    FilteringInvocationType context, gchar *extra_info)
848 {
849         gboolean ret;
850
851         if (prefs_common.enable_filtering_debug) {
852                 gchar *tmp = _("undetermined");
853 #ifndef G_OS_WIN32
854                 switch (context) {
855                 case FILTERING_INCORPORATION:
856                         tmp = _("incorporation");
857                         debug_filtering_session = prefs_common.enable_filtering_debug_inc;
858                         break;
859                 case FILTERING_MANUALLY:
860                         tmp = _("manually");
861                         debug_filtering_session = prefs_common.enable_filtering_debug_manual;
862                         break;
863                 case FILTERING_FOLDER_PROCESSING:
864                         tmp = _("folder processing");
865                         debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
866                         break;
867                 case FILTERING_PRE_PROCESSING:
868                         tmp = _("pre-processing");
869                         debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
870                         break;
871                 case FILTERING_POST_PROCESSING:
872                         tmp = _("post-processing");
873                         debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
874                         break;
875                 default:
876                         debug_filtering_session = FALSE;
877                         break;
878                 }
879 #else
880                 debug_filtering_session = FALSE;
881 #endif
882                 if (debug_filtering_session) {
883                         gchar *file = procmsg_get_message_file_path(info);
884                         gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');
885
886                         /* show context info and essential info about the message */
887                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
888                                 log_print(LOG_DEBUG_FILTERING,
889                                                 _("filtering message (%s%s%s)\n"
890                                                 "%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
891                                                 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
892                                                 spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
893                                                 spc, prefs_common_translated_header_name("From:"), info->from,
894                                                 spc, prefs_common_translated_header_name("To:"), info->to,
895                                                 spc, prefs_common_translated_header_name("Subject:"), info->subject);
896                         } else {
897                                 log_print(LOG_DEBUG_FILTERING,
898                                                 _("filtering message (%s%s%s)\n"
899                                                 "%smessage file: %s\n"),
900                                                 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
901                                                 spc, file);
902                         }
903                         g_free(file);
904                         g_free(spc);
905                 }
906         } else
907                 debug_filtering_session = FALSE;
908
909         ret = filter_msginfo(flist, info, ac_prefs);
910         debug_filtering_session = FALSE;
911         return ret;
912 }
913
914 gchar *filteringaction_to_string(gchar *dest, gint destlen, FilteringAction *action)
915 {
916         const gchar *command_str;
917         gchar * quoted_dest;
918         gchar * quoted_header;
919         
920         command_str = get_matchparser_tab_str(action->type);
921
922         if (command_str == NULL)
923                 return NULL;
924
925         switch(action->type) {
926         case MATCHACTION_MOVE:
927         case MATCHACTION_COPY:
928         case MATCHACTION_EXECUTE:
929         case MATCHACTION_SET_TAG:
930         case MATCHACTION_UNSET_TAG:
931                 quoted_dest = matcher_quote_str(action->destination);
932                 g_snprintf(dest, destlen, "%s \"%s\"", command_str, quoted_dest);
933                 g_free(quoted_dest);
934                 return dest;
935
936         case MATCHACTION_DELETE:
937         case MATCHACTION_MARK:
938         case MATCHACTION_UNMARK:
939         case MATCHACTION_LOCK:
940         case MATCHACTION_UNLOCK:
941         case MATCHACTION_MARK_AS_READ:
942         case MATCHACTION_MARK_AS_UNREAD:
943         case MATCHACTION_MARK_AS_SPAM:
944         case MATCHACTION_MARK_AS_HAM:
945         case MATCHACTION_STOP:
946         case MATCHACTION_HIDE:
947         case MATCHACTION_IGNORE:
948         case MATCHACTION_WATCH:
949         case MATCHACTION_CLEAR_TAGS:
950                 g_snprintf(dest, destlen, "%s", command_str);
951                 return dest;
952
953         case MATCHACTION_REDIRECT:
954         case MATCHACTION_FORWARD:
955         case MATCHACTION_FORWARD_AS_ATTACHMENT:
956                 quoted_dest = matcher_quote_str(action->destination);
957                 g_snprintf(dest, destlen, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
958                 g_free(quoted_dest);
959                 return dest; 
960
961         case MATCHACTION_COLOR:
962                 g_snprintf(dest, destlen, "%s %d", command_str, action->labelcolor);
963                 return dest;  
964
965         case MATCHACTION_CHANGE_SCORE:
966         case MATCHACTION_SET_SCORE:
967                 g_snprintf(dest, destlen, "%s %d", command_str, action->score);
968                 return dest;  
969
970         case MATCHACTION_ADD_TO_ADDRESSBOOK:
971                 quoted_header = matcher_quote_str(action->header);
972                 quoted_dest = matcher_quote_str(action->destination);
973                 g_snprintf(dest, destlen, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
974                 g_free(quoted_dest);
975                 g_free(quoted_header);
976                 return dest;
977
978         default:
979                 return NULL;
980         }
981 }
982
983 gchar * filteringaction_list_to_string(GSList * action_list)
984 {
985         gchar *action_list_str;
986         gchar  buf[256];
987         GSList * tmp;
988         gchar *list_str;
989
990         action_list_str = NULL;
991         for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
992                 gchar *action_str;
993                 FilteringAction * action;
994                 
995                 action = tmp->data;
996                 
997                 action_str = filteringaction_to_string(buf,
998                     sizeof buf, action);
999                 
1000                 if (action_list_str != NULL) {
1001                         list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1002                         g_free(action_list_str);
1003                 }
1004                 else {
1005                         list_str = g_strdup(action_str);
1006                 }
1007                 action_list_str = list_str;
1008         }
1009
1010         return action_list_str;
1011 }
1012
1013 gchar * filteringprop_to_string(FilteringProp * prop)
1014 {
1015         gchar *list_str;
1016         gchar *action_list_str;
1017         gchar *filtering_str;
1018
1019         if (prop == NULL)
1020                 return NULL;
1021
1022         action_list_str = filteringaction_list_to_string(prop->action_list);
1023
1024         if (action_list_str == NULL)
1025                 return NULL;
1026
1027         list_str = matcherlist_to_string(prop->matchers);
1028
1029         if (list_str == NULL) {
1030                 g_free(action_list_str);
1031                 return NULL;
1032         }
1033
1034         filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1035         g_free(action_list_str);
1036         g_free(list_str);
1037
1038         return filtering_str;
1039 }
1040
1041 static void prefs_filtering_free(GSList * prefs_filtering)
1042 {
1043         while (prefs_filtering != NULL) {
1044                 FilteringProp * filtering = (FilteringProp *)
1045                         prefs_filtering->data;
1046                 filteringprop_free(filtering);
1047                 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1048         }
1049 }
1050
1051 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1052 {
1053         FolderItem *item = node->data;
1054
1055         cm_return_val_if_fail(item, FALSE);
1056         cm_return_val_if_fail(item->prefs, FALSE);
1057
1058         prefs_filtering_free(item->prefs->processing);
1059         item->prefs->processing = NULL;
1060
1061         return FALSE;
1062 }
1063
1064 void prefs_filtering_clear(void)
1065 {
1066         GList * cur;
1067
1068         for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1069                 Folder *folder;
1070
1071                 folder = (Folder *) cur->data;
1072                 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1073                                 prefs_filtering_free_func, NULL);
1074         }
1075
1076         prefs_filtering_free(filtering_rules);
1077         filtering_rules = NULL;
1078         prefs_filtering_free(pre_global_processing);
1079         pre_global_processing = NULL;
1080         prefs_filtering_free(post_global_processing);
1081         post_global_processing = NULL;
1082 }
1083
1084 void prefs_filtering_clear_folder(Folder *folder)
1085 {
1086         cm_return_if_fail(folder);
1087         cm_return_if_fail(folder->node);
1088
1089         g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1090                         prefs_filtering_free_func, NULL);
1091         /* FIXME: Note folder settings were changed, where the updates? */
1092 }
1093
1094 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1095 /* return TRUE if there's at least one per-account filtering rule */
1096 {
1097         GSList *l;
1098
1099         for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1100                 FilteringProp * filtering = (FilteringProp *) l->data;
1101
1102                 if (filtering->enabled && (filtering->account_id != 0)) {
1103                         return TRUE;
1104                 }               
1105         }
1106
1107         return FALSE;
1108 }