9a3cd95a0fd6281941497e511c889d04c5bc5c63
[claws.git] / src / filtering.c
1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2015 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 #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 static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
794 {
795         GSList  *l;
796         gboolean final;
797         gboolean apply_next;
798         
799         cm_return_val_if_fail(info != NULL, TRUE);
800         
801         for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
802                 FilteringProp * filtering = (FilteringProp *) l->data;
803
804                 if (filtering->enabled) {
805                         if (debug_filtering_session) {
806                                 gchar *buf = filteringprop_to_string(filtering);
807                                 if (filtering->name && *filtering->name != '\0') {
808                                         log_print(LOG_DEBUG_FILTERING,
809                                                 _("processing rule '%s' [ %s ]\n"),
810                                                 filtering->name, buf);
811                                 } else {
812                                         log_print(LOG_DEBUG_FILTERING,
813                                                 _("processing rule <unnamed> [ %s ]\n"),
814                                                 buf);
815                                 }
816                                 g_free(buf);
817                         }
818
819                         if (filtering_match_condition(filtering, info, ac_prefs)) {
820                                 apply_next = filtering_apply_rule(filtering, info, &final);
821                                 if (final)
822                                         break;
823                         }
824
825                 } else {
826                         if (debug_filtering_session) {
827                                 gchar *buf = filteringprop_to_string(filtering);
828                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
829                                         if (filtering->name && *filtering->name != '\0') {
830                                                 log_status_skip(LOG_DEBUG_FILTERING,
831                                                                 _("disabled rule '%s' [ %s ]\n"),
832                                                                 filtering->name, buf);
833                                         } else {
834                                                 log_status_skip(LOG_DEBUG_FILTERING,
835                                                                 _("disabled rule <unnamed> [ %s ]\n"),
836                                                                 buf);
837                                         }
838                                 }
839                                 g_free(buf);
840                         }
841                 }
842         }
843
844     /* put in inbox if the last rule was not a final one, or
845      * a final rule could not be applied.
846      * Either of these cases is likely. */
847     if (!final || !apply_next) {
848                 return FALSE;
849         }
850
851         return TRUE;
852 }
853
854 /*!
855  *\brief        Filter a message against a list of rules.
856  *
857  *\param        flist List of filter rules.
858  *\param        info Message.
859  *
860  *\return       gboolean TRUE if filter rules handled the message.
861  *
862  *\note         Returning FALSE means the message was not handled,
863  *              and that the calling code should do the default
864  *              processing. E.g. \ref inc.c::inc_start moves the 
865  *              message to the inbox.   
866  */
867 gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
868                                                                    FilteringInvocationType context, gchar *extra_info)
869 {
870         gboolean ret;
871
872         if (prefs_common.enable_filtering_debug) {
873                 gchar *tmp = _("undetermined");
874
875                 switch (context) {
876                 case FILTERING_INCORPORATION:
877                         tmp = _("incorporation");
878                         debug_filtering_session = prefs_common.enable_filtering_debug_inc;
879                         break;
880                 case FILTERING_MANUALLY:
881                         tmp = _("manually");
882                         debug_filtering_session = prefs_common.enable_filtering_debug_manual;
883                         break;
884                 case FILTERING_FOLDER_PROCESSING:
885                         tmp = _("folder processing");
886                         debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
887                         break;
888                 case FILTERING_PRE_PROCESSING:
889                         tmp = _("pre-processing");
890                         debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
891                         break;
892                 case FILTERING_POST_PROCESSING:
893                         tmp = _("post-processing");
894                         debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
895                         break;
896                 default:
897                         debug_filtering_session = FALSE;
898                         break;
899                 }
900
901                 if (debug_filtering_session) {
902                         gchar *file = procmsg_get_message_file_path(info);
903                         gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');
904
905                         /* show context info and essential info about the message */
906                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
907                                 log_print(LOG_DEBUG_FILTERING,
908                                                 _("filtering message (%s%s%s)\n"
909                                                 "%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
910                                                 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
911                                                 spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
912                                                 spc, prefs_common_translated_header_name("From:"), info->from,
913                                                 spc, prefs_common_translated_header_name("To:"), info->to,
914                                                 spc, prefs_common_translated_header_name("Subject:"), info->subject);
915                         } else {
916                                 log_print(LOG_DEBUG_FILTERING,
917                                                 _("filtering message (%s%s%s)\n"
918                                                 "%smessage file: %s\n"),
919                                                 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
920                                                 spc, file);
921                         }
922                         g_free(file);
923                         g_free(spc);
924                 }
925         } else
926                 debug_filtering_session = FALSE;
927
928         ret = filter_msginfo(flist, info, ac_prefs);
929         debug_filtering_session = FALSE;
930         return ret;
931 }
932
933 gchar *filteringaction_to_string(FilteringAction *action)
934 {
935         const gchar *command_str;
936         gchar * quoted_dest;
937         gchar * quoted_header;
938         GString *dest = g_string_new("");
939         gchar *deststr = NULL;
940
941         command_str = get_matchparser_tab_str(action->type);
942
943         if (command_str == NULL)
944                 return NULL;
945
946         switch(action->type) {
947         case MATCHACTION_MOVE:
948         case MATCHACTION_COPY:
949         case MATCHACTION_EXECUTE:
950         case MATCHACTION_SET_TAG:
951         case MATCHACTION_UNSET_TAG:
952                 quoted_dest = matcher_quote_str(action->destination);
953                 g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
954                 g_free(quoted_dest);
955                 break;
956
957         case MATCHACTION_DELETE:
958         case MATCHACTION_MARK:
959         case MATCHACTION_UNMARK:
960         case MATCHACTION_LOCK:
961         case MATCHACTION_UNLOCK:
962         case MATCHACTION_MARK_AS_READ:
963         case MATCHACTION_MARK_AS_UNREAD:
964         case MATCHACTION_MARK_AS_SPAM:
965         case MATCHACTION_MARK_AS_HAM:
966         case MATCHACTION_STOP:
967         case MATCHACTION_HIDE:
968         case MATCHACTION_IGNORE:
969         case MATCHACTION_WATCH:
970         case MATCHACTION_CLEAR_TAGS:
971                 g_string_append_printf(dest, "%s", command_str);
972                 break;
973
974         case MATCHACTION_REDIRECT:
975         case MATCHACTION_FORWARD:
976         case MATCHACTION_FORWARD_AS_ATTACHMENT:
977                 quoted_dest = matcher_quote_str(action->destination);
978                 g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
979                 g_free(quoted_dest);
980                 break;
981
982         case MATCHACTION_COLOR:
983                 g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
984                 break;
985
986         case MATCHACTION_CHANGE_SCORE:
987         case MATCHACTION_SET_SCORE:
988                 g_string_append_printf(dest, "%s %d", command_str, action->score);
989                 break;
990
991         case MATCHACTION_ADD_TO_ADDRESSBOOK:
992                 quoted_header = matcher_quote_str(action->header);
993                 quoted_dest = matcher_quote_str(action->destination);
994                 g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
995                 g_free(quoted_dest);
996                 g_free(quoted_header);
997                 break;
998
999         default:
1000                 return NULL;
1001         }
1002         deststr = dest->str;
1003         g_string_free(dest, FALSE);
1004         return deststr;
1005 }
1006
1007 gchar * filteringaction_list_to_string(GSList * action_list)
1008 {
1009         gchar *action_list_str;
1010         GSList * tmp;
1011         gchar *list_str;
1012
1013         action_list_str = NULL;
1014         for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
1015                 gchar *action_str;
1016                 FilteringAction * action;
1017                 
1018                 action = tmp->data;
1019                 
1020                 action_str = filteringaction_to_string(action);
1021                 
1022                 if (action_list_str != NULL) {
1023                         list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1024                         g_free(action_list_str);
1025                 }
1026                 else {
1027                         list_str = g_strdup(action_str);
1028                 }
1029                 g_free(action_str);
1030                 action_list_str = list_str;
1031         }
1032
1033         return action_list_str;
1034 }
1035
1036 gchar * filteringprop_to_string(FilteringProp * prop)
1037 {
1038         gchar *list_str;
1039         gchar *action_list_str;
1040         gchar *filtering_str;
1041
1042         if (prop == NULL)
1043                 return NULL;
1044
1045         action_list_str = filteringaction_list_to_string(prop->action_list);
1046
1047         if (action_list_str == NULL)
1048                 return NULL;
1049
1050         list_str = matcherlist_to_string(prop->matchers);
1051
1052         if (list_str == NULL) {
1053                 g_free(action_list_str);
1054                 return NULL;
1055         }
1056
1057         filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1058         g_free(action_list_str);
1059         g_free(list_str);
1060
1061         return filtering_str;
1062 }
1063
1064 static void prefs_filtering_free(GSList * prefs_filtering)
1065 {
1066         while (prefs_filtering != NULL) {
1067                 FilteringProp * filtering = (FilteringProp *)
1068                         prefs_filtering->data;
1069                 filteringprop_free(filtering);
1070                 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1071         }
1072 }
1073
1074 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1075 {
1076         FolderItem *item = node->data;
1077
1078         cm_return_val_if_fail(item, FALSE);
1079         cm_return_val_if_fail(item->prefs, FALSE);
1080
1081         prefs_filtering_free(item->prefs->processing);
1082         item->prefs->processing = NULL;
1083
1084         return FALSE;
1085 }
1086
1087 void prefs_filtering_clear(void)
1088 {
1089         GList * cur;
1090
1091         for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1092                 Folder *folder;
1093
1094                 folder = (Folder *) cur->data;
1095                 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1096                                 prefs_filtering_free_func, NULL);
1097         }
1098
1099         prefs_filtering_free(filtering_rules);
1100         filtering_rules = NULL;
1101         prefs_filtering_free(pre_global_processing);
1102         pre_global_processing = NULL;
1103         prefs_filtering_free(post_global_processing);
1104         post_global_processing = NULL;
1105 }
1106
1107 void prefs_filtering_clear_folder(Folder *folder)
1108 {
1109         cm_return_if_fail(folder);
1110         cm_return_if_fail(folder->node);
1111
1112         g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1113                         prefs_filtering_free_func, NULL);
1114         /* FIXME: Note folder settings were changed, where the updates? */
1115 }
1116
1117 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1118 /* return TRUE if there's at least one per-account filtering rule */
1119 {
1120         GSList *l;
1121
1122         for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1123                 FilteringProp * filtering = (FilteringProp *) l->data;
1124
1125                 if (filtering->enabled && (filtering->account_id != 0)) {
1126                         return TRUE;
1127                 }               
1128         }
1129
1130         return FALSE;
1131 }
1132
1133 gboolean filtering_action_list_rename_path(GSList *action_list, const gchar *old_path,
1134                                            const gchar *new_path)
1135 {
1136         gchar *base;
1137         gchar *prefix;
1138         gchar *suffix;
1139         gchar *dest_path;
1140         gchar *old_path_with_sep;
1141         gint destlen;
1142         gint prefixlen;
1143         gint oldpathlen;
1144         GSList * action_cur;
1145         const gchar *separator=G_DIR_SEPARATOR_S;
1146         gboolean matched = FALSE;
1147 #ifdef G_OS_WIN32
1148 again:
1149 #endif
1150         oldpathlen = strlen(old_path);
1151         old_path_with_sep = g_strconcat(old_path,separator,NULL);
1152
1153         for(action_cur = action_list ; action_cur != NULL ;
1154                 action_cur = action_cur->next) {
1155
1156                 FilteringAction *action = action_cur->data;
1157                         
1158                 if (action->type == MATCHACTION_SET_TAG ||
1159                     action->type == MATCHACTION_UNSET_TAG)
1160                         continue;
1161                 if (!action->destination) 
1162                         continue;
1163                 
1164                 destlen = strlen(action->destination);
1165                         
1166                 if (destlen > oldpathlen) {
1167                         prefixlen = destlen - oldpathlen;
1168                         suffix = action->destination + prefixlen;
1169                                 
1170                         if (!strncmp(old_path, suffix, oldpathlen)) {
1171                                 prefix = g_malloc0(prefixlen + 1);
1172                                 strncpy2(prefix, action->destination, prefixlen);
1173                                         
1174                                 base = suffix + oldpathlen;
1175                                 while (*base == G_DIR_SEPARATOR) base++;
1176                                 if (*base == '\0')
1177                                         dest_path = g_strconcat(prefix, separator,
1178                                                                 new_path, NULL);
1179                                 else
1180                                         dest_path = g_strconcat(prefix,
1181                                                                 separator,
1182                                                                 new_path,
1183                                                                 separator,
1184                                                                 base, NULL);
1185                                         
1186                                         g_free(prefix);
1187                                         g_free(action->destination);
1188                                         action->destination = dest_path;
1189                                         matched = TRUE;
1190                         } else { /* for non-leaf folders */
1191                                 /* compare with trailing slash */
1192                                 if (!strncmp(old_path_with_sep, action->destination, oldpathlen+1)) {
1193                                                 
1194                                         suffix = action->destination + oldpathlen + 1;
1195                                         dest_path = g_strconcat(new_path, separator,
1196                                                                 suffix, NULL);
1197                                         g_free(action->destination);
1198                                         action->destination = dest_path;
1199                                         matched = TRUE;
1200                                 }
1201                         }
1202                 } else {
1203                         /* folder-moving a leaf */
1204                         if (!strcmp(old_path, action->destination)) {
1205                                 dest_path = g_strdup(new_path);
1206                                 g_free(action->destination);
1207                                 action->destination = dest_path;
1208                                 matched = TRUE;
1209                         }
1210                 }
1211         }
1212         
1213         g_free(old_path_with_sep);
1214 #ifdef G_OS_WIN32
1215         if (!strcmp(separator, G_DIR_SEPARATOR_S) && !matched) {
1216                 separator = "/";
1217                 goto again;
1218         }
1219 #endif
1220
1221         return matched;
1222 }