/*
 * Claws Mail -- a GTK based, lightweight, and fast e-mail client
 * Copyright (C) 1999-2026 the Claws Mail Team and Hiroyuki Yamamoto
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"
#include "defs.h"
#include <glib.h>
#include <glib/gi18n.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <gtk/gtk.h>
#include <stdio.h>

#include "utils.h"
#include "procheader.h"
#include "matcher.h"
#include "filtering.h"
#include "prefs_gtk.h"
#include "compose.h"
#include "prefs_common.h"
#include "addritem.h"
#ifndef USE_ALT_ADDRBOOK
	#include "addrbook.h"
	#include "addressbook.h"
#else
	#include "addressbook-dbus.h"
	#include "addressadd.h"
#endif
#include "addr_compl.h"
#include "tags.h"
#include "log.h"
#include "account.h"
#include "addrindex.h"
#include "folder_item_prefs.h"

GSList * pre_global_processing = NULL;
GSList * post_global_processing = NULL;
GSList * filtering_rules = NULL;

gboolean debug_filtering_session = FALSE;

static gboolean filtering_is_final_action(FilteringAction *filtering_action);

FilteringAction * filteringaction_new(int type, int account_id,
				      gchar * destination,
				      gint labelcolor, gint score, gchar * header)
{
	FilteringAction * action;

	action = g_new0(FilteringAction, 1);

	action->type = type;
	action->account_id = account_id;
	if (destination)
		action->destination = g_strdup(destination);
	if (header)
		action->header = g_strdup(header);
	action->labelcolor = labelcolor;	
        action->score = score;
	return action;
}

void filteringaction_free(FilteringAction * action)
{
	cm_return_if_fail(action);
	g_free(action->header);
	g_free(action->destination);
	g_free(action);
}

static gint action_list_sort(gconstpointer a, gconstpointer b)
{
	int first  = filtering_is_final_action((FilteringAction *) a) ? 1 : 0;
	int second = filtering_is_final_action((FilteringAction *) b) ? 1 : 0;
	
	return (first - second);
}

GSList *filtering_action_list_sort(GSList *action_list)
{
	return g_slist_sort(action_list, action_list_sort);
}

FilteringProp * filteringprop_new(gboolean enabled,
				  const gchar *name,
				  gint account_id,
				  MatcherList * matchers,
				  GSList * action_list)
{
	FilteringProp * filtering;

	filtering = g_new0(FilteringProp, 1);
	filtering->enabled = enabled;
	filtering->name = name ? g_strdup(name): NULL;
	filtering->account_id = account_id;
	filtering->matchers = matchers;
	filtering->action_list = filtering_action_list_sort(action_list);

	return filtering;
}

static FilteringAction * filteringaction_copy(FilteringAction * src)
{
        FilteringAction * new;
        
        new = g_new0(FilteringAction, 1);
        
	new->type = src->type;
	new->account_id = src->account_id;
	if (src->destination)
		new->destination = g_strdup(src->destination);
	new->labelcolor = src->labelcolor;
	new->score = src->score;

        return new;
}

FilteringProp * filteringprop_copy(FilteringProp *src)
{
	FilteringProp * new;
	GSList *tmp;
	
	new = g_new0(FilteringProp, 1);
	new->matchers = g_new0(MatcherList, 1);

	for (tmp = src->matchers->matchers; tmp != NULL && tmp->data != NULL;) {
		MatcherProp *matcher = (MatcherProp *)tmp->data;
		
		new->matchers->matchers = g_slist_append(new->matchers->matchers,
						   matcherprop_copy(matcher));
		tmp = tmp->next;
	}

	new->matchers->bool_and = src->matchers->bool_and;

        for (tmp = src->action_list ; tmp != NULL ; tmp = tmp->next) {
                FilteringAction *filtering_action;
                
                filtering_action = tmp->data;
                
                new->action_list = g_slist_append(new->action_list,
                    filteringaction_copy(filtering_action));
        }

	new->enabled = src->enabled;
	new->name = g_strdup(src->name);

	return new;
}

void filteringprop_free(FilteringProp * prop)
{
        GSList * tmp;

	cm_return_if_fail(prop);
	matcherlist_free(prop->matchers);
        
        for (tmp = prop->action_list ; tmp != NULL ; tmp = tmp->next) {
                filteringaction_free(tmp->data);
        }
	g_slist_free(prop->action_list);
	g_free(prop->name);
	g_free(prop);
}

/* move and copy messages by batches to be faster on IMAP */
void filtering_move_and_copy_msgs(GSList *msgs)
{
	GSList *messages = g_slist_copy(msgs);
	FolderItem *last_item = NULL;
	FiltOp cur_op = IS_NOTHING;

	debug_print("checking %d messages\n", g_slist_length(msgs));
	while (messages) {
		GSList *batch = NULL, *cur;
		gint found = 0;
		for (cur = messages; cur; cur = cur->next) {
			MsgInfo *info = (MsgInfo *)cur->data;
			if (last_item == NULL) {
				if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE)
					last_item = info->to_filter_folder;
				else if (info->filter_op == IS_DELE)
					last_item = info->folder;
			}
			if (last_item == NULL)
				continue;
			if (cur_op == IS_NOTHING) {
				if (info->filter_op == IS_COPY)
					cur_op = IS_COPY;
				else if (info->filter_op == IS_MOVE)
					cur_op = IS_MOVE;
				else if (info->filter_op == IS_DELE)
					cur_op = IS_DELE;
			}
			if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE) {
				if (info->to_filter_folder == last_item 
				&&  cur_op == info->filter_op) {
					found++;
					batch = g_slist_prepend(batch, info);
				}
			} else if (info->filter_op == IS_DELE) {
				if (info->folder == last_item 
				&&  cur_op == info->filter_op) {
					found++;
					batch = g_slist_prepend(batch, info);
				}
			}
		}
		if (found == 0) {
			debug_print("no more messages to move/copy/del\n");
			break;
		} else {
			debug_print("%d messages to %s in %s\n", found,
				cur_op==IS_COPY ? "copy":(cur_op==IS_DELE ?"delete":"move"), 
				last_item->name ? last_item->name:"(noname)");
		}
		for (cur = batch; cur; cur = cur->next) {
			MsgInfo *info = (MsgInfo *)cur->data;
			messages = g_slist_remove(messages, info);
			info->to_filter_folder = NULL;
			info->filter_op = IS_NOTHING;
		}
		batch = g_slist_reverse(batch);
		if (g_slist_length(batch)) {
			MsgInfo *info = (MsgInfo *)batch->data;
			if (cur_op == IS_COPY && last_item != info->folder) {
				folder_item_copy_msgs(last_item, batch);
			} else if (cur_op == IS_MOVE && last_item != info->folder) {
				if (folder_item_move_msgs(last_item, batch) < 0)
					folder_item_move_msgs(
						folder_get_default_inbox(), 
						batch);
			} else if (cur_op == IS_DELE && last_item == info->folder) {
				folder_item_remove_msgs(last_item, batch);
			}
			/* we don't reference the msginfos, because caller will do */
			if (prefs_common.real_time_sync)
				folder_item_synchronise(last_item);
			g_slist_free(batch);
			batch = NULL;
			GTK_EVENTS_FLUSH();
		}
		last_item = NULL;
		cur_op = IS_NOTHING;
	}
	/* we don't reference the msginfos, because caller will do */
	g_slist_free(messages);
}

