/*
 * LibSylph -- E-Mail client library
 * Copyright (C) 1999-2006 Hiroyuki Yamamoto
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include "defs.h"

#include <glib.h>
#include <glib/gi18n.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <sys/types.h>
#if USE_ONIGURUMA
#  include <onigposix.h>
#elif HAVE_REGEX_H
#  include <regex.h>
#endif
#include <time.h>

#include "filter.h"
#include "procmsg.h"
#include "procheader.h"
#include "folder.h"
#include "utils.h"
#include "xml.h"
#include "prefs.h"
#include "prefs_common.h"
#include "prefs_account.h"
#include "account.h"

typedef enum
{
	FLT_O_CONTAIN	= 1 << 0,
	FLT_O_CASE_SENS	= 1 << 1,
	FLT_O_REGEX	= 1 << 2
} FilterOldFlag;

static gboolean filter_match_cond	(FilterCond	*cond,
					 MsgInfo	*msginfo,
					 GSList		*hlist,
					 FilterInfo	*fltinfo);
static gboolean filter_match_header_cond(FilterCond	*cond,
					 GSList		*hlist);

static void filter_cond_free		(FilterCond	*cond);
static void filter_action_free		(FilterAction	*action);


gint filter_apply(GSList *fltlist, const gchar *file, FilterInfo *fltinfo)
{
	MsgInfo *msginfo;
	gint ret = 0;

	g_return_val_if_fail(file != NULL, -1);
	g_return_val_if_fail(fltinfo != NULL, -1);

	if (!fltlist) return 0;

	msginfo = procheader_parse_file(file, fltinfo->flags, FALSE);
	if (!msginfo) return 0;
	msginfo->file_path = g_strdup(file);

	ret = filter_apply_msginfo(fltlist, msginfo, fltinfo);

	procmsg_msginfo_free(msginfo);

	return ret;
}

gint filter_apply_msginfo(GSList *fltlist, MsgInfo *msginfo,
			  FilterInfo *fltinfo)
{
	gchar *file;
	GSList *hlist, *cur;
	FilterRule *rule;
	gint ret = 0;

	g_return_val_if_fail(msginfo != NULL, -1);
	g_return_val_if_fail(fltinfo != NULL, -1);

	if (!fltlist) return 0;

	file = procmsg_get_message_file(msginfo);
	hlist = procheader_get_header_list_from_file(file);
	if (!hlist) {
		g_free(file);
		return 0;
	}

	for (cur = fltlist; cur != NULL; cur = cur->next) {
		rule = (FilterRule *)cur->data;
		if (!rule->enabled) continue;
		if (filter_match_rule(rule, msginfo, hlist, fltinfo)) {
			ret = filter_action_exec(rule, msginfo, file, fltinfo);
			if (ret < 0) {
				g_warning("filter_action_exec() returned error\n");
				break;
			}
			if (fltinfo->drop_done == TRUE ||
			    fltinfo->actions[FLT_ACTION_STOP_EVAL] == TRUE)
				break;
		}
	}

	procheader_header_list_destroy(hlist);
	g_free(file);

	return ret;
}

gint filter_action_exec(FilterRule *rule, MsgInfo *msginfo, const gchar *file,
			FilterInfo *fltinfo)
{
	FolderItem *dest_folder = NULL;
	FilterAction *action;
	GSList *cur;
	gchar *cmdline;
	gboolean copy_to_self = FALSE;

	g_return_val_if_fail(rule != NULL, -1);
	g_return_val_if_fail(msginfo != NULL, -1);
	g_return_val_if_fail(file != NULL, -1);
	g_return_val_if_fail(fltinfo != NULL, -1);

	for (cur = rule->action_list; cur != NULL; cur = cur->next) {
		action = (FilterAction *)cur->data;

		switch (action->type) {
		case FLT_ACTION_MARK:
			debug_print("filter_action_exec(): mark\n");
			MSG_SET_PERM_FLAGS(fltinfo->flags, MSG_MARKED);
			fltinfo->actions[action->type] = TRUE;
			break;
		case FLT_ACTION_COLOR_LABEL:
			debug_print("filter_action_exec(): color label: %d\n",
				    action->int_value);
			MSG_UNSET_PERM_FLAGS(fltinfo->flags,
					     MSG_CLABEL_FLAG_MASK);
			MSG_SET_COLORLABEL_VALUE(fltinfo->flags,
						 action->int_value);
			fltinfo->actions[action->type] = TRUE;
			break;
		case FLT_ACTION_MARK_READ:
			debug_print("filter_action_exec(): mark as read\n");
			if (msginfo->folder) {
				if (MSG_IS_NEW(fltinfo->flags))
					msginfo->folder->new--;
				if (MSG_IS_UNREAD(fltinfo->flags))
					msginfo->folder->unread--;
			}
			MSG_UNSET_PERM_FLAGS(fltinfo->flags, MSG_NEW|MSG_UNREAD);
			fltinfo->actions[action->type] = TRUE;
			break;
		case FLT_ACTION_EXEC:
			cmdline = g_strconcat(action->str_value, " \"", file,
					      "\"", NULL);
			execute_command_line(cmdline, FALSE);
			g_free(cmdline);
			fltinfo->actions[action->type] = TRUE;
			break;
		case FLT_ACTION_EXEC_ASYNC:
			cmdline = g_strconcat(action->str_value, " \"", file,
					      "\"", NULL);
			execute_command_line(cmdline, TRUE);
			g_free(cmdline);
			fltinfo->actions[action->type] = TRUE;
			break;
		default:
			break;
		}
	}

	for (cur = rule->action_list; cur != NULL; cur = cur->next) {
		action = (FilterAction *)cur->data;

		switch (action->type) {
		case FLT_ACTION_MOVE:
		case FLT_ACTION_COPY:
			dest_folder = folder_find_item_from_identifier
				(action->str_value);
			if (!dest_folder) {
				g_warning("dest folder '%s' not found\n",
					  action->str_value);
				return -1;
			}
			debug_print("filter_action_exec(): %s: dest_folder = %s\n",
				    action->type == FLT_ACTION_COPY ?
				    "copy" : "move", action->str_value);

			if (msginfo->folder) {
				gint val;

				/* local filtering */
				if (msginfo->folder == dest_folder)
					copy_to_self = TRUE;
				else {
					if (action->type == FLT_ACTION_COPY) {
						val = folder_item_copy_msg
							(dest_folder, msginfo);
						if (val == -1)
							return -1;
					}
					fltinfo->actions[action->type] = TRUE;
				}
			} else {
				if (folder_item_add_msg(dest_folder, file,
							&fltinfo->flags,
							FALSE) < 0)
					return -1;
				fltinfo->actions[action->type] = TRUE;
			}

			fltinfo->dest_list = g_slist_append(fltinfo->dest_list,
							    dest_folder);
			if (action->type == FLT_ACTION_MOVE) {
				fltinfo->move_dest = dest_folder;
				fltinfo->drop_done = TRUE;
			}
			break;
		default:
			break;
		}
	}

	if (fltinfo->drop_done == TRUE)
		return 0;

	for (cur = rule->action_list; cur != NULL; cur = cur->next) {
		action = (FilterAction *)cur->data;

		switch (action->type) {
		case FLT_ACTION_NOT_RECEIVE:
			debug_print("filter_action_exec(): don't receive\n");
			fltinfo->drop_done = TRUE;
			fltinfo->actions[action->type] = TRUE;
			return 0;
		case FLT_ACTION_DELETE:
			debug_print("filter_action_exec(): delete\n");
			if (msginfo->folder) {
				/* local filtering */
				if (copy_to_self == FALSE)
					fltinfo->actions[action->type] = TRUE;
			} else
				fltinfo->actions[action->type] = TRUE;

			fltinfo->drop_done = TRUE;
			return 0;
		case FLT_ACTION_STOP_EVAL:
			debug_print("filter_action_exec(): stop evaluation\n");
			fltinfo->actions[action->type] = TRUE;
			return 0;
		default:
			break;
		}
	}

	return 0;
}

