/*
 * Copyright 2023 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include <contrib/libucl/ucl.h>
#include "config.h"
#include "util.h"
#include "cfg_file.h"
#include "rspamd.h"
#include "message.h"
#include "mime_expressions.h"
#include "libserver/html/html.h"
#include "lua/lua_common.h"
#include "utlist.h"

gboolean rspamd_compare_encoding(struct rspamd_task *task,
								 GArray *args,
								 void *unused);
gboolean rspamd_header_exists(struct rspamd_task *task,
							  GArray *args,
							  void *unused);
gboolean rspamd_parts_distance(struct rspamd_task *task,
							   GArray *args,
							   void *unused);
gboolean rspamd_recipients_distance(struct rspamd_task *task,
									GArray *args,
									void *unused);
gboolean rspamd_has_only_html_part(struct rspamd_task *task,
								   GArray *args,
								   void *unused);
gboolean rspamd_is_recipients_sorted(struct rspamd_task *task,
									 GArray *args,
									 void *unused);
gboolean rspamd_compare_transfer_encoding(struct rspamd_task *task,
										  GArray *args,
										  void *unused);
gboolean rspamd_is_html_balanced(struct rspamd_task *task,
								 GArray *args,
								 void *unused);
gboolean rspamd_has_html_tag(struct rspamd_task *task,
							 GArray *args,
							 void *unused);
gboolean rspamd_has_fake_html(struct rspamd_task *task,
							  GArray *args,
							  void *unused);
static gboolean rspamd_raw_header_exists(struct rspamd_task *task,
										 GArray *args,
										 void *unused);
static gboolean rspamd_check_smtp_data(struct rspamd_task *task,
									   GArray *args,
									   void *unused);
static gboolean rspamd_content_type_is_type(struct rspamd_task *task,
											GArray *args,
											void *unused);
static gboolean rspamd_content_type_is_subtype(struct rspamd_task *task,
											   GArray *args,
											   void *unused);
static gboolean rspamd_content_type_has_param(struct rspamd_task *task,
											  GArray *args,
											  void *unused);
static gboolean rspamd_content_type_compare_param(struct rspamd_task *task,
												  GArray *args,
												  void *unused);
static gboolean rspamd_has_content_part(struct rspamd_task *task,
										GArray *args,
										void *unused);
static gboolean rspamd_has_content_part_len(struct rspamd_task *task,
											GArray *args,
											void *unused);
static gboolean rspamd_is_empty_body(struct rspamd_task *task,
									 GArray *args,
									 void *unused);
static gboolean rspamd_has_flag_expr(struct rspamd_task *task,
									 GArray *args,
									 void *unused);
static gboolean rspamd_has_symbol_expr(struct rspamd_task *task,
									   GArray *args,
									   void *unused);

static rspamd_expression_atom_t *rspamd_mime_expr_parse(const char *line, gsize len,
														rspamd_mempool_t *pool, gpointer ud, GError **err);
static double rspamd_mime_expr_process(void *ud, rspamd_expression_atom_t *atom);
static int rspamd_mime_expr_priority(rspamd_expression_atom_t *atom);
static void rspamd_mime_expr_destroy(rspamd_expression_atom_t *atom);

/**
 * Regexp structure
 */
struct rspamd_regexp_atom {
	enum rspamd_re_type type; /**< regexp type										*/
	char *regexp_text;        /**< regexp text representation							*/
	rspamd_regexp_t *regexp;  /**< regexp structure									*/
	union {
		const char *header;   /**< header name for header regexps						*/
		const char *selector; /**< selector name for lua selector regexp				*/
	} extra;
	gboolean is_test;     /**< true if this expression must be tested				*/
	gboolean is_strong;   /**< true if headers search must be case sensitive		*/
	gboolean is_multiple; /**< true if we need to match all inclusions of atom	*/
};

/**
 * Rspamd expression function
 */
struct rspamd_function_atom {
	char *name;   /**< name of function								*/
	GArray *args; /**< its args										*/
};

enum rspamd_mime_atom_type {
	MIME_ATOM_REGEXP = 0,
	MIME_ATOM_INTERNAL_FUNCTION,
	MIME_ATOM_LUA_FUNCTION,
	MIME_ATOM_LOCAL_LUA_FUNCTION, /* New style */
};

struct rspamd_mime_atom {
	char *str;
	union {
		struct rspamd_regexp_atom *re;
		struct rspamd_function_atom *func;
		const char *lua_function;
		int lua_cbref;
	} d;
	enum rspamd_mime_atom_type type;
};

/*
 * List of internal functions of rspamd
 * Sorted by name to use bsearch
 */
static struct _fl {
	const char *name;
	rspamd_internal_func_t func;
	void *user_data;
} rspamd_functions_list[] = {
	{"check_smtp_data", rspamd_check_smtp_data, NULL},
	{"compare_encoding", rspamd_compare_encoding, NULL},
	{"compare_parts_distance", rspamd_parts_distance, NULL},
	{"compare_recipients_distance", rspamd_recipients_distance, NULL},
	{"compare_transfer_encoding", rspamd_compare_transfer_encoding, NULL},
	{"content_type_compare_param", rspamd_content_type_compare_param, NULL},
	{"content_type_has_param", rspamd_content_type_has_param, NULL},
	{"content_type_is_subtype", rspamd_content_type_is_subtype, NULL},
	{"content_type_is_type", rspamd_content_type_is_type, NULL},
	{"has_content_part", rspamd_has_content_part, NULL},
	{"has_content_part_len", rspamd_has_content_part_len, NULL},
	{"has_fake_html", rspamd_has_fake_html, NULL},
	{"has_flag", rspamd_has_flag_expr, NULL},
	{"has_html_tag", rspamd_has_html_tag, NULL},
	{"has_only_html_part", rspamd_has_only_html_part, NULL},
	{"has_symbol", rspamd_has_symbol_expr, NULL},
	{"header_exists", rspamd_header_exists, NULL},
	{"is_empty_body", rspamd_is_empty_body, NULL},
	{"is_html_balanced", rspamd_is_html_balanced, NULL},
	{"is_recipients_sorted", rspamd_is_recipients_sorted, NULL},
	{"raw_header_exists", rspamd_raw_header_exists, NULL},
};

const struct rspamd_atom_subr mime_expr_subr = {
	.parse = rspamd_mime_expr_parse,
	.process = rspamd_mime_expr_process,
	.priority = rspamd_mime_expr_priority,
	.destroy = rspamd_mime_expr_destroy};

static struct _fl *list_ptr = &rspamd_functions_list[0];
static uint32_t functions_number = sizeof(rspamd_functions_list) /
								   sizeof(struct _fl);
static gboolean list_allocated = FALSE;

/* Bsearch routine */
static int
fl_cmp(const void *s1, const void *s2)
{
	struct _fl *fl1 = (struct _fl *) s1;
	struct _fl *fl2 = (struct _fl *) s2;
	return strcmp(fl1->name, fl2->name);
}

static GQuark
rspamd_mime_expr_quark(void)
{
	return g_quark_from_static_string("mime-expressions");
}

#define TYPE_CHECK(str, type, len) (sizeof(type) - 1 == (len) && rspamd_lc_cmp((str), (type), (len)) == 0)
static gboolean
rspamd_parse_long_option(const char *start, gsize len,
						 struct rspamd_regexp_atom *a)
{
	gboolean ret = FALSE;