/*
  fitleringaction_apply
  runs the action on one MsgInfo
  return value : return TRUE if the action could be applied
*/

#define FLUSH_COPY_IF_NEEDED(info) {					\
	if (info->filter_op == IS_COPY && info->to_filter_folder) {	\
		debug_print("must debatch pending copy\n");		\
		folder_item_copy_msg(info->to_filter_folder, info);	\
		info->filter_op = IS_NOTHING;				\
	}								\
}

static gboolean filteringaction_apply(FilteringAction * action, MsgInfo * info)
{
	FolderItem * dest_folder;
	gint val;
	Compose * compose;
	PrefsAccount * account;
	gchar * cmd;

	switch(action->type) {
	case MATCHACTION_MOVE:
		if (MSG_IS_LOCKED(info->flags))
			return FALSE;
			
		dest_folder =
			folder_find_item_from_identifier(action->destination);
		if (!dest_folder) {
			debug_print("*** folder not found '%s'\n",
				action->destination ?action->destination :"(null)");
			return FALSE;
		}
		
		FLUSH_COPY_IF_NEEDED(info);
		/* mark message to be moved */		
		info->filter_op = IS_MOVE;
		info->to_filter_folder = dest_folder;
		return TRUE;

	case MATCHACTION_COPY:
		dest_folder =
			folder_find_item_from_identifier(action->destination);

		if (!dest_folder) {
			debug_print("*** folder not found '%s'\n",
				action->destination ?action->destination :"(null)");
			return FALSE;
		}

		FLUSH_COPY_IF_NEEDED(info);
		/* mark message to be copied */		
		info->filter_op = IS_COPY;
		info->to_filter_folder = dest_folder;
		return TRUE;

	case MATCHACTION_SET_TAG:
	case MATCHACTION_UNSET_TAG:
		val = tags_get_id_for_str(action->destination);
		if (val == -1) {
			debug_print("*** tag '%s' not found\n",
				action->destination ?action->destination :"(null)");
			return FALSE;
		}
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_update_tags(info, (action->type == MATCHACTION_SET_TAG), val);
		return TRUE;

	case MATCHACTION_CLEAR_TAGS:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_clear_tags(info);
		return TRUE;

	case MATCHACTION_DELETE:
		FLUSH_COPY_IF_NEEDED(info);
		info->filter_op = IS_DELE;
		return TRUE;

	case MATCHACTION_MARK:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_set_flags(info, MSG_MARKED, 0);
		return TRUE;

	case MATCHACTION_UNMARK:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_unset_flags(info, MSG_MARKED, 0);
		return TRUE;

	case MATCHACTION_LOCK:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_set_flags(info, MSG_LOCKED, 0);
		return TRUE;

	case MATCHACTION_UNLOCK:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_unset_flags(info, MSG_LOCKED, 0);	
		return TRUE;
		
	case MATCHACTION_MARK_AS_READ:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_unset_flags(info, MSG_UNREAD | MSG_NEW, 0);
		return TRUE;

	case MATCHACTION_MARK_AS_UNREAD:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_change_flags(info, MSG_UNREAD, 0, MSG_NEW, 0);
		return TRUE;
	
	case MATCHACTION_MARK_AS_SPAM:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_spam_learner_learn(info, NULL, TRUE);
		procmsg_msginfo_change_flags(info, MSG_SPAM, 0, MSG_NEW|MSG_UNREAD, 0);
		return TRUE;

	case MATCHACTION_MARK_AS_HAM:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_spam_learner_learn(info, NULL, FALSE);
		procmsg_msginfo_unset_flags(info, MSG_SPAM, 0);
		return TRUE;
	
	case MATCHACTION_COLOR:
		FLUSH_COPY_IF_NEEDED(info);
		procmsg_msginfo_unset_flags(info, MSG_CLABEL_FLAG_MASK, 0); 
		procmsg_msginfo_set_flags(info, MSG_COLORLABEL_TO_FLAGS(action->labelcolor), 0);
		return TRUE;

	case MATCHACTION_FORWARD:
	case MATCHACTION_FORWARD_AS_ATTACHMENT:
		account = account_find_from_id(action->account_id);
		compose = compose_forward(account, info,
			action->type == MATCHACTION_FORWARD ? FALSE : TRUE,
			NULL, TRUE, TRUE);
		compose_entry_append(compose, action->destination,
				     compose->account->protocol == A_NNTP
					    ? COMPOSE_NEWSGROUPS
					    : COMPOSE_TO, PREF_NONE);

		val = compose_send(compose);

		return val == 0 ? TRUE : FALSE;

	case MATCHACTION_REDIRECT:
		account = account_find_from_id(action->account_id);
		compose = compose_redirect(account, info, TRUE);
		if (compose->account->protocol == A_NNTP)
			break;
		else
			compose_entry_append(compose, action->destination,
					     COMPOSE_TO, PREF_NONE);

		val = compose_send(compose);
		
		return val == 0 ? TRUE : FALSE;

	case MATCHACTION_EXECUTE:
		cmd = matching_build_command(action->destination, info);
		if (cmd == NULL)
			return FALSE;
		else {
			if (system(cmd) == -1)
				g_warning("couldn't run %s", cmd);
			g_free(cmd);
		}
		return TRUE;

	case MATCHACTION_SET_SCORE:
		FLUSH_COPY_IF_NEEDED(info);
		info->score = action->score;
		return TRUE;

	case MATCHACTION_CHANGE_SCORE:
		FLUSH_COPY_IF_NEEDED(info);
		info->score += action->score;
		return TRUE;

	case MATCHACTION_STOP:
                return FALSE;

	case MATCHACTION_HIDE:
		FLUSH_COPY_IF_NEEDED(info);
                info->hidden = TRUE;
                return TRUE;

	case MATCHACTION_IGNORE:
		FLUSH_COPY_IF_NEEDED(info);
                procmsg_msginfo_set_flags(info, MSG_IGNORE_THREAD, 0);
                return TRUE;

	case MATCHACTION_WATCH:
		FLUSH_COPY_IF_NEEDED(info);
                procmsg_msginfo_set_flags(info, MSG_WATCH_THREAD, 0);
                return TRUE;

	case MATCHACTION_ADD_TO_ADDRESSBOOK:
		{
#ifndef USE_ALT_ADDRBOOK
			AddressDataSource *book = NULL;
			AddressBookFile *abf = NULL;
			ItemFolder *folder = NULL;
#endif
			gchar *buf = NULL;
			Header *header = NULL;
			gint errors = 0;

#ifndef USE_ALT_ADDRBOOK
			if (!addressbook_peek_folder_exists(action->destination, &book, &folder)) {
				g_warning("addressbook folder not found '%s'", action->destination?action->destination:"(null)");
				return FALSE;
			}
			if (!book) {
				g_warning("addressbook_peek_folder_exists returned NULL book");
				return FALSE;
			}

			abf = book->rawDataSource;
#endif
			/* get the header */
			if (procheader_get_header_from_msginfo(info, &buf, action->header) < 0)
				return FALSE;

			header = procheader_parse_header(buf);
			g_free(buf);

			/* add all addresses that are not already in */
			if (header && *header->body && (*header->body != '\0')) {
				GSList *address_list = NULL;
				GSList *walk = NULL;
				gchar *path = NULL;

				if (action->destination == NULL ||
						strcasecmp(action->destination, "Any") == 0 ||
						*(action->destination) == '\0')
					path = NULL;
				else
					path = action->destination;
				start_address_completion(path);

				address_list = g_slist_append(address_list, header->body);
				for (walk = address_list; walk != NULL; walk = walk->next) {
					gchar *stripped_addr = g_strdup(walk->data);
					extract_address(stripped_addr);

					if (complete_matches_found(walk->data) == 0) {
						gchar *name = procheader_get_fromname(walk->data);
						debug_print("adding '%s <%s>' to addressbook '%s'\n",
								name, stripped_addr, action->destination);
#ifndef USE_ALT_ADDRBOOK
						if (!addrbook_add_contact(abf, folder, name, stripped_addr, NULL)) {
#else
						if (!addressadd_selection(name, stripped_addr, NULL, NULL)) {
#endif
							g_warning("contact could not be added");
							errors++;
						}
						g_free(name);
					} else {
						debug_print("address '%s' already found in addressbook '%s', skipping\n",
								stripped_addr, action->destination);
					}
					g_free(stripped_addr);
				}

				g_slist_free(address_list);
				end_address_completion();
			} else {
				g_warning("header '%s' not set or empty", action->header?action->header:"(null)");
			}
			if (header)
				procheader_header_free(header);
			return (errors == 0);
		}
	default:
		break;
	}
	return FALSE;
}

gboolean filteringaction_apply_action_list(GSList *action_list, MsgInfo *info)
{
	GSList *p;
	cm_return_val_if_fail(action_list, FALSE);
	cm_return_val_if_fail(info, FALSE);
	for (p = action_list; p && p->data; p = g_slist_next(p)) {
		FilteringAction *a = (FilteringAction *) p->data;
		if (filteringaction_apply(a, info)) {
			if (filtering_is_final_action(a))
				break;
		} else
			return FALSE;
		
	}
	return TRUE;
}

static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info,
							PrefsAccount *ac_prefs)