static gboolean strmatch_regex(const gchar *haystack, const gchar *needle)
{
#if defined(USE_ONIGURUMA) || defined(HAVE_REGCOMP)
	gint ret = 0;
	regex_t preg;

#if USE_ONIGURUMA
	reg_set_encoding(REG_POSIX_ENCODING_UTF8);
#endif
	ret = regcomp(&preg, needle, REG_ICASE|REG_EXTENDED);
	if (ret != 0)
		return FALSE;

	ret = regexec(&preg, haystack, 0, NULL, 0);
	regfree(&preg);

	if (ret == 0)
		return TRUE;
	else
#endif
		return FALSE;
}

gboolean filter_match_rule(FilterRule *rule, MsgInfo *msginfo, GSList *hlist,
			   FilterInfo *fltinfo)
{
	FilterCond *cond;
	GSList *cur;
	gboolean matched;

	g_return_val_if_fail(rule->cond_list != NULL, FALSE);

	switch (rule->timing) {
	case FLT_TIMING_ANY:
		break;
	case FLT_TIMING_ON_RECEIVE:
		if (msginfo->folder != NULL)
			return FALSE;
		break;
	case FLT_TIMING_MANUAL:
		if (msginfo->folder == NULL)
			return FALSE;
		break;
	default:
		break;
	}

	if (rule->bool_op == FLT_AND) {
		for (cur = rule->cond_list; cur != NULL; cur = cur->next) {
			cond = (FilterCond *)cur->data;
			matched = filter_match_cond(cond, msginfo, hlist,
						    fltinfo);
			if (matched == FALSE)
				return FALSE;
		}

		return TRUE;
	} else if (rule->bool_op == FLT_OR) {
		for (cur = rule->cond_list; cur != NULL; cur = cur->next) {
			cond = (FilterCond *)cur->data;
			matched = filter_match_cond(cond, msginfo, hlist,
						    fltinfo);
			if (matched == TRUE)
				return TRUE;
		}

		return FALSE;
	}

	return FALSE;
}

static gboolean filter_match_cond(FilterCond *cond, MsgInfo *msginfo,
				  GSList *hlist, FilterInfo *fltinfo)
{
	gboolean matched = FALSE;
	gchar *file;
	gchar *cmdline;
	PrefsAccount *cond_ac;

	switch (cond->type) {
	case FLT_COND_HEADER:
	case FLT_COND_ANY_HEADER:
	case FLT_COND_TO_OR_CC:
		return filter_match_header_cond(cond, hlist);
	case FLT_COND_BODY:
		matched = procmime_find_string(msginfo, cond->str_value,
					       cond->match_func);
		break;
	case FLT_COND_CMD_TEST:
		file = procmsg_get_message_file(msginfo);
		cmdline = g_strconcat(cond->str_value, " \"", file, "\"", NULL);
		matched = (execute_command_line(cmdline, FALSE) == 0);
		g_free(cmdline);
		g_free(file);
		break;
	case FLT_COND_SIZE_GREATER:
		matched = (msginfo->size > cond->int_value * 1024);
		break;
	case FLT_COND_AGE_GREATER:
		matched = (time(NULL) - msginfo->date_t >
				cond->int_value * 24 * 60 * 60);
		break;
	case FLT_COND_UNREAD:
		matched = MSG_IS_UNREAD(msginfo->flags);
		break;
	case FLT_COND_MARK:
		matched = MSG_IS_MARKED(msginfo->flags);
		break;
	case FLT_COND_COLOR_LABEL:
		matched = (MSG_GET_COLORLABEL_VALUE(msginfo->flags) != 0);
		break;
	case FLT_COND_MIME:
		matched = MSG_IS_MIME(msginfo->flags);
		break;
	case FLT_COND_ACCOUNT:
		cond_ac = account_find_from_id(cond->int_value);
		matched = (cond_ac != NULL && cond_ac == fltinfo->account);
		break;
	default:
		g_warning("filter_match_cond(): unknown condition: %d\n",
			  cond->type);
		return FALSE;
	}

	if (FLT_IS_NOT_MATCH(cond->match_flag))
		matched = !matched;

	return matched;
}

