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