/* this function returns true if a filtering rule applies regarding to its account
   data and if it does, if the conditions list match.

   per-account data of a filtering rule is either matched against current account
   when filtering is done manually, or against the account currently used for
   retrieving messages when it's an manual or automatic fetching of messages.
   per-account data match doesn't apply to pre-/post-/folder-processing rules.

   when filtering messages manually:
    - either the filtering rule is not account-based and it will be processed
	- or it's per-account and we check if we HAVE TO match it against the current
	  account (according to user-land settings, per-account rules might have to
	  be skipped, or only the rules that match the current account have to be
	  applied, or all rules will have to be applied regardless to if they are
	  account-based or not)

   notes about debugging output in that function:
   when not matching, log_status_skip() is used, otherwise log_status_ok() is used
   no debug output is done when filtering_debug_level is low
*/
{
	gboolean matches = FALSE;

	if (ac_prefs != NULL) {
		matches = ((filtering->account_id == 0)
					|| (filtering->account_id == ac_prefs->account_id));

		/* debug output */
		if (debug_filtering_session) {
			if (matches) {
				/* either because rule is not account-based */
    			if (filtering->account_id == 0) {
					if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
       					log_status_ok(LOG_DEBUG_FILTERING,
	    						_("rule is not account-based\n"));
                    }
				} else {
					/* or because it is account-based, and matching current account */
					if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
						/* with fewer detail when rule is OK */
						log_status_ok(LOG_DEBUG_FILTERING,
								_("rule is account-based, "
								"matching the account currently used to retrieve messages\n"));
					} else {
						if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_HIGH) {
							/* with more detail when rule is OK */
							log_status_ok(LOG_DEBUG_FILTERING,
									_("rule is account-based [id=%d, name='%s'], "
									"matching the account currently used to retrieve messages\n"),
									ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
						}
					}
				}
			} else {
				if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
					/* with fewer detail when rule is skipped */
					log_status_skip(LOG_DEBUG_FILTERING,
							_("rule is account-based, "
							"not matching the account currently used to retrieve messages\n"));
				} else {
					if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_HIGH) {
						/* with more detail when rule is skipped */
						PrefsAccount *account = account_find_from_id(filtering->account_id);

						log_status_skip(LOG_DEBUG_FILTERING,
								_("rule is account-based [id=%d, name='%s'], "
								"not matching the account currently used to retrieve messages [id=%d, name='%s']\n"),
								filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
								ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
					}
				}
			}
		}
	} else {
		switch (prefs_common.apply_per_account_filtering_rules) {
		case FILTERING_ACCOUNT_RULES_FORCE:
			/* apply filtering rules regardless to the account info */
			matches = TRUE;

			/* debug output */
			if (debug_filtering_session) {
				if (filtering->account_id == 0) {
					if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
        				log_status_ok(LOG_DEBUG_FILTERING,
		    					_("rule is not account-based, "
			    				"but all rules are applied on user request anyway\n"));
                    }
				} else {
					if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
						/* with fewer detail when rule is OK */
						log_status_ok(LOG_DEBUG_FILTERING,
								_("rule is account-based, "
								"but all rules are applied on user request anyway\n"));
					} else {
						if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
							/* with more detail when rule is OK */
							PrefsAccount *account = account_find_from_id(filtering->account_id);

							log_status_ok(LOG_DEBUG_FILTERING,
									_("rule is account-based [id=%d, name='%s'], "
									"but all rules are applied on user request anyway\n"),
									filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
						}
					}
				}
			}
			break;
		case FILTERING_ACCOUNT_RULES_SKIP:
			/* don't apply filtering rules that belong to an account */
			matches = (filtering->account_id == 0);

			/* debug output */
			if (debug_filtering_session) {
				if (matches) {
					if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
    					log_status_ok(LOG_DEBUG_FILTERING,
	    						_("rule is not account-based\n"));
                    }
				} else {
					if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
						/* with fewer detail when rule is skipped */
						log_status_skip(LOG_DEBUG_FILTERING,
								_("rule is account-based, "
								"skipped on user request\n"));
					} else {
						if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_HIGH) {
							/* with more detail when rule is skipped */
							PrefsAccount *account = account_find_from_id(filtering->account_id);

							log_status_skip(LOG_DEBUG_FILTERING,
									_("rule is account-based [id=%d, name='%s'], "
									"skipped on user request\n"),
									filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
						}
					}
				}
			}
			break;
		case FILTERING_ACCOUNT_RULES_USE_CURRENT:
			matches = ((filtering->account_id == 0)
					|| (filtering->account_id == cur_account->account_id));

			/* debug output */
			if (debug_filtering_session) {
				if (matches) {
					if (filtering->account_id == 0) {
						if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
							log_status_ok(LOG_DEBUG_FILTERING,
									_("rule is not account-based\n"));
                        }
					} else {
						if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
							/* with fewer detail when rule is OK */
							log_status_ok(LOG_DEBUG_FILTERING,
									_("rule is account-based, "
									"matching current account\n"));
						} else {
							if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
								/* with more detail when rule is OK */
								PrefsAccount *account = account_find_from_id(filtering->account_id);

								log_status_ok(LOG_DEBUG_FILTERING,
										_("rule is account-based [id=%d, name='%s'], "
										"matching current account [id=%d, name='%s']\n"),
										account->account_id, account?account->account_name:_("NON_EXISTENT"),
										cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
							}
						}
					}
				} else {
					if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_MED) {
						/* with fewer detail when rule is skipped */
						log_status_skip(LOG_DEBUG_FILTERING,
								_("rule is account-based, "
								"not matching current account\n"));
					} else {
						if (prefs_common.filtering_debug_level == FILTERING_DEBUG_LEVEL_HIGH) {
							/* with more detail when rule is skipped */
							PrefsAccount *account = account_find_from_id(filtering->account_id);

							log_status_skip(LOG_DEBUG_FILTERING,
									_("rule is account-based [id=%d, name='%s'], "
									"not matching current account [id=%d, name='%s']\n"),
									filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
									cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
						}
					}
				}
			}
			break;
		}
	}

	return matches && matcherlist_match(filtering->matchers, info);
}