static gboolean filter_match_header_cond(FilterCond *cond, GSList *hlist)
{
	gboolean matched = FALSE;
	GSList *cur;
	Header *header;

	for (cur = hlist; cur != NULL; cur = cur->next) {
		header = (Header *)cur->data;

		switch (cond->type) {
		case FLT_COND_HEADER:
			if (!g_ascii_strcasecmp
				(header->name, cond->header_name)) {
				if (!cond->str_value ||
				    cond->match_func(header->body,
						     cond->str_value))
					matched = TRUE;
			}
			break;
		case FLT_COND_ANY_HEADER:
			if (!cond->str_value ||
			    cond->match_func(header->body, cond->str_value))
				matched = TRUE;
			break;
		case FLT_COND_TO_OR_CC:
			if (!g_ascii_strcasecmp(header->name, "To") ||
			    !g_ascii_strcasecmp(header->name, "Cc")) {
				if (!cond->str_value ||
				    cond->match_func(header->body,
						     cond->str_value))
					matched = TRUE;
			}
			break;
		default:
			break;
		}

		if (matched == TRUE)
			break;
	}

	if (FLT_IS_NOT_MATCH(cond->match_flag))
		matched = !matched;

	return matched;
}

gboolean filter_rule_requires_full_headers(FilterRule *rule)
{
	GSList *cur;

	for (cur = rule->cond_list; cur != NULL; cur = cur->next) {
		FilterCond *cond = (FilterCond *)cur->data;
		const gchar *name = cond->header_name;

		if (cond->type == FLT_COND_HEADER && name) {
			if (g_ascii_strcasecmp(name, "Date") != 0 &&
			    g_ascii_strcasecmp(name, "From") != 0 &&
			    g_ascii_strcasecmp(name, "To") != 0 &&
			    g_ascii_strcasecmp(name, "Newsgroups") != 0 &&
			    g_ascii_strcasecmp(name, "Subject") != 0)
				return TRUE;
		} else if (cond->type == FLT_COND_ANY_HEADER ||
			   cond->type == FLT_COND_TO_OR_CC)
			return TRUE;
	}

	return FALSE;
}

#define RETURN_IF_TAG_NOT_MATCH(tag_name)			\
	if (strcmp2(xmlnode->tag->tag, tag_name) != 0) {	\
		g_warning("tag name != \"" tag_name "\"\n");	\
		filter_cond_list_free(cond_list);		\
		filter_action_list_free(cond_list);		\
		return FALSE;					\
	}							\

#define STR_SWITCH(sw)		{ const gchar *sw_str = sw;
#define STR_CASE_BEGIN(str)	if (!strcmp(sw_str, str)) {
#define STR_CASE(str)		} else if (!strcmp(sw_str, str)) {
#define STR_CASE_END		} }

static gboolean filter_xml_node_func(GNode *node, gpointer data)
{
	XMLNode *xmlnode;
	GList *list;
	const gchar *name = NULL;
	FilterTiming timing = FLT_TIMING_ANY;
	gboolean enabled = TRUE;
	FilterRule *rule;
	FilterBoolOp bool_op = FLT_OR;
	const gchar *target_folder = NULL;
	gboolean recursive = FALSE;
	GSList *cond_list = NULL;
	GSList *action_list = NULL;
	GNode *child, *cond_child, *action_child;
	GSList **fltlist = (GSList **)data;

	if (g_node_depth(node) != 2) return FALSE;
	g_return_val_if_fail(node->data != NULL, FALSE);

	xmlnode = node->data;
	RETURN_IF_TAG_NOT_MATCH("rule");

	for (list = xmlnode->tag->attr; list != NULL; list = list->next) {
		XMLAttr *attr = (XMLAttr *)list->data;

		if (!attr || !attr->name || !attr->value) continue;
		if (!strcmp(attr->name, "name"))
			name = attr->value;
		else if (!strcmp(attr->name, "timing")) {
			if (!strcmp(attr->value, "any"))
				timing = FLT_TIMING_ANY;
			else if (!strcmp(attr->value, "receive"))
				timing = FLT_TIMING_ON_RECEIVE;
			else if (!strcmp(attr->value, "manual"))
				timing = FLT_TIMING_MANUAL;
		} else if (!strcmp(attr->name, "enabled")) {
			if (!strcmp(attr->value, "true"))
				enabled = TRUE;
			else
				enabled = FALSE;
		}
	}

	/* condition list */
	child = node->children;
	if (!child) {
		g_warning("<rule> must have children\n");
		return FALSE;
	}
	xmlnode = child->data;
	RETURN_IF_TAG_NOT_MATCH("condition-list");
	for (list = xmlnode->tag->attr; list != NULL; list = list->next) {
		XMLAttr *attr = (XMLAttr *)list->data;

		if (attr && attr->name && attr->value &&
		    !strcmp(attr->name, "bool")) {
			if (!strcmp(attr->value, "or"))
				bool_op = FLT_OR;
			else
				bool_op = FLT_AND;
		}
	}

	for (cond_child = child->children; cond_child != NULL;
	     cond_child = cond_child->next) {
		const gchar *type = NULL;
		const gchar *name = NULL;
		const gchar *value = NULL;
		gboolean case_sens = FALSE;
		FilterCond *cond;
		FilterCondType cond_type = FLT_COND_HEADER;
		FilterMatchType match_type = FLT_CONTAIN;
		FilterMatchFlag match_flag = 0;

		xmlnode = cond_child->data;
		if (!xmlnode || !xmlnode->tag || !xmlnode->tag->tag) continue;

		for (list = xmlnode->tag->attr; list != NULL; list = list->next) {
			XMLAttr *attr = (XMLAttr *)list->data;

			if (!attr || !attr->name || !attr->value) continue;

			STR_SWITCH(attr->name)
			STR_CASE_BEGIN("type")
				type = attr->value;
			STR_CASE("name")
				name = attr->value;
			STR_CASE("case")
				case_sens = TRUE;
			STR_CASE("recursive")
				if (!strcmp(attr->value, "true"))
					recursive = TRUE;
				else
					recursive = FALSE;
			STR_CASE_END
		}

		if (type) {
			filter_rule_match_type_str_to_enum
				(type, &match_type, &match_flag);
		}
		if (case_sens)
			match_flag |= FLT_CASE_SENS;
		value = xmlnode->element;

		STR_SWITCH(xmlnode->tag->tag)
		STR_CASE_BEGIN("match-header")
			cond_type = FLT_COND_HEADER;
		STR_CASE("match-any-header")
			cond_type = FLT_COND_ANY_HEADER;
		STR_CASE("match-to-or-cc")
			cond_type = FLT_COND_TO_OR_CC;
		STR_CASE("match-body-text")
			cond_type = FLT_COND_BODY;
		STR_CASE("command-test")
			cond_type = FLT_COND_CMD_TEST;
		STR_CASE("size")
			cond_type = FLT_COND_SIZE_GREATER;
		STR_CASE("age")
			cond_type = FLT_COND_AGE_GREATER;
		STR_CASE("unread")
			cond_type = FLT_COND_UNREAD;
		STR_CASE("mark")
			cond_type = FLT_COND_MARK;
		STR_CASE("color-label")
			cond_type = FLT_COND_COLOR_LABEL;
		STR_CASE("mime")
			cond_type = FLT_COND_MIME;
		STR_CASE("account-id")
			cond_type = FLT_COND_ACCOUNT;
		STR_CASE("target-folder")
			target_folder = value;
			continue;
		STR_CASE_END

		cond = filter_cond_new(cond_type, match_type, match_flag,
				       name, value);
		cond_list = g_slist_append(cond_list, cond);
	}

	/* action list */
	child = child->next;
	if (!child) {
		g_warning("<rule> must have multiple children\n");
		filter_cond_list_free(cond_list);
		return FALSE;
	}
	xmlnode = child->data;
	RETURN_IF_TAG_NOT_MATCH("action-list");

	for (action_child = child->children; action_child != NULL;
	     action_child = action_child->next) {
		FilterAction *action;
		FilterActionType action_type = FLT_ACTION_NONE;

		xmlnode = action_child->data;
		if (!xmlnode || !xmlnode->tag || !xmlnode->tag->tag) continue;

		STR_SWITCH(xmlnode->tag->tag)
		STR_CASE_BEGIN("move")
			action_type = FLT_ACTION_MOVE;
		STR_CASE("copy")
			action_type = FLT_ACTION_COPY;
		STR_CASE("not-receive")
			action_type = FLT_ACTION_NOT_RECEIVE;
		STR_CASE("delete")
			action_type = FLT_ACTION_DELETE;
		STR_CASE("exec")
			action_type = FLT_ACTION_EXEC;
		STR_CASE("exec-async")
			action_type = FLT_ACTION_EXEC_ASYNC;
		STR_CASE("mark")
			action_type = FLT_ACTION_MARK;
		STR_CASE("color-label")
			action_type = FLT_ACTION_COLOR_LABEL;
		STR_CASE("mark-as-read")
			action_type = FLT_ACTION_MARK_READ;
		STR_CASE("forward")
			action_type = FLT_ACTION_FORWARD;
		STR_CASE("forward-as-attachment")
			action_type = FLT_ACTION_FORWARD_AS_ATTACHMENT;
		STR_CASE("redirect")
			action_type = FLT_ACTION_REDIRECT;
		STR_CASE("stop-eval")
			action_type = FLT_ACTION_STOP_EVAL;
		STR_CASE_END

		action = filter_action_new(action_type, xmlnode->element);
		action_list = g_slist_append(action_list, action);
	}

	if (name && cond_list) {
		rule = filter_rule_new(name, bool_op, cond_list, action_list);
		rule->timing = timing;
		rule->enabled = enabled;
		if (target_folder) {
			rule->target_folder = g_strdup(target_folder);
			rule->recursive = recursive;
		}
		*fltlist = g_slist_prepend(*fltlist, rule);
	}

	return FALSE;
}

