fix typo (thanks to Charles A Edwards)
[claws.git] / src / filtering.c
blobff6eece6294047b85965a7de83c2d22ec29d0ed1
1 /*
2 * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2012 Hiroyuki Yamamoto & The Claws Mail Team
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/>.
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_NEW_ADDRBOOK
39 #include "addrbook.h"
40 #include "addressbook.h"
41 #else
42 #include "addressbook-dbus.h"
43 #include "addressadd.h"
44 #endif
45 #include "addr_compl.h"
46 #include "tags.h"
47 #include "log.h"
48 #include "account.h"
49 #include "addrindex.h"
50 #include "folder_item_prefs.h"
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 #define STRLEN_WITH_CHECK(expr) \
61 strlen_with_check(#expr, __LINE__, expr)
63 static inline gint strlen_with_check(const gchar *expr, gint fline, const gchar *str)
65 if (str)
66 return strlen(str);
67 else {
68 debug_print("%s(%d) - invalid string %s\n", __FILE__, fline, expr?expr:"(null)");
69 return 0;
73 FilteringAction * filteringaction_new(int type, int account_id,
74 gchar * destination,
75 gint labelcolor, gint score, gchar * header)
77 FilteringAction * action;
79 action = g_new0(FilteringAction, 1);
81 action->type = type;
82 action->account_id = account_id;
83 if (destination) {
84 action->destination = g_strdup(destination);
85 } else {
86 action->destination = NULL;
88 if (header) {
89 action->header = g_strdup(header);
90 } else {
91 action->header = NULL;
93 action->labelcolor = labelcolor;
94 action->score = score;
95 return action;
98 void filteringaction_free(FilteringAction * action)
100 cm_return_if_fail(action);
101 g_free(action->header);
102 g_free(action->destination);
103 g_free(action);
106 static gint action_list_sort(gconstpointer a, gconstpointer b)
108 int first = filtering_is_final_action((FilteringAction *) a) ? 1 : 0;
109 int second = filtering_is_final_action((FilteringAction *) b) ? 1 : 0;
111 return (first - second);
114 GSList *filtering_action_list_sort(GSList *action_list)
116 return g_slist_sort(action_list, action_list_sort);
119 FilteringProp * filteringprop_new(gboolean enabled,
120 const gchar *name,
121 gint account_id,
122 MatcherList * matchers,
123 GSList * action_list)
125 FilteringProp * filtering;
127 filtering = g_new0(FilteringProp, 1);
128 filtering->enabled = enabled;
129 filtering->name = name ? g_strdup(name): NULL;
130 filtering->account_id = account_id;
131 filtering->matchers = matchers;
132 filtering->action_list = filtering_action_list_sort(action_list);
134 return filtering;
137 static FilteringAction * filteringaction_copy(FilteringAction * src)
139 FilteringAction * new;
141 new = g_new0(FilteringAction, 1);
143 new->type = src->type;
144 new->account_id = src->account_id;
145 if (src->destination)
146 new->destination = g_strdup(src->destination);
147 else
148 new->destination = NULL;
149 new->labelcolor = src->labelcolor;
150 new->score = src->score;
152 return new;
155 FilteringProp * filteringprop_copy(FilteringProp *src)
157 FilteringProp * new;
158 GSList *tmp;
160 new = g_new0(FilteringProp, 1);
161 new->matchers = g_new0(MatcherList, 1);
163 for (tmp = src->matchers->matchers; tmp != NULL && tmp->data != NULL;) {
164 MatcherProp *matcher = (MatcherProp *)tmp->data;
166 new->matchers->matchers = g_slist_append(new->matchers->matchers,
167 matcherprop_copy(matcher));
168 tmp = tmp->next;
171 new->matchers->bool_and = src->matchers->bool_and;
173 new->action_list = NULL;
175 for (tmp = src->action_list ; tmp != NULL ; tmp = tmp->next) {
176 FilteringAction *filtering_action;
178 filtering_action = tmp->data;
180 new->action_list = g_slist_append(new->action_list,
181 filteringaction_copy(filtering_action));
184 new->enabled = src->enabled;
185 new->name = g_strdup(src->name);
187 return new;
190 void filteringprop_free(FilteringProp * prop)
192 GSList * tmp;
194 cm_return_if_fail(prop);
195 matcherlist_free(prop->matchers);
197 for (tmp = prop->action_list ; tmp != NULL ; tmp = tmp->next) {
198 filteringaction_free(tmp->data);
200 g_slist_free(prop->action_list);
201 g_free(prop->name);
202 g_free(prop);
205 /* move and copy messages by batches to be faster on IMAP */
206 void filtering_move_and_copy_msgs(GSList *msgs)
208 GSList *messages = g_slist_copy(msgs);
209 FolderItem *last_item = NULL;
210 FiltOp cur_op = IS_NOTHING;
212 debug_print("checking %d messages\n", g_slist_length(msgs));
213 while (messages) {
214 GSList *batch = NULL, *cur;
215 gint found = 0;
216 for (cur = messages; cur; cur = cur->next) {
217 MsgInfo *info = (MsgInfo *)cur->data;
218 if (last_item == NULL) {
219 if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE)
220 last_item = info->to_filter_folder;
221 else if (info->filter_op == IS_DELE)
222 last_item = info->folder;
224 if (last_item == NULL)
225 continue;
226 if (cur_op == IS_NOTHING) {
227 if (info->filter_op == IS_COPY)
228 cur_op = IS_COPY;
229 else if (info->filter_op == IS_MOVE)
230 cur_op = IS_MOVE;
231 else if (info->filter_op == IS_DELE)
232 cur_op = IS_DELE;
234 if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE) {
235 if (info->to_filter_folder == last_item
236 && cur_op == info->filter_op) {
237 found++;
238 batch = g_slist_prepend(batch, info);
240 } else if (info->filter_op == IS_DELE) {
241 if (info->folder == last_item
242 && cur_op == info->filter_op) {
243 found++;
244 batch = g_slist_prepend(batch, info);
248 if (found == 0) {
249 debug_print("no more messages to move/copy/del\n");
250 break;
251 } else {
252 debug_print("%d messages to %s in %s\n", found,
253 cur_op==IS_COPY ? "copy":(cur_op==IS_DELE ?"delete":"move"),
254 last_item?(last_item->name ? last_item->name:"(noname)"):"nowhere");
256 for (cur = batch; cur; cur = cur->next) {
257 MsgInfo *info = (MsgInfo *)cur->data;
258 messages = g_slist_remove(messages, info);
259 info->to_filter_folder = NULL;
260 info->filter_op = IS_NOTHING;
262 batch = g_slist_reverse(batch);
263 if (g_slist_length(batch)) {
264 MsgInfo *info = (MsgInfo *)batch->data;
265 if (cur_op == IS_COPY && last_item != info->folder) {
266 folder_item_copy_msgs(last_item, batch);
267 } else if (cur_op == IS_MOVE && last_item != info->folder) {
268 if (folder_item_move_msgs(last_item, batch) < 0)
269 folder_item_move_msgs(
270 folder_get_default_inbox(),
271 batch);
272 } else if (cur_op == IS_DELE && last_item == info->folder) {
273 folder_item_remove_msgs(last_item, batch);
275 /* we don't reference the msginfos, because caller will do */
276 if (prefs_common.real_time_sync)
277 folder_item_synchronise(last_item);
278 g_slist_free(batch);
279 batch = NULL;
280 GTK_EVENTS_FLUSH();
282 last_item = NULL;
283 cur_op = IS_NOTHING;
285 /* we don't reference the msginfos, because caller will do */
286 g_slist_free(messages);
290 fitleringaction_apply
291 runs the action on one MsgInfo
292 return value : return TRUE if the action could be applied
295 #define FLUSH_COPY_IF_NEEDED(info) { \
296 if (info->filter_op == IS_COPY && info->to_filter_folder) { \
297 debug_print("must debatch pending copy\n"); \
298 folder_item_copy_msg(info->to_filter_folder, info); \
299 info->filter_op = IS_NOTHING; \
303 static gboolean filteringaction_apply(FilteringAction * action, MsgInfo * info)
305 FolderItem * dest_folder;
306 gint val;
307 Compose * compose;
308 PrefsAccount * account;
309 gchar * cmd;
311 switch(action->type) {
312 case MATCHACTION_MOVE:
313 if (MSG_IS_LOCKED(info->flags))
314 return FALSE;
316 dest_folder =
317 folder_find_item_from_identifier(action->destination);
318 if (!dest_folder) {
319 debug_print("*** folder not found '%s'\n",
320 action->destination ?action->destination :"(null)");
321 return FALSE;
324 FLUSH_COPY_IF_NEEDED(info);
325 /* mark message to be moved */
326 info->filter_op = IS_MOVE;
327 info->to_filter_folder = dest_folder;
328 return TRUE;
330 case MATCHACTION_COPY:
331 dest_folder =
332 folder_find_item_from_identifier(action->destination);
334 if (!dest_folder) {
335 debug_print("*** folder not found '%s'\n",
336 action->destination ?action->destination :"(null)");
337 return FALSE;
340 FLUSH_COPY_IF_NEEDED(info);
341 /* mark message to be copied */
342 info->filter_op = IS_COPY;
343 info->to_filter_folder = dest_folder;
344 return TRUE;
346 case MATCHACTION_SET_TAG:
347 case MATCHACTION_UNSET_TAG:
348 val = tags_get_id_for_str(action->destination);
349 if (val == -1) {
350 debug_print("*** tag '%s' not found\n",
351 action->destination ?action->destination :"(null)");
352 return FALSE;
354 FLUSH_COPY_IF_NEEDED(info);
355 procmsg_msginfo_update_tags(info, (action->type == MATCHACTION_SET_TAG), val);
356 return TRUE;
358 case MATCHACTION_CLEAR_TAGS:
359 FLUSH_COPY_IF_NEEDED(info);
360 procmsg_msginfo_clear_tags(info);
361 return TRUE;
363 case MATCHACTION_DELETE:
364 FLUSH_COPY_IF_NEEDED(info);
365 info->filter_op = IS_DELE;
366 return TRUE;
368 case MATCHACTION_MARK:
369 FLUSH_COPY_IF_NEEDED(info);
370 procmsg_msginfo_set_flags(info, MSG_MARKED, 0);
371 return TRUE;
373 case MATCHACTION_UNMARK:
374 FLUSH_COPY_IF_NEEDED(info);
375 procmsg_msginfo_unset_flags(info, MSG_MARKED, 0);
376 return TRUE;
378 case MATCHACTION_LOCK:
379 FLUSH_COPY_IF_NEEDED(info);
380 procmsg_msginfo_set_flags(info, MSG_LOCKED, 0);
381 return TRUE;
383 case MATCHACTION_UNLOCK:
384 FLUSH_COPY_IF_NEEDED(info);
385 procmsg_msginfo_unset_flags(info, MSG_LOCKED, 0);
386 return TRUE;
388 case MATCHACTION_MARK_AS_READ:
389 FLUSH_COPY_IF_NEEDED(info);
390 procmsg_msginfo_unset_flags(info, MSG_UNREAD | MSG_NEW, 0);
391 return TRUE;
393 case MATCHACTION_MARK_AS_UNREAD:
394 FLUSH_COPY_IF_NEEDED(info);
395 procmsg_msginfo_set_flags(info, MSG_UNREAD, 0);
396 return TRUE;
398 case MATCHACTION_MARK_AS_SPAM:
399 FLUSH_COPY_IF_NEEDED(info);
400 procmsg_spam_learner_learn(info, NULL, TRUE);
401 procmsg_msginfo_change_flags(info, MSG_SPAM, 0, MSG_NEW|MSG_UNREAD, 0);
402 if (procmsg_spam_get_folder(info)) {
403 info->filter_op = IS_MOVE;
404 info->to_filter_folder = procmsg_spam_get_folder(info);
406 return TRUE;
408 case MATCHACTION_MARK_AS_HAM:
409 FLUSH_COPY_IF_NEEDED(info);
410 procmsg_spam_learner_learn(info, NULL, FALSE);
411 procmsg_msginfo_unset_flags(info, MSG_SPAM, 0);
412 return TRUE;
414 case MATCHACTION_COLOR:
415 FLUSH_COPY_IF_NEEDED(info);
416 procmsg_msginfo_unset_flags(info, MSG_CLABEL_FLAG_MASK, 0);
417 procmsg_msginfo_set_flags(info, MSG_COLORLABEL_TO_FLAGS(action->labelcolor), 0);
418 return TRUE;
420 case MATCHACTION_FORWARD:
421 case MATCHACTION_FORWARD_AS_ATTACHMENT:
422 account = account_find_from_id(action->account_id);
423 compose = compose_forward(account, info,
424 action->type == MATCHACTION_FORWARD ? FALSE : TRUE,
425 NULL, TRUE, TRUE);
426 compose_entry_append(compose, action->destination,
427 compose->account->protocol == A_NNTP
428 ? COMPOSE_NEWSGROUPS
429 : COMPOSE_TO, PREF_NONE);
431 val = compose_send(compose);
433 return val == 0 ? TRUE : FALSE;
435 case MATCHACTION_REDIRECT:
436 account = account_find_from_id(action->account_id);
437 compose = compose_redirect(account, info, TRUE);
438 if (compose->account->protocol == A_NNTP)
439 break;
440 else
441 compose_entry_append(compose, action->destination,
442 COMPOSE_TO, PREF_NONE);
444 val = compose_send(compose);
446 return val == 0 ? TRUE : FALSE;
448 case MATCHACTION_EXECUTE:
449 cmd = matching_build_command(action->destination, info);
450 if (cmd == NULL)
451 return FALSE;
452 else {
453 if (system(cmd) == -1)
454 g_warning("couldn't run %s", cmd);
455 g_free(cmd);
457 return TRUE;
459 case MATCHACTION_SET_SCORE:
460 FLUSH_COPY_IF_NEEDED(info);
461 info->score = action->score;
462 return TRUE;
464 case MATCHACTION_CHANGE_SCORE:
465 FLUSH_COPY_IF_NEEDED(info);
466 info->score += action->score;
467 return TRUE;
469 case MATCHACTION_STOP:
470 return FALSE;
472 case MATCHACTION_HIDE:
473 FLUSH_COPY_IF_NEEDED(info);
474 info->hidden = TRUE;
475 return TRUE;
477 case MATCHACTION_IGNORE:
478 FLUSH_COPY_IF_NEEDED(info);
479 procmsg_msginfo_set_flags(info, MSG_IGNORE_THREAD, 0);
480 return TRUE;
482 case MATCHACTION_WATCH:
483 FLUSH_COPY_IF_NEEDED(info);
484 procmsg_msginfo_set_flags(info, MSG_WATCH_THREAD, 0);
485 return TRUE;
487 case MATCHACTION_ADD_TO_ADDRESSBOOK:
489 #ifndef USE_NEW_ADDRBOOK
490 AddressDataSource *book = NULL;
491 AddressBookFile *abf = NULL;
492 ItemFolder *folder = NULL;
493 #endif
494 gchar buf[BUFFSIZE];
495 Header *header;
496 gint errors = 0;
498 #ifndef USE_NEW_ADDRBOOK
499 if (!addressbook_peek_folder_exists(action->destination, &book, &folder)) {
500 g_warning("addressbook folder not found '%s'\n", action->destination?action->destination:"(null)");
501 return FALSE;
503 if (!book) {
504 g_warning("addressbook_peek_folder_exists returned NULL book\n");
505 return FALSE;
508 abf = book->rawDataSource;
509 #endif
510 /* get the header */
511 if (procheader_get_header_from_msginfo(info, buf,
512 sizeof(buf), action->header) < 0)
513 return FALSE;
515 header = procheader_parse_header(buf);
517 /* add all addresses that are not already in */
518 if (header && *header->body && (*header->body != '\0')) {
519 GSList *address_list = NULL;
520 GSList *walk = NULL;
521 gchar *path = NULL;
523 if (action->destination == NULL ||
524 strcasecmp(action->destination, "Any") == 0 ||
525 *(action->destination) == '\0')
526 path = NULL;
527 else
528 path = action->destination;
529 start_address_completion(path);
531 address_list = address_list_append(address_list, header->body);
532 for (walk = address_list; walk != NULL; walk = walk->next) {
533 gchar *stripped_addr = g_strdup(walk->data);
534 extract_address(stripped_addr);
536 if (complete_matches_found(walk->data) == 0) {
537 debug_print("adding address '%s' to addressbook '%s'\n",
538 stripped_addr, action->destination);
539 #ifndef USE_NEW_ADDRBOOK
540 if (!addrbook_add_contact(abf, folder, stripped_addr, stripped_addr, NULL)) {
541 #else
542 if (!addressadd_selection(NULL, stripped_addr, NULL, NULL)) {
543 #endif
544 g_warning("contact could not been added\n");
545 errors++;
547 } else {
548 debug_print("address '%s' already found in addressbook '%s', skipping\n",
549 stripped_addr, action->destination);
551 g_free(stripped_addr);
554 g_slist_free(address_list);
555 end_address_completion();
556 } else {
557 g_warning("header '%s' not set or empty\n", action->header?action->header:"(null)");
559 return (errors == 0);
561 default:
562 break;
564 return FALSE;
567 gboolean filteringaction_apply_action_list(GSList *action_list, MsgInfo *info)
569 GSList *p;
570 cm_return_val_if_fail(action_list, FALSE);
571 cm_return_val_if_fail(info, FALSE);
572 for (p = action_list; p && p->data; p = g_slist_next(p)) {
573 FilteringAction *a = (FilteringAction *) p->data;
574 if (filteringaction_apply(a, info)) {
575 if (filtering_is_final_action(a))
576 break;
577 } else
578 return FALSE;
581 return TRUE;
584 static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info,
585 PrefsAccount *ac_prefs)
587 /* this function returns true if a filtering rule applies regarding to its account
588 data and if it does, if the conditions list match.
590 per-account data of a filtering rule is either matched against current account
591 when filtering is done manually, or against the account currently used for
592 retrieving messages when it's an manual or automatic fetching of messages.
593 per-account data match doesn't apply to pre-/post-/folder-processing rules.
595 when filtering messages manually:
596 - either the filtering rule is not account-based and it will be processed
597 - or it's per-account and we check if we HAVE TO match it against the current
598 account (according to user-land settings, per-account rules might have to
599 be skipped, or only the rules that match the current account have to be
600 applied, or all rules will have to be applied regardless to if they are
601 account-based or not)
603 notes about debugging output in that function:
604 when not matching, log_status_skip() is used, otherwise log_status_ok() is used
605 no debug output is done when filtering_debug_level is low
608 gboolean matches = FALSE;
610 if (ac_prefs != NULL) {
611 matches = ((filtering->account_id == 0)
612 || (filtering->account_id == ac_prefs->account_id));
614 /* debug output */
615 if (debug_filtering_session) {
616 if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
617 if (filtering->account_id == 0) {
618 log_status_ok(LOG_DEBUG_FILTERING,
619 _("rule is not account-based\n"));
620 } else {
621 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
622 log_status_ok(LOG_DEBUG_FILTERING,
623 _("rule is account-based [id=%d, name='%s'], "
624 "matching the account currently used to retrieve messages\n"),
625 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
629 else
630 if (!matches) {
631 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
632 log_status_skip(LOG_DEBUG_FILTERING,
633 _("rule is account-based, "
634 "not matching the account currently used to retrieve messages\n"));
635 } else {
636 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
637 PrefsAccount *account = account_find_from_id(filtering->account_id);
639 log_status_skip(LOG_DEBUG_FILTERING,
640 _("rule is account-based [id=%d, name='%s'], "
641 "not matching the account currently used to retrieve messages [id=%d, name='%s']\n"),
642 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
643 ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
648 } else {
649 switch (prefs_common.apply_per_account_filtering_rules) {
650 case FILTERING_ACCOUNT_RULES_FORCE:
651 /* apply filtering rules regardless to the account info */
652 matches = TRUE;
654 /* debug output */
655 if (debug_filtering_session) {
656 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
657 if (filtering->account_id == 0) {
658 log_status_ok(LOG_DEBUG_FILTERING,
659 _("rule is not account-based, "
660 "all rules are applied on user request anyway\n"));
661 } else {
662 PrefsAccount *account = account_find_from_id(filtering->account_id);
664 log_status_ok(LOG_DEBUG_FILTERING,
665 _("rule is account-based [id=%d, name='%s'], "
666 "but all rules are applied on user request\n"),
667 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
671 break;
672 case FILTERING_ACCOUNT_RULES_SKIP:
673 /* don't apply filtering rules that belong to an account */
674 matches = (filtering->account_id == 0);
676 /* debug output */
677 if (debug_filtering_session) {
678 if (!matches) {
679 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
680 PrefsAccount *account = account_find_from_id(filtering->account_id);
682 log_status_skip(LOG_DEBUG_FILTERING,
683 _("rule is account-based [id=%d, name='%s'], "
684 "skipped on user request\n"),
685 filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
686 } else {
687 log_status_skip(LOG_DEBUG_FILTERING,
688 _("rule is account-based, "
689 "skipped on user request\n"));
691 } else {
692 if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
693 log_status_ok(LOG_DEBUG_FILTERING,
694 _("rule is not account-based\n"));
698 break;
699 case FILTERING_ACCOUNT_RULES_USE_CURRENT:
700 matches = ((filtering->account_id == 0)
701 || (filtering->account_id == cur_account->account_id));
703 /* debug output */
704 if (debug_filtering_session) {
705 if (!matches) {
706 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
707 PrefsAccount *account = account_find_from_id(filtering->account_id);
709 log_status_skip(LOG_DEBUG_FILTERING,
710 _("rule is account-based [id=%d, name='%s'], "
711 "not matching current account [id=%d, name='%s']\n"),
712 filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
713 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
714 } else {
715 log_status_skip(LOG_DEBUG_FILTERING,
716 _("rule is account-based, "
717 "not matching current account\n"));
719 } else {
720 if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
721 if (filtering->account_id == 0) {
722 log_status_ok(LOG_DEBUG_FILTERING,
723 _("rule is not account-based\n"));
724 } else {
725 PrefsAccount *account = account_find_from_id(filtering->account_id);
727 log_status_ok(LOG_DEBUG_FILTERING,
728 _("rule is account-based [id=%d, name='%s'], "
729 "current account [id=%d, name='%s']\n"),
730 account->account_id, account?account->account_name:_("NON_EXISTENT"),
731 cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
736 break;
740 return matches && matcherlist_match(filtering->matchers, info);
744 *\brief Apply a rule on message.
746 *\param filtering List of filtering rules.
747 *\param info Message to apply rules on.
748 *\param final Variable returning TRUE or FALSE if one of the
749 * encountered actions was final.
750 * See also \ref filtering_is_final_action.
752 *\return gboolean TRUE to continue applying rules.
754 static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info,
755 gboolean * final)
757 gboolean result = TRUE;
758 gchar *buf;
759 GSList * tmp;
761 * final = FALSE;
762 for (tmp = filtering->action_list ; tmp != NULL ; tmp = tmp->next) {
763 FilteringAction * action;
765 action = tmp->data;
766 buf = filteringaction_to_string(action);
767 if (debug_filtering_session)
768 log_print(LOG_DEBUG_FILTERING, _("applying action [ %s ]\n"), buf);
770 if (FALSE == (result = filteringaction_apply(action, info))) {
771 if (debug_filtering_session) {
772 if (action->type != MATCHACTION_STOP)
773 log_warning(LOG_DEBUG_FILTERING, _("action could not apply\n"));
774 log_print(LOG_DEBUG_FILTERING,
775 _("no further processing after action [ %s ]\n"), buf);
777 debug_print("No further processing after rule %s\n", buf);
779 g_free(buf);
780 if (filtering_is_final_action(action)) {
781 * final = TRUE;
782 break;
786 return result;
790 *\brief Check if an action is "final", i.e. should break further
791 * processing.
793 *\param filtering_action Action to check.
795 *\return gboolean TRUE if \a filtering_action is final.
797 static gboolean filtering_is_final_action(FilteringAction *filtering_action)
799 switch(filtering_action->type) {
800 case MATCHACTION_MOVE:
801 case MATCHACTION_DELETE:
802 case MATCHACTION_STOP:
803 case MATCHACTION_MARK_AS_SPAM:
804 return TRUE; /* MsgInfo invalid for message */
805 default:
806 return FALSE;
810 static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
812 GSList *l;
813 gboolean final;
814 gboolean apply_next;
816 cm_return_val_if_fail(info != NULL, TRUE);
818 for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
819 FilteringProp * filtering = (FilteringProp *) l->data;
821 if (filtering->enabled) {
822 if (debug_filtering_session) {
823 gchar *buf = filteringprop_to_string(filtering);
824 if (filtering->name && *filtering->name != '\0') {
825 log_print(LOG_DEBUG_FILTERING,
826 _("processing rule '%s' [ %s ]\n"),
827 filtering->name, buf);
828 } else {
829 log_print(LOG_DEBUG_FILTERING,
830 _("processing rule <unnamed> [ %s ]\n"),
831 buf);
833 g_free(buf);
836 if (filtering_match_condition(filtering, info, ac_prefs)) {
837 apply_next = filtering_apply_rule(filtering, info, &final);
838 if (final)
839 break;
842 } else {
843 if (debug_filtering_session) {
844 gchar *buf = filteringprop_to_string(filtering);
845 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
846 if (filtering->name && *filtering->name != '\0') {
847 log_status_skip(LOG_DEBUG_FILTERING,
848 _("disabled rule '%s' [ %s ]\n"),
849 filtering->name, buf);
850 } else {
851 log_status_skip(LOG_DEBUG_FILTERING,
852 _("disabled rule <unnamed> [ %s ]\n"),
853 buf);
856 g_free(buf);
861 /* put in inbox if the last rule was not a final one, or
862 * a final rule could not be applied.
863 * Either of these cases is likely. */
864 if (!final || !apply_next) {
865 return FALSE;
868 return TRUE;
872 *\brief Filter a message against a list of rules.
874 *\param flist List of filter rules.
875 *\param info Message.
877 *\return gboolean TRUE if filter rules handled the message.
879 *\note Returning FALSE means the message was not handled,
880 * and that the calling code should do the default
881 * processing. E.g. \ref inc.c::inc_start moves the
882 * message to the inbox.
884 gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
885 FilteringInvocationType context, gchar *extra_info)
887 gboolean ret;
889 if (prefs_common.enable_filtering_debug) {
890 gchar *tmp = _("undetermined");
891 #ifndef G_OS_WIN32
892 switch (context) {
893 case FILTERING_INCORPORATION:
894 tmp = _("incorporation");
895 debug_filtering_session = prefs_common.enable_filtering_debug_inc;
896 break;
897 case FILTERING_MANUALLY:
898 tmp = _("manually");
899 debug_filtering_session = prefs_common.enable_filtering_debug_manual;
900 break;
901 case FILTERING_FOLDER_PROCESSING:
902 tmp = _("folder processing");
903 debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
904 break;
905 case FILTERING_PRE_PROCESSING:
906 tmp = _("pre-processing");
907 debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
908 break;
909 case FILTERING_POST_PROCESSING:
910 tmp = _("post-processing");
911 debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
912 break;
913 default:
914 debug_filtering_session = FALSE;
915 break;
917 #else
918 debug_filtering_session = FALSE;
919 #endif
920 if (debug_filtering_session) {
921 gchar *file = procmsg_get_message_file_path(info);
922 gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');
924 /* show context info and essential info about the message */
925 if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
926 log_print(LOG_DEBUG_FILTERING,
927 _("filtering message (%s%s%s)\n"
928 "%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
929 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
930 spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
931 spc, prefs_common_translated_header_name("From:"), info->from,
932 spc, prefs_common_translated_header_name("To:"), info->to,
933 spc, prefs_common_translated_header_name("Subject:"), info->subject);
934 } else {
935 log_print(LOG_DEBUG_FILTERING,
936 _("filtering message (%s%s%s)\n"
937 "%smessage file: %s\n"),
938 tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
939 spc, file);
941 g_free(file);
942 g_free(spc);
944 } else
945 debug_filtering_session = FALSE;
947 ret = filter_msginfo(flist, info, ac_prefs);
948 debug_filtering_session = FALSE;
949 return ret;
952 gchar *filteringaction_to_string(FilteringAction *action)
954 const gchar *command_str;
955 gchar * quoted_dest;
956 gchar * quoted_header;
957 GString *dest = g_string_new("");
958 gchar *deststr = NULL;
960 command_str = get_matchparser_tab_str(action->type);
962 if (command_str == NULL)
963 return NULL;
965 switch(action->type) {
966 case MATCHACTION_MOVE:
967 case MATCHACTION_COPY:
968 case MATCHACTION_EXECUTE:
969 case MATCHACTION_SET_TAG:
970 case MATCHACTION_UNSET_TAG:
971 quoted_dest = matcher_quote_str(action->destination);
972 g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
973 g_free(quoted_dest);
974 break;
976 case MATCHACTION_DELETE:
977 case MATCHACTION_MARK:
978 case MATCHACTION_UNMARK:
979 case MATCHACTION_LOCK:
980 case MATCHACTION_UNLOCK:
981 case MATCHACTION_MARK_AS_READ:
982 case MATCHACTION_MARK_AS_UNREAD:
983 case MATCHACTION_MARK_AS_SPAM:
984 case MATCHACTION_MARK_AS_HAM:
985 case MATCHACTION_STOP:
986 case MATCHACTION_HIDE:
987 case MATCHACTION_IGNORE:
988 case MATCHACTION_WATCH:
989 case MATCHACTION_CLEAR_TAGS:
990 g_string_append_printf(dest, "%s", command_str);
991 break;
993 case MATCHACTION_REDIRECT:
994 case MATCHACTION_FORWARD:
995 case MATCHACTION_FORWARD_AS_ATTACHMENT:
996 quoted_dest = matcher_quote_str(action->destination);
997 g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
998 g_free(quoted_dest);
999 break;
1001 case MATCHACTION_COLOR:
1002 g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
1003 break;
1005 case MATCHACTION_CHANGE_SCORE:
1006 case MATCHACTION_SET_SCORE:
1007 g_string_append_printf(dest, "%s %d", command_str, action->score);
1008 break;
1010 case MATCHACTION_ADD_TO_ADDRESSBOOK:
1011 quoted_header = matcher_quote_str(action->header);
1012 quoted_dest = matcher_quote_str(action->destination);
1013 g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
1014 g_free(quoted_dest);
1015 g_free(quoted_header);
1016 break;
1018 default:
1019 return NULL;
1021 deststr = dest->str;
1022 g_string_free(dest, FALSE);
1023 return deststr;
1026 gchar * filteringaction_list_to_string(GSList * action_list)
1028 gchar *action_list_str;
1029 GSList * tmp;
1030 gchar *list_str;
1032 action_list_str = NULL;
1033 for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
1034 gchar *action_str;
1035 FilteringAction * action;
1037 action = tmp->data;
1039 action_str = filteringaction_to_string(action);
1041 if (action_list_str != NULL) {
1042 list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1043 g_free(action_list_str);
1045 else {
1046 list_str = g_strdup(action_str);
1048 g_free(action_str);
1049 action_list_str = list_str;
1052 return action_list_str;
1055 gchar * filteringprop_to_string(FilteringProp * prop)
1057 gchar *list_str;
1058 gchar *action_list_str;
1059 gchar *filtering_str;
1061 if (prop == NULL)
1062 return NULL;
1064 action_list_str = filteringaction_list_to_string(prop->action_list);
1066 if (action_list_str == NULL)
1067 return NULL;
1069 list_str = matcherlist_to_string(prop->matchers);
1071 if (list_str == NULL) {
1072 g_free(action_list_str);
1073 return NULL;
1076 filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1077 g_free(action_list_str);
1078 g_free(list_str);
1080 return filtering_str;
1083 static void prefs_filtering_free(GSList * prefs_filtering)
1085 while (prefs_filtering != NULL) {
1086 FilteringProp * filtering = (FilteringProp *)
1087 prefs_filtering->data;
1088 filteringprop_free(filtering);
1089 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1093 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1095 FolderItem *item = node->data;
1097 cm_return_val_if_fail(item, FALSE);
1098 cm_return_val_if_fail(item->prefs, FALSE);
1100 prefs_filtering_free(item->prefs->processing);
1101 item->prefs->processing = NULL;
1103 return FALSE;
1106 void prefs_filtering_clear(void)
1108 GList * cur;
1110 for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1111 Folder *folder;
1113 folder = (Folder *) cur->data;
1114 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1115 prefs_filtering_free_func, NULL);
1118 prefs_filtering_free(filtering_rules);
1119 filtering_rules = NULL;
1120 prefs_filtering_free(pre_global_processing);
1121 pre_global_processing = NULL;
1122 prefs_filtering_free(post_global_processing);
1123 post_global_processing = NULL;
1126 void prefs_filtering_clear_folder(Folder *folder)
1128 cm_return_if_fail(folder);
1129 cm_return_if_fail(folder->node);
1131 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1132 prefs_filtering_free_func, NULL);
1133 /* FIXME: Note folder settings were changed, where the updates? */
1136 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1137 /* return TRUE if there's at least one per-account filtering rule */
1139 GSList *l;
1141 for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1142 FilteringProp * filtering = (FilteringProp *) l->data;
1144 if (filtering->enabled && (filtering->account_id != 0)) {
1145 return TRUE;
1149 return FALSE;
1152 gboolean filtering_action_list_rename_path(GSList *action_list, const gchar *old_path,
1153 const gchar *new_path)
1155 gchar *base;
1156 gchar *prefix;
1157 gchar *suffix;
1158 gchar *dest_path;
1159 gchar *old_path_with_sep;
1160 gint destlen;
1161 gint prefixlen;
1162 gint oldpathlen;
1163 GSList * action_cur;
1164 const gchar *separator=G_DIR_SEPARATOR_S;
1165 gboolean matched = FALSE;
1166 #ifdef G_OS_WIN32
1167 again:
1168 #endif
1169 oldpathlen = strlen(old_path);
1170 old_path_with_sep = g_strconcat(old_path,separator,NULL);
1172 for(action_cur = action_list ; action_cur != NULL ;
1173 action_cur = action_cur->next) {
1175 FilteringAction *action = action_cur->data;
1177 if (action->type == MATCHACTION_SET_TAG ||
1178 action->type == MATCHACTION_UNSET_TAG)
1179 continue;
1180 if (!action->destination)
1181 continue;
1183 destlen = strlen(action->destination);
1185 if (destlen > oldpathlen) {
1186 prefixlen = destlen - oldpathlen;
1187 suffix = action->destination + prefixlen;
1189 if (!strncmp(old_path, suffix, oldpathlen)) {
1190 prefix = g_malloc0(prefixlen + 1);
1191 strncpy2(prefix, action->destination, prefixlen);
1193 base = suffix + oldpathlen;
1194 while (*base == G_DIR_SEPARATOR) base++;
1195 if (*base == '\0')
1196 dest_path = g_strconcat(prefix, separator,
1197 new_path, NULL);
1198 else
1199 dest_path = g_strconcat(prefix,
1200 separator,
1201 new_path,
1202 separator,
1203 base, NULL);
1205 g_free(prefix);
1206 g_free(action->destination);
1207 action->destination = dest_path;
1208 matched = TRUE;
1209 } else { /* for non-leaf folders */
1210 /* compare with trailing slash */
1211 if (!strncmp(old_path_with_sep, action->destination, oldpathlen+1)) {
1213 suffix = action->destination + oldpathlen + 1;
1214 dest_path = g_strconcat(new_path, separator,
1215 suffix, NULL);
1216 g_free(action->destination);
1217 action->destination = dest_path;
1218 matched = TRUE;
1221 } else {
1222 /* folder-moving a leaf */
1223 if (!strcmp(old_path, action->destination)) {
1224 dest_path = g_strdup(new_path);
1225 g_free(action->destination);
1226 action->destination = dest_path;
1227 matched = TRUE;
1232 g_free(old_path_with_sep);
1233 #ifdef G_OS_WIN32
1234 if (!strcmp(separator, G_DIR_SEPARATOR_S) && !matched) {
1235 separator = "/";
1236 goto again;
1238 #endif
1240 return matched;