/*!
 *\brief	Apply a rule on message.
 *
 *\param	filtering List of filtering rules.
 *\param	info Message to apply rules on.
 *\param	final Variable returning TRUE or FALSE if one of the
 *		encountered actions was final. 
 *		See also \ref filtering_is_final_action.
 *
 *\return	gboolean TRUE to continue applying rules.
 */
static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info,
    gboolean * final)
{
	gboolean result = TRUE;
	gchar    *buf;
        GSList * tmp;
        
        * final = FALSE;
        for (tmp = filtering->action_list ; tmp != NULL ; tmp = tmp->next) {
                FilteringAction * action;

                action = tmp->data;
		buf = filteringaction_to_string(action);
		if (debug_filtering_session)
			log_print(LOG_DEBUG_FILTERING, _("applying action [ %s ]\n"), buf);

                if (FALSE == (result = filteringaction_apply(action, info))) {
					if (debug_filtering_session) {
						if (action->type != MATCHACTION_STOP)
							log_warning(LOG_DEBUG_FILTERING, _("action could not apply\n"));
						log_print(LOG_DEBUG_FILTERING,
								_("no further processing after action [ %s ]\n"), buf);
					}
 					debug_print("No further processing after rule %s\n", buf);
                }
                g_free(buf);
                if (filtering_is_final_action(action)) {
                        * final = TRUE;
                        break;
                }
		
        }
	return result;
}

