/*-
 * Copyright 2016 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 "config.h"
#include "util.h"
#include "cfg_file.h"
#include "rspamd.h"
#include "message.h"
#include "mime_expressions.h"
#include "html.h"
#include "email_addr.h"
#include "lua/lua_common.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 rspamd_expression_atom_t * rspamd_mime_expr_parse (const gchar *line, gsize len,
		rspamd_mempool_t *pool, gpointer ud, GError **err);
static gint rspamd_mime_expr_process (gpointer input, rspamd_expression_atom_t *atom);
static gint 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										*/
	gchar *regexp_text;                             /**< regexp text representation							*/
	rspamd_regexp_t *regexp;                        /**< regexp structure									*/
	gchar *header;                                  /**< header name for header regexps						*/
	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 {
	gchar *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
};

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

/*
 * List of internal functions of rspamd
 * Sorted by name to use bsearch
 */
static struct _fl {
	const gchar *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_html_tag", rspamd_has_html_tag, NULL},
	{"has_only_html_part", rspamd_has_only_html_part, NULL},
	{"header_exists", rspamd_header_exists, 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 guint32 functions_number = sizeof (rspamd_functions_list) /
	sizeof (struct _fl);
static gboolean list_allocated = FALSE;

/* Bsearch routine */
static gint
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");
}

static gboolean
rspamd_parse_long_option (const gchar *start, gsize len,
		struct rspamd_regexp_atom *a)
{
	gboolean ret = FALSE;

