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