#undef RETURN_IF_TAG_NOT_MATCH
#undef STR_SWITCH
#undef STR_CASE_BEGIN
#undef STR_CASE
#undef STR_CASE_END

GSList *filter_xml_node_to_filter_list(GNode *node)
{
	GSList *fltlist = NULL;

	g_return_val_if_fail(node != NULL, NULL);

	g_node_traverse(node, G_PRE_ORDER, G_TRAVERSE_ALL, 2,
			filter_xml_node_func, &fltlist);
	fltlist = g_slist_reverse(fltlist);

	return fltlist;
}

GSList *filter_read_file(const gchar *file)
{
	GNode *node;
	GSList *list;

	g_return_val_if_fail(file != NULL, NULL);

	debug_print("Reading %s\n", file);

	if (!is_file_exist(file))
		return NULL;

	node = xml_parse_file(file);
	if (!node) {
		g_warning("Can't parse %s\n", file);
		return NULL;
	}

	list = filter_xml_node_to_filter_list(node);

	xml_free_tree(node);

	return list;
}

void filter_read_config(void)
{
	gchar *rcpath;

	debug_print("Reading filter configuration...\n");

	/* remove all previous filter list */
	while (prefs_common.fltlist != NULL) {
		FilterRule *rule = (FilterRule *)prefs_common.fltlist->data;

		filter_rule_free(rule);
		prefs_common.fltlist = g_slist_remove(prefs_common.fltlist,
						      rule);
	}

	rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, FILTER_LIST,
			     NULL);
	prefs_common.fltlist = filter_read_file(rcpath);
	g_free(rcpath);
}

#define NODE_NEW(tag, text) \
	node = xml_node_new(xml_tag_new(tag), text)
#define ADD_ATTR(name, value) \
	xml_tag_add_attr(node->tag, xml_attr_new(name, value))

