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