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