void filter_write_file(GSList *list, const gchar *file)
{
	PrefFile *pfile;
	GSList *cur;

	g_return_if_fail(file != NULL);

	if ((pfile = prefs_file_open(file)) == NULL) {
		g_warning("failed to write filter configuration to file: %s\n",
			  file);
		return;
	}

	xml_file_put_xml_decl(pfile->fp);
	fputs("\n<filter>\n", pfile->fp);

	for (cur = list; cur != NULL; cur = cur->next) {
		FilterRule *rule = (FilterRule *)cur->data;
		GSList *cur_cond;
		GSList *cur_action;
		gchar match_type[16];

		fputs("    <rule name=\"", pfile->fp);
		xml_file_put_escape_str(pfile->fp, rule->name);
		fprintf(pfile->fp, "\" timing=\"%s\"",
			rule->timing == FLT_TIMING_ON_RECEIVE ? "receive" :
			rule->timing == FLT_TIMING_MANUAL ? "manual" : "any");
		fprintf(pfile->fp, " enabled=\"%s\">\n",
			rule->enabled ? "true" : "false");

		fprintf(pfile->fp, "        <condition-list bool=\"%s\">\n",
			rule->bool_op == FLT_OR ? "or" : "and");

		for (cur_cond = rule->cond_list; cur_cond != NULL;
		     cur_cond = cur_cond->next) {
			FilterCond *cond = (FilterCond *)cur_cond->data;
			XMLNode *node = NULL;

			switch (cond->match_type) {
			case FLT_CONTAIN:
				strcpy(match_type,
				       FLT_IS_NOT_MATCH(cond->match_flag)
				       ? "not-contain" : "contains");
				break;
			case FLT_EQUAL:
				strcpy(match_type,
				       FLT_IS_NOT_MATCH(cond->match_flag)
				       ? "is-not" : "is");
				break;
			case FLT_REGEX:
				strcpy(match_type,
				       FLT_IS_NOT_MATCH(cond->match_flag)
				       ? "not-regex" : "regex");
				break;
			default:
				match_type[0] = '\0';
				break;
			}

			switch (cond->type) {
			case FLT_COND_HEADER:
				NODE_NEW("match-header", cond->str_value);
				ADD_ATTR("type", match_type);
				ADD_ATTR("name", cond->header_name);
				if (FLT_IS_CASE_SENS(cond->match_flag))
					ADD_ATTR("case", "true");
				break;
			case FLT_COND_ANY_HEADER:
				NODE_NEW("match-any-header", cond->str_value);
				ADD_ATTR("type", match_type);
				if (FLT_IS_CASE_SENS(cond->match_flag))
					ADD_ATTR("case", "true");
				break;
			case FLT_COND_TO_OR_CC:
				NODE_NEW("match-to-or-cc", cond->str_value);
				ADD_ATTR("type", match_type);
				if (FLT_IS_CASE_SENS(cond->match_flag))
					ADD_ATTR("case", "true");
				break;
			case FLT_COND_BODY:
				NODE_NEW("match-body-text", cond->str_value);
				ADD_ATTR("type", match_type);
				if (FLT_IS_CASE_SENS(cond->match_flag))
					ADD_ATTR("case", "true");
				break;
			case FLT_COND_CMD_TEST:
				NODE_NEW("command-test", cond->str_value);
				break;
			case FLT_COND_SIZE_GREATER:
				NODE_NEW("size", itos(cond->int_value));
				ADD_ATTR("type",
					 FLT_IS_NOT_MATCH(cond->match_flag)
					 ? "lt" : "gt");
				break;
			case FLT_COND_AGE_GREATER:
				NODE_NEW("age", itos(cond->int_value));
				ADD_ATTR("type",
					 FLT_IS_NOT_MATCH(cond->match_flag)
					 ? "lt" : "gt");
				break;
			case FLT_COND_UNREAD:
				NODE_NEW("unread", NULL);
				ADD_ATTR("type",
					 FLT_IS_NOT_MATCH(cond->match_flag)
					 ? "is-not" : "is");
				break;
			case FLT_COND_MARK:
				NODE_NEW("mark", NULL);
				ADD_ATTR("type",
					 FLT_IS_NOT_MATCH(cond->match_flag)
					 ? "is-not" : "is");
				break;
			case FLT_COND_COLOR_LABEL:
				NODE_NEW("color-label", NULL);
				ADD_ATTR("type",
					 FLT_IS_NOT_MATCH(cond->match_flag)
					 ? "is-not" : "is");
				break;
			case FLT_COND_MIME:
				NODE_NEW("mime", NULL);
				ADD_ATTR("type",
					 FLT_IS_NOT_MATCH(cond->match_flag)
					 ? "is-not" : "is");
				break;
			case FLT_COND_ACCOUNT:
				 NODE_NEW("account-id", itos(cond->int_value));
				 break;
			default:
				 break;
			}

			if (node) {
				fputs("            ", pfile->fp);
				xml_file_put_node(pfile->fp, node);
				xml_free_node(node);
			}
		}

		if (rule->target_folder) {
			XMLNode *node;

			NODE_NEW("target-folder", rule->target_folder);
			ADD_ATTR("recursive", rule->recursive
				 ? "true" : "false");
			fputs("            ", pfile->fp);
			xml_file_put_node(pfile->fp, node);
			xml_free_node(node);
		}

		fputs("        </condition-list>\n", pfile->fp);

		fputs("        <action-list>\n", pfile->fp);

		for (cur_action = rule->action_list; cur_action != NULL;
		     cur_action = cur_action->next) {
			FilterAction *action = (FilterAction *)cur_action->data;
			XMLNode *node = NULL;

			switch (action->type) {
			case FLT_ACTION_MOVE:
				NODE_NEW("move", action->str_value);
				break;
			case FLT_ACTION_COPY:
				NODE_NEW("copy", action->str_value);
				break;
			case FLT_ACTION_NOT_RECEIVE:
				NODE_NEW("not-receive", NULL);
				break;
			case FLT_ACTION_DELETE:
				NODE_NEW("delete", NULL);
				break;
			case FLT_ACTION_EXEC:
				NODE_NEW("exec", action->str_value);
				break;
			case FLT_ACTION_EXEC_ASYNC:
				NODE_NEW("exec-async", action->str_value);
				break;
			case FLT_ACTION_MARK:
				NODE_NEW("mark", NULL);
				break;
			case FLT_ACTION_COLOR_LABEL:
				NODE_NEW("color-label", action->str_value);
				break;
			case FLT_ACTION_MARK_READ:
				NODE_NEW("mark-as-read", NULL);
				break;
			case FLT_ACTION_FORWARD:
				NODE_NEW("forward", action->str_value);
				break;
			case FLT_ACTION_FORWARD_AS_ATTACHMENT:
				NODE_NEW("forward-as-attachment",
					 action->str_value);
				break;
			case FLT_ACTION_REDIRECT:
				NODE_NEW("redirect", action->str_value);
				break;
			case FLT_ACTION_STOP_EVAL:
				NODE_NEW("stop-eval", NULL);
				break;
			default:
				break;
			}

			if (node) {
				fputs("            ", pfile->fp);
				xml_file_put_node(pfile->fp, node);
				xml_free_node(node);
			}
		}

		fputs("        </action-list>\n", pfile->fp);

		fputs("    </rule>\n", pfile->fp);
	}

	fputs("</filter>\n", pfile->fp);

	if (prefs_file_close(pfile) < 0) {
		g_warning("failed to write filter configuration to file: %s\n",
			  file);
		return;
	}
}

