image viewer: don't make the 'load image' button larger than necessary
[claws.git] / src / filtering.c
blob93a9589050590a903b98c08ab8c2f5f96f7af783
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("");
993 gchar *deststr = NULL;
995 command_str = get_matchparser_tab_str(action->type);
997 if (command_str == NULL) {
998 g_string_free(dest, TRUE);
999 return NULL;
1002 switch(action->type) {
1003 case MATCHACTION_MOVE:
1004 case MATCHACTION_COPY:
1005 case MATCHACTION_EXECUTE:
1006 case MATCHACTION_SET_TAG:
1007 case MATCHACTION_UNSET_TAG:
1008 quoted_dest = matcher_quote_str(action->destination);
1009 g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
1010 g_free(quoted_dest);
1011 break;
1013 case MATCHACTION_DELETE:
1014 case MATCHACTION_MARK:
1015 case MATCHACTION_UNMARK:
1016 case MATCHACTION_LOCK:
1017 case MATCHACTION_UNLOCK:
1018 case MATCHACTION_MARK_AS_READ:
1019 case MATCHACTION_MARK_AS_UNREAD:
1020 case MATCHACTION_MARK_AS_SPAM:
1021 case MATCHACTION_MARK_AS_HAM:
1022 case MATCHACTION_STOP:
1023 case MATCHACTION_HIDE:
1024 case MATCHACTION_IGNORE:
1025 case MATCHACTION_WATCH:
1026 case MATCHACTION_CLEAR_TAGS:
1027 g_string_append_printf(dest, "%s", command_str);
1028 break;
1030 case MATCHACTION_REDIRECT:
1031 case MATCHACTION_FORWARD:
1032 case MATCHACTION_FORWARD_AS_ATTACHMENT:
1033 quoted_dest = matcher_quote_str(action->destination);
1034 g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
1035 g_free(quoted_dest);
1036 break;
1038 case MATCHACTION_COLOR:
1039 g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
1040 break;
1042 case MATCHACTION_CHANGE_SCORE:
1043 case MATCHACTION_SET_SCORE:
1044 g_string_append_printf(dest, "%s %d", command_str, action->score);
1045 break;
1047 case MATCHACTION_ADD_TO_ADDRESSBOOK:
1048 quoted_header = matcher_quote_str(action->header);
1049 quoted_dest = matcher_quote_str(action->destination);
1050 g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
1051 g_free(quoted_dest);
1052 g_free(quoted_header);
1053 break;
1055 default:
1056 g_string_free(dest, TRUE);
1057 return NULL;
1059 deststr = dest->str;
1060 g_string_free(dest, FALSE);
1061 return deststr;
1064 gchar * filteringaction_list_to_string(GSList * action_list)
1066 gchar *action_list_str;
1067 GSList * tmp;
1068 gchar *list_str;
1070 action_list_str = NULL;
1071 for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
1072 gchar *action_str;
1073 FilteringAction * action;
1075 action = tmp->data;
1077 action_str = filteringaction_to_string(action);
1079 if (action_list_str != NULL) {
1080 list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1081 g_free(action_list_str);
1083 else {
1084 list_str = g_strdup(action_str);
1086 g_free(action_str);
1087 action_list_str = list_str;
1090 return action_list_str;
1093 gchar * filteringprop_to_string(FilteringProp * prop)
1095 gchar *list_str;
1096 gchar *action_list_str;
1097 gchar *filtering_str;
1099 if (prop == NULL)
1100 return NULL;
1102 action_list_str = filteringaction_list_to_string(prop->action_list);
1104 if (action_list_str == NULL)
1105 return NULL;
1107 list_str = matcherlist_to_string(prop->matchers);
1109 if (list_str == NULL) {
1110 g_free(action_list_str);
1111 return NULL;
1114 filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1115 g_free(action_list_str);
1116 g_free(list_str);
1118 return filtering_str;
1121 static void prefs_filtering_free(GSList * prefs_filtering)
1123 while (prefs_filtering != NULL) {
1124 FilteringProp * filtering = (FilteringProp *)
1125 prefs_filtering->data;
1126 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1127 filteringprop_free(filtering);
1131 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1133 FolderItem *item = node->data;
1135 cm_return_val_if_fail(item, FALSE);
1136 cm_return_val_if_fail(item->prefs, FALSE);
1138 prefs_filtering_free(item->prefs->processing);
1139 item->prefs->processing = NULL;
1141 return FALSE;
1144 void prefs_filtering_clear(void)
1146 GList * cur;
1148 for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1149 Folder *folder;
1151 folder = (Folder *) cur->data;
1152 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1153 prefs_filtering_free_func, NULL);
1156 prefs_filtering_free(filtering_rules);
1157 filtering_rules = NULL;
1158 prefs_filtering_free(pre_global_processing);
1159 pre_global_processing = NULL;
1160 prefs_filtering_free(post_global_processing);
1161 post_global_processing = NULL;
1164 void prefs_filtering_clear_folder(Folder *folder)
1166 cm_return_if_fail(folder);
1167 cm_return_if_fail(folder->node);
1169 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1170 prefs_filtering_free_func, NULL);
1171 /* FIXME: Note folder settings were changed, where the updates? */
1174 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1175 /* return TRUE if there's at least one per-account filtering rule */
1177 GSList *l;
1179 for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1180 FilteringProp * filtering = (FilteringProp *) l->data;
1182 if (filtering->enabled && (filtering->account_id != 0)) {
1183 return TRUE;
1187 return FALSE;
1190 gboolean filtering_action_list_rename_path(GSList *action_list, const gchar *old_path,
1191 const gchar *new_path)
1193 gchar *base;
1194 gchar *prefix;
1195 gchar *suffix;
1196 gchar *dest_path;
1197 gchar *old_path_with_sep;
1198 gint destlen;
1199 gint prefixlen;
1200 gint oldpathlen;
1201 GSList * action_cur;
1202 const gchar *separator=G_DIR_SEPARATOR_S;
1203 gboolean matched = FALSE;
1204 #ifdef G_OS_WIN32
1205 again:
1206 #endif
1207 oldpathlen = strlen(old_path);
1208 old_path_with_sep = g_strconcat(old_path,separator,NULL);
1210 for(action_cur = action_list ; action_cur != NULL ;
1211 action_cur = action_cur->next) {
1213 FilteringAction *action = action_cur->data;
1215 if (action->type == MATCHACTION_SET_TAG ||
1216 action->type == MATCHACTION_UNSET_TAG)
1217 continue;
1218 if (!action->destination)
1219 continue;
1221 destlen = strlen(action->destination);
1223 if (destlen > oldpathlen) {
1224 prefixlen = destlen - oldpathlen;
1225 suffix = action->destination + prefixlen;
1227 if (!strncmp(old_path, suffix, oldpathlen)) {
1228 prefix = g_malloc0(prefixlen + 1);
1229 strncpy2(prefix, action->destination, prefixlen);
1231 base = suffix + oldpathlen;
1232 while (*base == G_DIR_SEPARATOR)
1233 base++;
1234 if (*base == '\0')
1235 dest_path = g_strconcat(prefix, separator, new_path, NULL);
1236 else
1237 dest_path = g_strconcat(prefix, separator, new_path, separator, base, NULL);
1239 g_free(prefix);
1240 g_free(action->destination);
1241 action->destination = dest_path;
1242 matched = TRUE;
1243 } else { /* for non-leaf folders */
1244 /* compare with trailing slash */
1245 if (!strncmp(old_path_with_sep, action->destination, oldpathlen+1)) {
1247 suffix = action->destination + oldpathlen + 1;
1248 dest_path = g_strconcat(new_path, separator,
1249 suffix, NULL);
1250 g_free(action->destination);
1251 action->destination = dest_path;
1252 matched = TRUE;
1255 } else {
1256 /* folder-moving a leaf */
1257 if (!strcmp(old_path, action->destination)) {
1258 dest_path = g_strdup(new_path);
1259 g_free(action->destination);
1260 action->destination = dest_path;
1261 matched = TRUE;
1266 g_free(old_path_with_sep);
1267 #ifdef G_OS_WIN32
1268 if (!strcmp(separator, G_DIR_SEPARATOR_S) && !matched) {
1269 separator = "/";
1270 goto again;
1272 #endif
1274 return matched;