	if (TYPE_CHECK(start, "body", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_BODY;
	}
	else if (TYPE_CHECK(start, "part", len) ||
			 TYPE_CHECK(start, "mime", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_MIME;
	}
	else if (TYPE_CHECK(start, "raw_part", len) ||
			 TYPE_CHECK(start, "raw_mime", len) ||
			 TYPE_CHECK(start, "mime_raw", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_RAWMIME;
	}
	else if (TYPE_CHECK(start, "header", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_HEADER;
	}
	else if (TYPE_CHECK(start, "mime_header", len) ||
			 TYPE_CHECK(start, "header_mime", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_MIMEHEADER;
	}
	else if (TYPE_CHECK(start, "raw_header", len) ||
			 TYPE_CHECK(start, "header_raw", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_RAWHEADER;
	}
	else if (TYPE_CHECK(start, "all_header", len) ||
			 TYPE_CHECK(start, "header_all", len) ||
			 TYPE_CHECK(start, "all_headers", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_ALLHEADER;
	}
	else if (TYPE_CHECK(start, "url", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_URL;
	}
	else if (TYPE_CHECK(start, "email", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_EMAIL;
	}
	else if (TYPE_CHECK(start, "sa_body", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_SABODY;
	}
	else if (TYPE_CHECK(start, "sa_raw_body", len) ||
			 TYPE_CHECK(start, "sa_body_raw", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_SARAWBODY;
	}
	else if (TYPE_CHECK(start, "words", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_WORDS;
	}
	else if (TYPE_CHECK(start, "raw_words", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_RAWWORDS;
	}
	else if (TYPE_CHECK(start, "stem_words", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_STEMWORDS;
	}
	else if (TYPE_CHECK(start, "selector", len)) {
		ret = TRUE;
		a->type = RSPAMD_RE_SELECTOR;
	}

	return ret;
}

/*
 * Rspamd regexp utility functions
 */
static struct rspamd_regexp_atom *
rspamd_mime_expr_parse_regexp_atom(rspamd_mempool_t *pool, const char *line,
								   struct rspamd_config *cfg)
{
	const char *begin, *end, *p, *src, *start, *brace;
	char *dbegin, *dend, *extra = NULL;
	struct rspamd_regexp_atom *result;
	GError *err = NULL;
	GString *re_flags;

	if (line == NULL) {
		msg_err_pool("cannot parse NULL line");
		return NULL;
	}

	src = line;
	result = rspamd_mempool_alloc0(pool, sizeof(struct rspamd_regexp_atom));
	/* Skip whitespaces */
	while (g_ascii_isspace(*line)) {
		line++;
	}
	if (*line == '\0') {
		msg_warn_pool("got empty regexp");
		return NULL;
	}

	result->type = RSPAMD_RE_MAX;

	start = line;
	/* First try to find header name */
	begin = strchr(line, '/');
	if (begin != NULL) {
		p = begin;
		end = NULL;
		while (p != line) {
			if (*p == '=') {
				end = p;
				break;
			}
			p--;
		}

		if (end) {
			extra = rspamd_mempool_alloc(pool, end - line + 1);
			rspamd_strlcpy(extra, line, end - line + 1);
			line = end;
		}
	}
	else {
		extra = rspamd_mempool_strdup(pool, line);
		result->type = RSPAMD_RE_MAX;
		line = start;
	}
	/* Find begin of regexp */
	while (*line && *line != '/') {
		line++;
	}
	if (*line != '\0') {
		begin = line + 1;
	}
	else if (extra == NULL) {
		/* Assume that line without // is just a header name */
		extra = rspamd_mempool_strdup(pool, line);
		result->type = RSPAMD_RE_HEADER;
		return result;
	}
	else {
		/* We got header name earlier but have not found // expression, so it is invalid regexp */
		msg_warn_pool(
			"got no header name (eg. header=) but without corresponding regexp, %s",
			src);
		return NULL;
	}
	/* Find end */
	end = begin;
	while (*end && (*end != '/' || *(end - 1) == '\\')) {
		end++;
	}
	if (end == begin || *end != '/') {
		msg_warn_pool("no trailing / in regexp %s", src);
		return NULL;
	}
	/* Parse flags */
	p = end + 1;
	re_flags = g_string_sized_new(32);

	while (p != NULL) {
		switch (*p) {
		case 'i':
		case 'm':
		case 's':
		case 'x':
		case 'u':
		case 'O':
		case 'r':
		case 'L':
			/* Handled by rspamd_regexp_t */
			g_string_append_c(re_flags, *p);
			p++;
			break;
		case 'o':
			p++;
			break;
		/* Type flags */
		case 'H':
			result->type = RSPAMD_RE_HEADER;
			p++;
			break;
		case 'R':
			result->type = RSPAMD_RE_ALLHEADER;
			p++;
			break;
		case 'B':
			result->type = RSPAMD_RE_MIMEHEADER;
			p++;
			break;
		case 'C':
			result->type = RSPAMD_RE_SABODY;
			p++;
			break;
		case 'D':
			result->type = RSPAMD_RE_SARAWBODY;
			p++;
			break;
		case 'M':
			result->type = RSPAMD_RE_BODY;
			p++;
			break;
		case 'P':
			result->type = RSPAMD_RE_MIME;
			p++;
			break;
		case 'Q':
			result->type = RSPAMD_RE_RAWMIME;
			p++;
			break;
		case 'U':
			result->type = RSPAMD_RE_URL;
			p++;
			break;
		case 'X':
			result->type = RSPAMD_RE_RAWHEADER;
			p++;
			break;
		case '$':
			result->type = RSPAMD_RE_SELECTOR;
			p++;
			break;
		case '{':
			/* Long definition */
			if ((brace = strchr(p + 1, '}')) != NULL) {
				if (!rspamd_parse_long_option(p + 1, brace - (p + 1), result)) {
					msg_warn_pool("invalid long regexp type: %*s in '%s'",
								  (int) (brace - (p + 1)), p + 1, src);
					p = NULL;
				}
				else {
					p = brace + 1;
				}
			}
			else {
				p = NULL;
			}
			break;
		/* Other flags */
		case 'T':
			result->is_test = TRUE;
			p++;
			break;
		case 'S':
			result->is_strong = TRUE;
			p++;
			break;
		case 'A':
			result->is_multiple = TRUE;
			p++;
			break;
		/* Stop flags parsing */
		default:
			p = NULL;
			break;
		}
	}

	if (result->type >= RSPAMD_RE_MAX) {
		if (extra) {
			/* Assume header regexp */
			result->extra.header = extra;
			result->type = RSPAMD_RE_HEADER;
		}
		else {
			msg_err_pool("could not read regexp: %s, unknown type", src);
			return NULL;
		}
	}

	if ((result->type == RSPAMD_RE_HEADER ||
		 result->type == RSPAMD_RE_RAWHEADER ||
		 result->type == RSPAMD_RE_MIMEHEADER)) {
		if (extra == NULL) {
			msg_err_pool("header regexp: '%s' has no header part", src);
			return NULL;
		}
		else {
			result->extra.header = extra;
		}
	}

	if (result->type == RSPAMD_RE_SELECTOR) {
		if (extra == NULL) {
			msg_err_pool("selector regexp: '%s' has no selector part", src);
			return NULL;
		}
		else {
			result->extra.selector = extra;
		}
	}


	result->regexp_text = rspamd_mempool_strdup(pool, start);
	dbegin = result->regexp_text + (begin - start);
	dend = result->regexp_text + (end - start);
	*dend = '\0';

	result->regexp = rspamd_regexp_new(dbegin, re_flags->str,
									   &err);

	g_string_free(re_flags, TRUE);

	if (result->regexp == NULL || err != NULL) {
		msg_warn_pool("could not read regexp: %s while reading regexp %e",
					  src, err);

		if (err) {
			g_error_free(err);
		}

		return NULL;
	}

	if (result->is_multiple) {
		rspamd_regexp_set_maxhits(result->regexp, 0);
	}
	else {
		rspamd_regexp_set_maxhits(result->regexp, 1);
	}

	rspamd_regexp_set_ud(result->regexp, result);

	*dend = '/';

	return result;
}

struct rspamd_function_atom *
rspamd_mime_expr_parse_function_atom(rspamd_mempool_t *pool, const char *input)
{
	const char *obrace, *ebrace, *p, *c;
	char t, *databuf;
	unsigned int len;
	struct rspamd_function_atom *res;
	struct expression_argument arg;
	GError *err = NULL;
	enum {
		start_read_argument = 0,
		in_string,
		in_regexp,
		got_backslash,
		got_comma
	} state,
		prev_state = 0;

	obrace = strchr(input, '(');
	ebrace = strrchr(input, ')');

	g_assert(obrace != NULL && ebrace != NULL);

	res = rspamd_mempool_alloc0(pool, sizeof(*res));
	res->name = rspamd_mempool_alloc(pool, obrace - input + 1);
	rspamd_strlcpy(res->name, input, obrace - input + 1);
	res->args = g_array_new(FALSE, FALSE, sizeof(struct expression_argument));

	p = obrace + 1;
	c = p;
	state = start_read_argument;

	/* Read arguments */
	while (p <= ebrace) {
		t = *p;
		switch (state) {
		case start_read_argument:
			if (t == '/') {
				state = in_regexp;
				c = p;
			}
			else if (!g_ascii_isspace(t)) {
				state = in_string;

				if (t == '\'' || t == '\"') {
					c = p + 1;
				}
				else {
					c = p;
				}
			}
			p++;
			break;
		case in_regexp:
			if (t == '\\') {
				state = got_backslash;
				prev_state = in_regexp;
			}
			else if (t == ',' || p == ebrace) {
				len = p - c + 1;
				databuf = rspamd_mempool_alloc(pool, len);
				rspamd_strlcpy(databuf, c, len);
				arg.type = EXPRESSION_ARGUMENT_REGEXP;
				arg.data = rspamd_regexp_cache_create(NULL, databuf, NULL, &err);

				if (arg.data == NULL) {
					/* Fallback to string */
					msg_warn("cannot parse slashed argument %s as regexp: %s",
							 databuf, err->message);
					g_error_free(err);
					arg.type = EXPRESSION_ARGUMENT_NORMAL;
					arg.data = databuf;
				}

				g_array_append_val(res->args, arg);
				state = got_comma;
			}
			p++;
			break;
		case in_string:
			if (t == '\\') {
				state = got_backslash;
				prev_state = in_string;
			}
			else if (t == ',' || p == ebrace) {
				if (*(p - 1) == '\'' || *(p - 1) == '\"') {
					len = p - c;
				}
				else {
					len = p - c + 1;
				}

				databuf = rspamd_mempool_alloc(pool, len);
				rspamd_strlcpy(databuf, c, len);
				arg.type = EXPRESSION_ARGUMENT_NORMAL;
				arg.data = databuf;
				g_array_append_val(res->args, arg);
				state = got_comma;
			}
			p++;
			break;
		case got_backslash:
			state = prev_state;
			p++;
			break;
		case got_comma:
			state = start_read_argument;
			break;
		}
	}

	return res;
}

static rspamd_expression_atom_t *
rspamd_mime_expr_parse(const char *line, gsize len,
					   rspamd_mempool_t *pool, gpointer ud, GError **err)
{
	rspamd_expression_atom_t *a = NULL;
	struct rspamd_mime_atom *mime_atom = NULL;
	const char *p, *end, *c = NULL;
	struct rspamd_mime_expr_ud *real_ud = (struct rspamd_mime_expr_ud *) ud;
	struct rspamd_config *cfg;
	rspamd_regexp_t *own_re;
	char t;
	int type = MIME_ATOM_REGEXP, obraces = 0, ebraces = 0;
	enum {
		in_header = 0,
		got_slash,
		in_regexp,
		got_backslash,
		got_second_slash,
		in_flags,
		in_flags_brace,
		got_obrace,
		in_function,
		in_local_function,
		got_ebrace,
		end_atom,
		bad_atom
	} state = 0,
	  prev_state = 0;

	p = line;
	end = p + len;
	cfg = real_ud->cfg;

	while (p < end) {
		t = *p;

		switch (state) {
		case in_header:
			if (t == '/') {
				/* Regexp */
				state = got_slash;
			}
			else if (t == '(') {
				/* Function */
				state = got_obrace;
			}
			else if (!g_ascii_isalnum(t) && t != '_' && t != '-' && t != '=') {
				if (t == ':') {
					if (p - line == 3 && memcmp(line, "lua", 3) == 0) {
						type = MIME_ATOM_LOCAL_LUA_FUNCTION;
						state = in_local_function;
						c = p + 1;
					}
				}
				else {
					/* Likely lua function, identified by just a string */
					type = MIME_ATOM_LUA_FUNCTION;
					state = end_atom;
					/* Do not increase p */
					continue;
				}
			}
			else if (g_ascii_isspace(t)) {
				state = bad_atom;
			}
			p++;
			break;
		case got_slash:
			state = in_regexp;
			break;
		case in_regexp:
			if (t == '\\') {
				state = got_backslash;
				prev_state = in_regexp;
			}
			else if (t == '/') {
				state = got_second_slash;
			}
			p++;
			break;
		case got_second_slash:
			state = in_flags;
			break;
		case in_flags:
			if (t == '{') {
				state = in_flags_brace;
				p++;
			}
			else if (!g_ascii_isalpha(t) && t != '$') {
				state = end_atom;
			}
			else {
				p++;
			}
			break;
		case in_flags_brace:
			if (t == '}') {
				state = in_flags;
			}
			p++;
			break;
		case got_backslash:
			state = prev_state;
			p++;
			break;
		case got_obrace:
			state = in_function;
			type = MIME_ATOM_INTERNAL_FUNCTION;
			obraces++;
			break;
		case in_function:
			if (t == '\\') {
				state = got_backslash;
				prev_state = in_function;
			}
			else if (t == '(') {
				obraces++;
			}
			else if (t == ')') {
				ebraces++;
				if (ebraces == obraces) {
					state = got_ebrace;
				}
			}
			p++;
			break;
		case in_local_function:
			if (!(g_ascii_isalnum(t) || t == '-' || t == '_')) {
				g_assert(c != NULL);
				state = end_atom;
			}
			else {
				p++;
			}
			break;
		case got_ebrace:
			state = end_atom;
			break;
		case bad_atom:
			g_set_error(err, rspamd_mime_expr_quark(), 100, "cannot parse"
															" mime atom '%s' when reading symbol '%c' at offset %d, "
															"near %.*s",
						line, t, (int) (p - line),
						(int) MIN(end - p, 10), p);
			return NULL;
		case end_atom:
			goto set;
		}
	}
set:

	if (p - line == 0 || (state != got_ebrace && state != got_second_slash &&
						  state != in_flags && state != end_atom)) {
		g_set_error(err, rspamd_mime_expr_quark(), 200, "incomplete or empty"
														" mime atom");
		return NULL;
	}

	mime_atom = rspamd_mempool_alloc(pool, sizeof(*mime_atom));
	mime_atom->type = type;
	mime_atom->str = rspamd_mempool_alloc(pool, p - line + 1);
	rspamd_strlcpy(mime_atom->str, line, p - line + 1);

	if (type == MIME_ATOM_REGEXP) {
		mime_atom->d.re = rspamd_mime_expr_parse_regexp_atom(pool,
															 mime_atom->str, cfg);
		if (mime_atom->d.re == NULL) {
			g_set_error(err, rspamd_mime_expr_quark(), 200,
						"cannot parse regexp '%s'",
						mime_atom->str);
			goto err;
		}
		else {
			int lua_cbref = -1;

			/* Check regexp condition */
			if (real_ud->conf_obj != NULL) {
				const ucl_object_t *re_conditions = ucl_object_lookup(real_ud->conf_obj,
																	  "re_conditions");

				if (re_conditions != NULL) {
					if (ucl_object_type(re_conditions) != UCL_OBJECT) {
						g_set_error(err, rspamd_mime_expr_quark(), 320,
									"re_conditions is not a table for '%s'",
									mime_atom->str);
						rspamd_regexp_unref(mime_atom->d.re->regexp);
						goto err;
					}

					const ucl_object_t *function_obj = ucl_object_lookup(re_conditions,
																		 mime_atom->str);

					if (function_obj != NULL) {
						if (ucl_object_type(function_obj) != UCL_USERDATA) {
							g_set_error(err, rspamd_mime_expr_quark(), 320,
										"condition for '%s' is invalid, must be function",
										mime_atom->str);
							rspamd_regexp_unref(mime_atom->d.re->regexp);
							goto err;
						}

						struct ucl_lua_funcdata *fd = function_obj->value.ud;

						lua_cbref = fd->idx;
					}
				}
			}

			if (lua_cbref != -1) {
				msg_info_config("added condition for regexp %s", mime_atom->str);
				/* Add SOM_LEFTMOST_FLAG implicitly */
				rspamd_regexp_set_flags(mime_atom->d.re->regexp, rspamd_regexp_get_flags(mime_atom->d.re->regexp) |
																	 RSPAMD_REGEXP_FLAG_LEFTMOST);
			}

			/* Register new item in the cache */
			if (mime_atom->d.re->type == RSPAMD_RE_HEADER ||
				mime_atom->d.re->type == RSPAMD_RE_RAWHEADER ||
				mime_atom->d.re->type == RSPAMD_RE_MIMEHEADER) {

				if (mime_atom->d.re->extra.header != NULL) {
					own_re = mime_atom->d.re->regexp;
					mime_atom->d.re->regexp = rspamd_re_cache_add(cfg->re_cache,
																  mime_atom->d.re->regexp,
																  mime_atom->d.re->type,
																  mime_atom->d.re->extra.header,
																  strlen(mime_atom->d.re->extra.header) + 1,
																  lua_cbref);
					/* Pass ownership to the cache */
					rspamd_regexp_unref(own_re);
				}
				else {
					/* We have header regexp, but no header name is detected */
					g_set_error(err,
								rspamd_mime_expr_quark(),
								200,
								"no header name in header regexp: '%s'",
								mime_atom->str);
					rspamd_regexp_unref(mime_atom->d.re->regexp);
					goto err;
				}
			}
			else if (mime_atom->d.re->type == RSPAMD_RE_SELECTOR) {
				if (mime_atom->d.re->extra.selector != NULL) {
					own_re = mime_atom->d.re->regexp;
					mime_atom->d.re->regexp = rspamd_re_cache_add(cfg->re_cache,
																  mime_atom->d.re->regexp,
																  mime_atom->d.re->type,
																  mime_atom->d.re->extra.selector,
																  strlen(mime_atom->d.re->extra.selector) + 1,
																  lua_cbref);
					/* Pass ownership to the cache */
					rspamd_regexp_unref(own_re);
				}
				else {
					/* We have selector regexp, but no selector name is detected */
					g_set_error(err,
								rspamd_mime_expr_quark(),
								200,
								"no selector name in selector regexp: '%s'",
								mime_atom->str);
					rspamd_regexp_unref(mime_atom->d.re->regexp);
					goto err;
				}
			}
			else {
				own_re = mime_atom->d.re->regexp;
				mime_atom->d.re->regexp = rspamd_re_cache_add(cfg->re_cache,
															  mime_atom->d.re->regexp,
															  mime_atom->d.re->type,
															  NULL,
															  0,
															  lua_cbref);
				/* Pass ownership to the cache */
				rspamd_regexp_unref(own_re);
			}
		}
	}
	else if (type == MIME_ATOM_LUA_FUNCTION) {
		mime_atom->d.lua_function = mime_atom->str;

		lua_getglobal(cfg->lua_state, mime_atom->str);

		if (lua_type(cfg->lua_state, -1) != LUA_TFUNCTION) {
			g_set_error(err, rspamd_mime_expr_quark(), 200,
						"no such lua function '%s'",
						mime_atom->str);
			lua_pop(cfg->lua_state, 1);

			goto err;
		}

		lua_pop(cfg->lua_state, 1);
	}
	else if (type == MIME_ATOM_LOCAL_LUA_FUNCTION) {
		/* p pointer is set to the start of Lua function name */

		if (real_ud->conf_obj == NULL) {
			g_set_error(err, rspamd_mime_expr_quark(), 300,
						"no config object for '%s'",
						mime_atom->str);
			goto err;
		}

		const ucl_object_t *functions = ucl_object_lookup(real_ud->conf_obj,
														  "functions");

		if (functions == NULL) {
			g_set_error(err, rspamd_mime_expr_quark(), 310,
						"no functions defined for '%s'",
						mime_atom->str);
			goto err;
		}

		if (ucl_object_type(functions) != UCL_OBJECT) {
			g_set_error(err, rspamd_mime_expr_quark(), 320,
						"functions is not a table for '%s'",
						mime_atom->str);
			goto err;
		}

		const ucl_object_t *function_obj;

		function_obj = ucl_object_lookup_len(functions, c,
											 p - c);

		if (function_obj == NULL) {
			g_set_error(err, rspamd_mime_expr_quark(), 320,
						"function %.*s is not found for '%s'",
						(int) (p - c), c, mime_atom->str);
			goto err;
		}

		if (ucl_object_type(function_obj) != UCL_USERDATA) {
			g_set_error(err, rspamd_mime_expr_quark(), 320,
						"function %.*s has invalid type for '%s'",
						(int) (p - c), c, mime_atom->str);
			goto err;
		}

		struct ucl_lua_funcdata *fd = function_obj->value.ud;

		mime_atom->d.lua_cbref = fd->idx;
	}
	else {
		mime_atom->d.func = rspamd_mime_expr_parse_function_atom(pool,
																 mime_atom->str);
		if (mime_atom->d.func == NULL) {
			g_set_error(err, rspamd_mime_expr_quark(), 200,
						"cannot parse function '%s'",
						mime_atom->str);
			goto err;
		}
	}

	a = rspamd_mempool_alloc0(pool, sizeof(*a));
	a->len = p - line;
	a->priority = 0;
	a->data = mime_atom;

	return a;

err:

	return NULL;
}

static int
rspamd_mime_expr_process_regexp(struct rspamd_regexp_atom *re,
								struct rspamd_task *task)
{
	int ret;

	if (re == NULL) {
		msg_info_task("invalid regexp passed");
		return 0;
	}

	if (re->type == RSPAMD_RE_HEADER || re->type == RSPAMD_RE_RAWHEADER) {
		ret = rspamd_re_cache_process(task,
									  re->regexp,
									  re->type,
									  re->extra.header,
									  strlen(re->extra.header),
									  re->is_strong);
	}
	else if (re->type == RSPAMD_RE_SELECTOR) {
		ret = rspamd_re_cache_process(task,
									  re->regexp,
									  re->type,
									  re->extra.selector,
									  strlen(re->extra.selector),
									  re->is_strong);
	}
	else {
		ret = rspamd_re_cache_process(task,
									  re->regexp,
									  re->type,
									  NULL,
									  0,
									  re->is_strong);
	}

	if (re->is_test) {
		msg_info_task("test %s regexp '%s' returned %d",
					  rspamd_re_cache_type_to_string(re->type),
					  re->regexp_text, ret);
	}

	return ret;
}


static int
rspamd_mime_expr_priority(rspamd_expression_atom_t *atom)
{
	struct rspamd_mime_atom *mime_atom = atom->data;
	int ret = 0;

	switch (mime_atom->type) {
	case MIME_ATOM_INTERNAL_FUNCTION:
		/* Prioritize internal functions slightly */
		ret = RSPAMD_EXPRESSION_MAX_PRIORITY - RSPAMD_EXPRESSION_MAX_PRIORITY / 8;
		break;
	case MIME_ATOM_LUA_FUNCTION:
	case MIME_ATOM_LOCAL_LUA_FUNCTION:
		ret = RSPAMD_EXPRESSION_MAX_PRIORITY - RSPAMD_EXPRESSION_MAX_PRIORITY / 4;
		break;
	case MIME_ATOM_REGEXP:
		switch (mime_atom->d.re->type) {
		case RSPAMD_RE_HEADER:
		case RSPAMD_RE_RAWHEADER:
			ret = RSPAMD_EXPRESSION_MAX_PRIORITY - RSPAMD_EXPRESSION_MAX_PRIORITY / 16;
			break;
		case RSPAMD_RE_URL:
		case RSPAMD_RE_EMAIL:
			ret = RSPAMD_EXPRESSION_MAX_PRIORITY - RSPAMD_EXPRESSION_MAX_PRIORITY / 8;
			break;
		case RSPAMD_RE_SELECTOR:
			ret = RSPAMD_EXPRESSION_MAX_PRIORITY - RSPAMD_EXPRESSION_MAX_PRIORITY / 8;
			break;
		case RSPAMD_RE_MIME:
		case RSPAMD_RE_RAWMIME:
			ret = RSPAMD_EXPRESSION_MAX_PRIORITY - RSPAMD_EXPRESSION_MAX_PRIORITY / 2;
			break;
		case RSPAMD_RE_WORDS:
		case RSPAMD_RE_RAWWORDS:
		case RSPAMD_RE_STEMWORDS:
		default:
			/* For expensive regexps */
			ret = 0;
			break;
		}
	}

	return ret;
}

static void
rspamd_mime_expr_destroy(rspamd_expression_atom_t *atom)
{
	struct rspamd_mime_atom *mime_atom = atom->data;

	if (mime_atom) {
		if (mime_atom->type == MIME_ATOM_INTERNAL_FUNCTION) {
			/* Need to cleanup arguments */
			g_array_free(mime_atom->d.func->args, TRUE);
		}
	}
}

static gboolean
rspamd_mime_expr_process_function(struct rspamd_function_atom *func,
								  struct rspamd_task *task,
								  lua_State *L)
{
	struct _fl *selected, key;

	key.name = func->name;

	selected = bsearch(&key,
					   list_ptr,
					   functions_number,
					   sizeof(struct _fl),
					   fl_cmp);
	if (selected == NULL) {
		/* Try to check lua function */
		return FALSE;
	}

	return selected->func(task, func->args, selected->user_data);
}

static double
rspamd_mime_expr_process(void *ud, rspamd_expression_atom_t *atom)
{
	struct rspamd_task *task = (struct rspamd_task *) ud;
	struct rspamd_mime_atom *mime_atom;
	lua_State *L;
	double ret = 0;

	g_assert(task != NULL);
	g_assert(atom != NULL);

	mime_atom = atom->data;

	if (mime_atom->type == MIME_ATOM_REGEXP) {
		ret = rspamd_mime_expr_process_regexp(mime_atom->d.re, task);
	}
	else if (mime_atom->type == MIME_ATOM_LUA_FUNCTION) {
		L = task->cfg->lua_state;
		lua_getglobal(L, mime_atom->d.lua_function);
		rspamd_lua_task_push(L, task);

		if (lua_pcall(L, 1, 1, 0) != 0) {
			msg_info_task("lua call to global function '%s' for atom '%s' failed: %s",
						  mime_atom->d.lua_function,
						  mime_atom->str,
						  lua_tostring(L, -1));
			lua_pop(L, 1);
		}
		else {
			if (lua_type(L, -1) == LUA_TBOOLEAN) {
				ret = lua_toboolean(L, -1);
			}
			else if (lua_type(L, -1) == LUA_TNUMBER) {
				ret = lua_tonumber(L, 1);
			}
			else {
				msg_err_task("%s returned wrong return type: %s",
							 mime_atom->str, lua_typename(L, lua_type(L, -1)));
			}
			/* Remove result */
			lua_pop(L, 1);
		}
	}
	else if (mime_atom->type == MIME_ATOM_LOCAL_LUA_FUNCTION) {
		int err_idx, old_top;

		L = task->cfg->lua_state;
		old_top = lua_gettop(L);
		lua_pushcfunction(L, &rspamd_lua_traceback);
		err_idx = lua_gettop(L);

		lua_rawgeti(L, LUA_REGISTRYINDEX, mime_atom->d.lua_cbref);
		rspamd_lua_task_push(L, task);

		if (lua_pcall(L, 1, 1, err_idx) != 0) {
			msg_info_task("lua call to local function for atom '%s' failed: %s",
						  mime_atom->str,
						  lua_tostring(L, -1));
		}
		else {
			if (lua_type(L, -1) == LUA_TBOOLEAN) {
				ret = lua_toboolean(L, -1);
			}
			else if (lua_type(L, -1) == LUA_TNUMBER) {
				ret = lua_tonumber(L, 1);
			}
			else {
				msg_err_task("%s returned wrong return type: %s",
							 mime_atom->str, lua_typename(L, lua_type(L, -1)));
			}
		}

		lua_settop(L, old_top);
	}
	else {
		ret = rspamd_mime_expr_process_function(mime_atom->d.func, task,
												task->cfg->lua_state);
	}

	return ret;
}

void register_expression_function(const char *name,
								  rspamd_internal_func_t func,
								  void *user_data)
{
	static struct _fl *new;

	functions_number++;

	new = g_new(struct _fl, functions_number);
	memcpy(new, list_ptr, (functions_number - 1) * sizeof(struct _fl));
	if (list_allocated) {
		g_free(list_ptr);
	}

	list_allocated = TRUE;
	new[functions_number - 1].name = name;
	new[functions_number - 1].func = func;
	new[functions_number - 1].user_data = user_data;
	qsort(new, functions_number, sizeof(struct _fl), fl_cmp);
	list_ptr = new;
}

gboolean
rspamd_compare_encoding(struct rspamd_task *task, GArray *args, void *unused)
{
	struct expression_argument *arg;

	if (args == NULL || task == NULL) {
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);
	if (!arg || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("invalid argument to function is passed");
		return FALSE;
	}

	/* XXX: really write this function */
	return TRUE;
}

gboolean
rspamd_header_exists(struct rspamd_task *task, GArray *args, void *unused)
{
	struct expression_argument *arg;
	struct rspamd_mime_header *rh;

	if (args == NULL || task == NULL) {
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);
	if (!arg || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("invalid argument to function is passed");
		return FALSE;
	}

	rh = rspamd_message_get_header_array(task,
										 (char *) arg->data, FALSE);

	debug_task("try to get header %s: %d", (char *) arg->data,
			   (rh != NULL));

	if (rh) {
		return TRUE;
	}

	return FALSE;
}


/*
 * This function is designed to find difference between text/html and text/plain parts
 * It takes one argument: difference threshold, if we have two text parts, compare
 * its hashes and check for threshold, if value is greater than threshold, return TRUE
 * and return FALSE otherwise.
 */
gboolean
rspamd_parts_distance(struct rspamd_task *task, GArray *args, void *unused)
{
	int threshold, threshold2 = -1;
	struct expression_argument *arg;
	double *pdiff, diff;

	if (args == NULL || args->len == 0) {
		debug_task("no threshold is specified, assume it 100");
		threshold = 100;
	}
	else {
		errno = 0;
		arg = &g_array_index(args, struct expression_argument, 0);
		if (!arg || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
			msg_warn_task("invalid argument to function is passed");
			return FALSE;
		}

		threshold = strtoul((char *) arg->data, NULL, 10);
		if (errno != 0) {
			msg_info_task("bad numeric value for threshold \"%s\", assume it 100",
						  (char *) arg->data);
			threshold = 100;
		}
		if (args->len >= 2) {
			arg = &g_array_index(args, struct expression_argument, 1);
			if (!arg || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
				msg_warn_task("invalid argument to function is passed");
				return FALSE;
			}

			errno = 0;
			threshold2 = strtoul((char *) arg->data, NULL, 10);
			if (errno != 0) {
				msg_info_task("bad numeric value for threshold \"%s\", ignore it",
							  (char *) arg->data);
				threshold2 = -1;
			}
		}
	}

	if ((pdiff =
			 rspamd_mempool_get_variable(task->task_pool,
										 "parts_distance")) != NULL) {
		diff = (1.0 - (*pdiff)) * 100.0;

		if (diff != -1) {
			if (threshold2 > 0) {
				if (diff >= MIN(threshold, threshold2) &&
					diff < MAX(threshold, threshold2)) {

					return TRUE;
				}
			}
			else {
				if (diff <= threshold) {
					return TRUE;
				}
			}
			return FALSE;
		}
		else {
			return FALSE;
		}
	}

	return FALSE;
}

struct addr_list {
	const char *name;
	unsigned int namelen;
	const char *addr;
	unsigned int addrlen;
};

static int
addr_list_cmp_func(const void *a, const void *b)
{
	const struct addr_list *addra = (struct addr_list *) a,
						   *addrb = (struct addr_list *) b;

	if (addra->addrlen != addrb->addrlen) {
		return addra->addrlen - addrb->addrlen;
	}

	return memcmp(addra->addr, addrb->addr, addra->addrlen);
}

#define COMPARE_RCPT_LEN 3
#define MIN_RCPT_TO_COMPARE 7

gboolean
rspamd_recipients_distance(struct rspamd_task *task, GArray *args,
						   void *unused)
{
	struct expression_argument *arg;
	struct rspamd_email_address *cur;
	double threshold;
	struct addr_list *ar;
	int num, i, hits = 0;

	if (args == NULL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);
	if (!arg || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("invalid argument to function is passed");
		return FALSE;
	}

	errno = 0;
	threshold = strtod((char *) arg->data, NULL);

	if (errno != 0) {
		msg_warn_task("invalid numeric value '%s': %s",
					  (char *) arg->data,
					  strerror(errno));
		return FALSE;
	}

	if (!MESSAGE_FIELD(task, rcpt_mime)) {
		return FALSE;
	}

	num = MESSAGE_FIELD(task, rcpt_mime)->len;

	if (num < MIN_RCPT_TO_COMPARE) {
		return FALSE;
	}

	ar = rspamd_mempool_alloc0(task->task_pool, num * sizeof(struct addr_list));

	/* Fill array */
	num = 0;
	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, rcpt_mime), i, cur)
	{
		if (cur->addr_len > COMPARE_RCPT_LEN) {
			ar[num].name = cur->addr;
			ar[num].namelen = cur->addr_len;
			ar[num].addr = cur->domain;
			ar[num].addrlen = cur->domain_len;
			num++;
		}
	}

	qsort(ar, num, sizeof(*ar), addr_list_cmp_func);

	/* Cycle all elements in array */
	for (i = 0; i < num; i++) {
		if (i < num - 1) {
			if (ar[i].namelen == ar[i + 1].namelen) {
				if (rspamd_lc_cmp(ar[i].name, ar[i + 1].name, COMPARE_RCPT_LEN) == 0) {
					hits++;
				}
			}
		}
	}

	if ((hits * num / 2.) / (double) num >= threshold) {
		return TRUE;
	}

	return FALSE;
}

gboolean
rspamd_has_only_html_part(struct rspamd_task *task, GArray *args,
						  void *unused)
{
	struct rspamd_mime_text_part *p;
	unsigned int i;

	/*
	 * Return TRUE if there's any HTML part (not attachment) that has no
	 * alternative in its multipart/alternative parent.
	 *
	 * We check two things:
	 * 1. alt_text_part: a text/plain sibling found in text_parts
	 * 2. MIME structure: a sibling leaf text/* (non-html) part in a
	 *    multipart/alternative ancestor — covers alternatives like
	 *    text/calendar that may not be in text_parts due to no_text flag
	 */
	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, p)
	{
		if (!IS_TEXT_PART_ATTACHMENT(p) && IS_TEXT_PART_HTML(p)) {
			if (p->alt_text_part != NULL) {
				/* Has a text/plain alternative */
				continue;
			}

			/* Check MIME structure: walk up to find multipart/alternative */
			struct rspamd_mime_part *mime_part = p->mime_part;
			struct rspamd_mime_part *parent = mime_part->parent_part;
			gboolean has_structural_alt = FALSE;
			rspamd_ftok_t alt_tok = {.begin = "alternative", .len = 11};
			rspamd_ftok_t html_tok = {.begin = "html", .len = 4};

			while (parent) {
				if (IS_PART_MULTIPART(parent) && parent->ct &&
					rspamd_ftok_casecmp(&parent->ct->subtype, &alt_tok) == 0) {

					if (parent->specific.mp && parent->specific.mp->children) {
						GPtrArray *children = parent->specific.mp->children;

						/* Find which direct child branch contains our HTML part */
						struct rspamd_mime_part *our_branch = mime_part;
						while (our_branch->parent_part &&
							   our_branch->parent_part != parent) {
							our_branch = our_branch->parent_part;
						}

						/* Check siblings for leaf text/* non-html parts */
						for (unsigned int j = 0; j < children->len; j++) {
							struct rspamd_mime_part *sibling =
								g_ptr_array_index(children, j);

							if (sibling == our_branch) {
								continue;
							}

							/* A leaf text/* part that isn't html is a
							 * real alternative (e.g. text/calendar) */
							if (sibling->ct &&
								(sibling->ct->flags & RSPAMD_CONTENT_TYPE_TEXT) &&
								rspamd_ftok_casecmp(&sibling->ct->subtype,
													&html_tok) != 0) {
								has_structural_alt = TRUE;
								break;
							}
						}
					}
					break;
				}
				parent = parent->parent_part;
			}

			if (!has_structural_alt) {
				return TRUE;
			}
		}
	}

	return FALSE;
}

static gboolean
is_recipient_list_sorted(GPtrArray *ar)
{
	struct rspamd_email_address *addr;
	gboolean res = TRUE;
	rspamd_ftok_t cur, prev;
	int i;

	/* Do not check to short address lists */
	if (ar == NULL || ar->len < MIN_RCPT_TO_COMPARE) {
		return FALSE;
	}

	prev.len = 0;
	prev.begin = NULL;

	PTR_ARRAY_FOREACH(ar, i, addr)
	{
		cur.begin = addr->addr;
		cur.len = addr->addr_len;

		if (prev.len != 0) {
			if (rspamd_ftok_casecmp(&cur, &prev) <= 0) {
				res = FALSE;
				break;
			}
		}

		prev = cur;
	}

	return res;
}

gboolean
rspamd_is_recipients_sorted(struct rspamd_task *task,
							GArray *args,
							void *unused)
{
	/* Check all types of addresses */

	if (MESSAGE_FIELD(task, rcpt_mime)) {
		return is_recipient_list_sorted(MESSAGE_FIELD(task, rcpt_mime));
	}

	return FALSE;
}

gboolean
rspamd_compare_transfer_encoding(struct rspamd_task *task,
								 GArray *args,
								 void *unused)
{
	struct expression_argument *arg;
	unsigned int i;
	struct rspamd_mime_part *part;
	enum rspamd_cte cte;

	if (args == NULL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);
	if (!arg || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("invalid argument to function is passed");
		return FALSE;
	}

	cte = rspamd_cte_from_string(arg->data);

	if (cte == RSPAMD_CTE_UNKNOWN) {
		msg_warn_task("unknown cte: %s", arg->data);
		return FALSE;
	}

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, part)
	{
		if (IS_PART_TEXT(part)) {
			if (part->cte == cte) {
				return TRUE;
			}
		}
	}

	return FALSE;
}

gboolean
rspamd_is_html_balanced(struct rspamd_task *task, GArray *args, void *unused)
{
	/* Totally broken but seems to be never used */
	return TRUE;
}

gboolean
rspamd_has_html_tag(struct rspamd_task *task, GArray *args, void *unused)
{
	struct rspamd_mime_text_part *p;
	struct expression_argument *arg;
	unsigned int i;
	gboolean res = FALSE;

	if (args == NULL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);
	if (!arg || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("invalid argument to function is passed");
		return FALSE;
	}

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, p)
	{
		if (IS_TEXT_PART_HTML(p) && p->html) {
			res = rspamd_html_tag_seen(p->html, arg->data);
		}

		if (res) {
			break;
		}
	}

	return res;
}

gboolean
rspamd_has_fake_html(struct rspamd_task *task, GArray *args, void *unused)
{
	struct rspamd_mime_text_part *p;
	unsigned int i;
	gboolean res = FALSE;

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, p)
	{
		if (IS_TEXT_PART_HTML(p) && (rspamd_html_get_tags_count(p->html) < 2)) {
			res = TRUE;
		}

		if (res) {
			break;
		}
	}

	return res;
}

static gboolean
rspamd_raw_header_exists(struct rspamd_task *task, GArray *args, void *unused)
{
	struct expression_argument *arg;

	if (args == NULL || task == NULL) {
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);
	if (!arg || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("invalid argument to function is passed");
		return FALSE;
	}

	return rspamd_message_get_header_array(task, arg->data, FALSE) != NULL;
}

static gboolean
match_smtp_data(struct rspamd_task *task,
				struct expression_argument *arg,
				const char *what, gsize len)
{
	rspamd_regexp_t *re;
	int r = 0;

	if (arg->type == EXPRESSION_ARGUMENT_REGEXP) {
		/* This is a regexp */
		re = arg->data;
		if (re == NULL) {
			msg_warn_task("cannot compile regexp for function");
			return FALSE;
		}


		if (len > 0) {
			r = rspamd_regexp_search(re, what, len, NULL, NULL, FALSE, NULL);
		}

		return r;
	}
	else if (arg->type == EXPRESSION_ARGUMENT_NORMAL &&
			 g_ascii_strncasecmp(arg->data, what, len) == 0) {
		return TRUE;
	}

	return FALSE;
}

static gboolean
rspamd_check_smtp_data(struct rspamd_task *task, GArray *args, void *unused)
{
	struct expression_argument *arg;
	struct rspamd_email_address *addr = NULL;
	GPtrArray *rcpts = NULL;
	const char *type, *str = NULL;
	unsigned int i;

	if (args == NULL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);

	if (!arg || !arg->data || arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}
	else {
		type = arg->data;
		switch (*type) {
		case 'f':
		case 'F':
			if (g_ascii_strcasecmp(type, "from") == 0) {
				addr = rspamd_task_get_sender(task);
			}
			else {
				msg_warn_task("bad argument to function: %s", type);
				return FALSE;
			}
			break;
		case 'h':
		case 'H':
			if (g_ascii_strcasecmp(type, "helo") == 0) {
				str = task->helo;
			}
			else {
				msg_warn_task("bad argument to function: %s", type);
				return FALSE;
			}
			break;
		case 'u':
		case 'U':
			if (g_ascii_strcasecmp(type, "user") == 0) {
				str = task->auth_user;
			}
			else {
				msg_warn_task("bad argument to function: %s", type);
				return FALSE;
			}
			break;
		case 's':
		case 'S':
			if (g_ascii_strcasecmp(type, "subject") == 0) {
				str = MESSAGE_FIELD(task, subject);
			}
			else {
				msg_warn_task("bad argument to function: %s", type);
				return FALSE;
			}
			break;
		case 'r':
		case 'R':
			if (g_ascii_strcasecmp(type, "rcpt") == 0) {
				rcpts = task->rcpt_envelope;
			}
			else {
				msg_warn_task("bad argument to function: %s", type);
				return FALSE;
			}
			break;
		default:
			msg_warn_task("bad argument to function: %s", type);
			return FALSE;
		}
	}

	if (str == NULL && addr == NULL && rcpts == NULL) {
		/* Not enough data so regexp would NOT be found anyway */
		return FALSE;
	}

	/* We would process only one more argument, others are ignored */
	if (args->len >= 2) {
		arg = &g_array_index(args, struct expression_argument, 1);

		if (arg) {
			if (str != NULL) {
				return match_smtp_data(task, arg, str, strlen(str));
			}
			else if (addr != NULL && addr->addr) {
				return match_smtp_data(task, arg, addr->addr, addr->addr_len);
			}
			else {
				if (rcpts != NULL) {
					for (i = 0; i < rcpts->len; i++) {
						addr = g_ptr_array_index(rcpts, i);

						if (addr && addr->addr &&
							match_smtp_data(task, arg,
											addr->addr, addr->addr_len)) {
							return TRUE;
						}
					}
				}
			}
		}
	}

	return FALSE;
}

static inline gboolean
rspamd_check_ct_attr(const char *begin, gsize len,
					 struct expression_argument *arg_pattern)
{
	rspamd_regexp_t *re;
	gboolean r = FALSE;

	if (arg_pattern->type == EXPRESSION_ARGUMENT_REGEXP) {
		re = arg_pattern->data;

		if (len > 0) {
			r = rspamd_regexp_search(re,
									 begin, len,
									 NULL, NULL, FALSE, NULL);
		}

		if (r) {
			return TRUE;
		}
	}
	else {
		/* Just do strcasecmp */
		gsize plen = strlen(arg_pattern->data);

		if (plen == len &&
			g_ascii_strncasecmp(arg_pattern->data, begin, len) == 0) {
			return TRUE;
		}
	}

	return FALSE;
}

static gboolean
rspamd_content_type_compare_param(struct rspamd_task *task,
								  GArray *args,
								  void *unused)
{

	struct expression_argument *arg, *arg1, *arg_pattern;
	gboolean recursive = FALSE;
	struct rspamd_mime_part *cur_part;
	unsigned int i;
	rspamd_ftok_t srch;
	struct rspamd_content_type_param *found = NULL, *cur;
	const char *param_name;

	if (args == NULL || args->len < 2) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);
	g_assert(arg->type == EXPRESSION_ARGUMENT_NORMAL);
	param_name = arg->data;
	arg_pattern = &g_array_index(args, struct expression_argument, 1);

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, cur_part)
	{
		if (args->len >= 3) {
			arg1 = &g_array_index(args, struct expression_argument, 2);
			if (g_ascii_strncasecmp(arg1->data, "true",
									sizeof("true") - 1) == 0) {
				recursive = TRUE;
			}
		}
		else {
			/*
			 * If user did not specify argument, let's assume that he wants
			 * recursive search if mime part is multipart/mixed
			 */
			if (IS_PART_MULTIPART(cur_part)) {
				recursive = TRUE;
			}
		}

		rspamd_ftok_t lit;
		RSPAMD_FTOK_FROM_STR(&srch, param_name);
		RSPAMD_FTOK_FROM_STR(&lit, "charset");

		if (rspamd_ftok_equal(&srch, &lit)) {
			if (rspamd_check_ct_attr(cur_part->ct->charset.begin,
									 cur_part->ct->charset.len, arg_pattern)) {
				return TRUE;
			}
		}

		RSPAMD_FTOK_FROM_STR(&lit, "boundary");
		if (rspamd_ftok_equal(&srch, &lit)) {
			if (rspamd_check_ct_attr(cur_part->ct->orig_boundary.begin,
									 cur_part->ct->orig_boundary.len, arg_pattern)) {
				return TRUE;
			}
		}

		if (cur_part->ct->attrs) {
			found = g_hash_table_lookup(cur_part->ct->attrs, &srch);

			if (found) {
				DL_FOREACH(found, cur)
				{
					if (rspamd_check_ct_attr(cur->value.begin,
											 cur->value.len, arg_pattern)) {
						return TRUE;
					}
				}
			}
		}

		if (!recursive) {
			break;
		}
	}

	return FALSE;
}

static gboolean
rspamd_content_type_has_param(struct rspamd_task *task,
							  GArray *args,
							  void *unused)
{
	struct expression_argument *arg, *arg1;
	gboolean recursive = FALSE;
	struct rspamd_mime_part *cur_part;
	unsigned int i;
	rspamd_ftok_t srch;
	struct rspamd_content_type_param *found = NULL;
	const char *param_name;

	if (args == NULL || args->len < 1) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	arg = &g_array_index(args, struct expression_argument, 0);
	g_assert(arg->type == EXPRESSION_ARGUMENT_NORMAL);
	param_name = arg->data;

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, cur_part)
	{
		if (args->len >= 2) {
			arg1 = &g_array_index(args, struct expression_argument, 1);
			if (g_ascii_strncasecmp(arg1->data, "true",
									sizeof("true") - 1) == 0) {
				recursive = TRUE;
			}
		}
		else {
			/*
			 * If user did not specify argument, let's assume that he wants
			 * recursive search if mime part is multipart/mixed
			 */
			if (IS_PART_MULTIPART(cur_part)) {
				recursive = TRUE;
			}
		}


		rspamd_ftok_t lit;
		RSPAMD_FTOK_FROM_STR(&srch, param_name);
		RSPAMD_FTOK_FROM_STR(&lit, "charset");

		if (rspamd_ftok_equal(&srch, &lit)) {
			if (cur_part->ct->charset.len > 0) {
				return TRUE;
			}
		}

		RSPAMD_FTOK_FROM_STR(&lit, "boundary");
		if (rspamd_ftok_equal(&srch, &lit)) {
			if (cur_part->ct->boundary.len > 0) {
				return TRUE;
			}
		}

		if (cur_part->ct->attrs) {
			found = g_hash_table_lookup(cur_part->ct->attrs, &srch);

			if (found) {
				return TRUE;
			}
		}

		if (!recursive) {
			break;
		}
	}

	return FALSE;
}

static gboolean
rspamd_content_type_check(struct rspamd_task *task,
						  GArray *args,
						  gboolean check_subtype)
{
	rspamd_ftok_t *param_data, srch;
	rspamd_regexp_t *re;
	struct expression_argument *arg1, *arg_pattern;
	struct rspamd_content_type *ct;
	int r = 0;
	unsigned int i;
	gboolean recursive = FALSE;
	struct rspamd_mime_part *cur_part;

	if (args == NULL || args->len < 1) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	arg_pattern = &g_array_index(args, struct expression_argument, 0);

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, cur_part)
	{
		ct = cur_part->ct;

		if (args->len >= 2) {
			arg1 = &g_array_index(args, struct expression_argument, 1);
			if (g_ascii_strncasecmp(arg1->data, "true",
									sizeof("true") - 1) == 0) {
				recursive = TRUE;
			}
		}
		else {
			/*
			 * If user did not specify argument, let's assume that he wants
			 * recursive search if mime part is multipart/mixed
			 */
			if (IS_PART_MULTIPART(cur_part)) {
				recursive = TRUE;
			}
		}

		if (check_subtype) {
			param_data = &ct->subtype;
		}
		else {
			param_data = &ct->type;
		}

		if (arg_pattern->type == EXPRESSION_ARGUMENT_REGEXP) {
			re = arg_pattern->data;

			if (param_data->len > 0) {
				r = rspamd_regexp_search(re, param_data->begin, param_data->len,
										 NULL, NULL, FALSE, NULL);
			}

			if (r) {
				return TRUE;
			}
		}
		else {
			/* Just do strcasecmp */
			srch.begin = arg_pattern->data;
			srch.len = strlen(arg_pattern->data);

			if (rspamd_ftok_casecmp(param_data, &srch) == 0) {
				return TRUE;
			}
		}

		/* Get next part */
		if (!recursive) {
			break;
		}
	}

	return FALSE;
}

static gboolean
rspamd_content_type_is_type(struct rspamd_task *task,
							GArray *args,
							void *unused)
{
	return rspamd_content_type_check(task, args, FALSE);
}

static gboolean
rspamd_content_type_is_subtype(struct rspamd_task *task,
							   GArray *args,
							   void *unused)
{
	return rspamd_content_type_check(task, args, TRUE);
}

static gboolean
compare_subtype(struct rspamd_task *task, struct rspamd_content_type *ct,
				struct expression_argument *subtype)
{
	rspamd_regexp_t *re;
	rspamd_ftok_t srch;
	int r = 0;

	if (subtype == NULL || ct == NULL) {
		msg_warn_task("invalid parameters passed");
		return FALSE;
	}
	if (subtype->type == EXPRESSION_ARGUMENT_REGEXP) {
		re = subtype->data;

		if (ct->subtype.len > 0) {
			r = rspamd_regexp_search(re, ct->subtype.begin, ct->subtype.len,
									 NULL, NULL, FALSE, NULL);
		}
	}
	else {
		srch.begin = subtype->data;
		srch.len = strlen(subtype->data);

		/* Just do strcasecmp */
		if (rspamd_ftok_casecmp(&ct->subtype, &srch) == 0) {
			return TRUE;
		}
	}

	return r;
}

static gboolean
compare_len(struct rspamd_mime_part *part, unsigned int min, unsigned int max)
{
	if (min == 0 && max == 0) {
		return TRUE;
	}

	if (min == 0) {
		return part->parsed_data.len <= max;
	}
	else if (max == 0) {
		return part->parsed_data.len >= min;
	}
	else {
		return part->parsed_data.len >= min && part->parsed_data.len <= max;
	}
}

static gboolean
common_has_content_part(struct rspamd_task *task,
						struct expression_argument *param_type,
						struct expression_argument *param_subtype,
						int min_len,
						int max_len)
{
	rspamd_regexp_t *re;
	struct rspamd_mime_part *part;
	struct rspamd_content_type *ct;
	rspamd_ftok_t srch;
	int r = 0;
	unsigned int i;

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, part)
	{
		ct = part->ct;

		if (ct == NULL) {
			continue;
		}

		if (param_type->type == EXPRESSION_ARGUMENT_REGEXP) {
			re = param_type->data;

			if (ct->type.len > 0) {
				r = rspamd_regexp_search(re, ct->type.begin, ct->type.len,
										 NULL, NULL, FALSE, NULL);
			}

			/* Also check subtype and length of the part */
			if (r && param_subtype) {
				r = compare_len(part, min_len, max_len) &&
					compare_subtype(task, ct, param_subtype);

				return r;
			}
		}
		else {
			/* Just do strcasecmp */
			srch.begin = param_type->data;
			srch.len = strlen(param_type->data);

			if (rspamd_ftok_casecmp(&ct->type, &srch) == 0) {
				if (param_subtype) {
					if (compare_subtype(task, ct, param_subtype)) {
						if (compare_len(part, min_len, max_len)) {
							return TRUE;
						}
					}
				}
				else {
					if (compare_len(part, min_len, max_len)) {
						return TRUE;
					}
				}
			}
		}
	}

	return FALSE;
}

static gboolean
rspamd_has_content_part(struct rspamd_task *task, GArray *args, void *unused)
{
	struct expression_argument *param_type = NULL, *param_subtype = NULL;

	if (args == NULL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	param_type = &g_array_index(args, struct expression_argument, 0);
	if (args->len >= 2) {
		param_subtype = &g_array_index(args, struct expression_argument, 1);
	}

	return common_has_content_part(task, param_type, param_subtype, 0, 0);
}

static gboolean
rspamd_has_content_part_len(struct rspamd_task *task,
							GArray *args,
							void *unused)
{
	struct expression_argument *param_type = NULL, *param_subtype = NULL;
	int min = 0, max = 0;
	struct expression_argument *arg;

	if (args == NULL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	param_type = &g_array_index(args, struct expression_argument, 0);

	if (args->len >= 2) {
		param_subtype = &g_array_index(args, struct expression_argument, 1);

		if (args->len >= 3) {
			arg = &g_array_index(args, struct expression_argument, 2);
			errno = 0;
			min = strtoul(arg->data, NULL, 10);
			g_assert(arg->type == EXPRESSION_ARGUMENT_NORMAL);

			if (errno != 0) {
				msg_warn_task("invalid numeric value '%s': %s",
							  (char *) arg->data,
							  strerror(errno));
				return FALSE;
			}

			if (args->len >= 4) {
				arg = &g_array_index(args, struct expression_argument, 3);
				g_assert(arg->type == EXPRESSION_ARGUMENT_NORMAL);
				max = strtoul(arg->data, NULL, 10);

				if (errno != 0) {
					msg_warn_task("invalid numeric value '%s': %s",
								  (char *) arg->data,
								  strerror(errno));
					return FALSE;
				}
			}
		}
	}

	return common_has_content_part(task, param_type, param_subtype, min, max);
}

static gboolean
rspamd_is_empty_body(struct rspamd_task *task,
					 GArray *args,
					 void *unused)
{
	struct rspamd_mime_part *part;
	unsigned int i;

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, part)
	{
		if (part->parsed_data.len > 0) {
			return FALSE;
		}
	}

	return TRUE;
}

#define TASK_FLAG_READ(flag)               \
	do {                                   \
		result = !!(task->flags & (flag)); \
	} while (0)

#define TASK_GET_FLAG(flag, strname, macro)           \
	do {                                              \
		if (!found && strcmp((flag), strname) == 0) { \
			TASK_FLAG_READ((macro));                  \
			found = TRUE;                             \
		}                                             \
	} while (0)

#define TASK_PROTOCOL_FLAG_READ(flag)               \
	do {                                            \
		result = !!(task->protocol_flags & (flag)); \
	} while (0)

#define TASK_GET_PROTOCOL_FLAG(flag, strname, macro)  \
	do {                                              \
		if (!found && strcmp((flag), strname) == 0) { \
			TASK_PROTOCOL_FLAG_READ((macro));         \
			found = TRUE;                             \
		}                                             \
	} while (0)


static gboolean
rspamd_has_flag_expr(struct rspamd_task *task,
					 GArray *args,
					 void *unused)
{
	gboolean found = FALSE, result = FALSE;
	struct expression_argument *flag_arg;
	const char *flag_str;

	if (args == NULL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	flag_arg = &g_array_index(args, struct expression_argument, 0);

	if (flag_arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("invalid parameter to function");
		return FALSE;
	}

	flag_str = (const char *) flag_arg->data;

	TASK_GET_FLAG(flag_str, "pass_all", RSPAMD_TASK_FLAG_PASS_ALL);
	TASK_GET_FLAG(flag_str, "no_log", RSPAMD_TASK_FLAG_NO_LOG);
	TASK_GET_FLAG(flag_str, "no_stat", RSPAMD_TASK_FLAG_NO_STAT);
	TASK_GET_FLAG(flag_str, "skip", RSPAMD_TASK_FLAG_SKIP);
	TASK_GET_PROTOCOL_FLAG(flag_str, "extended_urls",
						   RSPAMD_TASK_PROTOCOL_FLAG_EXT_URLS);
	TASK_GET_FLAG(flag_str, "learn_spam", RSPAMD_TASK_FLAG_LEARN_SPAM);
	TASK_GET_FLAG(flag_str, "learn_ham", RSPAMD_TASK_FLAG_LEARN_HAM);
	TASK_GET_FLAG(flag_str, "greylisted", RSPAMD_TASK_FLAG_GREYLISTED);
	TASK_GET_FLAG(flag_str, "broken_headers",
				  RSPAMD_TASK_FLAG_BROKEN_HEADERS);
	TASK_GET_FLAG(flag_str, "skip_process",
				  RSPAMD_TASK_FLAG_SKIP_PROCESS);
	TASK_GET_PROTOCOL_FLAG(flag_str, "milter",
						   RSPAMD_TASK_PROTOCOL_FLAG_MILTER);
	TASK_GET_FLAG(flag_str, "bad_unicode",
				  RSPAMD_TASK_FLAG_BAD_UNICODE);

	if (!found) {
		msg_warn_task("invalid flag name %s", flag_str);
		return FALSE;
	}

	return result;
}

static gboolean
rspamd_has_symbol_expr(struct rspamd_task *task,
					   GArray *args,
					   void *unused)
{
	struct expression_argument *sym_arg;
	const char *symbol_str;

	if (args == NULL) {
		msg_warn_task("no parameters to function");
		return FALSE;
	}

	sym_arg = &g_array_index(args, struct expression_argument, 0);

	if (sym_arg->type != EXPRESSION_ARGUMENT_NORMAL) {
		msg_warn_task("invalid parameter to function");
		return FALSE;
	}

	symbol_str = (const char *) sym_arg->data;

	if (rspamd_task_find_symbol_result(task, symbol_str, NULL)) {
		return TRUE;
	}

	return FALSE;
}
