fix bug 4773, 'remove obsolescent AC_C_CONST'
[claws.git] / src / filtering.c
blobb5353f46b4f5a0f8c030e30f2738e5fd7c797744
1 /*
2 * Claws Mail -- a GTK based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2020 the Claws Mail Team and Hiroyuki Yamamoto
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.
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.
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/>.
19 #include "config.h"
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>
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_ALT_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"
52 GSList * pre_global_processing = NULL;
53 GSList * post_global_processing = NULL;
54 GSList * filtering_rules = NULL;
56 gboolean debug_filtering_session = FALSE;
58 static gboolean filtering_is_final_action(FilteringAction *filtering_action);
60 FilteringAction * filteringaction_new(int type, int account_id,
61 gchar * destination,
62 gint labelcolor, gint score, gchar * header)
64 FilteringAction * action;
66 action = g_new0(FilteringAction, 1);
68 action->type = type;
69 action->account_id = account_id;
70 if (destination) {
71 action->destination = g_strdup(destination);
72 } else {
73 action->destination = NULL;
75 if (header) {
76 action->header = g_strdup(header);
77 } else {
78 action->header = NULL;
80 action->labelcolor = labelcolor;
81 action->score = score;
82 return action;
85 void filteringaction_free(FilteringAction * action)
87 cm_return_if_fail(action);
88 g_free(action->header);
89 g_free(action->destination);
90 g_free(action);
93 static gint action_list_sort(gconstpointer a, gconstpointer b)
95 int first = filtering_is_final_action((FilteringAction *) a) ? 1 : 0;
96 int second = filtering_is_final_action((FilteringAction *) b) ? 1 : 0;
98 return (first - second);
101 GSList *filtering_action_list_sort(GSList *action_list)
103 return g_slist_sort(action_list, action_list_sort);
106 FilteringProp * filteringprop_new(gboolean enabled,
107 const gchar *name,
108 gint account_id,
109 MatcherList * matchers,
110 GSList * action_list)
112 FilteringProp * filtering;
114 filtering = g_new0(FilteringProp, 1);
115 filtering->enabled = enabled;
116 filtering->name = name ? g_strdup(name): NULL;
117 filtering->account_id = account_id;
118 filtering->matchers = matchers;
119 filtering->action_list = filtering_action_list_sort(action_list);
121 return filtering;
124 static FilteringAction * filteringaction_copy(FilteringAction * src)
126 FilteringAction * new;
128 new = g_new0(FilteringAction, 1);
130 new->type = src->type;
131 new->account_id = src->account_id;
132 if (src->destination)
133 new->destination = g_strdup(src->destination);
134 else
135 new->destination = NULL;
136 new->labelcolor = src->labelcolor;
137 new->score = src->score;
139 return new;
142 FilteringProp * filteringprop_copy(FilteringProp *src)
144 FilteringProp * new;
145 GSList *tmp;
147 new = g_new0(FilteringProp, 1);
148 new->matchers = g_new0(MatcherList, 1);
150 for (tmp = src->matchers->matchers; tmp != NULL && tmp->data != NULL;) {
151 MatcherProp *matcher = (MatcherProp *)tmp->data;
153 new->matchers->matchers = g_slist_append(new->matchers->matchers,
154 matcherprop_copy(matcher));
155 tmp = tmp->next;
158 new->matchers->bool_and = src->matchers->bool_and;
160 new->action_list = NULL;
162 for (tmp = src->action_list ; tmp != NULL ; tmp = tmp->next) {
163 FilteringAction *filtering_action;
165 filtering_action = tmp->data;
167 new->action_list = g_slist_append(new->action_list,
168 filteringaction_copy(filtering_action));
171 new->enabled = src->enabled;
172 new->name = g_strdup(src->name);
174 return new;
177 void filteringprop_free(FilteringProp * prop)
179 GSList * tmp;
181 cm_return_if_fail(prop);
182 matcherlist_free(prop->matchers);
184 for (tmp = prop->action_list ; tmp != NULL ; tmp = tmp->next) {
185 filteringaction_free(tmp->data);
187 g_slist_free(prop->action_list);
188 g_free(prop->name);
189 g_free(prop);
192 /* move and copy messages by batches to be faster on IMAP */
193 void filtering_move_and_copy_msgs(GSList *msgs)
195 GSList *messages = g_slist_copy(msgs);
196 FolderItem *last_item = NULL;
197 FiltOp cur_op = IS_NOTHING;
199 debug_print("checking %d messages\n", g_slist_length(msgs));
200 while (messages) {
201 GSList *batch = NULL, *cur;
202 gint found = 0;
203 for (cur = messages; cur; cur = cur->next) {
204 MsgInfo *info = (MsgInfo *)cur->data;
205 if (last_item == NULL) {
206 if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE)
207 last_item = info->to_filter_folder;
208 else if (info->filter_op == IS_DELE)
209 last_item = info->folder;
211 if (last_item == NULL)
212 continue;
213 if (cur_op == IS_NOTHING) {
214 if (info->filter_op == IS_COPY)
215 cur_op = IS_COPY;
216 else if (info->filter_op == IS_MOVE)
217 cur_op = IS_MOVE;
218 else if (info->filter_op == IS_DELE)
219 cur_op = IS_DELE;
221 if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE) {
222 if (info->to_filter_folder == last_item
223 && cur_op == info->filter_op) {
224 found++;
225 batch = g_slist_prepend(batch, info);
227 } else if (info->filter_op == IS_DELE) {
228 if (info->folder == last_item
229 && cur_op == info->filter_op) {
230 found++;
231 batch = g_slist_prepend(batch, info);
235 if (found == 0) {
236 debug_print("no more messages to move/copy/del\n");
237 break;
238 } else {
239 debug_print("%d messages to %s in %s\n", found,
240 cur_op==IS_COPY ? "copy":(cur_op==IS_DELE ?"delete":"move"),
241 last_item->name ? last_item->name:"(noname)");
243 for (cur = batch; cur; cur = cur->next) {
244 MsgInfo *info = (MsgInfo *)cur->data;
245 messages = g_slist_remove(messages, info);
246 info->to_filter_folder = NULL;
247 info->filter_op = IS_NOTHING;
249 batch = g_slist_reverse(batch);
250 if (g_slist_length(batch)) {
251 MsgInfo *info = (MsgInfo *)batch->data;
252 if (cur_op == IS_COPY && last_item != info->folder) {
253 folder_item_copy_msgs(last_item, batch);
254 } else if (cur_op == IS_MOVE && last_item != info->folder) {
255 if (folder_item_move_msgs(last_item, batch) < 0)
256 folder_item_move_msgs(
257 folder_get_default_inbox(),
258 batch);
259 } else if (cur_op == IS_DELE && last_item == info->folder) {
260 folder_item_remove_msgs(last_item, batch);
262 /* we don't reference the msginfos, because caller will do */
263 if (prefs_common.real_time_sync)
264 folder_item_synchronise(last_item);
265 g_slist_free(batch);
266 batch = NULL;
267 GTK_EVENTS_FLUSH();
269 last_item = NULL;
270 cur_op = IS_NOTHING;
272 /* we don't reference the msginfos, because caller will do */
273 g_slist_free(messages);
277 fitleringaction_apply
278 runs the action on one MsgInfo
279 return value : return TRUE if the action could be applied
282 #define FLUSH_COPY_IF_NEEDED(info) { \
283 if (info->filter_op == IS_COPY && info->to_filter_folder) { \
284 debug_print("must debatch pending copy\n"); \
285 folder_item_copy_msg(info->to_filter_folder, info); \
286 info->filter_op = IS_NOTHING; \
290 static gboolean filteringaction_apply(FilteringAction * action, MsgInfo * info)
292 FolderItem * dest_folder;
293 gint val;
294 Compose * compose;
295 PrefsAccount * account;
296 gchar * cmd;
298 switch(action->type) {
299 case MATCHACTION_MOVE:
300 if (MSG_IS_LOCKED(info->flags))
301 return FALSE;
303 dest_folder =
304 folder_find_item_from_identifier(action->destination);
305 if (!dest_folder) {
306 debug_print("*** folder not found '%s'\n",
307 action->destination ?action->destination :"(null)");
308 return FALSE;
311 FLUSH_COPY_IF_NEEDED(info);
312 /* mark message to be moved */
313 info->filter_op = IS_MOVE;
314 info->to_filter_folder = dest_folder;
315 return TRUE;
317 case MATCHACTION_COPY:
318 dest_folder =
319 folder_find_item_from_identifier(action->destination);
321 if (!dest_folder) {
322 debug_print("*** folder not found '%s'\n",
323 action->destination ?action->destination :"(null)");
324 return FALSE;
327 FLUSH_COPY_IF_NEEDED(info);
328 /* mark message to be copied */
329 info->filter_op = IS_COPY;
330 info->to_filter_folder = dest_folder;
331 return TRUE;
333 case MATCHACTION_SET_TAG:
334 case MATCHACTION_UNSET_TAG:
335 val = tags_get_id_for_str(action->destination);
336 if (val == -1) {
337 debug_print("*** tag '%s' not found\n",
338 action->destination ?action->destination :"(null)");
339 return FALSE;
341 FLUSH_COPY_IF_NEEDED(info);
342 procmsg_msginfo_update_tags(info, (action->type == MATCHACTION_SET_TAG), val);
343 return TRUE;
345 case MATCHACTION_CLEAR_TAGS:
346 FLUSH_COPY_IF_NEEDED(info);
347 procmsg_msginfo_clear_tags(info);
348 return TRUE;
350 case MATCHACTION_DELETE:
351 FLUSH_COPY_IF_NEEDED(info);
352 info->filter_op = IS_DELE;
353 return TRUE;
355 case MATCHACTION_MARK:
356 FLUSH_COPY_IF_NEEDED(info);
357 procmsg_msginfo_set_flags(info, MSG_MARKED, 0);
358 return TRUE;
360 case MATCHACTION_UNMARK:
361 FLUSH_COPY_IF_NEEDED(info);
362 procmsg_msginfo_unset_flags(info, MSG_MARKED, 0);
363 return TRUE;
365 case MATCHACTION_LOCK:
366 FLUSH_COPY_IF_NEEDED(info);
367 procmsg_msginfo_set_flags(info, MSG_LOCKED, 0);
368 return TRUE;
370 case MATCHACTION_UNLOCK:
371 FLUSH_COPY_IF_NEEDED(info);
372 procmsg_msginfo_unset_flags(info, MSG_LOCKED, 0);
373 return TRUE;
375 case MATCHACTION_MARK_AS_READ:
376 FLUSH_COPY_IF_NEEDED(info);
377 procmsg_msginfo_unset_flags(info, MSG_UNREAD | MSG_NEW, 0);
378 return TRUE;
380 case MATCHACTION_MARK_AS_UNREAD:
381 FLUSH_COPY_IF_NEEDED(info);
382 procmsg_msginfo_change_flags(info, MSG_UNREAD, 0, MSG_NEW, 0);
383 return TRUE;
385 case MATCHACTION_MARK_AS_SPAM:
386 FLUSH_COPY_IF_NEEDED(info);
387 procmsg_spam_learner_learn(info, NULL, TRUE);
388 procmsg_msginfo_change_flags(info, MSG_SPAM, 0, MSG_NEW|MSG_UNREAD, 0);
389 return TRUE;
391 case MATCHACTION_MARK_AS_HAM:
392 FLUSH_COPY_IF_NEEDED(info);
393 procmsg_spam_learner_learn(info, NULL, FALSE);
394 procmsg_msginfo_unset_flags(info, MSG_SPAM, 0);
395 return TRUE;
397 case MATCHACTION_COLOR:
398 FLUSH_COPY_IF_NEEDED(info);
399 procmsg_msginfo_unset_flags(info, MSG_CLABEL_FLAG_MASK, 0);
400 procmsg_msginfo_set_flags(info, MSG_COLORLABEL_TO_FLAGS(action->labelcolor), 0);
401 return TRUE;
403 case MATCHACTION_FORWARD:
404 case MATCHACTION_FORWARD_AS_ATTACHMENT:
405 account = account_find_from_id(action->account_id);
406 compose = compose_forward(account, info,
407 action->type == MATCHACTION_FORWARD ? FALSE : TRUE,
408 NULL, TRUE, TRUE);
409 compose_entry_append(compose, action->destination,
410 compose->account->protocol == A_NNTP
411 ? COMPOSE_NEWSGROUPS
412 : COMPOSE_TO, PREF_NONE);
414 val = compose_send(compose);
416 return val == 0 ? TRUE : FALSE;
418 case MATCHACTION_REDIRECT:
419 account = account_find_from_id(action->account_id);
420 compose = compose_redirect(account, info, TRUE);
421 if (compose->account->protocol == A_NNTP)
422 break;
423 else
424 compose_entry_append(compose, action->destination,
425 COMPOSE_TO, PREF_NONE);
427 val = compose_send(compose);
429 return val == 0 ? TRUE : FALSE;
431 case MATCHACTION_EXECUTE:
432 cmd = matching_build_command(action->destination, info);
433 if (cmd == NULL)
434 return FALSE;
435 else {
436 if (system(cmd) == -1)
437 g_warning("couldn't run %s", cmd);
438 g_free(cmd);
440 return TRUE;
442 case MATCHACTION_SET_SCORE:
443 FLUSH_COPY_IF_NEEDED(info);
444 info->score = action->score;
445 return TRUE;
447 case MATCHACTION_CHANGE_SCORE:
448 FLUSH_COPY_IF_NEEDED(info);
449 info->score += action->score;
450 return TRUE;
452 case MATCHACTION_STOP:
453 return FALSE;
455 case MATCHACTION_HIDE:
456 FLUSH_COPY_IF_NEEDED(info);
457 info->hidden = TRUE;
458 return TRUE;
460 case MATCHACTION_IGNORE:
461 FLUSH_COPY_IF_NEEDED(info);
462 procmsg_msginfo_set_flags(info, MSG_IGNORE_THREAD, 0);
463 return TRUE;
465 case MATCHACTION_WATCH:
466 FLUSH_COPY_IF_NEEDED(info);
467 procmsg_msginfo_set_flags(info, MSG_WATCH_THREAD, 0);
468 return TRUE;
470 case MATCHACTION_ADD_TO_ADDRESSBOOK:
472 #ifndef USE_ALT_ADDRBOOK
473 AddressDataSource *book = NULL;
474 AddressBookFile *abf = NULL;
475 ItemFolder *folder = NULL;
476 #endif
477 gchar *buf = NULL;
478 Header *header = NULL;
479 gint errors = 0;
481 #ifndef USE_ALT_ADDRBOOK
482 if (!addressbook_peek_folder_exists(action->destination, &book, &folder)) {
483 g_warning("addressbook folder not found '%s'", action->destination?action->destination:"(null)");
484 return FALSE;
486 if (!book) {
487 g_warning("addressbook_peek_folder_exists returned NULL book");
488 return FALSE;
491 abf = book->rawDataSource;
492 #endif
493 /* get the header */
494 if (procheader_get_header_from_msginfo(info, &buf, action->header) < 0)
495 return FALSE;
497 header = procheader_parse_header(buf);
498 g_free(buf);
500 /* add all addresses that are not already in */
501 if (header && *header->body && (*header->body != '\0')) {
502 GSList *address_list = NULL;
503 GSList *walk = NULL;
504 gchar *path = NULL;
506 if (action->destination == NULL ||
507 strcasecmp(action->destination, "Any") == 0 ||
508 *(action->destination) == '\0')
509 path = NULL;
510 else
511 path = action->destination;
512 start_address_completion(path);
514 address_list = g_slist_append(address_list, header->body);
515 for (walk = address_list; walk != NULL; walk = walk->next) {
516 gchar *stripped_addr = g_strdup(walk->data);
517 extract_address(stripped_addr);
519 if (complete_matches_found(walk->data) == 0) {
520 gchar *name = procheader_get_fromname(walk->data);
521 debug_print("adding '%s <%s>' to addressbook '%s'\n",
522 name, stripped_addr, action->destination);
523 #ifndef USE_ALT_ADDRBOOK
524 if (!addrbook_add_contact(abf, folder, name, stripped_addr, NULL)) {
525 #else
526 if (!addressadd_selection(name, stripped_addr, NULL, NULL)) {
527 #endif
528 g_warning("contact could not be added");
529 errors++;
531 g_free(name);
532 } else {
533 debug_print("address '%s' already found in addressbook '%s', skipping\n",
534 stripped_addr, action->destination);
536 g_free(stripped_addr);
539 g_slist_free(address_list);
540 end_address_completion();
541 } else {
542 g_warning("header '%s' not set or empty", action->header?action->header:"(null)");
544 if (header)
545 procheader_header_free(header);
546 return (errors == 0);
548 default:
549 break;
551 return FALSE;
554 gboolean filteringaction_apply_action_list(GSList *action_list, MsgInfo *info)
556 GSList *p;
557 cm_return_val_if_fail(action_list, FALSE);
558 cm_return_val_if_fail(info, FALSE);
559 for (p = action_list; p && p->data; p = g_slist_next(p)) {
560 FilteringAction *a = (FilteringAction *) p->data;
561 if (filteringaction_apply(a, info)) {
562 if (filtering_is_final_action(a))
563 break;
564 } else
565 return FALSE;
568 return TRUE;
571 static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info,
572 PrefsAccount *ac_prefs)
574 /* this function returns true if a filtering rule applies regarding to its account
575 data and if it does, if the conditions list match.
577 per-account data of a filtering rule is either matched against current account
578 when filtering is done manually, or against the account currently used for
579 retrieving messages when it's an manual or automatic fetching of messages.
580 per-account data match doesn't apply to pre-/post-/folder-processing rules.
582 when filtering messages manually:
583 - either the filtering rule is not account-based and it will be processed
584 - or it's per-account and we check if we HAVE TO match it against the current
585 account (according to user-land settings, per-account rules might have to
586 be skipped, or only the rules that match the current account have to be
587 applied, or all rules will have to be applied regardless to if they are
588 account-based or not)
590 notes about debugging output in that function:
591 when not matching, log_status_skip() is used, otherwise log_status_ok() is used
592 no debug output is done when filtering_debug_level is low
595 gboolean matches = FALSE;
597 if (ac_prefs != NULL) {
598 matches = ((filtering->account_id == 0)
599 || (filtering->account_id == ac_prefs->account_id));
601 /* debug output */
602 if (debug_filtering_session) {
603 if (matches) {
604 /* either because rule is not account-based */
605 if (filtering->account_id == 0) {
606 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
607 log_status_ok(LOG_DEBUG_FILTERING,
608 _("rule is not account-based\n"));
610 } else {
611 /* or because it is account-based, and matching current account */
612 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
613 /* with fewer detail when rule is OK */
614 log_status_ok(LOG_DEBUG_FILTERING,
615 _("rule is account-based, "
616 "matching the account currently used to retrieve messages\n"));
617 } else {
618 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_HIGH) {
619 /* with more detail when rule is OK */
620 log_status_ok(LOG_DEBUG_FILTERING,
621 _("rule is account-based [id=%d, name='%s'], "
622 "matching the account currently used to retrieve messages\n"),
623 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
627 } else {
628 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
629 /* with fewer detail when rule is skipped */
630 log_status_skip(LOG_DEBUG_FILTERING,
631 _("rule is account-based, "
632 "not matching the account currently used to retrieve messages\n"));
633 } else {
634 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_HIGH) {
635 /* with more detail when rule is skipped */
636 PrefsAccount *account = account_find_from_id(filtering->account_id);
638 log_status_skip(LOG_DEBUG_FILTERING,
639 _("rule is account-based [id=%d, name='%s'], "
640 "not matching the account currently used to retrieve messages [id=%d, name='%s']\n"),
641 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
642 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
647 } else {
648 switch (prefs_common.apply_per_account_filtering_rules) {
649 case FILTERING_ACCOUNT_RULES_FORCE:
650 /* apply filtering rules regardless to the account info */
651 matches = TRUE;
653 /* debug output */
654 if (debug_filtering_session) {
655 if (filtering->account_id == 0) {
656 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
657 log_status_ok(LOG_DEBUG_FILTERING,
658 _("rule is not account-based, "
659 "but all rules are applied on user request anyway\n"));
661 } else {
662 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
663 /* with fewer detail when rule is OK */
664 log_status_ok(LOG_DEBUG_FILTERING,
665 _("rule is account-based, "
666 "but all rules are applied on user request anyway\n"));
667 } else {
668 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
669 /* with more detail when rule is OK */
670 PrefsAccount *account = account_find_from_id(filtering->account_id);
672 log_status_ok(LOG_DEBUG_FILTERING,
673 _("rule is account-based [id=%d, name='%s'], "
674 "but all rules are applied on user request anyway\n"),
675 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
680 break;
681 case FILTERING_ACCOUNT_RULES_SKIP:
682 /* don't apply filtering rules that belong to an account */
683 matches = (filtering->account_id == 0);
685 /* debug output */
686 if (debug_filtering_session) {
687 if (matches) {
688 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
689 log_status_ok(LOG_DEBUG_FILTERING,
690 _("rule is not account-based\n"));
692 } else {
693 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
694 /* with fewer detail when rule is skipped */
695 log_status_skip(LOG_DEBUG_FILTERING,
696 _("rule is account-based, "
697 "skipped on user request\n"));
698 } else {
699 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_HIGH) {
700 /* with more detail when rule is skipped */
701 PrefsAccount *account = account_find_from_id(filtering->account_id);
703 log_status_skip(LOG_DEBUG_FILTERING,
704 _("rule is account-based [id=%d, name='%s'], "
705 "skipped on user request\n"),
706 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
711 break;
712 case FILTERING_ACCOUNT_RULES_USE_CURRENT:
713 matches = ((filtering->account_id == 0)
714 || (filtering->account_id == cur_account->account_id));
716 /* debug output */
717 if (debug_filtering_session) {
718 if (matches) {
719 if (filtering->account_id == 0) {
720 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
721 log_status_ok(LOG_DEBUG_FILTERING,
722 _("rule is not account-based\n"));
724 } else {
725 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
726 /* with fewer detail when rule is OK */
727 log_status_ok(LOG_DEBUG_FILTERING,
728 _("rule is account-based, "
729 "matching current account\n"));
730 } else {
731 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
732 /* with more detail when rule is OK */
733 PrefsAccount *account = account_find_from_id(filtering->account_id);
735 log_status_ok(LOG_DEBUG_FILTERING,
736 _("rule is account-based [id=%d, name='%s'], "
737 "matching current account [id=%d, name='%s']\n"),
738 account->account_id, account?account->account_name:_("NON_EXISTENT"),
739 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
743 } else {
744 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
745 /* with fewer detail when rule is skipped */
746 log_status_skip(LOG_DEBUG_FILTERING,
747 _("rule is account-based, "
748 "not matching current account\n"));
749 } else {
750 if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_HIGH) {
751 /* with more detail when rule is skipped */
752 PrefsAccount *account = account_find_from_id(filtering->account_id);
754 log_status_skip(LOG_DEBUG_FILTERING,
755 _("rule is account-based [id=%d, name='%s'], "
756 "not matching current account [id=%d, name='%s']\n"),
757 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
758 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
763 break;
767 return matches && matcherlist_match(filtering->matchers, info);
771 *\brief Apply a rule on message.
773 *\param filtering List of filtering rules.
774 *\param info Message to apply rules on.
775 *\param final Variable returning TRUE or FALSE if one of the
776 * encountered actions was final.
777 * See also \ref filtering_is_final_action.
779 *\return gboolean TRUE to continue applying rules.
781 static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info,
782 gboolean * final)
784 gboolean result = TRUE;
785 gchar *buf;
786 GSList * tmp;
788 * final = FALSE;
789 for (tmp = filtering->action_list ; tmp != NULL ; tmp = tmp->next) {
790 FilteringAction * action;
792 action = tmp->data;
793 buf = filteringaction_to_string(action);
794 if (debug_filtering_session)
795 log_print(LOG_DEBUG_FILTERING, _("applying action [ %s ]\n"), buf);
797 if (FALSE == (result = filteringaction_apply(action, info))) {
798 if (debug_filtering_session) {
799 if (action->type != MATCHACTION_STOP)
800 log_warning(LOG_DEBUG_FILTERING, _("action could not apply\n"));
801 log_print(LOG_DEBUG_FILTERING,
802 _("no further processing after action [ %s ]\n"), buf);
804 debug_print("No further processing after rule %s\n", buf);
806 g_free(buf);
807 if (filtering_is_final_action(action)) {
808 * final = TRUE;
809 break;
813 return result;
817 *\brief Check if an action is "final", i.e. should break further
818 * processing.
820 *\param filtering_action Action to check.
822 *\return gboolean TRUE if \a filtering_action is final.
824 static gboolean filtering_is_final_action(FilteringAction *filtering_action)
826 switch(filtering_action->type) {
827 case MATCHACTION_MOVE:
828 case MATCHACTION_DELETE:
829 case MATCHACTION_STOP:
830 return TRUE; /* MsgInfo invalid for message */
831 default:
832 return FALSE;
836 gboolean processing_enabled(GSList *filtering_list)
838 GSList *l;
839 for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
840 FilteringProp * filtering = (FilteringProp *) l->data;
841 if (filtering->enabled)
842 return TRUE;
844 return FALSE;
847 static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
849 GSList *l;
850 gboolean final;
851 gboolean apply_next;
853 cm_return_val_if_fail(info != NULL, TRUE);
855 for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
856 FilteringProp * filtering = (FilteringProp *) l->data;
858 if (filtering->enabled) {
859 if (debug_filtering_session) {
860 gchar *buf = filteringprop_to_string(filtering);
861 if (filtering->name && *filtering->name != '\0') {
862 log_print(LOG_DEBUG_FILTERING,
863 _("processing rule '%s' [ %s ]\n"),
864 filtering->name, buf);
865 } else {
866 log_print(LOG_DEBUG_FILTERING,
867 _("processing rule <unnamed> [ %s ]\n"),
868 buf);
870 g_free(buf);
873 if (filtering_match_condition(filtering, info, ac_prefs)) {
874 apply_next = filtering_apply_rule(filtering, info, &final);
875 if (final)
876 break;
879 } else {
880 if (debug_filtering_session) {
881 gchar *buf = filteringprop_to_string(filtering);
882 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
883 if (filtering->name && *filtering->name != '\0') {
884 log_status_skip(LOG_DEBUG_FILTERING,
885 _("disabled rule '%s' [ %s ]\n"),
886 filtering->name, buf);
887 } else {
888 log_status_skip(LOG_DEBUG_FILTERING,
889 _("disabled rule <unnamed> [ %s ]\n"),
890 buf);
893 g_free(buf);
898 /* put in inbox if the last rule was not a final one, or
899 * a final rule could not be applied.
900 * Either of these cases is likely. */
901 if (!final || !apply_next) {
902 return FALSE;
905 return TRUE;
909 *\brief Filter a message against a list of rules.
911 *\param flist List of filter rules.
912 *\param info Message.
914 *\return gboolean TRUE if filter rules handled the message.
916 *\note Returning FALSE means the message was not handled,
917 * and that the calling code should do the default
918 * processing. E.g. \ref inc.c::inc_start moves the
919 * message to the inbox.
921 gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
922 FilteringInvocationType context, gchar *extra_info)
924 gboolean ret;
926 if (prefs_common.enable_filtering_debug) {
927 gchar *tmp = _("undetermined");
929 switch (context) {
930 case FILTERING_INCORPORATION:
931 tmp = _("incorporation");
932 debug_filtering_session = prefs_common.enable_filtering_debug_inc;
933 break;
934 case FILTERING_MANUALLY:
935 tmp = _("manually");
936 debug_filtering_session = prefs_common.enable_filtering_debug_manual;
937 break;
938 case FILTERING_FOLDER_PROCESSING:
939 tmp = _("folder processing");
940 debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
941 break;
942 case FILTERING_PRE_PROCESSING:
943 tmp = _("pre-processing");
944 debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
945 break;
946 case FILTERING_POST_PROCESSING:
947 tmp = _("post-processing");
948 debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
949 break;
950 default:
951 debug_filtering_session = FALSE;
952 break;
955 if (debug_filtering_session) {
956 gchar *file = procmsg_get_message_file_path(info);
957 gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');
959 /* show context info and essential info about the message */
960 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
961 log_print(LOG_DEBUG_FILTERING,
962 _("filtering message (%s%s%s)\n"
963 "%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
964 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
965 spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
966 spc, prefs_common_translated_header_name("From:"), info->from,
967 spc, prefs_common_translated_header_name("To:"), info->to,
968 spc, prefs_common_translated_header_name("Subject:"), info->subject);
969 } else {
970 log_print(LOG_DEBUG_FILTERING,
971 _("filtering message (%s%s%s)\n"
972 "%smessage file: %s\n"),
973 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
974 spc, file);
976 g_free(file);
977 g_free(spc);
979 } else
980 debug_filtering_session = FALSE;
982 ret = filter_msginfo(flist, info, ac_prefs);
983 debug_filtering_session = FALSE;
984 return ret;
987 gchar *filteringaction_to_string(FilteringAction *action)
989 const gchar *command_str;
990 gchar * quoted_dest;
991 gchar * quoted_header;
992 GString *dest = g_string_new("");
994 command_str = get_matchparser_tab_str(action->type);
996 if (command_str == NULL) {
997 g_string_free(dest, TRUE);
998 return NULL;
1001 switch(action->type) {
1002 case MATCHACTION_MOVE:
1003 case MATCHACTION_COPY:
1004 case MATCHACTION_EXECUTE:
1005 case MATCHACTION_SET_TAG:
1006 case MATCHACTION_UNSET_TAG:
1007 quoted_dest = matcher_quote_str(action->destination);
1008 g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
1009 g_free(quoted_dest);
1010 break;
1012 case MATCHACTION_DELETE:
1013 case MATCHACTION_MARK:
1014 case MATCHACTION_UNMARK:
1015 case MATCHACTION_LOCK:
1016 case MATCHACTION_UNLOCK:
1017 case MATCHACTION_MARK_AS_READ:
1018 case MATCHACTION_MARK_AS_UNREAD:
1019 case MATCHACTION_MARK_AS_SPAM:
1020 case MATCHACTION_MARK_AS_HAM:
1021 case MATCHACTION_STOP:
1022 case MATCHACTION_HIDE:
1023 case MATCHACTION_IGNORE:
1024 case MATCHACTION_WATCH:
1025 case MATCHACTION_CLEAR_TAGS:
1026 g_string_append_printf(dest, "%s", command_str);
1027 break;
1029 case MATCHACTION_REDIRECT:
1030 case MATCHACTION_FORWARD:
1031 case MATCHACTION_FORWARD_AS_ATTACHMENT:
1032 quoted_dest = matcher_quote_str(action->destination);
1033 g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
1034 g_free(quoted_dest);
1035 break;
1037 case MATCHACTION_COLOR:
1038 g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
1039 break;
1041 case MATCHACTION_CHANGE_SCORE:
1042 case MATCHACTION_SET_SCORE:
1043 g_string_append_printf(dest, "%s %d", command_str, action->score);
1044 break;
1046 case MATCHACTION_ADD_TO_ADDRESSBOOK:
1047 quoted_header = matcher_quote_str(action->header);
1048 quoted_dest = matcher_quote_str(action->destination);
1049 g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
1050 g_free(quoted_dest);
1051 g_free(quoted_header);
1052 break;
1054 default:
1055 g_string_free(dest, TRUE);
1056 return NULL;
1058 return g_string_free(dest, FALSE);
1061 gchar * filteringaction_list_to_string(GSList * action_list)
1063 gchar *action_list_str;
1064 GSList * tmp;
1065 gchar *list_str;
1067 action_list_str = NULL;
1068 for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
1069 gchar *action_str;
1070 FilteringAction * action;
1072 action = tmp->data;
1074 action_str = filteringaction_to_string(action);
1076 if (action_list_str != NULL) {
1077 list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1078 g_free(action_list_str);
1080 else {
1081 list_str = g_strdup(action_str);
1083 g_free(action_str);
1084 action_list_str = list_str;
1087 return action_list_str;
1090 gchar * filteringprop_to_string(FilteringProp * prop)
1092 gchar *list_str;
1093 gchar *action_list_str;
1094 gchar *filtering_str;
1096 if (prop == NULL)
1097 return NULL;
1099 action_list_str = filteringaction_list_to_string(prop->action_list);
1101 if (action_list_str == NULL)
1102 return NULL;
1104 list_str = matcherlist_to_string(prop->matchers);
1106 if (list_str == NULL) {
1107 g_free(action_list_str);
1108 return NULL;
1111 filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1112 g_free(action_list_str);
1113 g_free(list_str);
1115 return filtering_str;
1118 static void prefs_filtering_free(GSList * prefs_filtering)
1120 while (prefs_filtering != NULL) {
1121 FilteringProp * filtering = (FilteringProp *)
1122 prefs_filtering->data;
1123 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1124 filteringprop_free(filtering);
1128 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1130 FolderItem *item = node->data;
1132 cm_return_val_if_fail(item, FALSE);
1133 cm_return_val_if_fail(item->prefs, FALSE);
1135 prefs_filtering_free(item->prefs->processing);
1136 item->prefs->processing = NULL;
1138 return FALSE;
1141 void prefs_filtering_clear(void)
1143 GList * cur;
1145 for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1146 Folder *folder;
1148 folder = (Folder *) cur->data;
1149 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1150 prefs_filtering_free_func, NULL);
1153 prefs_filtering_free(filtering_rules);
1154 filtering_rules = NULL;
1155 prefs_filtering_free(pre_global_processing);
1156 pre_global_processing = NULL;
1157 prefs_filtering_free(post_global_processing);
1158 post_global_processing = NULL;
1161 void prefs_filtering_clear_folder(Folder *folder)
1163 cm_return_if_fail(folder);
1164 cm_return_if_fail(folder->node);
1166 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1167 prefs_filtering_free_func, NULL);
1168 /* FIXME: Note folder settings were changed, where the updates? */
1171 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1172 /* return TRUE if there's at least one per-account filtering rule */
1174 GSList *l;
1176 for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1177 FilteringProp * filtering = (FilteringProp *) l->data;
1179 if (filtering->enabled && (filtering->account_id != 0)) {
1180 return TRUE;
1184 return FALSE;
1187 gboolean filtering_action_list_rename_path(GSList *action_list, const gchar *old_path,
1188 const gchar *new_path)
1190 gchar *base;
1191 gchar *prefix;
1192 gchar *suffix;
1193 gchar *dest_path;
1194 gchar *old_path_with_sep;
1195 gint destlen;
1196 gint prefixlen;
1197 gint oldpathlen;
1198 GSList * action_cur;
1199 const gchar *separator=G_DIR_SEPARATOR_S;
1200 gboolean matched = FALSE;
1201 #ifdef G_OS_WIN32
1202 again:
1203 #endif
1204 oldpathlen = strlen(old_path);
1205 old_path_with_sep = g_strconcat(old_path,separator,NULL);
1207 for(action_cur = action_list ; action_cur != NULL ;
1208 action_cur = action_cur->next) {
1210 FilteringAction *action = action_cur->data;
1212 if (action->type == MATCHACTION_SET_TAG ||
1213 action->type == MATCHACTION_UNSET_TAG)
1214 continue;
1215 if (!action->destination)
1216 continue;
1218 destlen = strlen(action->destination);
1220 if (destlen > oldpathlen) {
1221 prefixlen = destlen - oldpathlen;
1222 suffix = action->destination + prefixlen;
1224 if (!strncmp(old_path, suffix, oldpathlen)) {
1225 prefix = g_malloc0(prefixlen + 1);
1226 strncpy2(prefix, action->destination, prefixlen);
1228 base = suffix + oldpathlen;
1229 while (*base == G_DIR_SEPARATOR)
1230 base++;
1231 if (*base == '\0')
1232 dest_path = g_strconcat(prefix, separator, new_path, NULL);
1233 else
1234 dest_path = g_strconcat(prefix, separator, new_path, separator, base, NULL);
1236 g_free(prefix);
1237 g_free(action->destination);
1238 action->destination = dest_path;
1239 matched = TRUE;
1240 } else { /* for non-leaf folders */
1241 /* compare with trailing slash */
1242 if (!strncmp(old_path_with_sep, action->destination, oldpathlen+1)) {
1244 suffix = action->destination + oldpathlen + 1;
1245 dest_path = g_strconcat(new_path, separator,
1246 suffix, NULL);
1247 g_free(action->destination);
1248 action->destination = dest_path;
1249 matched = TRUE;
1252 } else {
1253 /* folder-moving a leaf */
1254 if (!strcmp(old_path, action->destination)) {
1255 dest_path = g_strdup(new_path);
1256 g_free(action->destination);
1257 action->destination = dest_path;
1258 matched = TRUE;
1263 g_free(old_path_with_sep);
1264 #ifdef G_OS_WIN32
1265 if (!strcmp(separator, G_DIR_SEPARATOR_S) && !matched) {
1266 separator = "/";
1267 goto again;
1269 #endif
1271 return matched;