/*!
 *\brief	Check if an action is "final", i.e. should break further
 *		processing.
 *
 *\param	filtering_action Action to check.
 *
 *\return	gboolean TRUE if \a filtering_action is final.	
 */
static gboolean filtering_is_final_action(FilteringAction *filtering_action)
{
	switch(filtering_action->type) {
	case MATCHACTION_MOVE:
	case MATCHACTION_DELETE:
	case MATCHACTION_STOP:
		return TRUE; /* MsgInfo invalid for message */
	default:
		return FALSE;
	}
}

gboolean processing_enabled(GSList *filtering_list)
{
	GSList	*l;
	for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
		FilteringProp * filtering = (FilteringProp *) l->data;
		if (filtering->enabled)
			return TRUE;
	}
	return FALSE;
}

static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
{
	GSList	*l;
	gboolean final;
	gboolean apply_next;
	
	cm_return_val_if_fail(info != NULL, TRUE);
	
	for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
		FilteringProp * filtering = (FilteringProp *) l->data;

		if (filtering->enabled) {
			if (debug_filtering_session) {
				gchar *buf = filteringprop_to_string(filtering);
				if (filtering->name && *filtering->name != '\0') {
					log_print(LOG_DEBUG_FILTERING,
						_("processing rule '%s' [ %s ]\n"),
						filtering->name, buf);
				} else {
					log_print(LOG_DEBUG_FILTERING,
						_("processing rule <unnamed> [ %s ]\n"),
						buf);
				}
				g_free(buf);
			}

			if (filtering_match_condition(filtering, info, ac_prefs)) {
				apply_next = filtering_apply_rule(filtering, info, &final);
				if (final)
					break;
			}

		} else {
			if (debug_filtering_session) {
				gchar *buf = filteringprop_to_string(filtering);
				if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
					if (filtering->name && *filtering->name != '\0') {
						log_status_skip(LOG_DEBUG_FILTERING,
								_("disabled rule '%s' [ %s ]\n"),
								filtering->name, buf);
					} else {
						log_status_skip(LOG_DEBUG_FILTERING,
								_("disabled rule <unnamed> [ %s ]\n"),
								buf);
					}
				}
				g_free(buf);
			}
		}
	}

    /* put in inbox if the last rule was not a final one, or
     * a final rule could not be applied.
     * Either of these cases is likely. */
    if (!final || !apply_next) {
		return FALSE;
	}

	return TRUE;
}