void filter_write_config(void)
{
	gchar *rcpath;

	debug_print("Writing filter configuration...\n");

	rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, FILTER_LIST,
			     NULL);
	filter_write_file(prefs_common.fltlist, rcpath);
	g_free(rcpath);
}

#undef NODE_NEW
#undef ADD_ATTR

gchar *filter_get_str(FilterRule *rule)
{
	gchar *str;
	FilterCond *cond1, *cond2;
	FilterAction *action = NULL;
	GSList *cur;
	gint flag1 = 0, flag2 = 0;

	cond1 = (FilterCond *)rule->cond_list->data;
	if (rule->cond_list->next)
		cond2 = (FilterCond *)rule->cond_list->next->data;
	else
		cond2 = NULL;

	/* new -> old flag conversion */
	switch (cond1->match_type) {
	case FLT_CONTAIN:
	case FLT_EQUAL:
		flag1 = FLT_IS_NOT_MATCH(cond1->match_flag) ? 0 : FLT_O_CONTAIN;
		if (FLT_IS_CASE_SENS(cond1->match_flag))
			flag1 |= FLT_O_CASE_SENS;
		break;
	case FLT_REGEX:
		flag1 = FLT_O_REGEX; break;
	default:
		break;
	}
	if (cond2) {
		switch (cond2->match_type) {
		case FLT_CONTAIN:
		case FLT_EQUAL:
			flag2 = FLT_IS_NOT_MATCH(cond2->match_flag) ? 0 : FLT_O_CONTAIN;
			if (FLT_IS_CASE_SENS(cond2->match_flag))
				flag2 |= FLT_O_CASE_SENS;
			break;
		case FLT_REGEX:
			flag2 = FLT_O_REGEX; break;
		default:
			break;
		}
	} else
		flag2 = FLT_O_CONTAIN;

	for (cur = rule->action_list; cur != NULL; cur = cur->next) {
		action = (FilterAction *)cur->data;
		if (action->type == FLT_ACTION_MOVE ||
		    action->type == FLT_ACTION_NOT_RECEIVE ||
		    action->type == FLT_ACTION_DELETE)
			break;
	}

	str = g_strdup_printf
		("%s:%s:%c:%s:%s:%s:%d:%d:%c",
		 cond1->header_name, cond1->str_value ? cond1->str_value : "",
		 (cond2 && cond2->header_name) ?
		 	(rule->bool_op == FLT_AND ? '&' : '|') : ' ',
		 (cond2 && cond2->header_name) ? cond2->header_name : "",
		 (cond2 && cond2->str_value) ? cond2->str_value : "",
		 (action && action->str_value) ? action->str_value : "",
		 flag1, flag2,
		 (action && action->type == FLT_ACTION_MOVE) ? 'm' :
		 (action && action->type == FLT_ACTION_NOT_RECEIVE) ? 'n' :
		 (action && action->type == FLT_ACTION_DELETE) ? 'd' : ' ');

	return str;
}

#define PARSE_ONE_PARAM(p, srcp) \
{ \
	p = strchr(srcp, '\t'); \
	if (!p) return NULL; \
	else \
		*p++ = '\0'; \
}

FilterRule *filter_read_str(const gchar *str)
{
	FilterRule *rule;
	FilterBoolOp bool_op;
	gint flag;
	FilterCond *cond;
	FilterMatchType match_type;
	FilterMatchFlag match_flag;
	FilterAction *action;
	GSList *cond_list = NULL;
	GSList *action_list = NULL;
	gchar *tmp;
	gchar *rule_name;
	gchar *name1, *body1, *op, *name2, *body2, *dest;
	gchar *flag1 = NULL, *flag2 = NULL, *action1 = NULL;

	Xstrdup_a(tmp, str, return NULL);

	name1 = tmp;
	PARSE_ONE_PARAM(body1, name1);
	PARSE_ONE_PARAM(op, body1);
	PARSE_ONE_PARAM(name2, op);
	PARSE_ONE_PARAM(body2, name2);
	PARSE_ONE_PARAM(dest, body2);
	if (strchr(dest, '\t')) {
		gchar *p;

		PARSE_ONE_PARAM(flag1, dest);
		PARSE_ONE_PARAM(flag2, flag1);
		PARSE_ONE_PARAM(action1, flag2);
		if ((p = strchr(action1, '\t'))) *p = '\0';
	}

	bool_op = (*op == '&') ? FLT_AND : FLT_OR;

	if (*name1) {
		if (flag1)
			flag = (FilterOldFlag)strtoul(flag1, NULL, 10);
		else
			flag = FLT_O_CONTAIN;
		match_type = FLT_CONTAIN;
		match_flag = 0;
		if (flag & FLT_O_REGEX)
			match_type = FLT_REGEX;
		else if (!(flag & FLT_O_CONTAIN))
			match_flag = FLT_NOT_MATCH;
		if (flag & FLT_O_CASE_SENS)
			match_flag |= FLT_CASE_SENS;
		cond = filter_cond_new(FLT_COND_HEADER, match_type, match_flag,
				       name1, body1);
		cond_list = g_slist_append(cond_list, cond);
	}
	if (*name2) {
		if (flag2)
			flag = (FilterOldFlag)strtoul(flag2, NULL, 10);
		else
			flag = FLT_O_CONTAIN;
		match_type = FLT_CONTAIN;
		match_flag = 0;
		if (flag & FLT_O_REGEX)
			match_type = FLT_REGEX;
		else if (!(flag & FLT_O_CONTAIN))
			match_flag = FLT_NOT_MATCH;
		if (flag & FLT_O_CASE_SENS)
			match_flag |= FLT_CASE_SENS;
		cond = filter_cond_new(FLT_COND_HEADER, match_type, match_flag,
				       name2, body2);
		cond_list = g_slist_append(cond_list, cond);
	}

	action = filter_action_new(FLT_ACTION_MOVE,
				   *dest ? g_strdup(dest) : NULL);
	if (action1) {
		switch (*action1) {
		case 'm': action->type = FLT_ACTION_MOVE;		break;
		case 'n': action->type = FLT_ACTION_NOT_RECEIVE;	break;
		case 'd': action->type = FLT_ACTION_DELETE;		break;
		default:  g_warning("Invalid action: `%c'\n", *action1);
		}
	}
	action_list = g_slist_append(action_list, action);

	Xstrdup_a(rule_name, str, return NULL);
	subst_char(rule_name, '\t', ':');

	rule = filter_rule_new(rule_name, bool_op, cond_list, action_list);

	return rule;
}

