8aa7cffb146e64344784b85b2e0e08dde9e32cd3
[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                 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                         if (procheader_get_header_from_msginfo(info, buf, 
508                                 sizeof(buf), action->header) < 0)
509                                 return FALSE;
510
511                         header = procheader_parse_header(buf);
512
513                         /* add all addresses that are not already in */
514                         if (header && *header->body && (*header->body != '\0')) {
515                                 GSList *address_list = NULL;
516                                 GSList *walk = NULL;
517                                 gchar *path = NULL;
518
519                                 if (action->destination == NULL ||
520                                                 strcasecmp(action->destination, "Any") == 0 ||
521                                                 *(action->destination) == '\0')
522                                         path = NULL;
523                                 else
524                                         path = action->destination;
525                                 start_address_completion(path);
526
527                                 address_list = address_list_append(address_list, header->body);
528                                 for (walk = address_list; walk != NULL; walk = walk->next) {
529                                         gchar *stripped_addr = g_strdup(walk->data);
530                                         extract_address(stripped_addr);
531
532                                         if (complete_matches_found(walk->data) == 0) {
533                                                 debug_print("adding address '%s' to addressbook '%s'\n",
534                                                                 stripped_addr, action->destination);
535 #ifndef USE_NEW_ADDRBOOK
536                                                 if (!addrbook_add_contact(abf, folder, stripped_addr, stripped_addr, NULL)) {
537 #else
538                                                 if (!addressadd_selection(NULL, stripped_addr, NULL, NULL)) {
539 #endif
540                                                         g_warning("contact could not been added\n");
541                                                         errors++;
542                                                 }
543                                         } else {
544                                                 debug_print("address '%s' already found in addressbook '%s', skipping\n",
545                                                                 stripped_addr, action->destination);
546                                         }
547                                         g_free(stripped_addr);
548                                 }
549
550                                 g_slist_free(address_list);
551                                 end_address_completion();
552                         } else {
553                                 g_warning("header '%s' not set or empty\n", action->header?action->header:"(null)");
554                         }
555                         return (errors == 0);
556                 }
557         default:
558                 break;
559         }
560         return FALSE;
561 }
562
563 gboolean filteringaction_apply_action_list(GSList *action_list, MsgInfo *info)
564 {
565         GSList *p;
566         cm_return_val_if_fail(action_list, FALSE);
567         cm_return_val_if_fail(info, FALSE);
568         for (p = action_list; p && p->data; p = g_slist_next(p)) {
569                 FilteringAction *a = (FilteringAction *) p->data;
570                 if (filteringaction_apply(a, info)) {
571                         if (filtering_is_final_action(a))
572                                 break;
573                 } else
574                         return FALSE;
575                 
576         }
577         return TRUE;
578 }
579
580 static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info,
581                                                         PrefsAccount *ac_prefs)
582
583 /* this function returns true if a filtering rule applies regarding to its account
584    data and if it does, if the conditions list match.
585
586    per-account data of a filtering rule is either matched against current account
587    when filtering is done manually, or against the account currently used for
588    retrieving messages when it's an manual or automatic fetching of messages.
589    per-account data match doesn't apply to pre-/post-/folder-processing rules.
590
591    when filtering messages manually:
592     - either the filtering rule is not account-based and it will be processed
593         - or it's per-account and we check if we HAVE TO match it against the current
594           account (according to user-land settings, per-account rules might have to
595           be skipped, or only the rules that match the current account have to be
596           applied, or all rules will have to be applied regardless to if they are
597           account-based or not)
598
599    notes about debugging output in that function:
600    when not matching, log_status_skip() is used, otherwise log_status_ok() is used
601    no debug output is done when filtering_debug_level is low
602 */
603 {
604         gboolean matches = FALSE;
605
606         if (ac_prefs != NULL) {
607                 matches = ((filtering->account_id == 0)
608                                         || (filtering->account_id == ac_prefs->account_id));
609
610                 /* debug output */
611                 if (debug_filtering_session) {
612                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
613                                 if (filtering->account_id == 0) {
614                                         log_status_ok(LOG_DEBUG_FILTERING,
615                                                         _("rule is not account-based\n"));
616                                 } else {
617                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
618                                                 log_status_ok(LOG_DEBUG_FILTERING,
619                                                                 _("rule is account-based [id=%d, name='%s'], "
620                                                                 "matching the account currently used to retrieve messages\n"),
621                                                                 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
622                                         }
623                                 }
624                         }
625                         else
626                         if (!matches) {
627                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
628                                         log_status_skip(LOG_DEBUG_FILTERING,
629                                                         _("rule is account-based, "
630                                                         "not matching the account currently used to retrieve messages\n"));
631                                 } else {
632                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
633                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
634
635                                                 log_status_skip(LOG_DEBUG_FILTERING,
636                                                                 _("rule is account-based [id=%d, name='%s'], "
637                                                                 "not matching the account currently used to retrieve messages [id=%d, name='%s']\n"),
638                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
639                                                                 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
640                                         }
641                                 }
642                         }
643                 }
644         } else {
645                 switch (prefs_common.apply_per_account_filtering_rules) {
646                 case FILTERING_ACCOUNT_RULES_FORCE:
647                         /* apply filtering rules regardless to the account info */
648                         matches = TRUE;
649
650                         /* debug output */
651                         if (debug_filtering_session) {
652                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
653                                         if (filtering->account_id == 0) {
654                                                 log_status_ok(LOG_DEBUG_FILTERING,
655                                                                 _("rule is not account-based, "
656                                                                 "all rules are applied on user request anyway\n"));
657                                         } else {
658                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
659
660                                                 log_status_ok(LOG_DEBUG_FILTERING,
661                                                                 _("rule is account-based [id=%d, name='%s'], "
662                                                                 "but all rules are applied on user request\n"),
663                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
664                                         }
665                                 }
666                         }
667                         break;
668                 case FILTERING_ACCOUNT_RULES_SKIP:
669                         /* don't apply filtering rules that belong to an account */
670                         matches = (filtering->account_id == 0);
671
672                         /* debug output */
673                         if (debug_filtering_session) {
674                                 if (!matches) {
675                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
676                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
677
678                                                 log_status_skip(LOG_DEBUG_FILTERING,
679                                                                 _("rule is account-based [id=%d, name='%s'], "
680                                                                 "skipped on user request\n"),
681                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
682                                         } else {
683                                                 log_status_skip(LOG_DEBUG_FILTERING,
684                                                                 _("rule is account-based, "
685                                                                 "skipped on user request\n"));
686                                         }
687                                 } else {
688                                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
689                                                 log_status_ok(LOG_DEBUG_FILTERING,
690                                                                 _("rule is not account-based\n"));
691                                         }
692                                 }
693                         }
694                         break;
695                 case FILTERING_ACCOUNT_RULES_USE_CURRENT:
696                         matches = ((filtering->account_id == 0)
697                                         || (filtering->account_id == cur_account->account_id));
698
699                         /* debug output */
700                         if (debug_filtering_session) {
701                                 if (!matches) {
702                                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
703                                                 PrefsAccount *account = account_find_from_id(filtering->account_id);
704
705                                                 log_status_skip(LOG_DEBUG_FILTERING,
706                                                                 _("rule is account-based [id=%d, name='%s'], "
707                                                                 "not matching current account [id=%d, name='%s']\n"),
708                                                                 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
709                                                                 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
710                                         } else {
711                                                 log_status_skip(LOG_DEBUG_FILTERING,
712                                                                 _("rule is account-based, "
713                                                                 "not matching current account\n"));
714                                         }
715                                 } else {
716                                         if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
717                                                 if (filtering->account_id == 0) {
718                                                         log_status_ok(LOG_DEBUG_FILTERING,
719                                                                         _("rule is not account-based\n"));
720                                                 } else {
721                                                         PrefsAccount *account = account_find_from_id(filtering->account_id);
722
723                                                         log_status_ok(LOG_DEBUG_FILTERING,
724                                                                         _("rule is account-based [id=%d, name='%s'], "
725                                                                         "current account [id=%d, name='%s']\n"),
726                                                                         account->account_id, account?account->account_name:_("NON_EXISTENT"),
727                                                                         cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
728                                                 }
729                                         }
730                                 }
731                         }
732                         break;
733                 }
734         }
735
736         return matches && matcherlist_match(filtering->matchers, info);
737 }
738
739 /*!
740  *\brief        Apply a rule on message.
741  *
742  *\param        filtering List of filtering rules.
743  *\param        info Message to apply rules on.
744  *\param        final Variable returning TRUE or FALSE if one of the
745  *              encountered actions was final. 
746  *              See also \ref filtering_is_final_action.
747  *
748  *\return       gboolean TRUE to continue applying rules.
749  */
750 static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info,
751     gboolean * final)
752 {
753         gboolean result = TRUE;
754         gchar    *buf;
755         GSList * tmp;
756         
757         * final = FALSE;
758         for (tmp = filtering->action_list ; tmp != NULL ; tmp = tmp->next) {
759                 FilteringAction * action;
760
761                 action = tmp->data;
762                 buf = filteringaction_to_string(action);
763                 if (debug_filtering_session)
764                         log_print(LOG_DEBUG_FILTERING, _("applying action [ %s ]\n"), buf);
765
766                 if (FALSE == (result = filteringaction_apply(action, info))) {
767                                         if (debug_filtering_session) {
768                                                 if (action->type != MATCHACTION_STOP)
769                                                         log_warning(LOG_DEBUG_FILTERING, _("action could not apply\n"));
770                                                 log_print(LOG_DEBUG_FILTERING,
771                                                                 _("no further processing after action [ %s ]\n"), buf);
772                                         }
773                                         debug_print("No further processing after rule %s\n", buf);
774                 }
775                 g_free(buf);
776                 if (filtering_is_final_action(action)) {
777                         * final = TRUE;
778                         break;
779                 }
780                 
781         }
782         return result;
783 }
784
785 /*!
786  *\brief        Check if an action is "final", i.e. should break further
787  *              processing.
788  *
789  *\param        filtering_action Action to check.
790  *
791  *\return       gboolean TRUE if \a filtering_action is final.  
792  */
793 static gboolean filtering_is_final_action(FilteringAction *filtering_action)
794 {
795         switch(filtering_action->type) {
796         case MATCHACTION_MOVE:
797         case MATCHACTION_DELETE:
798         case MATCHACTION_STOP:
799         case MATCHACTION_MARK_AS_SPAM:
800                 return TRUE; /* MsgInfo invalid for message */
801         default:
802                 return FALSE;
803         }
804 }
805
806 static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
807 {
808         GSList  *l;
809         gboolean final;
810         gboolean apply_next;
811         
812         cm_return_val_if_fail(info != NULL, TRUE);
813         
814         for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
815                 FilteringProp * filtering = (FilteringProp *) l->data;
816
817                 if (filtering->enabled) {
818                         if (debug_filtering_session) {
819                                 gchar *buf = filteringprop_to_string(filtering);
820                                 if (filtering->name && *filtering->name != '\0') {
821                                         log_print(LOG_DEBUG_FILTERING,
822                                                 _("processing rule '%s' [ %s ]\n"),
823                                                 filtering->name, buf);
824                                 } else {
825                                         log_print(LOG_DEBUG_FILTERING,
826                                                 _("processing rule <unnamed> [ %s ]\n"),
827                                                 buf);
828                                 }
829                                 g_free(buf);
830                         }
831
832                         if (filtering_match_condition(filtering, info, ac_prefs)) {
833                                 apply_next = filtering_apply_rule(filtering, info, &final);
834                                 if (final)
835                                         break;
836                         }
837
838                 } else {
839                         if (debug_filtering_session) {
840                                 gchar *buf = filteringprop_to_string(filtering);
841                                 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
842                                         if (filtering->name && *filtering->name != '\0') {
843                                                 log_status_skip(LOG_DEBUG_FILTERING,
844                                                                 _("disabled rule '%s' [ %s ]\n"),
845                                                                 filtering->name, buf);
846                                         } else {
847                                                 log_status_skip(LOG_DEBUG_FILTERING,
848                                                                 _("disabled rule <unnamed> [ %s ]\n"),
849                                                                 buf);
850                                         }
851                                 }
852                                 g_free(buf);
853                         }
854                 }
855         }
856
857     /* put in inbox if the last rule was not a final one, or
858      * a final rule could not be applied.
859      * Either of these cases is likely. */
860     if (!final || !apply_next) {
861                 return FALSE;
862         }
863
864         return TRUE;
865 }
866
867 /*!
868  *\brief        Filter a message against a list of rules.
869  *
870  *\param        flist List of filter rules.
871  *\param        info Message.
872  *
873  *\return       gboolean TRUE if filter rules handled the message.
874  *
875  *\note         Returning FALSE means the message was not handled,
876  *              and that the calling code should do the default
877  *              processing. E.g. \ref inc.c::inc_start moves the 
878  *              message to the inbox.   
879  */
880 gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
881                                                                    FilteringInvocationType context, gchar *extra_info)
882 {
883         gboolean ret;
884
885         if (prefs_common.enable_filtering_debug) {
886                 gchar *tmp = _("undetermined");
887 #ifndef G_OS_WIN32
888                 switch (context) {
889                 case FILTERING_INCORPORATION:
890                         tmp = _("incorporation");
891                         debug_filtering_session = prefs_common.enable_filtering_debug_inc;
892                         break;
893                 case FILTERING_MANUALLY:
894                         tmp = _("manually");
895                         debug_filtering_session = prefs_common.enable_filtering_debug_manual;
896                         break;
897                 case FILTERING_FOLDER_PROCESSING:
898                         tmp = _("folder processing");
899                         debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
900                         break;
901                 case FILTERING_PRE_PROCESSING:
902                         tmp = _("pre-processing");
903                         debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
904                         break;
905                 case FILTERING_POST_PROCESSING:
906                         tmp = _("post-processing");
907                         debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
908                         break;
909                 default:
910                         debug_filtering_session = FALSE;
911                         break;
912                 }
913 #else
914                 debug_filtering_session = FALSE;
915 #endif
916                 if (debug_filtering_session) {
917                         gchar *file = procmsg_get_message_file_path(info);
918                         gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');
919
920                         /* show context info and essential info about the message */
921                         if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
922                                 log_print(LOG_DEBUG_FILTERING,
923                                                 _("filtering message (%s%s%s)\n"
924                                                 "%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
925                                                 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
926                                                 spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
927                                                 spc, prefs_common_translated_header_name("From:"), info->from,
928                                                 spc, prefs_common_translated_header_name("To:"), info->to,
929                                                 spc, prefs_common_translated_header_name("Subject:"), info->subject);
930                         } else {
931                                 log_print(LOG_DEBUG_FILTERING,
932                                                 _("filtering message (%s%s%s)\n"
933                                                 "%smessage file: %s\n"),
934                                                 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
935                                                 spc, file);
936                         }
937                         g_free(file);
938                         g_free(spc);
939                 }
940         } else
941                 debug_filtering_session = FALSE;
942
943         ret = filter_msginfo(flist, info, ac_prefs);
944         debug_filtering_session = FALSE;
945         return ret;
946 }
947
948 gchar *filteringaction_to_string(FilteringAction *action)
949 {
950         const gchar *command_str;
951         gchar * quoted_dest;
952         gchar * quoted_header;
953         GString *dest = g_string_new("");
954         gchar *deststr = NULL;
955
956         command_str = get_matchparser_tab_str(action->type);
957
958         if (command_str == NULL)
959                 return NULL;
960
961         switch(action->type) {
962         case MATCHACTION_MOVE:
963         case MATCHACTION_COPY:
964         case MATCHACTION_EXECUTE:
965         case MATCHACTION_SET_TAG:
966         case MATCHACTION_UNSET_TAG:
967                 quoted_dest = matcher_quote_str(action->destination);
968                 g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
969                 g_free(quoted_dest);
970                 break;
971
972         case MATCHACTION_DELETE:
973         case MATCHACTION_MARK:
974         case MATCHACTION_UNMARK:
975         case MATCHACTION_LOCK:
976         case MATCHACTION_UNLOCK:
977         case MATCHACTION_MARK_AS_READ:
978         case MATCHACTION_MARK_AS_UNREAD:
979         case MATCHACTION_MARK_AS_SPAM:
980         case MATCHACTION_MARK_AS_HAM:
981         case MATCHACTION_STOP:
982         case MATCHACTION_HIDE:
983         case MATCHACTION_IGNORE:
984         case MATCHACTION_WATCH:
985         case MATCHACTION_CLEAR_TAGS:
986                 g_string_append_printf(dest, "%s", command_str);
987                 break;
988
989         case MATCHACTION_REDIRECT:
990         case MATCHACTION_FORWARD:
991         case MATCHACTION_FORWARD_AS_ATTACHMENT:
992                 quoted_dest = matcher_quote_str(action->destination);
993                 g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
994                 g_free(quoted_dest);
995                 break;
996
997         case MATCHACTION_COLOR:
998                 g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
999                 break;
1000
1001         case MATCHACTION_CHANGE_SCORE:
1002         case MATCHACTION_SET_SCORE:
1003                 g_string_append_printf(dest, "%s %d", command_str, action->score);
1004                 break;
1005
1006         case MATCHACTION_ADD_TO_ADDRESSBOOK:
1007                 quoted_header = matcher_quote_str(action->header);
1008                 quoted_dest = matcher_quote_str(action->destination);
1009                 g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
1010                 g_free(quoted_dest);
1011                 g_free(quoted_header);
1012                 break;
1013
1014         default:
1015                 return NULL;
1016         }
1017         deststr = dest->str;
1018         g_string_free(dest, FALSE);
1019         return deststr;
1020 }
1021
1022 gchar * filteringaction_list_to_string(GSList * action_list)
1023 {
1024         gchar *action_list_str;
1025         GSList * tmp;
1026         gchar *list_str;
1027
1028         action_list_str = NULL;
1029         for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
1030                 gchar *action_str;
1031                 FilteringAction * action;
1032                 
1033                 action = tmp->data;
1034                 
1035                 action_str = filteringaction_to_string(action);
1036                 
1037                 if (action_list_str != NULL) {
1038                         list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1039                         g_free(action_list_str);
1040                 }
1041                 else {
1042                         list_str = g_strdup(action_str);
1043                 }
1044                 g_free(action_str);
1045                 action_list_str = list_str;
1046         }
1047
1048         return action_list_str;
1049 }
1050
1051 gchar * filteringprop_to_string(FilteringProp * prop)
1052 {
1053         gchar *list_str;
1054         gchar *action_list_str;
1055         gchar *filtering_str;
1056
1057         if (prop == NULL)
1058                 return NULL;
1059
1060         action_list_str = filteringaction_list_to_string(prop->action_list);
1061
1062         if (action_list_str == NULL)
1063                 return NULL;
1064
1065         list_str = matcherlist_to_string(prop->matchers);
1066
1067         if (list_str == NULL) {
1068                 g_free(action_list_str);
1069                 return NULL;
1070         }
1071
1072         filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1073         g_free(action_list_str);
1074         g_free(list_str);
1075
1076         return filtering_str;
1077 }
1078
1079 static void prefs_filtering_free(GSList * prefs_filtering)
1080 {
1081         while (prefs_filtering != NULL) {
1082                 FilteringProp * filtering = (FilteringProp *)
1083                         prefs_filtering->data;
1084                 filteringprop_free(filtering);
1085                 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1086         }
1087 }
1088
1089 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1090 {
1091         FolderItem *item = node->data;
1092
1093         cm_return_val_if_fail(item, FALSE);
1094         cm_return_val_if_fail(item->prefs, FALSE);
1095
1096         prefs_filtering_free(item->prefs->processing);
1097         item->prefs->processing = NULL;
1098
1099         return FALSE;
1100 }
1101
1102 void prefs_filtering_clear(void)
1103 {
1104         GList * cur;
1105
1106         for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1107                 Folder *folder;
1108
1109                 folder = (Folder *) cur->data;
1110                 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1111                                 prefs_filtering_free_func, NULL);
1112         }
1113
1114         prefs_filtering_free(filtering_rules);
1115         filtering_rules = NULL;
1116         prefs_filtering_free(pre_global_processing);
1117         pre_global_processing = NULL;
1118         prefs_filtering_free(post_global_processing);
1119         post_global_processing = NULL;
1120 }
1121
1122 void prefs_filtering_clear_folder(Folder *folder)
1123 {
1124         cm_return_if_fail(folder);
1125         cm_return_if_fail(folder->node);
1126
1127         g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1128                         prefs_filtering_free_func, NULL);
1129         /* FIXME: Note folder settings were changed, where the updates? */
1130 }
1131
1132 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1133 /* return TRUE if there's at least one per-account filtering rule */
1134 {
1135         GSList *l;
1136
1137         for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1138                 FilteringProp * filtering = (FilteringProp *) l->data;
1139
1140                 if (filtering->enabled && (filtering->account_id != 0)) {
1141                         return TRUE;
1142                 }               
1143         }
1144
1145         return FALSE;
1146 }
1147
1148 gboolean filtering_action_list_rename_path(GSList *action_list, const gchar *old_path,
1149                                            const gchar *new_path)
1150 {
1151         gchar *base;
1152         gchar *prefix;
1153         gchar *suffix;
1154         gchar *dest_path;
1155         gchar *old_path_with_sep;
1156         gint destlen;
1157         gint prefixlen;
1158         gint oldpathlen;
1159         GSList * action_cur;
1160         const gchar *separator=G_DIR_SEPARATOR_S;
1161         gboolean matched = FALSE;
1162 #ifdef G_OS_WIN32
1163 again:
1164 #endif
1165         oldpathlen = strlen(old_path);
1166         old_path_with_sep = g_strconcat(old_path,separator,NULL);
1167
1168         for(action_cur = action_list ; action_cur != NULL ;
1169                 action_cur = action_cur->next) {
1170
1171                 FilteringAction *action = action_cur->data;
1172                         
1173                 if (action->type == MATCHACTION_SET_TAG ||
1174                     action->type == MATCHACTION_UNSET_TAG)
1175                         continue;
1176                 if (!action->destination) 
1177                         continue;
1178                 
1179                 destlen = strlen(action->destination);
1180                         
1181                 if (destlen > oldpathlen) {
1182                         prefixlen = destlen - oldpathlen;
1183                         suffix = action->destination + prefixlen;
1184                                 
1185                         if (!strncmp(old_path, suffix, oldpathlen)) {
1186                                 prefix = g_malloc0(prefixlen + 1);
1187                                 strncpy2(prefix, action->destination, prefixlen);
1188                                         
1189                                 base = suffix + oldpathlen;
1190                                 while (*base == G_DIR_SEPARATOR) base++;
1191                                 if (*base == '\0')
1192                                         dest_path = g_strconcat(prefix, separator,
1193                                                                 new_path, NULL);
1194                                 else
1195                                         dest_path = g_strconcat(prefix,
1196                                                                 separator,
1197                                                                 new_path,
1198                                                                 separator,
1199                                                                 base, NULL);
1200                                         
1201                                         g_free(prefix);
1202                                         g_free(action->destination);
1203                                         action->destination = dest_path;
1204                                         matched = TRUE;
1205                         } else { /* for non-leaf folders */
1206                                 /* compare with trailing slash */
1207                                 if (!strncmp(old_path_with_sep, action->destination, oldpathlen+1)) {
1208                                                 
1209                                         suffix = action->destination + oldpathlen + 1;
1210                                         dest_path = g_strconcat(new_path, separator,
1211                                                                 suffix, NULL);
1212                                         g_free(action->destination);
1213                                         action->destination = dest_path;
1214                                         matched = TRUE;
1215                                 }
1216                         }
1217                 } else {
1218                         /* folder-moving a leaf */
1219                         if (!strcmp(old_path, action->destination)) {
1220                                 dest_path = g_strdup(new_path);
1221                                 g_free(action->destination);
1222                                 action->destination = dest_path;
1223                                 matched = TRUE;
1224                         }
1225                 }
1226         }
1227         
1228         g_free(old_path_with_sep);
1229 #ifdef G_OS_WIN32
1230         if (!strcmp(separator, G_DIR_SEPARATOR_S) && !matched) {
1231                 separator = "/";
1232                 goto again;
1233         }
1234 #endif
1235
1236         return matched;
1237 }