enable external editor on windows. patch by Thorsten Maerz
[claws.git] / src / filtering.c
blobedd0f3a9c770e22220ac7ec1ecc36c1e1f9ed48f
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 "defs.h"
20 #include <glib.h>
21 #include <glib/gi18n.h>
22 #include <ctype.h>
23 #include <string.h>
24 #include <stdlib.h>
25 #include <errno.h>
26 #include <gtk/gtk.h>
27 #include <stdio.h>
29 #include "utils.h"
30 #include "procheader.h"
31 #include "matcher.h"
32 #include "filtering.h"
33 #include "prefs_gtk.h"
34 #include "compose.h"
35 #include "prefs_common.h"
36 #include "addritem.h"
37 #ifndef USE_ALT_ADDRBOOK
38 #include "addrbook.h"
39 #include "addressbook.h"
40 #else
41 #include "addressbook-dbus.h"
42 #include "addressadd.h"
43 #endif
44 #include "addr_compl.h"
45 #include "tags.h"
46 #include "log.h"
47 #include "account.h"
48 #include "addrindex.h"
49 #include "folder_item_prefs.h"
51 GSList * pre_global_processing = NULL;
52 GSList * post_global_processing = NULL;
53 GSList * filtering_rules = NULL;
55 gboolean debug_filtering_session = FALSE;
57 static gboolean filtering_is_final_action(FilteringAction *filtering_action);
59 FilteringAction * filteringaction_new(int type, int account_id,
60 gchar * destination,
61 gint labelcolor, gint score, gchar * header)
63 FilteringAction * action;
65 action = g_new0(FilteringAction, 1);
67 action->type = type;
68 action->account_id = account_id;
69 if (destination) {
70 action->destination = g_strdup(destination);
71 } else {
72 action->destination = NULL;
74 if (header) {
75 action->header = g_strdup(header);
76 } else {
77 action->header = NULL;
79 action->labelcolor = labelcolor;
80 action->score = score;
81 return action;
84 void filteringaction_free(FilteringAction * action)
86 cm_return_if_fail(action);
87 g_free(action->header);
88 g_free(action->destination);
89 g_free(action);
92 static gint action_list_sort(gconstpointer a, gconstpointer b)
94 int first = filtering_is_final_action((FilteringAction *) a) ? 1 : 0;
95 int second = filtering_is_final_action((FilteringAction *) b) ? 1 : 0;
97 return (first - second);
100 GSList *filtering_action_list_sort(GSList *action_list)
102 return g_slist_sort(action_list, action_list_sort);
105 FilteringProp * filteringprop_new(gboolean enabled,
106 const gchar *name,
107 gint account_id,
108 MatcherList * matchers,
109 GSList * action_list)
111 FilteringProp * filtering;
113 filtering = g_new0(FilteringProp, 1);
114 filtering->enabled = enabled;
115 filtering->name = name ? g_strdup(name): NULL;
116 filtering->account_id = account_id;
117 filtering->matchers = matchers;
118 filtering->action_list = filtering_action_list_sort(action_list);
120 return filtering;
123 static FilteringAction * filteringaction_copy(FilteringAction * src)
125 FilteringAction * new;
127 new = g_new0(FilteringAction, 1);
129 new->type = src->type;
130 new->account_id = src->account_id;
131 if (src->destination)
132 new->destination = g_strdup(src->destination);
133 else
134 new->destination = NULL;
135 new->labelcolor = src->labelcolor;
136 new->score = src->score;
138 return new;
141 FilteringProp * filteringprop_copy(FilteringProp *src)
143 FilteringProp * new;
144 GSList *tmp;
146 new = g_new0(FilteringProp, 1);
147 new->matchers = g_new0(MatcherList, 1);
149 for (tmp = src->matchers->matchers; tmp != NULL && tmp->data != NULL;) {
150 MatcherProp *matcher = (MatcherProp *)tmp->data;
152 new->matchers->matchers = g_slist_append(new->matchers->matchers,
153 matcherprop_copy(matcher));
154 tmp = tmp->next;
157 new->matchers->bool_and = src->matchers->bool_and;
159 new->action_list = NULL;
161 for (tmp = src->action_list ; tmp != NULL ; tmp = tmp->next) {
162 FilteringAction *filtering_action;
164 filtering_action = tmp->data;
166 new->action_list = g_slist_append(new->action_list,
167 filteringaction_copy(filtering_action));
170 new->enabled = src->enabled;
171 new->name = g_strdup(src->name);
173 return new;
176 void filteringprop_free(FilteringProp * prop)
178 GSList * tmp;
180 cm_return_if_fail(prop);
181 matcherlist_free(prop->matchers);
183 for (tmp = prop->action_list ; tmp != NULL ; tmp = tmp->next) {
184 filteringaction_free(tmp->data);
186 g_slist_free(prop->action_list);
187 g_free(prop->name);
188 g_free(prop);
191 /* move and copy messages by batches to be faster on IMAP */
192 void filtering_move_and_copy_msgs(GSList *msgs)
194 GSList *messages = g_slist_copy(msgs);
195 FolderItem *last_item = NULL;
196 FiltOp cur_op = IS_NOTHING;
198 debug_print("checking %d messages\n", g_slist_length(msgs));
199 while (messages) {
200 GSList *batch = NULL, *cur;
201 gint found = 0;
202 for (cur = messages; cur; cur = cur->next) {
203 MsgInfo *info = (MsgInfo *)cur->data;
204 if (last_item == NULL) {
205 if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE)
206 last_item = info->to_filter_folder;
207 else if (info->filter_op == IS_DELE)
208 last_item = info->folder;
210 if (last_item == NULL)
211 continue;
212 if (cur_op == IS_NOTHING) {
213 if (info->filter_op == IS_COPY)
214 cur_op = IS_COPY;
215 else if (info->filter_op == IS_MOVE)
216 cur_op = IS_MOVE;
217 else if (info->filter_op == IS_DELE)
218 cur_op = IS_DELE;
220 if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE) {
221 if (info->to_filter_folder == last_item
222 && cur_op == info->filter_op) {
223 found++;
224 batch = g_slist_prepend(batch, info);
226 } else if (info->filter_op == IS_DELE) {
227 if (info->folder == last_item
228 && cur_op == info->filter_op) {
229 found++;
230 batch = g_slist_prepend(batch, info);
234 if (found == 0) {
235 debug_print("no more messages to move/copy/del\n");
236 break;
237 } else {
238 debug_print("%d messages to %s in %s\n", found,
239 cur_op==IS_COPY ? "copy":(cur_op==IS_DELE ?"delete":"move"),
240 last_item->name ? last_item->name:"(noname)");
242 for (cur = batch; cur; cur = cur->next) {
243 MsgInfo *info = (MsgInfo *)cur->data;
244 messages = g_slist_remove(messages, info);
245 info->to_filter_folder = NULL;
246 info->filter_op = IS_NOTHING;
248 batch = g_slist_reverse(batch);
249 if (g_slist_length(batch)) {
250 MsgInfo *info = (MsgInfo *)batch->data;
251 if (cur_op == IS_COPY && last_item != info->folder) {
252 folder_item_copy_msgs(last_item, batch);
253 } else if (cur_op == IS_MOVE && last_item != info->folder) {
254 if (folder_item_move_msgs(last_item, batch) < 0)
255 folder_item_move_msgs(
256 folder_get_default_inbox(),
257 batch);
258 } else if (cur_op == IS_DELE && last_item == info->folder) {
259 folder_item_remove_msgs(last_item, batch);
261 /* we don't reference the msginfos, because caller will do */
262 if (prefs_common.real_time_sync)
263 folder_item_synchronise(last_item);
264 g_slist_free(batch);
265 batch = NULL;
266 GTK_EVENTS_FLUSH();
268 last_item = NULL;
269 cur_op = IS_NOTHING;
271 /* we don't reference the msginfos, because caller will do */
272 g_slist_free(messages);
276 fitleringaction_apply
277 runs the action on one MsgInfo
278 return value : return TRUE if the action could be applied
281 #define FLUSH_COPY_IF_NEEDED(info) { \
282 if (info->filter_op == IS_COPY && info->to_filter_folder) { \
283 debug_print("must debatch pending copy\n"); \
284 folder_item_copy_msg(info->to_filter_folder, info); \
285 info->filter_op = IS_NOTHING; \
289 static gboolean filteringaction_apply(FilteringAction * action, MsgInfo * info)
291 FolderItem * dest_folder;
292 gint val;
293 Compose * compose;
294 PrefsAccount * account;
295 gchar * cmd;
297 switch(action->type) {
298 case MATCHACTION_MOVE:
299 if (MSG_IS_LOCKED(info->flags))
300 return FALSE;
302 dest_folder =
303 folder_find_item_from_identifier(action->destination);
304 if (!dest_folder) {
305 debug_print("*** folder not found '%s'\n",
306 action->destination ?action->destination :"(null)");
307 return FALSE;
310 FLUSH_COPY_IF_NEEDED(info);
311 /* mark message to be moved */
312 info->filter_op = IS_MOVE;
313 info->to_filter_folder = dest_folder;
314 return TRUE;
316 case MATCHACTION_COPY:
317 dest_folder =
318 folder_find_item_from_identifier(action->destination);
320 if (!dest_folder) {
321 debug_print("*** folder not found '%s'\n",
322 action->destination ?action->destination :"(null)");
323 return FALSE;
326 FLUSH_COPY_IF_NEEDED(info);
327 /* mark message to be copied */
328 info->filter_op = IS_COPY;
329 info->to_filter_folder = dest_folder;
330 return TRUE;
332 case MATCHACTION_SET_TAG:
333 case MATCHACTION_UNSET_TAG:
334 val = tags_get_id_for_str(action->destination);
335 if (val == -1) {
336 debug_print("*** tag '%s' not found\n",
337 action->destination ?action->destination :"(null)");
338 return FALSE;
340 FLUSH_COPY_IF_NEEDED(info);
341 procmsg_msginfo_update_tags(info, (action->type == MATCHACTION_SET_TAG), val);
342 return TRUE;
344 case MATCHACTION_CLEAR_TAGS:
345 FLUSH_COPY_IF_NEEDED(info);
346 procmsg_msginfo_clear_tags(info);
347 return TRUE;
349 case MATCHACTION_DELETE:
350 FLUSH_COPY_IF_NEEDED(info);
351 info->filter_op = IS_DELE;
352 return TRUE;
354 case MATCHACTION_MARK:
355 FLUSH_COPY_IF_NEEDED(info);
356 procmsg_msginfo_set_flags(info, MSG_MARKED, 0);
357 return TRUE;
359 case MATCHACTION_UNMARK:
360 FLUSH_COPY_IF_NEEDED(info);
361 procmsg_msginfo_unset_flags(info, MSG_MARKED, 0);
362 return TRUE;
364 case MATCHACTION_LOCK:
365 FLUSH_COPY_IF_NEEDED(info);
366 procmsg_msginfo_set_flags(info, MSG_LOCKED, 0);
367 return TRUE;
369 case MATCHACTION_UNLOCK:
370 FLUSH_COPY_IF_NEEDED(info);
371 procmsg_msginfo_unset_flags(info, MSG_LOCKED, 0);
372 return TRUE;
374 case MATCHACTION_MARK_AS_READ:
375 FLUSH_COPY_IF_NEEDED(info);
376 procmsg_msginfo_unset_flags(info, MSG_UNREAD | MSG_NEW, 0);
377 return TRUE;
379 case MATCHACTION_MARK_AS_UNREAD:
380 FLUSH_COPY_IF_NEEDED(info);
381 procmsg_msginfo_change_flags(info, MSG_UNREAD, 0, MSG_NEW, 0);
382 return TRUE;
384 case MATCHACTION_MARK_AS_SPAM:
385 FLUSH_COPY_IF_NEEDED(info);
386 procmsg_spam_learner_learn(info, NULL, TRUE);
387 procmsg_msginfo_change_flags(info, MSG_SPAM, 0, MSG_NEW|MSG_UNREAD, 0);
388 return TRUE;
390 case MATCHACTION_MARK_AS_HAM:
391 FLUSH_COPY_IF_NEEDED(info);
392 procmsg_spam_learner_learn(info, NULL, FALSE);
393 procmsg_msginfo_unset_flags(info, MSG_SPAM, 0);
394 return TRUE;
396 case MATCHACTION_COLOR:
397 FLUSH_COPY_IF_NEEDED(info);
398 procmsg_msginfo_unset_flags(info, MSG_CLABEL_FLAG_MASK, 0);
399 procmsg_msginfo_set_flags(info, MSG_COLORLABEL_TO_FLAGS(action->labelcolor), 0);
400 return TRUE;
402 case MATCHACTION_FORWARD:
403 case MATCHACTION_FORWARD_AS_ATTACHMENT:
404 account = account_find_from_id(action->account_id);
405 compose = compose_forward(account, info,
406 action->type == MATCHACTION_FORWARD ? FALSE : TRUE,
407 NULL, TRUE, TRUE);
408 compose_entry_append(compose, action->destination,
409 compose->account->protocol == A_NNTP
410 ? COMPOSE_NEWSGROUPS
411 : COMPOSE_TO, PREF_NONE);
413 val = compose_send(compose);
415 return val == 0 ? TRUE : FALSE;
417 case MATCHACTION_REDIRECT:
418 account = account_find_from_id(action->account_id);
419 compose = compose_redirect(account, info, TRUE);
420 if (compose->account->protocol == A_NNTP)
421 break;
422 else
423 compose_entry_append(compose, action->destination,
424 COMPOSE_TO, PREF_NONE);
426 val = compose_send(compose);
428 return val == 0 ? TRUE : FALSE;
430 case MATCHACTION_EXECUTE:
431 cmd = matching_build_command(action->destination, info);
432 if (cmd == NULL)
433 return FALSE;
434 else {
435 if (system(cmd) == -1)
436 g_warning("couldn't run %s", cmd);
437 g_free(cmd);
439 return TRUE;
441 case MATCHACTION_SET_SCORE:
442 FLUSH_COPY_IF_NEEDED(info);
443 info->score = action->score;
444 return TRUE;
446 case MATCHACTION_CHANGE_SCORE:
447 FLUSH_COPY_IF_NEEDED(info);
448 info->score += action->score;
449 return TRUE;
451 case MATCHACTION_STOP:
452 return FALSE;
454 case MATCHACTION_HIDE:
455 FLUSH_COPY_IF_NEEDED(info);
456 info->hidden = TRUE;
457 return TRUE;
459 case MATCHACTION_IGNORE:
460 FLUSH_COPY_IF_NEEDED(info);
461 procmsg_msginfo_set_flags(info, MSG_IGNORE_THREAD, 0);
462 return TRUE;
464 case MATCHACTION_WATCH:
465 FLUSH_COPY_IF_NEEDED(info);
466 procmsg_msginfo_set_flags(info, MSG_WATCH_THREAD, 0);
467 return TRUE;
469 case MATCHACTION_ADD_TO_ADDRESSBOOK:
471 #ifndef USE_ALT_ADDRBOOK
472 AddressDataSource *book = NULL;
473 AddressBookFile *abf = NULL;
474 ItemFolder *folder = NULL;
475 #endif
476 gchar *buf = NULL;
477 Header *header = NULL;
478 gint errors = 0;
480 #ifndef USE_ALT_ADDRBOOK
481 if (!addressbook_peek_folder_exists(action->destination, &book, &folder)) {
482 g_warning("addressbook folder not found '%s'", action->destination?action->destination:"(null)");
483 return FALSE;
485 if (!book) {
486 g_warning("addressbook_peek_folder_exists returned NULL book");
487 return FALSE;
490 abf = book->rawDataSource;
491 #endif
492 /* get the header */
493 if (procheader_get_header_from_msginfo(info, &buf, action->header) < 0)
494 return FALSE;
496 header = procheader_parse_header(buf);
497 g_free(buf);
499 /* add all addresses that are not already in */
500 if (header && *header->body && (*header->body != '\0')) {
501 GSList *address_list = NULL;
502 GSList *walk = NULL;
503 gchar *path = NULL;
505 if (action->destination == NULL ||
506 strcasecmp(action->destination, "Any") == 0 ||
507 *(action->destination) == '\0')
508 path = NULL;
509 else
510 path = action->destination;
511 start_address_completion(path);
513 address_list = g_slist_append(address_list, header->body);
514 for (walk = address_list; walk != NULL; walk = walk->next) {
515 gchar *stripped_addr = g_strdup(walk->data);
516 extract_address(stripped_addr);
518 if (complete_matches_found(walk->data) == 0) {
519 gchar *name = procheader_get_fromname(walk->data);
520 debug_print("adding '%s <%s>' to addressbook '%s'\n",
521 name, stripped_addr, action->destination);
522 #ifndef USE_ALT_ADDRBOOK
523 if (!addrbook_add_contact(abf, folder, name, stripped_addr, NULL)) {
524 #else
525 if (!addressadd_selection(name, stripped_addr, NULL, NULL)) {
526 #endif
527 g_warning("contact could not be added");
528 errors++;
530 g_free(name);
531 } else {
532 debug_print("address '%s' already found in addressbook '%s', skipping\n",
533 stripped_addr, action->destination);
535 g_free(stripped_addr);
538 g_slist_free(address_list);
539 end_address_completion();
540 } else {
541 g_warning("header '%s' not set or empty", action->header?action->header:"(null)");
543 return (errors == 0);
545 default:
546 break;
548 return FALSE;
551 gboolean filteringaction_apply_action_list(GSList *action_list, MsgInfo *info)
553 GSList *p;
554 cm_return_val_if_fail(action_list, FALSE);
555 cm_return_val_if_fail(info, FALSE);
556 for (p = action_list; p && p->data; p = g_slist_next(p)) {
557 FilteringAction *a = (FilteringAction *) p->data;
558 if (filteringaction_apply(a, info)) {
559 if (filtering_is_final_action(a))
560 break;
561 } else
562 return FALSE;
565 return TRUE;
568 static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info,
569 PrefsAccount *ac_prefs)
571 /* this function returns true if a filtering rule applies regarding to its account
572 data and if it does, if the conditions list match.
574 per-account data of a filtering rule is either matched against current account
575 when filtering is done manually, or against the account currently used for
576 retrieving messages when it's an manual or automatic fetching of messages.
577 per-account data match doesn't apply to pre-/post-/folder-processing rules.
579 when filtering messages manually:
580 - either the filtering rule is not account-based and it will be processed
581 - or it's per-account and we check if we HAVE TO match it against the current
582 account (according to user-land settings, per-account rules might have to
583 be skipped, or only the rules that match the current account have to be
584 applied, or all rules will have to be applied regardless to if they are
585 account-based or not)
587 notes about debugging output in that function:
588 when not matching, log_status_skip() is used, otherwise log_status_ok() is used
589 no debug output is done when filtering_debug_level is low
592 gboolean matches = FALSE;
594 if (ac_prefs != NULL) {
595 matches = ((filtering->account_id == 0)
596 || (filtering->account_id == ac_prefs->account_id));
598 /* debug output */
599 if (debug_filtering_session) {
600 if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
601 if (filtering->account_id == 0) {
602 log_status_ok(LOG_DEBUG_FILTERING,
603 _("rule is not account-based\n"));
604 } else {
605 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
606 log_status_ok(LOG_DEBUG_FILTERING,
607 _("rule is account-based [id=%d, name='%s'], "
608 "matching the account currently used to retrieve messages\n"),
609 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
613 else
614 if (!matches) {
615 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
616 log_status_skip(LOG_DEBUG_FILTERING,
617 _("rule is account-based, "
618 "not matching the account currently used to retrieve messages\n"));
619 } else {
620 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
621 PrefsAccount *account = account_find_from_id(filtering->account_id);
623 log_status_skip(LOG_DEBUG_FILTERING,
624 _("rule is account-based [id=%d, name='%s'], "
625 "not matching the account currently used to retrieve messages [id=%d, name='%s']\n"),
626 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
627 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
632 } else {
633 switch (prefs_common.apply_per_account_filtering_rules) {
634 case FILTERING_ACCOUNT_RULES_FORCE:
635 /* apply filtering rules regardless to the account info */
636 matches = TRUE;
638 /* debug output */
639 if (debug_filtering_session) {
640 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
641 if (filtering->account_id == 0) {
642 log_status_ok(LOG_DEBUG_FILTERING,
643 _("rule is not account-based, "
644 "all rules are applied on user request anyway\n"));
645 } else {
646 PrefsAccount *account = account_find_from_id(filtering->account_id);
648 log_status_ok(LOG_DEBUG_FILTERING,
649 _("rule is account-based [id=%d, name='%s'], "
650 "but all rules are applied on user request\n"),
651 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
655 break;
656 case FILTERING_ACCOUNT_RULES_SKIP:
657 /* don't apply filtering rules that belong to an account */
658 matches = (filtering->account_id == 0);
660 /* debug output */
661 if (debug_filtering_session) {
662 if (!matches) {
663 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
664 PrefsAccount *account = account_find_from_id(filtering->account_id);
666 log_status_skip(LOG_DEBUG_FILTERING,
667 _("rule is account-based [id=%d, name='%s'], "
668 "skipped on user request\n"),
669 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
670 } else {
671 log_status_skip(LOG_DEBUG_FILTERING,
672 _("rule is account-based, "
673 "skipped on user request\n"));
675 } else {
676 if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
677 log_status_ok(LOG_DEBUG_FILTERING,
678 _("rule is not account-based\n"));
682 break;
683 case FILTERING_ACCOUNT_RULES_USE_CURRENT:
684 matches = ((filtering->account_id == 0)
685 || (filtering->account_id == cur_account->account_id));
687 /* debug output */
688 if (debug_filtering_session) {
689 if (!matches) {
690 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
691 PrefsAccount *account = account_find_from_id(filtering->account_id);
693 log_status_skip(LOG_DEBUG_FILTERING,
694 _("rule is account-based [id=%d, name='%s'], "
695 "not matching current account [id=%d, name='%s']\n"),
696 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
697 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
698 } else {
699 log_status_skip(LOG_DEBUG_FILTERING,
700 _("rule is account-based, "
701 "not matching current account\n"));
703 } else {
704 if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
705 if (filtering->account_id == 0) {
706 log_status_ok(LOG_DEBUG_FILTERING,
707 _("rule is not account-based\n"));
708 } else {
709 PrefsAccount *account = account_find_from_id(filtering->account_id);
711 log_status_ok(LOG_DEBUG_FILTERING,
712 _("rule is account-based [id=%d, name='%s'], "
713 "current account [id=%d, name='%s']\n"),
714 account->account_id, account?account->account_name:_("NON_EXISTENT"),
715 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
720 break;
724 return matches && matcherlist_match(filtering->matchers, info);
728 *\brief Apply a rule on message.
730 *\param filtering List of filtering rules.
731 *\param info Message to apply rules on.
732 *\param final Variable returning TRUE or FALSE if one of the
733 * encountered actions was final.
734 * See also \ref filtering_is_final_action.
736 *\return gboolean TRUE to continue applying rules.
738 static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info,
739 gboolean * final)
741 gboolean result = TRUE;
742 gchar *buf;
743 GSList * tmp;
745 * final = FALSE;
746 for (tmp = filtering->action_list ; tmp != NULL ; tmp = tmp->next) {
747 FilteringAction * action;
749 action = tmp->data;
750 buf = filteringaction_to_string(action);
751 if (debug_filtering_session)
752 log_print(LOG_DEBUG_FILTERING, _("applying action [ %s ]\n"), buf);
754 if (FALSE == (result = filteringaction_apply(action, info))) {
755 if (debug_filtering_session) {
756 if (action->type != MATCHACTION_STOP)
757 log_warning(LOG_DEBUG_FILTERING, _("action could not apply\n"));
758 log_print(LOG_DEBUG_FILTERING,
759 _("no further processing after action [ %s ]\n"), buf);
761 debug_print("No further processing after rule %s\n", buf);
763 g_free(buf);
764 if (filtering_is_final_action(action)) {
765 * final = TRUE;
766 break;
770 return result;
774 *\brief Check if an action is "final", i.e. should break further
775 * processing.
777 *\param filtering_action Action to check.
779 *\return gboolean TRUE if \a filtering_action is final.
781 static gboolean filtering_is_final_action(FilteringAction *filtering_action)
783 switch(filtering_action->type) {
784 case MATCHACTION_MOVE:
785 case MATCHACTION_DELETE:
786 case MATCHACTION_STOP:
787 return TRUE; /* MsgInfo invalid for message */
788 default:
789 return FALSE;
793 gboolean processing_enabled(GSList *filtering_list)
795 GSList *l;
796 for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
797 FilteringProp * filtering = (FilteringProp *) l->data;
798 if (filtering->enabled)
799 return TRUE;
801 return FALSE;
804 static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
806 GSList *l;
807 gboolean final;
808 gboolean apply_next;
810 cm_return_val_if_fail(info != NULL, TRUE);
812 for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
813 FilteringProp * filtering = (FilteringProp *) l->data;
815 if (filtering->enabled) {
816 if (debug_filtering_session) {
817 gchar *buf = filteringprop_to_string(filtering);
818 if (filtering->name && *filtering->name != '\0') {
819 log_print(LOG_DEBUG_FILTERING,
820 _("processing rule '%s' [ %s ]\n"),
821 filtering->name, buf);
822 } else {
823 log_print(LOG_DEBUG_FILTERING,
824 _("processing rule <unnamed> [ %s ]\n"),
825 buf);
827 g_free(buf);
830 if (filtering_match_condition(filtering, info, ac_prefs)) {
831 apply_next = filtering_apply_rule(filtering, info, &final);
832 if (final)
833 break;
836 } else {
837 if (debug_filtering_session) {
838 gchar *buf = filteringprop_to_string(filtering);
839 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
840 if (filtering->name && *filtering->name != '\0') {
841 log_status_skip(LOG_DEBUG_FILTERING,
842 _("disabled rule '%s' [ %s ]\n"),
843 filtering->name, buf);
844 } else {
845 log_status_skip(LOG_DEBUG_FILTERING,
846 _("disabled rule <unnamed> [ %s ]\n"),
847 buf);
850 g_free(buf);
855 /* put in inbox if the last rule was not a final one, or
856 * a final rule could not be applied.
857 * Either of these cases is likely. */
858 if (!final || !apply_next) {
859 return FALSE;
862 return TRUE;
866 *\brief Filter a message against a list of rules.
868 *\param flist List of filter rules.
869 *\param info Message.
871 *\return gboolean TRUE if filter rules handled the message.
873 *\note Returning FALSE means the message was not handled,
874 * and that the calling code should do the default
875 * processing. E.g. \ref inc.c::inc_start moves the
876 * message to the inbox.
878 gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
879 FilteringInvocationType context, gchar *extra_info)
881 gboolean ret;
883 if (prefs_common.enable_filtering_debug) {
884 gchar *tmp = _("undetermined");
886 switch (context) {
887 case FILTERING_INCORPORATION:
888 tmp = _("incorporation");
889 debug_filtering_session = prefs_common.enable_filtering_debug_inc;
890 break;
891 case FILTERING_MANUALLY:
892 tmp = _("manually");
893 debug_filtering_session = prefs_common.enable_filtering_debug_manual;
894 break;
895 case FILTERING_FOLDER_PROCESSING:
896 tmp = _("folder processing");
897 debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
898 break;
899 case FILTERING_PRE_PROCESSING:
900 tmp = _("pre-processing");
901 debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
902 break;
903 case FILTERING_POST_PROCESSING:
904 tmp = _("post-processing");
905 debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
906 break;
907 default:
908 debug_filtering_session = FALSE;
909 break;
912 if (debug_filtering_session) {
913 gchar *file = procmsg_get_message_file_path(info);
914 gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');
916 /* show context info and essential info about the message */
917 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
918 log_print(LOG_DEBUG_FILTERING,
919 _("filtering message (%s%s%s)\n"
920 "%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
921 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
922 spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
923 spc, prefs_common_translated_header_name("From:"), info->from,
924 spc, prefs_common_translated_header_name("To:"), info->to,
925 spc, prefs_common_translated_header_name("Subject:"), info->subject);
926 } else {
927 log_print(LOG_DEBUG_FILTERING,
928 _("filtering message (%s%s%s)\n"
929 "%smessage file: %s\n"),
930 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
931 spc, file);
933 g_free(file);
934 g_free(spc);
936 } else
937 debug_filtering_session = FALSE;
939 ret = filter_msginfo(flist, info, ac_prefs);
940 debug_filtering_session = FALSE;
941 return ret;
944 gchar *filteringaction_to_string(FilteringAction *action)
946 const gchar *command_str;
947 gchar * quoted_dest;
948 gchar * quoted_header;
949 GString *dest = g_string_new("");
950 gchar *deststr = NULL;
952 command_str = get_matchparser_tab_str(action->type);
954 if (command_str == NULL)
955 return NULL;
957 switch(action->type) {
958 case MATCHACTION_MOVE:
959 case MATCHACTION_COPY:
960 case MATCHACTION_EXECUTE:
961 case MATCHACTION_SET_TAG:
962 case MATCHACTION_UNSET_TAG:
963 quoted_dest = matcher_quote_str(action->destination);
964 g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
965 g_free(quoted_dest);
966 break;
968 case MATCHACTION_DELETE:
969 case MATCHACTION_MARK:
970 case MATCHACTION_UNMARK:
971 case MATCHACTION_LOCK:
972 case MATCHACTION_UNLOCK:
973 case MATCHACTION_MARK_AS_READ:
974 case MATCHACTION_MARK_AS_UNREAD:
975 case MATCHACTION_MARK_AS_SPAM:
976 case MATCHACTION_MARK_AS_HAM:
977 case MATCHACTION_STOP:
978 case MATCHACTION_HIDE:
979 case MATCHACTION_IGNORE:
980 case MATCHACTION_WATCH:
981 case MATCHACTION_CLEAR_TAGS:
982 g_string_append_printf(dest, "%s", command_str);
983 break;
985 case MATCHACTION_REDIRECT:
986 case MATCHACTION_FORWARD:
987 case MATCHACTION_FORWARD_AS_ATTACHMENT:
988 quoted_dest = matcher_quote_str(action->destination);
989 g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
990 g_free(quoted_dest);
991 break;
993 case MATCHACTION_COLOR:
994 g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
995 break;
997 case MATCHACTION_CHANGE_SCORE:
998 case MATCHACTION_SET_SCORE:
999 g_string_append_printf(dest, "%s %d", command_str, action->score);
1000 break;
1002 case MATCHACTION_ADD_TO_ADDRESSBOOK:
1003 quoted_header = matcher_quote_str(action->header);
1004 quoted_dest = matcher_quote_str(action->destination);
1005 g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
1006 g_free(quoted_dest);
1007 g_free(quoted_header);
1008 break;
1010 default:
1011 return NULL;
1013 deststr = dest->str;
1014 g_string_free(dest, FALSE);
1015 return deststr;
1018 gchar * filteringaction_list_to_string(GSList * action_list)
1020 gchar *action_list_str;
1021 GSList * tmp;
1022 gchar *list_str;
1024 action_list_str = NULL;
1025 for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
1026 gchar *action_str;
1027 FilteringAction * action;
1029 action = tmp->data;
1031 action_str = filteringaction_to_string(action);
1033 if (action_list_str != NULL) {
1034 list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1035 g_free(action_list_str);
1037 else {
1038 list_str = g_strdup(action_str);
1040 g_free(action_str);
1041 action_list_str = list_str;
1044 return action_list_str;
1047 gchar * filteringprop_to_string(FilteringProp * prop)
1049 gchar *list_str;
1050 gchar *action_list_str;
1051 gchar *filtering_str;
1053 if (prop == NULL)
1054 return NULL;
1056 action_list_str = filteringaction_list_to_string(prop->action_list);
1058 if (action_list_str == NULL)
1059 return NULL;
1061 list_str = matcherlist_to_string(prop->matchers);
1063 if (list_str == NULL) {
1064 g_free(action_list_str);
1065 return NULL;
1068 filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1069 g_free(action_list_str);
1070 g_free(list_str);
1072 return filtering_str;
1075 static void prefs_filtering_free(GSList * prefs_filtering)
1077 while (prefs_filtering != NULL) {
1078 FilteringProp * filtering = (FilteringProp *)
1079 prefs_filtering->data;
1080 filteringprop_free(filtering);
1081 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1085 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1087 FolderItem *item = node->data;
1089 cm_return_val_if_fail(item, FALSE);
1090 cm_return_val_if_fail(item->prefs, FALSE);
1092 prefs_filtering_free(item->prefs->processing);
1093 item->prefs->processing = NULL;
1095 return FALSE;
1098 void prefs_filtering_clear(void)
1100 GList * cur;
1102 for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1103 Folder *folder;
1105 folder = (Folder *) cur->data;
1106 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1107 prefs_filtering_free_func, NULL);
1110 prefs_filtering_free(filtering_rules);
1111 filtering_rules = NULL;
1112 prefs_filtering_free(pre_global_processing);
1113 pre_global_processing = NULL;
1114 prefs_filtering_free(post_global_processing);
1115 post_global_processing = NULL;
1118 void prefs_filtering_clear_folder(Folder *folder)
1120 cm_return_if_fail(folder);
1121 cm_return_if_fail(folder->node);
1123 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1124 prefs_filtering_free_func, NULL);
1125 /* FIXME: Note folder settings were changed, where the updates? */
1128 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1129 /* return TRUE if there's at least one per-account filtering rule */
1131 GSList *l;
1133 for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1134 FilteringProp * filtering = (FilteringProp *) l->data;
1136 if (filtering->enabled && (filtering->account_id != 0)) {
1137 return TRUE;
1141 return FALSE;
1144 gboolean filtering_action_list_rename_path(GSList *action_list, const gchar *old_path,
1145 const gchar *new_path)
1147 gchar *base;
1148 gchar *prefix;
1149 gchar *suffix;
1150 gchar *dest_path;
1151 gchar *old_path_with_sep;
1152 gint destlen;
1153 gint prefixlen;
1154 gint oldpathlen;
1155 GSList * action_cur;
1156 const gchar *separator=G_DIR_SEPARATOR_S;
1157 gboolean matched = FALSE;
1158 #ifdef G_OS_WIN32
1159 again:
1160 #endif
1161 oldpathlen = strlen(old_path);
1162 old_path_with_sep = g_strconcat(old_path,separator,NULL);
1164 for(action_cur = action_list ; action_cur != NULL ;
1165 action_cur = action_cur->next) {
1167 FilteringAction *action = action_cur->data;
1169 if (action->type == MATCHACTION_SET_TAG ||
1170 action->type == MATCHACTION_UNSET_TAG)
1171 continue;
1172 if (!action->destination)
1173 continue;
1175 destlen = strlen(action->destination);
1177 if (destlen > oldpathlen) {
1178 prefixlen = destlen - oldpathlen;
1179 suffix = action->destination + prefixlen;
1181 if (!strncmp(old_path, suffix, oldpathlen)) {
1182 prefix = g_malloc0(prefixlen + 1);
1183 strncpy2(prefix, action->destination, prefixlen);
1185 base = suffix + oldpathlen;
1186 while (*base == G_DIR_SEPARATOR) base++;
1187 if (*base == '\0')
1188 dest_path = g_strconcat(prefix, separator,
1189 new_path, NULL);
1190 else
1191 dest_path = g_strconcat(prefix,
1192 separator,
1193 new_path,
1194 separator,
1195 base, NULL);
1197 g_free(prefix);
1198 g_free(action->destination);
1199 action->destination = dest_path;
1200 matched = TRUE;
1201 } else { /* for non-leaf folders */
1202 /* compare with trailing slash */
1203 if (!strncmp(old_path_with_sep, action->destination, oldpathlen+1)) {
1205 suffix = action->destination + oldpathlen + 1;
1206 dest_path = g_strconcat(new_path, separator,
1207 suffix, NULL);
1208 g_free(action->destination);
1209 action->destination = dest_path;
1210 matched = TRUE;
1213 } else {
1214 /* folder-moving a leaf */
1215 if (!strcmp(old_path, action->destination)) {
1216 dest_path = g_strdup(new_path);
1217 g_free(action->destination);
1218 action->destination = dest_path;
1219 matched = TRUE;
1224 g_free(old_path_with_sep);
1225 #ifdef G_OS_WIN32
1226 if (!strcmp(separator, G_DIR_SEPARATOR_S) && !matched) {
1227 separator = "/";
1228 goto again;
1230 #endif
1232 return matched;