/*!
 *\brief	Filter a message against a list of rules.
 *
 *\param	flist List of filter rules.
 *\param	info Message.
 *
 *\return	gboolean TRUE if filter rules handled the message.
 *
 *\note		Returning FALSE means the message was not handled,
 *		and that the calling code should do the default
 *		processing. E.g. \ref inc.c::inc_start moves the 
 *		message to the inbox. 	
 */
gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
								   FilteringInvocationType context, gchar *extra_info)
{
	gboolean ret;

	if (prefs_common.enable_filtering_debug) {
		gchar *tmp = _("undetermined");

		switch (context) {
		case FILTERING_INCORPORATION:
			tmp = _("incorporation");
			debug_filtering_session = prefs_common.enable_filtering_debug_inc;
			break;
		case FILTERING_MANUALLY:
			tmp = _("manually");
			debug_filtering_session = prefs_common.enable_filtering_debug_manual;
			break;
		case FILTERING_FOLDER_PROCESSING:
			tmp = _("folder processing");
			debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
			break;
		case FILTERING_PRE_PROCESSING:
			tmp = _("pre-processing");
			debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
			break;
		case FILTERING_POST_PROCESSING:
			tmp = _("post-processing");
			debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
			break;
		default:
			debug_filtering_session = FALSE;
			break;
		}

		if (debug_filtering_session) {
			gchar *file = procmsg_get_message_file_path(info);
			gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');

			/* show context info and essential info about the message */
			if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
				log_print(LOG_DEBUG_FILTERING,
						_("filtering message (%s%s%s)\n"
						"%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
						tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
						spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
						spc, prefs_common_translated_header_name("From:"), info->from,
						spc, prefs_common_translated_header_name("To:"), info->to,
						spc, prefs_common_translated_header_name("Subject:"), info->subject);
			} else {
				log_print(LOG_DEBUG_FILTERING,
						_("filtering message (%s%s%s)\n"
						"%smessage file: %s\n"),
						tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
						spc, file);
			}
			g_free(file);
			g_free(spc);
		}
	} else
		debug_filtering_session = FALSE;

	ret = filter_msginfo(flist, info, ac_prefs);
	debug_filtering_session = FALSE;
	return ret;
}

