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