FilterRule *filter_rule_new(const gchar *name, FilterBoolOp bool_op,
			    GSList *cond_list, GSList *action_list)
{
	FilterRule *rule;

	rule = g_new0(FilterRule, 1);
	rule->name = g_strdup(name);
	rule->bool_op = bool_op;
	rule->cond_list = cond_list;
	rule->action_list = action_list;
	rule->timing = FLT_TIMING_ANY;
	rule->enabled = TRUE;

	return rule;
}

FilterCond *filter_cond_new(FilterCondType type,
			    FilterMatchType match_type,
			    FilterMatchFlag match_flag,
			    const gchar *header, const gchar *value)
{
	FilterCond *cond;

	cond = g_new0(FilterCond, 1);
	cond->type = type;
	cond->match_type = match_type;
	cond->match_flag = match_flag;

	if (type == FLT_COND_HEADER)
		cond->header_name =
			(header && *header) ? g_strdup(header) : NULL;
	else
		cond->header_name = NULL;

	cond->str_value = (value && *value) ? g_strdup(value) : NULL;
	if (type == FLT_COND_SIZE_GREATER || type == FLT_COND_AGE_GREATER)
		cond->int_value = atoi(value);
	else
		cond->int_value = 0;

	if (match_type == FLT_REGEX)
		cond->match_func = strmatch_regex;
	else if (match_type == FLT_EQUAL) {
		if (FLT_IS_CASE_SENS(match_flag))
			cond->match_func = str_find_equal;
		else
			cond->match_func = str_case_find_equal;
	} else {
		if (FLT_IS_CASE_SENS(match_flag))
			cond->match_func = str_find;
		else
			cond->match_func = str_case_find;
	}

	return cond;
}

FilterAction *filter_action_new(FilterActionType type, const gchar *str)
{
	FilterAction *action;

	action = g_new0(FilterAction, 1);
	action->type = type;

	action->str_value = (str && *str) ? g_strdup(str) : NULL;
	if (type == FLT_ACTION_COLOR_LABEL && str)
		action->int_value = atoi(str);
	else
		action->int_value = 0;

	return action;
}

FilterInfo *filter_info_new(void)
{
	FilterInfo *fltinfo;

	fltinfo = g_new0(FilterInfo, 1);
	fltinfo->dest_list = NULL;
	fltinfo->move_dest = NULL;
	fltinfo->drop_done = FALSE;

	return fltinfo;
}

void filter_rule_rename_dest_path(FilterRule *rule, const gchar *old_path,
				  const gchar *new_path)
{
	FilterAction *action;
	GSList *cur;
	gchar *base;
	gchar *dest_path;
	gint oldpathlen;

	oldpathlen = strlen(old_path);

	for (cur = rule->action_list; cur != NULL; cur = cur->next) {
		action = (FilterAction *)cur->data;

		if (action->type != FLT_ACTION_MOVE &&
		    action->type != FLT_ACTION_COPY)
			continue;

		if (action->str_value &&
		    !strncmp(old_path, action->str_value, oldpathlen)) {
			base = action->str_value + oldpathlen;
			if (*base != G_DIR_SEPARATOR && *base != '\0')
				continue;
			while (*base == G_DIR_SEPARATOR) base++;
			if (*base == '\0')
				dest_path = g_strdup(new_path);
			else
				dest_path = g_strconcat(new_path,
							G_DIR_SEPARATOR_S,
							base, NULL);
			debug_print("filter_rule_rename_dest_path(): "
				    "renaming %s -> %s\n",
				    action->str_value, dest_path);
			g_free(action->str_value);
			action->str_value = dest_path;
		}
	}
}

void filter_rule_delete_action_by_dest_path(FilterRule *rule, const gchar *path)
{
	FilterAction *action;
	GSList *cur;
	GSList *next;
	gint pathlen;

	pathlen = strlen(path);

	for (cur = rule->action_list; cur != NULL; cur = next) {
		action = (FilterAction *)cur->data;
		next = cur->next;

		if (action->type != FLT_ACTION_MOVE &&
		    action->type != FLT_ACTION_COPY)
			continue;

		if (action->str_value &&
		    !strncmp(path, action->str_value, pathlen) &&
		    (action->str_value[pathlen] == G_DIR_SEPARATOR ||
		     action->str_value[pathlen] == '\0')) {
			debug_print("filter_rule_delete_action_by_dest_path(): "
				    "deleting %s\n", action->str_value);
			rule->action_list = g_slist_remove
				(rule->action_list, action);
			filter_action_free(action);
		}
	}
}

void filter_list_rename_path(const gchar *old_path, const gchar *new_path)
{
	GSList *cur;

	g_return_if_fail(old_path != NULL);
	g_return_if_fail(new_path != NULL);

	for (cur = prefs_common.fltlist; cur != NULL; cur = cur->next) {
		FilterRule *rule = (FilterRule *)cur->data;
		filter_rule_rename_dest_path(rule, old_path, new_path);
	}

	filter_write_config();
}

void filter_list_delete_path(const gchar *path)
{
	GSList *cur;
	GSList *next;

	g_return_if_fail(path != NULL);

	for (cur = prefs_common.fltlist; cur != NULL; cur = next) {
		FilterRule *rule = (FilterRule *)cur->data;
		next = cur->next;

		filter_rule_delete_action_by_dest_path(rule, path);
		if (!rule->action_list) {
			prefs_common.fltlist =
				g_slist_remove(prefs_common.fltlist, rule);
			filter_rule_free(rule);
		}
	}

	filter_write_config();
}

