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