	if (rspamd_lc_cmp (start, "body", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_BODY;
	}
	else if (rspamd_lc_cmp (start, "part", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_MIME;
	}
	else if (rspamd_lc_cmp (start, "raw_part", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_RAWMIME;
	}
	else if (rspamd_lc_cmp (start, "header", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_HEADER;
	}
	else if (rspamd_lc_cmp (start, "mime_header", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_MIMEHEADER;
	}
	else if (rspamd_lc_cmp (start, "raw_header", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_RAWHEADER;
	}
	else if (rspamd_lc_cmp (start, "all_header", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_ALLHEADER;
	}
	else if (rspamd_lc_cmp (start, "url", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_URL;
	}
	else if (rspamd_lc_cmp (start, "sa_body", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_SABODY;
	}
	else if (rspamd_lc_cmp (start, "sa_raw_body", len) == 0) {
		ret = TRUE;
		a->type = RSPAMD_RE_SARAWBODY;
	}

	return ret;
}

/*
 * Rspamd regexp utility functions
 */
static struct rspamd_regexp_atom *
rspamd_mime_expr_parse_regexp_atom (rspamd_mempool_t * pool, const gchar *line,
		struct rspamd_config *cfg)
{
	const gchar *begin, *end, *p, *src, *start, *brace;
	gchar *dbegin, *dend;
	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;
	}
	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) {
			result->header = rspamd_mempool_alloc (pool, end - line + 1);
			rspamd_strlcpy (result->header, line, end - line + 1);
			result->type = RSPAMD_RE_HEADER;
			line = end;
		}
	}
	else {
		result->header = 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 (result->header == NULL) {
		/* Assume that line without // is just a header name */
		result->header = 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':
			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_RAWHEADER;
			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 '{':
			/* 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) {
		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) &&
			result->header == NULL) {
		msg_err_pool ("header regexp: '%s' has no header part", src);
		return NULL;
	}


	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 (const gchar *input)
{
	const gchar *obrace, *ebrace, *p, *c;
	gchar t, *databuf;
	guint 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 = g_slice_alloc0 (sizeof (*res));
	res->name = g_malloc (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 = g_malloc (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;
				}
				else {
					g_free (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 = g_malloc (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 gchar *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 gchar *p, *end;
	struct rspamd_config *cfg = ud;
	rspamd_regexp_t *own_re;
	gchar t;
	gint 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,
		got_ebrace,
		end_atom,
		bad_atom
	} state = 0, prev_state = 0;

	p = line;
	end = p + len;

	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 != '=') {
				/* 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)) {
				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 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, (gint)(p - line),
					(gint)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 = g_slice_alloc (sizeof (*mime_atom));
	mime_atom->type = type;
	mime_atom->str = g_malloc (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 {
			/* 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->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->header,
							strlen (mime_atom->d.re->header) + 1);
					/* 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 /H regexp: '%s'",
							mime_atom->str);
					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);
				/* 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 {
		mime_atom->d.func = rspamd_mime_expr_parse_function_atom (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:
	g_free (mime_atom->str);
	g_slice_free1 (sizeof (*mime_atom), mime_atom);

	return NULL;
}

static gint
rspamd_mime_expr_process_regexp (struct rspamd_regexp_atom *re,
		struct rspamd_task *task)
{
	gint 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->header,
				strlen (re->header),
				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 gint
rspamd_mime_expr_priority (rspamd_expression_atom_t *atom)
{
	struct rspamd_mime_atom *mime_atom = atom->data;
	gint ret = 0;

	switch (mime_atom->type) {
	case MIME_ATOM_INTERNAL_FUNCTION:
		/* Prioritize internal functions slightly */
		ret = 50;
		break;
	case MIME_ATOM_LUA_FUNCTION:
		ret = 50;
		break;
	case MIME_ATOM_REGEXP:
		switch (mime_atom->d.re->type) {
		case RSPAMD_RE_HEADER:
		case RSPAMD_RE_RAWHEADER:
			ret = 100;
			break;
		case RSPAMD_RE_URL:
			ret = 90;
			break;
		case RSPAMD_RE_MIME:
		case RSPAMD_RE_RAWMIME:
			ret = 10;
			break;
		default:
			/* For message regexp */
			ret = 0;
			break;
		}
	}

	return ret;
}

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

	if (mime_atom) {
		if (mime_atom->type == MIME_ATOM_INTERNAL_FUNCTION) {
			/* Need to cleanup arguments */
			for (i = 0; i < mime_atom->d.func->args->len; i ++) {
				arg = &g_array_index (mime_atom->d.func->args,
						struct expression_argument, i);

				if (arg->type == EXPRESSION_ARGUMENT_NORMAL) {
					g_free (arg->data);
				}
			}
			g_array_free (mime_atom->d.func->args, TRUE);
		}
		/* XXX: regexp shouldn't be special */
		g_slice_free1 (sizeof (*mime_atom), mime_atom);
	}
}

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 gint
rspamd_mime_expr_process (gpointer input, rspamd_expression_atom_t *atom)
{
	struct rspamd_task *task = input;
	struct rspamd_mime_atom *mime_atom;
	lua_State *L;
	gint 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 {
		ret = rspamd_mime_expr_process_function (mime_atom->d.func, task,
				task->cfg->lua_state);
	}

	return ret;
}

void
register_expression_function (const gchar *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;
	GPtrArray *headerlist;

	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;
	}

	debug_task ("try to get header %s", (gchar *)arg->data);
	headerlist = rspamd_message_get_header_array (task,
			(gchar *)arg->data,
			FALSE);

	if (headerlist) {
		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)
{
	gint threshold, threshold2 = -1;
	struct expression_argument *arg;
	gdouble *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 ((gchar *)arg->data, NULL, 10);
		if (errno != 0) {
			msg_info_task ("bad numeric value for threshold \"%s\", assume it 100",
				(gchar *)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 ((gchar *)arg->data, NULL, 10);
			if (errno != 0) {
				msg_info_task ("bad numeric value for threshold \"%s\", ignore it",
					(gchar *)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 gchar *name;
	const gchar *addr;
};

#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;
	InternetAddressList *cur;
	double threshold;
	struct addr_list *ar;
	gchar *c;
	gint num, i, j, hits = 0, total = 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 ((gchar *)arg->data, NULL);

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

	if (!task->rcpt_mime) {
		return FALSE;
	}

	num = internet_address_list_length (task->rcpt_mime);

	if (num < MIN_RCPT_TO_COMPARE) {
		return FALSE;
	}
	ar =
		rspamd_mempool_alloc0 (task->task_pool, num *
			sizeof (struct addr_list));

	/* Fill array */
	cur = task->rcpt_mime;
#ifdef GMIME24
	for (i = 0; i < num; i++) {
		InternetAddress *iaelt =
			internet_address_list_get_address(cur, i);
		InternetAddressMailbox *iamb =
			INTERNET_ADDRESS_IS_MAILBOX(iaelt) ?
			INTERNET_ADDRESS_MAILBOX (iaelt) : NULL;
		if (iamb) {
			ar[i].name = internet_address_mailbox_get_addr (iamb);
			if (ar[i].name != NULL && (c = strchr (ar[i].name, '@')) != NULL) {
				ar[i].addr = c + 1;
			}
		}
	}
#else
	InternetAddress *addr;
	i = 0;
	while (cur) {
		addr = internet_address_list_get_address (cur);
		if (addr && internet_address_get_type (addr) == INTERNET_ADDRESS_NAME) {
			ar[i].name = rspamd_mempool_strdup (task->task_pool,
					internet_address_get_addr (addr));
			if (ar[i].name != NULL && (c = strchr (ar[i].name, '@')) != NULL) {
				*c = '\0';
				ar[i].addr = c + 1;
			}
			cur = internet_address_list_next (cur);
			i++;
		}
		else {
			cur = internet_address_list_next (cur);
		}
	}
#endif

	/* Cycle all elements in array */
	for (i = 0; i < num; i++) {
		for (j = i + 1; j < num; j++) {
			if (ar[i].name && ar[j].name &&
				g_ascii_strncasecmp (ar[i].name, ar[j].name,
				COMPARE_RCPT_LEN) == 0) {
				/* Common name part */
				hits++;
			}
#if 0
			/* XXX: when we have a typical mail that is headed towards
			 * several users within the same domain, then this rule
			 * leads to a false-positive.
			 * We actually need to match host against tld, but this is currently
			 * too expensive.
			 *
			 * TODO: think about normal representation of InternetAddress shit
			 */
			else if (ar[i].addr && ar[j].addr &&
				g_ascii_strcasecmp (ar[i].addr, ar[j].addr) == 0) {
				/* Common address part, but different name */
				hits++;
			}
#endif
			total++;
		}
	}

	if ((double)(hits * num / 2.) / (double)total >= 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;
	gboolean res = FALSE;

	if (task->text_parts->len == 1) {
		p = g_ptr_array_index (task->text_parts, 0);

		if (IS_PART_HTML (p)) {
			res = TRUE;
		}
		else {
			res = FALSE;
		}
	}

	return res;
}

static gboolean
is_recipient_list_sorted (const InternetAddressList * ia)
{
	const InternetAddressList *cur;
	InternetAddress *addr;
	InternetAddressMailbox *addr_mb;
	gboolean res = TRUE;
	struct addr_list current = { NULL, NULL }, previous = {
		NULL, NULL
	};
#ifdef GMIME24
	gint num, i;
#endif

	/* Do not check to short address lists */
	if (internet_address_list_length ((InternetAddressList *)ia) <
		MIN_RCPT_TO_COMPARE) {
		return FALSE;
	}
#ifdef GMIME24
	num = internet_address_list_length ((InternetAddressList *)ia);
	cur = ia;
	for (i = 0; i < num; i++) {
		addr =
			internet_address_list_get_address ((InternetAddressList *)cur, i);
		if (INTERNET_ADDRESS_IS_MAILBOX (addr)) {
			addr_mb = INTERNET_ADDRESS_MAILBOX (addr);
			current.addr = (gchar *) internet_address_mailbox_get_addr (addr_mb);
		}

		if (previous.addr != NULL) {
			if (current.addr &&
				g_ascii_strcasecmp (current.addr, previous.addr) <= 0) {
				res = FALSE;
				break;
			}
		}
		previous.addr = current.addr;
	}
#else
	cur = ia;
	while (cur) {
		addr = internet_address_list_get_address (cur);
		if (internet_address_get_type (addr) == INTERNET_ADDRESS_NAME) {
			current.addr = internet_address_get_addr (addr);
			if (previous.addr != NULL) {
				if (current.addr &&
					g_ascii_strcasecmp (current.addr, previous.addr) < 0) {
					res = FALSE;
					break;
				}
			}
			previous.addr = current.addr;
		}
		cur = internet_address_list_next (cur);
	}
#endif

	return res;
}

gboolean
rspamd_is_recipients_sorted (struct rspamd_task * task,
	GArray * args,
	void *unused)
{
	/* Check all types of addresses */
	if (is_recipient_list_sorted (g_mime_message_get_recipients (task->message,
		GMIME_RECIPIENT_TYPE_TO)) == TRUE) {
		return TRUE;
	}
	if (is_recipient_list_sorted (g_mime_message_get_recipients (task->message,
		GMIME_RECIPIENT_TYPE_BCC)) == TRUE) {
		return TRUE;
	}
	if (is_recipient_list_sorted (g_mime_message_get_recipients (task->message,
		GMIME_RECIPIENT_TYPE_CC)) == TRUE) {
		return TRUE;
	}

	return FALSE;
}

gboolean
rspamd_compare_transfer_encoding (struct rspamd_task * task,
	GArray * args,
	void *unused)
{
	GPtrArray *headerlist;
	struct expression_argument *arg;
	guint i;
	struct raw_header *rh;
	static const char *hname = "Content-Transfer-Encoding";

	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;
	}

	headerlist = rspamd_message_get_header_array (task, hname, FALSE);

	if (headerlist) {
		for (i = 0; i < headerlist->len; i ++) {
			rh = g_ptr_array_index (headerlist, i);

			if (rh->decoded == NULL) {
				continue;
			}

			if (g_ascii_strcasecmp (rh->decoded, arg->data) == 0) {
				return TRUE;
			}
		}
	}

	/*
	 * In fact, we need to check 'Content-Transfer-Encoding' for each part
	 * as gmime has 'strange' assumptions
	 */
	headerlist = rspamd_message_get_mime_header_array (task,
			arg->data,
			FALSE);

	if (headerlist) {
		for (i = 0; i < headerlist->len; i ++) {
			rh = g_ptr_array_index (headerlist, i);

			if (rh->decoded == NULL) {
				continue;
			}

			if (g_ascii_strcasecmp (rh->decoded, arg->data) == 0) {
				return TRUE;
			}
		}
	}

	return FALSE;
}

gboolean
rspamd_is_html_balanced (struct rspamd_task * task, GArray * args, void *unused)
{
	struct rspamd_mime_text_part *p;
	guint i;
	gboolean res = TRUE;

	for (i = 0; i < task->text_parts->len; i ++) {

		p = g_ptr_array_index (task->text_parts, i);
		if (!IS_PART_EMPTY (p) && IS_PART_HTML (p)) {
			if (p->flags & RSPAMD_MIME_TEXT_PART_FLAG_BALANCED) {
				res = TRUE;
			}
			else {
				res = FALSE;
				break;
			}
		}
	}

	return res;

}

gboolean
rspamd_has_html_tag (struct rspamd_task * task, GArray * args, void *unused)
{
	struct rspamd_mime_text_part *p;
	struct expression_argument *arg;
	guint 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;
	}

	for (i = 0; i < task->text_parts->len; i ++) {
		p = g_ptr_array_index (task->text_parts, i);

		if (!IS_PART_EMPTY (p) && IS_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;
	guint i;
	gboolean res = FALSE;

	for (i = 0; i < task->text_parts->len; i ++) {
		p = g_ptr_array_index (task->text_parts, i);

		if (!IS_PART_EMPTY (p) && IS_PART_HTML (p) && p->html->html_tags == NULL) {
			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 g_hash_table_lookup (task->raw_headers, arg->data) != NULL;
}

static gboolean
match_smtp_data (struct rspamd_task *task,
	struct expression_argument *arg,
	const gchar *what, gsize len)
{
	rspamd_regexp_t *re;
	gint r;

	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;
		}


		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 gchar *type, *str = NULL;
	guint 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->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 = 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 gboolean
rspamd_content_type_compare_param (struct rspamd_task * task,
	GArray * args,
	void *unused)
{
	const gchar *param_name;
	const gchar *param_data;
	rspamd_regexp_t *re;
	struct expression_argument *arg, *arg1, *arg_pattern;
	GMimeObject *part;
	GMimeContentType *ct;
	gint r;
	guint i;
	gboolean recursive = FALSE;
	struct rspamd_mime_part *cur_part;

	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);

	for (i = 0; i < task->parts->len; i ++) {
		cur_part = g_ptr_array_index (task->parts, i);
		part = cur_part->mime;
		ct = (GMimeContentType *)g_mime_object_get_content_type (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 (g_mime_content_type_is_type (ct, "multipart", "*")) {
				recursive = TRUE;
			}
		}
#ifndef GMIME24
		g_object_unref (part);
#endif

		if ((param_data =
				g_mime_content_type_get_parameter ((GMimeContentType *)ct,
						param_name)) != NULL) {
			if (arg_pattern->type == EXPRESSION_ARGUMENT_REGEXP) {
				re = arg_pattern->data;
				r = rspamd_regexp_search (re, param_data, 0,
							NULL, NULL, FALSE, NULL);

				if (r) {
					return TRUE;
				}
			}
			else {
				/* Just do strcasecmp */
				if (g_ascii_strcasecmp (param_data, arg_pattern->data) == 0) {
					return TRUE;
				}
			}
		}
		/* Get next part */
		if (!recursive) {
			break;
		}
	}

	return FALSE;
}

static gboolean
rspamd_content_type_has_param (struct rspamd_task * task,
	GArray * args,
	void *unused)
{
	gchar *param_name;
	const gchar *param_data;
	struct expression_argument *arg, *arg1;
	GMimeObject *part;
	GMimeContentType *ct;
	gboolean recursive = FALSE, result = FALSE;
	guint i;
	struct rspamd_mime_part *cur_part;

	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;

	for (i = 0; i < task->parts->len; i ++) {
		cur_part = g_ptr_array_index (task->parts, i);
		part = cur_part->mime;
		ct = (GMimeContentType *)g_mime_object_get_content_type (part);

		if (args->len >= 2) {
			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 (g_mime_content_type_is_type (ct, "multipart", "*")) {
				recursive = TRUE;
			}
		}

#ifndef GMIME24
		g_object_unref (part);
#endif
		if ((param_data =
				g_mime_content_type_get_parameter ((GMimeContentType *)ct,
						param_name)) != NULL) {
			return TRUE;
		}
		/* Get next part */
		if (!recursive) {
			break;
		}
	}

	return result;
}

static gboolean
rspamd_content_type_check (struct rspamd_task *task,
	GArray * args,
	gboolean check_subtype)
{
	const gchar *param_data;
	rspamd_regexp_t *re;
	struct expression_argument *arg1, *arg_pattern;
	GMimeObject *part;
	GMimeContentType *ct;
	gint r;
	guint 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);

	for (i = 0; i < task->parts->len; i ++) {
		cur_part = g_ptr_array_index (task->parts, i);
		part = cur_part->mime;
		ct = (GMimeContentType *)g_mime_object_get_content_type (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 (g_mime_content_type_is_type (ct, "multipart", "*")) {
				recursive = TRUE;
			}
		}

#ifndef GMIME24
		g_object_unref (part);
#endif
		if (check_subtype) {
			param_data = ct->subtype;
		}
		else {
			param_data = ct->type;
		}

		if (arg_pattern->type == EXPRESSION_ARGUMENT_REGEXP) {
			re = arg_pattern->data;
			r = rspamd_regexp_search (re, param_data, 0,
					NULL, NULL, FALSE, NULL);

			if (r) {
				return TRUE;
			}
		}
		else {
			/* Just do strcasecmp */
			if (g_ascii_strcasecmp (param_data, arg_pattern->data) == 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, GMimeContentType * ct,
	struct expression_argument *subtype)
{
	rspamd_regexp_t *re;
	gint r = 0;

	if (subtype == NULL || ct == NULL) {
		msg_warn_task ("invalid parameters passed");
		return FALSE;
	}
	if (subtype->type == EXPRESSION_ARGUMENT_REGEXP) {
		re = subtype->data;
		r = rspamd_regexp_search (re, ct->subtype, 0,
				NULL, NULL, FALSE, NULL);
	}
	else {
		/* Just do strcasecmp */
		if (ct->subtype && g_ascii_strcasecmp (ct->subtype, subtype->data) == 0) {
			return TRUE;
		}
	}

	return r;
}

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

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

static gboolean
common_has_content_part (struct rspamd_task * task,
	struct expression_argument *param_type,
	struct expression_argument *param_subtype,
	gint min_len,
	gint max_len)
{
	rspamd_regexp_t *re;
	struct rspamd_mime_part *part;
	GMimeContentType *ct;
	gint r;
	guint i;

	for (i = 0; i < task->parts->len; i ++) {
		part = g_ptr_array_index (task->parts, i);
		ct = part->type;

		if (ct == NULL) {
			continue;
		}

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

			r = rspamd_regexp_search (re, ct->type, 0,
					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 */
			if (ct->type && g_ascii_strcasecmp (ct->type, param_type->data) == 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;
	gint 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",
					(gchar *)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",
						(gchar *)arg->data,
						strerror (errno));
					return FALSE;
				}
			}
		}
	}

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