void filter_rule_match_type_str_to_enum(const gchar *match_type,
					FilterMatchType *type,
					FilterMatchFlag *flag)
{
	g_return_if_fail(match_type != NULL);

	*type = FLT_CONTAIN;
	*flag = 0;

	if (!strcmp(match_type, "contains")) {
		*type = FLT_CONTAIN;
	} else if (!strcmp(match_type, "not-contain")) {
		*type = FLT_CONTAIN;
		*flag = FLT_NOT_MATCH;
	} else if (!strcmp(match_type, "is")) {
		*type = FLT_EQUAL;
	} else if (!strcmp(match_type, "is-not")) {
		*type = FLT_EQUAL;
		*flag = FLT_NOT_MATCH;
	} else if (!strcmp(match_type, "regex")) {
		*type = FLT_REGEX;
	} else if (!strcmp(match_type, "not-regex")) {
		*type = FLT_REGEX;
		*flag = FLT_NOT_MATCH;
	} else if (!strcmp(match_type, "gt")) {
	} else if (!strcmp(match_type, "lt")) {
		*flag = FLT_NOT_MATCH;
	}
}

void filter_get_keyword_from_msg(MsgInfo *msginfo, gchar **header, gchar **key,
				 FilterCreateType type)
{
	static HeaderEntry hentry[] = {{"List-Id:",	   NULL, TRUE},
				       {"X-ML-Name:",	   NULL, TRUE},
				       {"X-List:",	   NULL, TRUE},
				       {"X-Mailing-list:", NULL, TRUE},
				       {"X-Sequence:",	   NULL, TRUE},
				       {NULL,		   NULL, FALSE}};
	enum
	{
		H_LIST_ID	 = 0,
		H_X_ML_NAME	 = 1,
		H_X_LIST	 = 2,
		H_X_MAILING_LIST = 3,
		H_X_SEQUENCE	 = 4
	};

	FILE *fp;

	g_return_if_fail(msginfo != NULL);
	g_return_if_fail(header != NULL);
	g_return_if_fail(key != NULL);

	*header = NULL;
	*key = NULL;

	switch (type) {
	case FLT_BY_NONE:
		return;
	case FLT_BY_AUTO:
		if ((fp = procmsg_open_message(msginfo)) == NULL)
			return;
		procheader_get_header_fields(fp, hentry);
		fclose(fp);

#define SET_FILTER_KEY(hstr, idx)	\
{					\
	*header = g_strdup(hstr);	\
	*key = hentry[idx].body;	\
	hentry[idx].body = NULL;	\
}

		if (hentry[H_LIST_ID].body != NULL) {
			SET_FILTER_KEY("List-Id", H_LIST_ID);
			extract_list_id_str(*key);
		} else if (hentry[H_X_ML_NAME].body != NULL) {
			SET_FILTER_KEY("X-ML-Name", H_X_ML_NAME);
		} else if (hentry[H_X_LIST].body != NULL) {
			SET_FILTER_KEY("X-List", H_X_LIST);
		} else if (hentry[H_X_MAILING_LIST].body != NULL) {
			SET_FILTER_KEY("X-Mailing-list", H_X_MAILING_LIST);
		} else if (hentry[H_X_SEQUENCE].body != NULL) {
			gchar *p;

			SET_FILTER_KEY("X-Sequence", H_X_SEQUENCE);
			p = *key;
			while (*p != '\0') {
				while (*p != '\0' && !g_ascii_isspace(*p)) p++;
				while (g_ascii_isspace(*p)) p++;
				if (g_ascii_isdigit(*p)) {
					*p = '\0';
					break;
				}
			}
			g_strstrip(*key);
		} else if (msginfo->subject) {
			*header = g_strdup("Subject");
			*key = g_strdup(msginfo->subject);
		}

#undef SET_FILTER_KEY

		g_free(hentry[H_LIST_ID].body);
		hentry[H_LIST_ID].body = NULL;
		g_free(hentry[H_X_ML_NAME].body);
		hentry[H_X_ML_NAME].body = NULL;
		g_free(hentry[H_X_LIST].body);
		hentry[H_X_LIST].body = NULL;
		g_free(hentry[H_X_MAILING_LIST].body);
		hentry[H_X_MAILING_LIST].body = NULL;

		break;
	case FLT_BY_FROM:
		*header = g_strdup("From");
		*key = g_strdup(msginfo->from);
		break;
	case FLT_BY_TO:
		*header = g_strdup("To");
		*key = g_strdup(msginfo->to);
		break;
	case FLT_BY_SUBJECT:
		*header = g_strdup("Subject");
		*key = g_strdup(msginfo->subject);
		break;
	default:
		break;
	}
}

void filter_rule_list_free(GSList *fltlist)
{
	GSList *cur;

	for (cur = fltlist; cur != NULL; cur = cur->next)
		filter_rule_free((FilterRule *)cur->data);
	g_slist_free(fltlist);
}

void filter_rule_free(FilterRule *rule)
{
	if (!rule) return;

	g_free(rule->name);
	g_free(rule->target_folder);

	filter_cond_list_free(rule->cond_list);
	filter_action_list_free(rule->action_list);

	g_free(rule);
}

void filter_cond_list_free(GSList *cond_list)
{
	GSList *cur;

	for (cur = cond_list; cur != NULL; cur = cur->next)
		filter_cond_free((FilterCond *)cur->data);
	g_slist_free(cond_list);
}

void filter_action_list_free(GSList *action_list)
{
	GSList *cur;

	for (cur = action_list; cur != NULL; cur = cur->next)
		filter_action_free((FilterAction *)cur->data);
	g_slist_free(action_list);
}

static void filter_cond_free(FilterCond *cond)
{
	g_free(cond->header_name);
	g_free(cond->str_value);
	g_free(cond);
}

static void filter_action_free(FilterAction *action)
{
	g_free(action->str_value);
	g_free(action);
}

void filter_info_free(FilterInfo *fltinfo)
{
	g_slist_free(fltinfo->dest_list);
	g_free(fltinfo);
}
