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