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