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