gchar *filteringaction_to_string(FilteringAction *action)
{
	const gchar *command_str;
	gchar * quoted_dest;
	gchar * quoted_header;
	GString *dest = g_string_new("");

	command_str = get_matchparser_tab_str(action->type);

	if (command_str == NULL) {
		g_string_free(dest, TRUE);
		return NULL;
	}
    
	switch(action->type) {
	case MATCHACTION_MOVE:
	case MATCHACTION_COPY:
	case MATCHACTION_EXECUTE:
	case MATCHACTION_SET_TAG:
	case MATCHACTION_UNSET_TAG:
		quoted_dest = matcher_quote_str(action->destination);
		g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
		g_free(quoted_dest);
		break;

	case MATCHACTION_DELETE:
	case MATCHACTION_MARK:
	case MATCHACTION_UNMARK:
	case MATCHACTION_LOCK:
	case MATCHACTION_UNLOCK:
	case MATCHACTION_MARK_AS_READ:
	case MATCHACTION_MARK_AS_UNREAD:
	case MATCHACTION_MARK_AS_SPAM:
	case MATCHACTION_MARK_AS_HAM:
	case MATCHACTION_STOP:
	case MATCHACTION_HIDE:
	case MATCHACTION_IGNORE:
	case MATCHACTION_WATCH:
	case MATCHACTION_CLEAR_TAGS:
		g_string_append_printf(dest, "%s", command_str);
		break;

	case MATCHACTION_REDIRECT:
	case MATCHACTION_FORWARD:
	case MATCHACTION_FORWARD_AS_ATTACHMENT:
		quoted_dest = matcher_quote_str(action->destination);
		g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
		g_free(quoted_dest);
		break;

	case MATCHACTION_COLOR:
		g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
		break;

	case MATCHACTION_CHANGE_SCORE:
	case MATCHACTION_SET_SCORE:
		g_string_append_printf(dest, "%s %d", command_str, action->score);
		break;

	case MATCHACTION_ADD_TO_ADDRESSBOOK:
		quoted_header = matcher_quote_str(action->header);
		quoted_dest = matcher_quote_str(action->destination);
		g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
		g_free(quoted_dest);
		g_free(quoted_header);
		break;

	default:
		g_string_free(dest, TRUE);
		return NULL;
	}
	return g_string_free(dest, FALSE);
}

gchar * filteringaction_list_to_string(GSList * action_list)
{
	gchar *action_list_str;
        GSList * tmp;
	gchar *list_str;

        action_list_str = NULL;
        for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
                gchar *action_str;
                FilteringAction * action;
                
                action = tmp->data;
                
                action_str = filteringaction_to_string(action);
                
                if (action_list_str != NULL) {
                        list_str = g_strconcat(action_list_str, " ", action_str, NULL);
                        g_free(action_list_str);
                }
                else {
                        list_str = g_strdup(action_str);
                }
		g_free(action_str);
                action_list_str = list_str;
        }

        return action_list_str;
}

