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