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