gchar * filteringprop_to_string(FilteringProp * prop)
{
	gchar *list_str;
	gchar *action_list_str;
	gchar *filtering_str;

	if (prop == NULL)
		return NULL;

        action_list_str = filteringaction_list_to_string(prop->action_list);

	if (action_list_str == NULL)
		return NULL;

	list_str = matcherlist_to_string(prop->matchers);

	if (list_str == NULL) {
                g_free(action_list_str);
		return NULL;
        }

	filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
	g_free(action_list_str);
	g_free(list_str);

	return filtering_str;
}

static void prefs_filtering_free(GSList * prefs_filtering)
{
 	while (prefs_filtering != NULL) {
 		FilteringProp * filtering = (FilteringProp *)
			prefs_filtering->data;
 		prefs_filtering = g_slist_remove(prefs_filtering, filtering);
 		filteringprop_free(filtering);
 	}
}

static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
{
	FolderItem *item = node->data;

	cm_return_val_if_fail(item, FALSE);
	cm_return_val_if_fail(item->prefs, FALSE);

	prefs_filtering_free(item->prefs->processing);
	item->prefs->processing = NULL;

	return FALSE;
}

void prefs_filtering_clear(void)
{
	GList * cur;

	for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
		Folder *folder;

		folder = (Folder *) cur->data;
		g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
				prefs_filtering_free_func, NULL);
	}

	prefs_filtering_free(filtering_rules);
	filtering_rules = NULL;
	prefs_filtering_free(pre_global_processing);
	pre_global_processing = NULL;
	prefs_filtering_free(post_global_processing);
	post_global_processing = NULL;
}

void prefs_filtering_clear_folder(Folder *folder)
{
	cm_return_if_fail(folder);
	cm_return_if_fail(folder->node);

	g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
			prefs_filtering_free_func, NULL);
	/* FIXME: Note folder settings were changed, where the updates? */
}

gboolean filtering_peek_per_account_rules(GSList *filtering_list)
/* return TRUE if there's at least one per-account filtering rule */
{
	GSList *l;

	for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
		FilteringProp * filtering = (FilteringProp *) l->data;

		if (filtering->enabled && (filtering->account_id != 0)) {
			return TRUE;
		}		
	}

	return FALSE;
}

gboolean filtering_action_list_rename_path(GSList *action_list, const gchar *old_path,
					   const gchar *new_path)
{
	gchar *base;
	gchar *prefix;
	gchar *suffix;
	gchar *dest_path;
	gchar *old_path_with_sep;
	gint destlen;
	gint prefixlen;
	gint oldpathlen;
	GSList * action_cur;
	const gchar *separator=G_DIR_SEPARATOR_S;
	gboolean matched = FALSE;
#ifdef G_OS_WIN32
again:
#endif
	oldpathlen = strlen(old_path);
	old_path_with_sep = g_strconcat(old_path,separator,NULL);

	for(action_cur = action_list ; action_cur != NULL ;
		action_cur = action_cur->next) {

		FilteringAction *action = action_cur->data;

		if (action->type == MATCHACTION_SET_TAG ||
		    action->type == MATCHACTION_UNSET_TAG)
			continue;
		if (!action->destination) 
			continue;
		
		destlen = strlen(action->destination);

		if (destlen > oldpathlen) {
			prefixlen = destlen - oldpathlen;
			suffix = action->destination + prefixlen;

			if (!strncmp(old_path, suffix, oldpathlen)) {
				prefix = g_malloc0(prefixlen + 1);
				strncpy2(prefix, action->destination, prefixlen);

				base = suffix + oldpathlen;
				while (*base == G_DIR_SEPARATOR)
					base++;
				if (*base == '\0')
					dest_path = g_strconcat(prefix, separator, new_path, NULL);
				else
					dest_path = g_strconcat(prefix, separator, new_path, separator, base, NULL);

				g_free(prefix);
				g_free(action->destination);
				action->destination = dest_path;
				matched = TRUE;
			} else { /* for non-leaf folders */
				/* compare with trailing slash */
				if (!strncmp(old_path_with_sep, action->destination, oldpathlen+1)) {

					suffix = action->destination + oldpathlen + 1;
					dest_path = g_strconcat(new_path, separator,
								suffix, NULL);
					g_free(action->destination);
					action->destination = dest_path;
					matched = TRUE;
				}
			}
		} else {
			/* folder-moving a leaf */
			if (!strcmp(old_path, action->destination)) {
				dest_path = g_strdup(new_path);
				g_free(action->destination);
				action->destination = dest_path;
				matched = TRUE;
			}
		}
	}
	
	g_free(old_path_with_sep);
#ifdef G_OS_WIN32
	if (!strcmp(separator, G_DIR_SEPARATOR_S) && !matched) {
		separator = "/";
		goto again;
	}
#endif

	return matched;
}
