/*
 * Copyright 2025 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 "lua_common.h"
#include "lua_url.h"

#include "message.h"
#include "images.h"
#include "archives.h"
#include "utlist.h"
#include "unix-std.h"
#include "libmime/smtp_parsers.h"
#include "libserver/mempool_vars_internal.h"
#include "libserver/dkim.h"
#include "libserver/task.h"
#include "libserver/cfg_file_private.h"
#include "libserver/settings_merge.h"
#include "libmime/scan_result_private.h"
#include "libstat/stat_api.h"
#include "libserver/maps/map_helpers.h"

#include <math.h>
#include "libmime/received.h"
#include "libserver/html/html_url_rewrite_c.h"
#include "lua_html_url_rewrite.h"

/***
 * @module rspamd_task
 * This module provides routines for tasks manipulation in rspamd. Tasks usually
 * represent messages being scanned, and this API provides access to such elements
 * as headers, symbols, metrics and so on and so forth. Normally, task objects
 * are passed to the lua callbacks allowing to check specific properties of messages
 * and add the corresponding symbols to the scan's results.
@example
rspamd_config.DATE_IN_PAST = function(task)
	local dm = task:get_date{format = 'message', gmt = true}
	local dt = task:get_date{format = 'connect', gmt = true}
	-- A day
	if dt - dm > 86400 then
		return true
	end

	return false
end
 */

/* Task methods */

/***
 * @function rspamd_task.create([cfg])
 * Create a new empty task
 * @return {rspamd_task} new task
 */
LUA_FUNCTION_DEF(task, create);
/***
 * @function rspamd_task.load_from_file(filename[, cfg])
 * Loads a message from specific file
 * @return {boolean,rspamd_task|error} status + new task or error message
 */
LUA_FUNCTION_DEF(task, load_from_file);
/***
 * @function rspamd_task.load_from_string(message[, cfg])
 * Loads a message from specific file
 * @return {boolean,rspamd_task|error} status + new task or error message
 */
LUA_FUNCTION_DEF(task, load_from_string);
/***
 * @method task:get_message()
 * Returns task raw message content as opaque text
 * @return {rspamd_text} task raw content
 */
LUA_FUNCTION_DEF(task, get_message);
/***
 * @method task:set_message(msg)
 * Updates task message with another message; It also parses a message to
 * fill the internal structures.
 * Input might be a string, a lua_text or a table of the former stuff.
 * @param {string/text/table} msg new message to set
 * @return {boolean,number} if a message has been set + its raw size
 */
LUA_FUNCTION_DEF(task, set_message);
/***
 * @method task:process_message()
 * Parses message
 */
LUA_FUNCTION_DEF(task, process_message);
/***
 * @method task:get_cfg()
 * Get configuration object for a task.
 * @return {rspamd_config} (config.md)[configuration object] for the task
 */
LUA_FUNCTION_DEF(task, get_cfg);
LUA_FUNCTION_DEF(task, set_cfg);
LUA_FUNCTION_DEF(task, destroy);
/***
 * @method task:get_mempool()
 * Returns memory pool valid for a lifetime of task. It is used internally by
 * many rspamd routines.
 * @return {rspamd_mempool} memory pool object
 */
LUA_FUNCTION_DEF(task, get_mempool);
/***
 * @method task:get_session()
 * Returns asynchronous session object that is used by many rspamd asynchronous
 * utilities internally.
 * @return {rspamd_session} session object
 */
LUA_FUNCTION_DEF(task, get_session);
/***
 * @method task:set_session(session)
 * Sets new async session for a task
 */
LUA_FUNCTION_DEF(task, set_session);
/***
 * @method task:get_ev_base()
 * Return asynchronous event base for using in callbacks and resolver.
 * @return {rspamd_ev_base} event base
 */
LUA_FUNCTION_DEF(task, get_ev_base);
/***
 * @method task:get_worker()
 * Returns a worker object associated with the task
 * @return {rspamd_worker} worker object
 */
LUA_FUNCTION_DEF(task, get_worker);
/***
 * @method task:insert_result([enforce_symbol,]symbol, weight[, option1, ...])
 * Insert specific symbol to the tasks scanning results assigning the initial
 * weight to it.
 * @param {boolean} enforce_symbol if represented and true, then insert symbol even if it is not registered in the metric
 * @param {string} symbol symbol to insert
 * @param {number} weight initial weight (this weight is multiplied by the metric weight)
 * @param {string} options list of optional options attached to a symbol inserted
@example
local function cb(task)
	if task:get_header('Some header') then
		task:insert_result('SOME_HEADER', 1.0, 'Got some header')
	end
end
 */
LUA_FUNCTION_DEF(task, insert_result);
/***
 * @method task:insert_result_named(shadow_result, [enforce_symbol,]symbol, weight[, option1, ...])
 * Insert specific symbol to the tasks scanning results assigning the initial
 * weight to it.
 * @param {string} shadow_result name of shadow result
 * @param {boolean} enforce_symbol if represented and true, then insert symbol even if it is not registered in the metric
 * @param {string} symbol symbol to insert
 * @param {number} weight initial weight (this weight is multiplied by the metric weight)
 * @param {string} options list of optional options attached to a symbol inserted
 */
LUA_FUNCTION_DEF(task, insert_result_named);

/***
 * @method task:adjust_result(symbol, score[, option1, ...])
 * Alters the existing symbol's score to a new score. It is not affected by
 * metric score or grow factor. You can also add new options
 * using this method. Symbol must be already inserted into metric or an error
 * will be emitted.
 * @param {string} symbol symbol to adjust
 * @param {number} score this value is NOT multiplied by the metric score
 * @param {string/table} options list of optional options attached to a symbol adjusted
 */
LUA_FUNCTION_DEF(task, adjust_result);

/***
 * @method task:remove_result(symbol[, shadow_result])
 * Removes the symbol from a named or unnamed/default result
 * @param {string} symbol symbol to remove
 * @param {string} shadow_result name of shadow result
 * @return {boolean} true if a symbol has been removed
 */
LUA_FUNCTION_DEF(task, remove_result);
/***
 * @method task:set_pre_result(action, [message, [module], [score], [priority], [flags])
 * Sets pre-result for a task. It is used in pre-filters to specify early result
 * of the task scanned. If a pre-filter sets some result, then further processing
 * may be skipped. For selecting action it is possible to use global table
 * `rspamd_actions` or a string value:
 *
 * - `reject`: reject message permanently
 * - `add header`: add spam header
 * - `rewrite subject`: rewrite subject to spam subject
 * - `greylist`: greylist message
 * - `accept` or `no action`: whitelist message
 * - `quarantine`: quarantine the message; depending on the MTA, it can be done via HOLD.
 *   Note: the message parameter must be `nil` for proper operation.
 * - `discard`: drop the message from the queue without returning an error to the sender.
 *
 * Actions should be defined in the `actions.conf` configuration file.
 *
 * This function also accepts a table from Rspamd 2.6 with the following keys:
 * - action: string required
 * - message: string
 * - module: string
 * - score: number
 * - priority: integer
 * - flags: flags string
 * - result: named result if needed
 *
 * @param {rspamd_action or string} action a numeric or string action value
 * @param {string} message action message
 * @param {string} module optional module name
 * @param {number/nil} score optional explicit score
 * @param {number/nil} priority optional explicit priority
 * @param {string/nil} flags optional flags (e.g. 'least' for least action)
@example
local function cb(task)
	local gr = task:get_header('Greylist')
	if gr and gr == 'greylist' then
		task:set_pre_result('soft reject', 'Greylisting required')
	end
end
 */
LUA_FUNCTION_DEF(task, set_pre_result);

/***
 * @method task:has_pre_result()
 * Returns true if task has some pre-result being set.
 * If result has been set this function also returns pre result action,
 * message and module as strings in this order.
 *
 * @return {boolean,[string,string,string]} true if task has some pre-result being set
 */
LUA_FUNCTION_DEF(task, has_pre_result);
/***
 * @method task:append_message(message, [category])
 * Adds a message to scanning output.
 * @param {string} message
 * @param {category} message category
@example
local function cb(task)
	task:append_message('Example message')
end
 */
LUA_FUNCTION_DEF(task, append_message);
/***
 * @method task:get_urls([need_emails|list_protos][, need_images])
 * Get all URLs found in a message. Telephone urls and emails are not included unless explicitly asked in `list_protos`
 * Content URLs (extracted from PDF and other content types) are included by default unless
 * `include_content_urls` global option is set to false.
 * @param {boolean} need_emails if `true` then return also email urls, this can be a comma separated string of protocols desired or a table (e.g. `mailto` or `telephone`)
 * @param {boolean} need_images return urls from images (<img src=...>) as well
 * @return {table rspamd_url} list of all urls found
@example
local function phishing_cb(task)
	local urls = task:get_urls({'https', 'http'});

	if urls then
		for _,url in ipairs(urls) do
			if url:is_phished() then
				return true
			end
		end
	end
	return false
end
 */
LUA_FUNCTION_DEF(task, get_urls);
/***
 * @method task:get_urls_filtered([{flags_include}, [{flags_exclude}]], [{protocols_mask}])
 * Get urls managed by either exclude or include flags list
 * - If flags include are nil then all but excluded urls are returned
 * - If flags exclude are nil then only included explicitly urls are returned
 * - If both parameters are nil then all urls are included
 * @param {table} flags_include included flags
 * @param {table} flags_exclude excluded flags
 * @param {table} protocols_mask include only specific protocols
 * @return {table rspamd_url} list of urls matching conditions
 */
LUA_FUNCTION_DEF(task, get_urls_filtered);
/***
 * @method task:has_urls([need_emails])
 * Returns 'true' if a task has urls listed
 * @param {boolean} need_emails if `true` then return also email urls
 * @return {boolean} true if a task has urls (urls or emails if `need_emails` is true)
 */
LUA_FUNCTION_DEF(task, has_urls);
/***
 * @method task:inject_url(url)
 * Inserts an url into a task (useful for redirected urls)
 * @param {lua_url} url url to inject
 */
LUA_FUNCTION_DEF(task, inject_url);
/***
 * @method task:get_content()
 * Get raw content for the specified task
 * @return {text} the data contained in the task
 */
LUA_FUNCTION_DEF(task, get_content);

/***
 * @method task:get_filename()
 * Returns filename for a specific task
 * @return {string|nil} filename or nil if unknown
 */
LUA_FUNCTION_DEF(task, get_filename);

/***
 * @method task:get_rawbody()
 * Get raw body for the specified task
 * @return {text} the data contained in the task
 */
LUA_FUNCTION_DEF(task, get_rawbody);

/***
 * @method task:get_emails()
 * Get all email addresses found in a message.
 * @return {table rspamd_url} list of all email addresses found
 */
LUA_FUNCTION_DEF(task, get_emails);
/***
 * @method task:inject_part(type, content[, original_part])
 * Injects a virtual mime part into the task structure
 * @param {string} type part type (currently only "text" is supported)
 * @param {string/text/table} content part content (accepts string, rspamd_text, or table of rspamd_text chunks - will be efficiently concatenated in C)
 * @param {rspamd_mimepart} original_part optional original mime part that this injected part is derived from (sets parent relationship)
 * @return {boolean} true if part was injected
 */
LUA_FUNCTION_DEF(task, inject_part);
/***
 * @method task:get_text_parts([include_virtual])
 * Get all text (and HTML) parts found in a message. By default, injected/virtual parts are excluded.
 * @param {boolean} include_virtual if true, include injected/virtual parts (default: false)
 * @return {table rspamd_text_part} list of text parts
 */
LUA_FUNCTION_DEF(task, get_text_parts);
/***
 * @method task:get_parts([include_virtual])
 * Get all mime parts found in a message. By default, injected/virtual parts are excluded.
 * @param {boolean} include_virtual if true, include injected/virtual parts (default: false)
 * @return {table rspamd_mime_part} list of mime parts
 */
LUA_FUNCTION_DEF(task, get_parts);
/***
 * @method task:get_meta_words([how='stem'])
 * Get meta words from task (subject and displayed names)
 * - `stem`: stemmed words (default)
 * - `norm`: normalised words (utf normalised + lowercased)
 * - `raw`: raw words in utf (if possible)
 * - `full`: list of tables, each table has the following fields:
 *   - [1] - stemmed word
 *   - [2] - normalised word
 *   - [3] - raw word
 *   - [4] - flags (table of strings)
 */
LUA_FUNCTION_DEF(task, get_meta_words);
/***
 * @method task:get_request_header(name)
 * Get value of a HTTP request header.
 * @param {string} name name of header to get
 * @return {rspamd_text} value of an HTTP header
 */
LUA_FUNCTION_DEF(task, get_request_header);
/***
 * @method task:set_request_header(name, value)
 * Set value of a HTTP request header. If value is omitted, then a header is removed
 * @param {string} name name of header to get
 * @param {rspamd_text/string} value new header's value
 */
LUA_FUNCTION_DEF(task, set_request_header);
/***
 * @method task:get_subject()
 * Returns task subject (either from the protocol override or from a header)
 * @return {string} value of a subject (decoded)
 */
LUA_FUNCTION_DEF(task, get_subject);
/***
 * @method task:get_header(name[, case_sensitive])
 * Get decoded value of a header specified with optional case_sensitive flag.
 * By default headers are searched in caseless matter.
 * @param {string} name name of header to get
 * @param {boolean} case_sensitive case sensitiveness flag to search for a header
 * @return {string} decoded value of a header
 */
LUA_FUNCTION_DEF(task, get_header);
/***
 * @method task:has_header(name[, case_sensitive])
 * Get decoded value of a header specified with optional case_sensitive flag.
 * By default headers are searched in the case insensitive matter.
 * @param {string} name name of header to get
 * @param {boolean} case_sensitive case sensitiveness flag to search for a header
 * @return {boolean} true if header exists
 */
LUA_FUNCTION_DEF(task, has_header);
/***
 * @method task:get_header_raw(name[, case_sensitive])
 * Get raw value of a header specified with optional case_sensitive flag.
 * By default headers are searched in caseless matter.
 * @param {string} name name of header to get
 * @param {boolean} case_sensitive case sensitiveness flag to search for a header
 * @return {string} raw value of a header
 */
LUA_FUNCTION_DEF(task, get_header_raw);
/***
 * @method task:get_header_full(name[, case_sensitive[, need_modified]])
 * Get raw value of a header specified with optional case_sensitive flag.
 * By default headers are searched in caseless matter. This method returns more
 * information about the header as a list of tables with the following structure:
 *
 * - `name` - name of a header
 * - `value` - raw value of a header
 * - `decoded` - decoded value of a header
 * - `tab_separated` - `true` if a header and a value are separated by `tab` character
 * - `empty_separator` - `true` if there are no separator between a header and a value
 * @param {string} name name of header to get
 * @param {boolean} case_sensitive case sensitiveness flag to search for a header
 * @param {boolean} need_modified return a modified value of a header if presented
 * @return {list of tables} all values of a header as specified above
@example
function check_header_delimiter_tab(task, header_name)
	for _,rh in ipairs(task:get_header_full(header_name)) do
		if rh['tab_separated'] then return true end
	end
	return false
end
 */
LUA_FUNCTION_DEF(task, get_header_full);
/***
 * @method task:get_header_count(name[, case_sensitive])
 * Lightweight version if you need just a header's count
 *  * By default headers are searched in caseless matter.
 * @param {string} name name of header to get
 * @param {boolean} case_sensitive case sensitiveness flag to search for a header
 * @return {number} number of header's occurrences or 0 if not found
 */
LUA_FUNCTION_DEF(task, get_header_count);
/***
 * @method task:get_raw_headers()
 * Get all undecoded headers of a message as a string
 * @return {rspamd_text} all raw headers for a message as opaque text
 */
LUA_FUNCTION_DEF(task, get_raw_headers);

/***
 * @method task:get_headers([need_modified=false])
 * Get all headers of a message in the same format as get_header_full
 * @return {table of headers data} all headers for a message
 */
LUA_FUNCTION_DEF(task, get_headers);

/***
 * @method task:modify_header(name, mods)
 * Modify an existing or non-existing header with the name `name`
 * Mods is a table with the following structure:
 * {
 *   "add" = { {order, value}, {order, value} },
 *   "remove" = {order, order, order}
 * }
 * Modifications are evaluated in order: remove, add, so headers are first
 * removed (if any) and then added
 * Order in remove starts from 1, where 0 means 'remove all', and negative value means
 * remove from the end
 * Order in addition means addition from the top: 0 means the most top header, 1 one after, etc
 * negative order means addition to the end, e.g. -1 is appending header.
 * @return {bool} true if header could be modified (always true unless we don't have an unparsed message)
 */
LUA_FUNCTION_DEF(task, modify_header);

/***
 * @method task:get_received_headers()
 * Returns a list of tables of parsed received headers. A tables returned have
 * the following structure:
 *
 * - `from_hostname` - string that represents hostname provided by a peer
 * - `from_ip` - string representation of sending IP address
 * - `real_hostname` - hostname as resolved by MTA
 * - `real_ip` - rspamd_ip object representing sending IP address
 * - `by_hostname` - MTA hostname
 * - `proto` - protocol, e.g. ESMTP or ESMTPS
 * - `timestamp` - received timestamp
 * - `for` - for value (unparsed mailbox)
 *
 * Please note that in some situations rspamd cannot parse all the fields of received headers.
 * In that case you should check all strings for validity.
 * @return {table of tables} list of received headers described above
 */
LUA_FUNCTION_DEF(task, get_received_headers);
/***
 * @method task:get_queue_id()
 * Returns queue ID of the message being processed.
 */
LUA_FUNCTION_DEF(task, get_queue_id);
/***
 * @method task:get_uid()
 * Returns ID of the task being processed.
 */
LUA_FUNCTION_DEF(task, get_uid);
/***
 * @method task:get_uuid()
 * Returns UUID v7 of the task (time-ordered, suitable for cross-system correlation).
 */
LUA_FUNCTION_DEF(task, get_uuid);
/***
 * @method task:get_resolver()
 * Returns ready to use rspamd_resolver object suitable for making asynchronous DNS requests.
 * @return {rspamd_resolver} resolver object associated with the task's session
 * @example
local logger = require "rspamd_logger"

local function task_cb(task)
	local function dns_cb(resolver, to_resolve, results, err)
		-- task object is available due to closure
		task:inc_dns_req()
		if results then
			logger.info(string.format('<%s> [%s] resolved for symbol: %s',
				task:get_message_id(), to_resolve, 'EXAMPLE_SYMBOL'))
			task:insert_result('EXAMPLE_SYMBOL', 1)
		end
	end
	local r = task:get_resolver()
	r:resolve_a(task:get_session(), task:get_mempool(), 'example.com', dns_cb)
end
 */
LUA_FUNCTION_DEF(task, get_resolver);
/***
 * @method task:set_resolver(resolver)
 * Sets rspamd_resolver for a specified task.
 */
LUA_FUNCTION_DEF(task, set_resolver);
/***
* @method task:inc_dns_req()
* Increment number of DNS requests for the task. Is used just for logging purposes.
*/
LUA_FUNCTION_DEF(task, inc_dns_req);
/***
 * @method task:get_dns_req()
 * Get number of dns requests being sent in the task
 * @return {number} number of DNS requests
 */
LUA_FUNCTION_DEF(task, get_dns_req);

/***
 * @method task:has_recipients([type])
 * Return true if there are SMTP or MIME recipients for a task.
 * @param {integer|string} type if specified has the following meaning: `0` or `any` means try SMTP recipients and fallback to MIME if failed, `1` or `smtp` means checking merely SMTP recipients and `2` or `mime` means MIME recipients only
 * @return {bool,integer} `true` if there are recipients of the following type and a number of such a recipients excluding artificial ones
 */
LUA_FUNCTION_DEF(task, has_recipients);

/***
 * @method task:get_recipients([type])
 * Return SMTP or MIME recipients for a task. This function returns list of internet addresses each one is a table with the following structure:
 *
 * - `name` - name of internet address in UTF8, e.g. for `Vsevolod Stakhov <blah@foo.com>` it returns `Vsevolod Stakhov`
 * - `addr` - address part of the address
 * - `user` - user part (if present) of the address, e.g. `blah`
 * - `domain` - domain part (if present), e.g. `foo.com`
 * @param {integer|string} type if specified has the following meaning: `0` or `any` means try SMTP recipients and fallback to MIME if failed, `1` or `smtp` means checking merely SMTP recipients and `2` or `mime` means MIME recipients only
 * @return {list of addresses} list of recipients or `nil`
 */
LUA_FUNCTION_DEF(task, get_recipients);

/***
 * @method task:get_principal_recipient()
 * Returns a single string with so called `principal recipient` for a message. The order
 * of check is the following:
 *
 * - deliver-to request header
 * - the first recipient (envelope)
 * - the first recipient (mime)
 * @return {string} principal recipient
 */
LUA_FUNCTION_DEF(task, get_principal_recipient);
/***
 * @method task:get_reply_sender()
 * Returns a single string with address that should be used to reply on a message
 *
 * - reply-to header
 * - from header
 * - smtp from as a last resort
 * @return {address} email address
 */
LUA_FUNCTION_DEF(task, get_reply_sender);

/***
 * @method task:set_recipients([type], {rcpt1, rcpt2...}, [how='add'])
 * Sets recipients for a task. This function accepts table that will be converted to the address.
 * If some fields are missing they are subsequently reconstructed by this function. E.g. if you
 * specify 'user' and 'domain', then address and raw string will be reconstructed
 *
 * - `name` - name of internet address in UTF8, e.g. for `Vsevolod Stakhov <blah@foo.com>` it returns `Vsevolod Stakhov`
 * - `addr` - address part of the address
 * - `user` - user part (if present) of the address, e.g. `blah`
 * - `domain` - domain part (if present), e.g. `foo.com`
 * @param {integer|string} type if specified has the following meaning: `0` or `any` means try SMTP recipients and fallback to MIME if failed, `1` or `smtp` means checking merely SMTP recipients and `2` or `mime` means MIME recipients only
 * @param {list of tables} recipients recipients to set
 * @param {string} how define how to set recipients: `rewrite` - rewrite existing recipients, `alias` - treat existing recipients as aliased recipients, `add` - add new recipients
 * @return {boolean} result of the operation
 */
LUA_FUNCTION_DEF(task, set_recipients);

/***
 * @method task:has_from([type])
 * Return true if there is SMTP or MIME sender for a task.
 * @param {integer|string} type if specified has the following meaning: `0` or `any` means try SMTP recipients and fallback to MIME if failed, `1` or `smtp` means checking merely SMTP recipients and `2` or `mime` means MIME recipients only
 * @return {bool} `true` if there is sender of the following type
 */
LUA_FUNCTION_DEF(task, has_from);

/***
 * @method task:get_from([type])
 * Return SMTP or MIME sender for a task. This function returns an internet address which one is a table with the following structure:
 *
 * - `raw` - the original value without any processing
 * - `name` - name of internet address in UTF8, e.g. for `Vsevolod Stakhov <blah@foo.com>` it returns `Vsevolod Stakhov`
 * - `addr` - address part of the address
 * - `user` - user part (if present) of the address, e.g. `blah`
 * - `domain` - domain part (if present), e.g. `foo.com`
 * - `flags` - table with following keys set to true if given condition fulfilled:
 *   - [valid] - valid SMTP address in conformity with https://tools.ietf.org/html/rfc5321#section-4.1.
 *   - [ip] - domain is IPv4/IPv6 address
 *   - [braced] - angled `<blah@foo.com>` address
 *   - [quoted] - quoted user part
 *   - [empty] - empty address
 *   - [backslash] - user part contains backslash
 *   - [8bit] - contains 8bit characters
 * @param {integer|string} type if specified has the following meaning: `0` or `any` means try SMTP sender and fallback to MIME if failed, `1` or `smtp` means checking merely SMTP sender and `2` or `mime` means MIME `From:` only
 * @return {address} sender or `nil`
 */
LUA_FUNCTION_DEF(task, get_from);

/***
 * @method task:set_from(type, addr)
 * Sets sender for a task. This function accepts table that will be converted to the address.
 * If some fields are missing they are subsequently reconstructed by this function. E.g. if you
 * specify 'user' and 'domain', then address and raw string will be reconstructed
 *
 * - `name` - name of internet address in UTF8, e.g. for `Vsevolod Stakhov <blah@foo.com>` it returns `Vsevolod Stakhov`
 * - `addr` - address part of the address
 * - `user` - user part (if present) of the address, e.g. `blah`
 * - `domain` - domain part (if present), e.g. `foo.com`
 * @param {integer|string} type if specified has the following meaning: `0` or `any` means try SMTP sender and fallback to MIME if failed, `1` or `smtp` means checking merely SMTP sender and `2` or `mime` means MIME `From:` only
 * @param {table
 * @return {boolean} success or not
 */
LUA_FUNCTION_DEF(task, set_from);

/***
 * @method task:get_mail_esmtp_args()
 * Returns a table of ESMTP arguments from MAIL FROM command (milter only).
 * Each argument is a key-value pair where the key is the argument name and
 * the value is the argument value (or empty string if no value).
 * @return {table} ESMTP arguments or nil if not available
 * @example
 * local esmtp_args = task:get_mail_esmtp_args()
 * if esmtp_args then
 *   -- Check for DSN arguments
 *   local ret = esmtp_args['RET']
 *   local envid = esmtp_args['ENVID']
 * end
 */
LUA_FUNCTION_DEF(task, get_mail_esmtp_args);

/***
 * @method task:get_rcpt_esmtp_args([idx])
 * Returns a table of ESMTP arguments from RCPT TO command (milter only).
 * If idx is specified, returns arguments for the recipient at that index (0-based).
 * If idx is not specified, returns an array of tables, one per recipient.
 * Each argument is a key-value pair where the key is the argument name and
 * the value is the argument value (or empty string if no value).
 * @param {integer} idx optional index of the recipient (0-based)
 * @return {table|array of tables} ESMTP arguments or nil if not available
 * @example
 * -- Get args for all recipients
 * local rcpt_args = task:get_rcpt_esmtp_args()
 * if rcpt_args then
 *   for i, args in ipairs(rcpt_args) do
 *     if args and args['NOTIFY'] then
 *       -- Process NOTIFY argument
 *     end
 *   end
 * end
 *
 * -- Get args for first recipient only
 * local first_rcpt_args = task:get_rcpt_esmtp_args(0)
 */
LUA_FUNCTION_DEF(task, get_rcpt_esmtp_args);

/***
 * @method task:get_user()
 * Returns authenticated user name for this task if specified by an MTA.
 * @return {string} username or nil
 */
LUA_FUNCTION_DEF(task, get_user);
/***
 * @method task:set_user([username])
 * Sets or resets (if username is not specified) authenticated user name for this task.
 * @return {string} the previously set username or nil
 */
LUA_FUNCTION_DEF(task, set_user);
/***
 * @method task:get_from_ip()
 * Returns [rspamd_ip](rspamd_ip.md) object of a sender that is provided by MTA
 * @return {rspamd_ip} ip address object
 */
LUA_FUNCTION_DEF(task, get_from_ip);
/***
 * @method task:set_from_ip(str)
 * Set tasks's IP address based on the passed string
 * @param {string} str string representation of ip
 */
LUA_FUNCTION_DEF(task, set_from_ip);
LUA_FUNCTION_DEF(task, get_from_ip_num);
/***
 * @method task:get_client_ip()
 * Returns [rspamd_ip](rspamd_ip.md) object of a client connected to rspamd (normally, it is an IP address of MTA)
 * @return {rspamd_ip} ip address object
 */
LUA_FUNCTION_DEF(task, get_client_ip);
/***
 * @method task:get_helo()
 * Returns the value of SMTP helo provided by MTA.
 * @return {string} HELO value
 */
LUA_FUNCTION_DEF(task, get_helo);
LUA_FUNCTION_DEF(task, set_helo);
/***
 * @method task:get_hostname()
 * Returns the value of sender's hostname provided by MTA
 * @return {string} hostname value
 */
LUA_FUNCTION_DEF(task, get_hostname);
LUA_FUNCTION_DEF(task, set_hostname);
/***
 * @method task:get_images()
 * Returns list of all images found in a task as a table of `rspamd_image`.
 * Each image has the following methods:
 *
 * * `get_width` - return width of an image in pixels
 * * `get_height` - return height of an image in pixels
 * * `get_type` - return string representation of image's type (e.g. 'jpeg')
 * * `get_filename` - return string with image's file name
 * * `get_size` - return size in bytes
 * @return {list of rspamd_image} images found in a message
 */
LUA_FUNCTION_DEF(task, get_images);
/***
 * @method task:get_archives()
 * Returns list of all archives found in a task as a table of `rspamd_archive`.
 * Each archive has the following methods available:
 *
 * * `get_files` - return list of strings with filenames inside archive
 * * `get_files_full` - return list of tables with all information about files
 * * `is_encrypted` - return true if an archive is encrypted
 * * `get_type` - return string representation of image's type (e.g. 'zip')
 * * `get_filename` - return string with archive's file name
 * * `get_size` - return size in bytes
 * @return {list of rspamd_archive} archives found in a message
 */
LUA_FUNCTION_DEF(task, get_archives);
/***
 * @method task:get_dkim_results()
 * Returns list of all dkim check results as table of maps. Callee must ensure that
 * dkim checks have been completed by adding dependency on `DKIM_TRACE` symbol.
 * Fields in map:
 *
 * * `result` - string result of check:
 *    - `reject`
 *    - `allow`
 *    - `tempfail`
 *    - `permfail`
 *    - `not found`
 *    - `bad record`
 * * `domain` - dkim domain
 * * `selector` - dkim selector
 * * `bhash` - short version of b tag (8 base64 symbols)
 * * `fail_reason` - reason of failure (if applicable)
 * @return {list of maps} dkim check results
 */
LUA_FUNCTION_DEF(task, get_dkim_results);
/***
 * @method task:get_symbol(name, [shadow_result_name])
 * Searches for a symbol `name` in all metrics results and returns a list of tables
 * one per metric that describes the symbol inserted.
 * Please note, that for using this function you need to ensure that the symbol
 * being queried is already checked. This is guaranteed if there is a dependency
 * between the caller symbol and the checked symbol (either virtual or real).
 * Please check `rspamd_config:register_dependency` method for details.
 * The symbols are returned as the list of the following tables:
 *
 * - `metric` - name of metric
 * - `score` - score of a symbol in that metric
 * - `options` - a table of strings representing options of a symbol
 * - `group` - a group of symbol (or 'ungrouped')
 * @param {string} name symbol's name
 * @return {list of tables} list of tables or nil if symbol was not found
 */
LUA_FUNCTION_DEF(task, get_symbol);
/***
 * @method task:get_symbols_all()
 * Returns array of symbols matched in default metric with all metadata
 * @return {table} table of tables formatted as in `task:get_symbol()` except that `metric` is absent and `name` is added
 */
LUA_FUNCTION_DEF(task, get_symbols_all);
/***
 * @method task:get_symbols([shadow_result_name])
 * Returns array of all symbols matched for this task
 * @return {table, table} table of strings with symbols names + table of theirs scores
 */
LUA_FUNCTION_DEF(task, get_symbols);

/***
 * @method task:get_groups([need_private])
 * Returns a map [group -> group_score] for matched group. If `need_private` is
 * unspecified, then the global option `public_groups_only` is used for default.
 * @return {table, number} a map [group -> group_score]
 */
LUA_FUNCTION_DEF(task, get_groups);

/***
 * @method task:get_symbols_numeric()
 * Returns array of all symbols matched for this task
 * @return {table|number, table|number} table of numbers with symbols ids + table of theirs scores
 */
LUA_FUNCTION_DEF(task, get_symbols_numeric);

/***
 * @method task:get_symbols_tokens()
 * Returns array of all symbols as statistical tokens
 * @return {table|number} table of numbers
 */
LUA_FUNCTION_DEF(task, get_symbols_tokens);

/***
 * @method task:process_ann_tokens(symbols, ann_tokens, offset, [min])
 * Processes ann tokens
 * @param {table|string} symbols list of symbols in this profile
 * @param {table|number} ann_tokens list of tokens (including metatokens)
 * @param {integer} offset offset for symbols token (#metatokens)
 * @param {number} min minimum value for symbols found (e.g. for 0 score symbols)
 * @return nothing
 */
LUA_FUNCTION_DEF(task, process_ann_tokens);

/***
 * @method task:has_symbol(name, [shadow_result_name])
 * Fast path to check if a specified symbol is in the task's results.
 * Please note, that for using this function you need to ensure that the symbol
 * being queried is already checked. This is guaranteed if there is a dependency
 * between the caller symbol and the checked symbol (either virtual or real).
 * Please check `rspamd_config:register_dependency` method for details.
 * @param {string} name symbol's name
 * @return {boolean} `true` if symbol has been found
 */
LUA_FUNCTION_DEF(task, has_symbol);
/***
 * @method task:enable_symbol(name)
 * Enable specified symbol for this particular task
 * @param {string} name symbol's name
 * @return {boolean} `true` if symbol has been found
 */
LUA_FUNCTION_DEF(task, enable_symbol);
/***
 * @method task:disable_symbol(name)
 * Disable specified symbol for this particular task
 * @param {string} name symbol's name
 * @return {boolean} `true` if symbol has been found
 */
LUA_FUNCTION_DEF(task, disable_symbol);
/***
 * @method task:get_date(type[, gmt])
 * Returns timestamp for a connection or for a MIME message. This function can be called with a
 * single table arguments with the following fields:
 *
 * * `format` - a format of date returned:
 * 	- `message` - returns a mime date as integer (unix timestamp)
 * 	- `connect` - returns a unix timestamp of a connection to rspamd
 * * `gmt` - returns date in `GMT` timezone (normal for unix timestamps)
 *
 * By default this function returns connection time in numeric format.
 * @param {string} type date format as described above
 * @param {boolean} gmt gmt flag as described above
 * @return {string/number} date representation according to format
 * @example
rspamd_config.DATE_IN_PAST = function(task)
	local dm = task:get_date{format = 'message', gmt = true}
	local dt = task:get_date{format = 'connect', gmt = true}
	-- A day
	if dt - dm > 86400 then
		return true
	end

	return false
end
 */
LUA_FUNCTION_DEF(task, get_date);
/***
 * @method task:get_message_id()
 * Returns message identifier from the `Message-ID` header.  Angle brackets (`<>`)
 * are stripped off if present.  If a Message-ID header is missing `undef` is
 * returned.
 * @return {string} ID of the message
 */
LUA_FUNCTION_DEF(task, get_message_id);
/***
 * @method task:get_timeval([raw])
 * Returns the timestamp for a task start processing time.
 * @param {boolean} raw if true then two float numbers are returned: task start timestamp and timeout event timestamp
 * @return {table} table with fields as described in `struct timeval` in C
 */
LUA_FUNCTION_DEF(task, get_timeval);
/***
 * @method task:get_scan_time([set])
 * Returns 2 floating point numbers: scan real time and scan virtual time.
 * If `set` is `true`, then the finishing time is also set (enabled by default).
 * This function should be normally called on idempotent phase.
 * @return {number,number} real and virtual times in seconds with floating point
 */
LUA_FUNCTION_DEF(task, get_scan_time);
/***
 * @method task:get_metric_result()
 * Get full result of a metric as a table:
 * - `score`: current score
 * - `action`: current action as a string
 * - `nnegative`: number of negative rules matched
 * - `npositive`: number of positive rules matched
 * - `positive_score`: total score for positive rules
 * - `negative_score`: total score for negative rules
 * - `passthrough`: set to true if message has a passthrough result
 * @return {table} metric result
 */
LUA_FUNCTION_DEF(task, get_metric_result);
/***
 * @method task:get_metric_score(name)
 * Get the current score of metric `name` (must be nil or 'default') . Should be used in idempotent filters only.
 * @param {string} name name of a metric
 * @return {number,number} 2 numbers containing the current score and required score of the metric
 */
LUA_FUNCTION_DEF(task, get_metric_score);

/***
 * @method task:get_metric_threshold(action)
 * Get the current threshold of the action `action` for the default metric. Should be used after settings have been applied.
 * @param {string} action name of a action
 * @return {number} the current threshold of the action
 */
LUA_FUNCTION_DEF(task, get_metric_threshold);
/***
 * @method task:get_metric_action(name)
 * Get the current action of metric `name` (must be nil or 'default'). Should be used in idempotent filters only.
 * @param {string} name name of a metric
 * @return {string} the current action of the metric as a string
 */
LUA_FUNCTION_DEF(task, get_metric_action);
/***
 * @method task:set_metric_score(name, score)
 * Set the current score of metric `name`. Should be used in post-filters only.
 * @param {string} name name of a metric
 * @param {number} score the current score of the metric
 */
LUA_FUNCTION_DEF(task, set_metric_score);
/***
 * @method task:set_metric_subject(subject)
 * Set the subject in the default metric
 * @param {string} subject subject to set
 */
LUA_FUNCTION_DEF(task, set_metric_subject);

/***
 * @method task:learn(is_spam[, classifier)
 * Learn classifier `classifier` with the task. If `is_spam` is true then message
 * is learnt as spam. Otherwise HAM is learnt. By default, this function learns
 * `bayes` classifier.
 * @param {boolean} is_spam learn spam or ham
 * @param {string} classifier classifier's name
 * @return {boolean} `true` if classifier has been learnt successfully
 */
LUA_FUNCTION_DEF(task, learn);
/***
 * @method task:set_settings(obj)
 * Set users settings object for a task. The format of this object is described
 * [here](https://rspamd.com/doc/configuration/settings.html).
 * @param {any} obj any lua object that corresponds to the settings format
 */
LUA_FUNCTION_DEF(task, set_settings);

/***
 * @method task:set_settings_id(id)
 * Set users settings id for a task (must be registered previously)
 * @available 2.0+
 * @param {number} id numeric id
 * @return {boolean} true if settings id has been replaced from the existing one
 */
LUA_FUNCTION_DEF(task, set_settings_id);

/***
 * @method task:merge_and_apply_settings(layers)
 * Merge multiple settings layers and apply the result.
 * Each layer is a table: {level=N, name="str", settings_id=N, apply={...}}
 * Layers are merged according to the settings merge rules.
 * @param {table} layers array of layer tables
 */
LUA_FUNCTION_DEF(task, merge_and_apply_settings);

/***
 * @method task:get_settings()
 * Gets users settings object for a task. The format of this object is described
 * [here](https://rspamd.com/doc/configuration/settings.html).
 * @return {lua object} lua object generated from UCL
 */
LUA_FUNCTION_DEF(task, get_settings);

/***
 * @method task:lookup_settings(key)
 * Gets users settings object with the specified key for a task.
 * @param {string} key key to lookup
 * @return {lua object} lua object generated from UCL
 */
LUA_FUNCTION_DEF(task, lookup_settings);

/***
 * @method task:get_settings_id()
 * Get numeric hash of settings id if specified for this task. 0 is returned otherwise.
 * @return {number} settings-id hash
 */
LUA_FUNCTION_DEF(task, get_settings_id);

/***
 * @method task:set_milter_reply(obj)
 * Set special reply for milter
 * @param {any} obj any lua object that corresponds to the settings format
 * @example
task:set_milter_reply({
	add_headers = {['X-Lua'] = 'test'},
	-- 1 is the position of header to remove
	remove_headers = {['DKIM-Signature'] = 1},
	-- change smtp from
	change_from = 'sender@example.com',
	-- add and/or remove smtp rcpts
	add_rcpt = {'new-rcpt1@example.com', 'new-rcpt2@example.com'},
	del_rcpt = {'old-rcpt1@example.com', 'old-rcpt2@example.com'},
})
 */
LUA_FUNCTION_DEF(task, set_milter_reply);

/***
 * @method task:process_re(params)
 * Processes the specified regexp and returns number of captures (cached or new)
 * Params is the table with the following fields (mandatory fields are marked with `*`):
 * - `re`* : regular expression object
 * - `type`*: type of regular expression:
 *   + `mime`: mime regexp
 *   + `header`: header regexp
 *   + `rawheader`: raw header expression
 *   + `rawmime`: raw mime regexp
 *   + `body`: raw body regexp
 *   + `url`: url regexp
 * - `header`: for header and rawheader regexp means the name of header
 * - `strong`: case sensitive match for headers
 * @return {number} number of regexp occurrences in the task (limited by 255 so far)
 */
LUA_FUNCTION_DEF(task, process_regexp);

/***
 * @method task:cache_set(key, value)
 * Store some value to the task cache
 * @param {string} key key to use
 * @param {any} value any value (including functions and tables)
 */
LUA_FUNCTION_DEF(task, cache_set);
/***
 * @method task:cache_get(key)
 * Returns cached value or nil if nothing is cached
 * @param {string} key key to use
 * @return {any} cached value
 */
LUA_FUNCTION_DEF(task, cache_get);

/***
 * @method task:get_size()
 * Returns size of the task in bytes (that includes headers + parts size)
 * @return {number} size in bytes
 */
LUA_FUNCTION_DEF(task, get_size);

/***
 * @method task:set_flag(flag_name[, set])
 * Set specific flag for task:
 *
 * - `no_log`: do not log task summary
 * - `no_stat`: do not include task into scanned stats
 * - `pass_all`: check all filters for task
 * - `extended_urls`: output extended info about urls
 * - `skip`: skip task processing
 * - `learn_spam`: learn message as spam
 * - `learn_ham`: learn message as ham
 * - `broken_headers`: header data is broken for a message
 * @param {string} flag to set
 * @param {boolean} set set or clear flag (default is set)
@example
--[[
For messages with undefined queue ID (scanned with rspamc or WebUI)
do not include results into statistics and do not log task summary
(it will not appear in the WebUI history as well).
]]--

-- Callback function to set flags
local function no_log_stat_cb(task)
  if not task:get_queue_id() then
    task:set_flag('no_log')
    task:set_flag('no_stat')
  end
end

rspamd_config:register_symbol({
  name = 'LOCAL_NO_LOG_STAT',
  type = 'postfilter',
  callback = no_log_stat_cb
})
 */
LUA_FUNCTION_DEF(task, set_flag);


/***
 * @method task:has_flag(flag_name)
 * Checks for a specific flag in task:
 *
 * - `no_log`: do not log task summary
 * - `no_stat`: do not include task into scanned stats
 * - `pass_all`: check all filters for task
 * - `extended_urls`: output extended info about urls
 * - `skip`: skip task processing
 * - `learn_spam`: learn message as spam
 * - `learn_ham`: learn message as ham
 * - `broken_headers`: header data is broken for a message
 * @param {string} flag to check
 * @return {boolean} true if flags is set
 */
LUA_FUNCTION_DEF(task, has_flag);


/***
 * @method task:get_flags()
 * Get list of flags for task:
 *
 * - `no_log`: do not log task summary
 * - `no_stat`: do not include task into scanned stats
 * - `pass_all`: check all filters for task
 * - `extended_urls`: output extended info about urls
 * - `skip`: skip task processing
 * - `learn_spam`: learn message as spam
 * - `learn_ham`: learn message as ham
 * - `broken_headers`: header data is broken for a message
 * - `milter`: task is initiated by milter connection
 * @return {array of strings} table with all flags as strings
 */
LUA_FUNCTION_DEF(task, get_flags);

/***
 * @method task:get_digest()
 * Returns message's unique digest (32 hex symbols)
 * @return {string} hex digest
 */
LUA_FUNCTION_DEF(task, get_digest);

/***
 * @method task:store_in_file([mode|table])
 * If task was loaded using file scan, then this method just returns its name,
 * otherwise, a fresh temporary file is created and its name is returned. Default
 * mode is 0600. To convert lua number to the octal mode you can use the following
 * trick: `tonumber("0644", 8)`. The file is automatically removed when task is
 * destroyed.
 *
 * If table argument is specified, the following extra fields are allowed:
 *
 * - `mode`: same as mode argument
 * - `force_new`: always create a new file
 * - `filename`: use specific filename instead of a temporary one
 * - `tmpmask`: use specific tempmask, e.g. '/tmp/file-XXXXX', where XXXX will be replaced by some random letters
 * - `keep`: do not remove file after task is processed
 *
 * @param {number} mode mode for new file
 * @return {string} file name with task content
 */
LUA_FUNCTION_DEF(task, store_in_file);

/***
 * @method task:get_protocol_reply([flags])
 * This method being called from a **postfilter** will return reply for a message
 * as it is returned to a client. This method returns the Lua table corresponding
 * to the UCL object. Flags is a table that specify which information should be
 * there in a reply:
 *
 * - `basic`: basic info, such as message-id
 * - `metrics`: metrics and symbols
 * - `messages`: messages
 * - `dkim`: dkim signature
 * - `milter`: milter control block
 * - `extra`: extra data, such as profiling
 * - `urls`: list of all urls in a message
 *
 * @param {table} flags table of flags (default is all flags but `urls`)
 * @return {table} ucl object corresponding to the reply
 */
LUA_FUNCTION_DEF(task, get_protocol_reply);

/***
 * @method task:headers_foreach(callback, [params])
 * This method calls `callback` for each header that satisfies some condition.
 * By default, all headers are iterated unless `callback` returns `true`. Nil or
 * false means continue of iterations.
 * Params could be as following:
 *
 * - `full`: header value is full table of all attributes @see task:get_header_full for details
 * - `regexp`: return headers that satisfies the specified regexp
 * @param {function} callback function from header name and header value
 * @param {table} params optional parameters
 */
LUA_FUNCTION_DEF(task, headers_foreach);

/***
 * @method task:disable_action(action)
 * Disables some action for this task (e.g. 'greylist')
 *
 * @param {string} action action to disable
 * @return {boolean} true if an action was enabled and is disabled after the method call
 */
LUA_FUNCTION_DEF(task, disable_action);

/***
 * @method task:get_newlines_type()
 * Returns the most frequent newlines type met in a task
 *
 * @return {string} "cr" for \r, "lf" for \n, "crlf" for \r\n
 */
LUA_FUNCTION_DEF(task, get_newlines_type);

/***
 * @method task:get_stat_tokens()
 * Returns list of tables the statistical tokens:
 * - `data`: 64 bit number encoded as a string
 * - `t1`: the first token (if any)
 * - `t2`: the second token (if any)
 * - `win`: window index
 * - `flag`: table of strings:
 *    - `text`: text token
 *    - `meta`: meta token
 *    - `lua`: lua meta token
 *    - `exception`: exception
 *    - `subject`: subject token
 *    - `unigram`: unigram token
 *
 * @return {table of tables}
 */
LUA_FUNCTION_DEF(task, get_stat_tokens);

/***
 * @method task:lookup_words(map, function({o, n, s, f}) ... end)
 * Matches words in a task (including meta words) against some map (set, regexp and so on)
 * and call the specified function with a table containing 4 values:
 *   - [1] - stemmed word
 *   - [2] - normalised word
 *   - [3] - raw word
 *   - [4] - flags (table of strings)
 */
LUA_FUNCTION_DEF(task, lookup_words);

/**
 * @method task:topointer()
 *
 * Returns raw C pointer (lightuserdata) associated with task. This might be
 * broken with luajit and GC64, use with caution.
 */
LUA_FUNCTION_DEF(task, topointer);

/**
 * @method task:add_named_result(name, symbol_control_function)
 *
 * Adds a new named result for this task. symbol_control_function is a callback
 * called with 3 parameters:
 * `function(task, symbol, result_name)` and it should return boolean that
 * specifies if this symbol should be added to this named result.
 * @param {string} name for this result
 * @param {function} symbol_control_function predicate for symbols
 */
LUA_FUNCTION_DEF(task, add_named_result);

/**
 * @method task:get_all_named_results()
 *
 * Returns all named results registered for the task as a table of strings
 * @return {table|string} all named results starting from `default`
 */
LUA_FUNCTION_DEF(task, get_all_named_results);

/***
 * @method task:get_dns_req()
 * Get number of dns requests being sent in the task
 * @return {number} number of DNS requests
 */
LUA_FUNCTION_DEF(task, get_dns_req);

/***
 * @method task:add_timer(timeout, callback)
 * Creates a delayed execution task for the specific callback at given timeout (in seconds)
 *
 */
LUA_FUNCTION_DEF(task, add_timer);

/***
 * @method task:rewrite_html_urls(callback)
 * Rewrites URLs in HTML parts using the specified Lua callback function.
 * The callback receives (task, url) and should return the replacement URL or nil.
 * @param {function} callback Lua function to call for each URL
 * @return {table|nil} table of rewritten HTML parts indexed by part number, or nil on error
 */
LUA_FUNCTION_DEF(task, rewrite_html_urls);

/***
 * @method task:get_html_urls()
 * Extracts all URLs from HTML parts without rewriting.
 * Useful for async URL checking workflows where URLs need to be batched.
 * @return {table|nil} table indexed by part number, each containing an array of URL info tables with keys: url, attr, tag
 */
/* Implemented in lua_html_url_rewrite.cxx as C++ binding */
extern int lua_task_get_html_urls(lua_State *L);

static const struct luaL_reg tasklib_f[] = {
	LUA_INTERFACE_DEF(task, create),
	LUA_INTERFACE_DEF(task, load_from_file),
	LUA_INTERFACE_DEF(task, load_from_string),
	{NULL, NULL}};

static const struct luaL_reg tasklib_m[] = {
	LUA_INTERFACE_DEF(task, load_from_file),
	LUA_INTERFACE_DEF(task, load_from_string),
	LUA_INTERFACE_DEF(task, get_message),
	LUA_INTERFACE_DEF(task, set_message),
	LUA_INTERFACE_DEF(task, destroy),
	LUA_INTERFACE_DEF(task, process_message),
	LUA_INTERFACE_DEF(task, set_cfg),
	LUA_INTERFACE_DEF(task, get_cfg),
	LUA_INTERFACE_DEF(task, get_mempool),
	LUA_INTERFACE_DEF(task, get_session),
	LUA_INTERFACE_DEF(task, set_session),
	LUA_INTERFACE_DEF(task, get_ev_base),
	LUA_INTERFACE_DEF(task, get_worker),
	LUA_INTERFACE_DEF(task, insert_result),
	LUA_INTERFACE_DEF(task, insert_result_named),
	LUA_INTERFACE_DEF(task, adjust_result),
	LUA_INTERFACE_DEF(task, remove_result),
	LUA_INTERFACE_DEF(task, set_pre_result),
	LUA_INTERFACE_DEF(task, has_pre_result),
	LUA_INTERFACE_DEF(task, append_message),
	LUA_INTERFACE_DEF(task, has_urls),
	LUA_INTERFACE_DEF(task, get_urls),
	LUA_INTERFACE_DEF(task, get_urls_filtered),
	LUA_INTERFACE_DEF(task, inject_url),
	LUA_INTERFACE_DEF(task, get_content),
	LUA_INTERFACE_DEF(task, get_filename),
	LUA_INTERFACE_DEF(task, get_rawbody),
	LUA_INTERFACE_DEF(task, get_emails),
	LUA_INTERFACE_DEF(task, get_text_parts),
	LUA_INTERFACE_DEF(task, inject_part),
	LUA_INTERFACE_DEF(task, get_parts),
	LUA_INTERFACE_DEF(task, get_request_header),
	LUA_INTERFACE_DEF(task, set_request_header),
	LUA_INTERFACE_DEF(task, get_header),
	LUA_INTERFACE_DEF(task, has_header),
	LUA_INTERFACE_DEF(task, get_header_raw),
	LUA_INTERFACE_DEF(task, get_header_full),
	LUA_INTERFACE_DEF(task, get_header_count),
	LUA_INTERFACE_DEF(task, get_raw_headers),
	LUA_INTERFACE_DEF(task, get_headers),
	LUA_INTERFACE_DEF(task, modify_header),
	LUA_INTERFACE_DEF(task, get_received_headers),
	LUA_INTERFACE_DEF(task, get_queue_id),
	LUA_INTERFACE_DEF(task, get_uid),
	LUA_INTERFACE_DEF(task, get_uuid),
	LUA_INTERFACE_DEF(task, get_resolver),
	LUA_INTERFACE_DEF(task, set_resolver),
	LUA_INTERFACE_DEF(task, inc_dns_req),
	LUA_INTERFACE_DEF(task, get_dns_req),
	LUA_INTERFACE_DEF(task, has_recipients),
	LUA_INTERFACE_DEF(task, get_recipients),
	LUA_INTERFACE_DEF(task, set_recipients),
	LUA_INTERFACE_DEF(task, get_principal_recipient),
	LUA_INTERFACE_DEF(task, get_reply_sender),
	LUA_INTERFACE_DEF(task, has_from),
	LUA_INTERFACE_DEF(task, get_from),
	LUA_INTERFACE_DEF(task, set_from),
	LUA_INTERFACE_DEF(task, get_mail_esmtp_args),
	LUA_INTERFACE_DEF(task, get_rcpt_esmtp_args),
	LUA_INTERFACE_DEF(task, get_user),
	LUA_INTERFACE_DEF(task, set_user),
	{"get_addr", lua_task_get_from_ip},
	{"get_ip", lua_task_get_from_ip},
	{"get_from_addr", lua_task_get_from_ip},
	LUA_INTERFACE_DEF(task, get_from_ip),
	LUA_INTERFACE_DEF(task, set_from_ip),
	LUA_INTERFACE_DEF(task, get_from_ip_num),
	LUA_INTERFACE_DEF(task, get_client_ip),
	LUA_INTERFACE_DEF(task, get_subject),
	LUA_INTERFACE_DEF(task, get_helo),
	LUA_INTERFACE_DEF(task, set_helo),
	LUA_INTERFACE_DEF(task, get_hostname),
	LUA_INTERFACE_DEF(task, set_hostname),
	LUA_INTERFACE_DEF(task, get_images),
	LUA_INTERFACE_DEF(task, get_archives),
	LUA_INTERFACE_DEF(task, get_dkim_results),
	LUA_INTERFACE_DEF(task, get_symbol),
	LUA_INTERFACE_DEF(task, get_symbols),
	LUA_INTERFACE_DEF(task, get_symbols_all),
	LUA_INTERFACE_DEF(task, get_symbols_numeric),
	LUA_INTERFACE_DEF(task, get_symbols_tokens),
	LUA_INTERFACE_DEF(task, get_groups),
	LUA_INTERFACE_DEF(task, process_ann_tokens),
	LUA_INTERFACE_DEF(task, has_symbol),
	LUA_INTERFACE_DEF(task, enable_symbol),
	LUA_INTERFACE_DEF(task, disable_symbol),
	LUA_INTERFACE_DEF(task, get_date),
	LUA_INTERFACE_DEF(task, get_message_id),
	LUA_INTERFACE_DEF(task, get_timeval),
	LUA_INTERFACE_DEF(task, get_scan_time),
	LUA_INTERFACE_DEF(task, get_metric_result),
	LUA_INTERFACE_DEF(task, get_metric_score),
	LUA_INTERFACE_DEF(task, get_metric_action),
	LUA_INTERFACE_DEF(task, get_metric_threshold),
	LUA_INTERFACE_DEF(task, set_metric_score),
	LUA_INTERFACE_DEF(task, set_metric_subject),
	{"set_subject", lua_task_set_metric_subject},
	LUA_INTERFACE_DEF(task, learn),
	LUA_INTERFACE_DEF(task, set_settings),
	LUA_INTERFACE_DEF(task, get_settings),
	LUA_INTERFACE_DEF(task, lookup_settings),
	LUA_INTERFACE_DEF(task, get_settings_id),
	LUA_INTERFACE_DEF(task, set_settings_id),
	LUA_INTERFACE_DEF(task, merge_and_apply_settings),
	LUA_INTERFACE_DEF(task, cache_get),
	LUA_INTERFACE_DEF(task, cache_set),
	LUA_INTERFACE_DEF(task, process_regexp),
	LUA_INTERFACE_DEF(task, get_size),
	LUA_INTERFACE_DEF(task, set_flag),
	LUA_INTERFACE_DEF(task, get_flags),
	LUA_INTERFACE_DEF(task, has_flag),
	{"set_rmilter_reply", lua_task_set_milter_reply},
	LUA_INTERFACE_DEF(task, set_milter_reply),
	LUA_INTERFACE_DEF(task, get_digest),
	LUA_INTERFACE_DEF(task, store_in_file),
	LUA_INTERFACE_DEF(task, get_protocol_reply),
	LUA_INTERFACE_DEF(task, headers_foreach),
	LUA_INTERFACE_DEF(task, disable_action),
	LUA_INTERFACE_DEF(task, get_newlines_type),
	LUA_INTERFACE_DEF(task, get_stat_tokens),
	LUA_INTERFACE_DEF(task, get_meta_words),
	LUA_INTERFACE_DEF(task, lookup_words),
	LUA_INTERFACE_DEF(task, add_named_result),
	LUA_INTERFACE_DEF(task, get_all_named_results),
	LUA_INTERFACE_DEF(task, topointer),
	LUA_INTERFACE_DEF(task, add_timer),
	LUA_INTERFACE_DEF(task, rewrite_html_urls),
	LUA_INTERFACE_DEF(task, get_html_urls),
	{"__tostring", rspamd_lua_class_tostring},
	{NULL, NULL}};

/* Image methods */
LUA_FUNCTION_DEF(image, get_width);
LUA_FUNCTION_DEF(image, get_height);
LUA_FUNCTION_DEF(image, get_type);
LUA_FUNCTION_DEF(image, get_filename);
LUA_FUNCTION_DEF(image, get_size);

static const struct luaL_reg imagelib_m[] = {
	LUA_INTERFACE_DEF(image, get_width),
	LUA_INTERFACE_DEF(image, get_height),
	LUA_INTERFACE_DEF(image, get_type),
	LUA_INTERFACE_DEF(image, get_filename),
	LUA_INTERFACE_DEF(image, get_size),
	{"__tostring", rspamd_lua_class_tostring},
	{NULL, NULL}};

/* Archive methods */
LUA_FUNCTION_DEF(archive, get_type);
LUA_FUNCTION_DEF(archive, get_files);
LUA_FUNCTION_DEF(archive, get_files_full);
/* TODO: Export archive flags as integers to use bitops for that */
LUA_FUNCTION_DEF(archive, is_encrypted);
LUA_FUNCTION_DEF(archive, is_obfuscated);
LUA_FUNCTION_DEF(archive, is_unreadable);
LUA_FUNCTION_DEF(archive, get_filename);
LUA_FUNCTION_DEF(archive, get_size);

static const struct luaL_reg archivelib_m[] = {
	LUA_INTERFACE_DEF(archive, get_type),
	LUA_INTERFACE_DEF(archive, get_files),
	LUA_INTERFACE_DEF(archive, get_files_full),
	LUA_INTERFACE_DEF(archive, is_encrypted),
	LUA_INTERFACE_DEF(archive, is_obfuscated),
	LUA_INTERFACE_DEF(archive, is_unreadable),
	LUA_INTERFACE_DEF(archive, get_filename),
	LUA_INTERFACE_DEF(archive, get_size),
	{"__tostring", rspamd_lua_class_tostring},
	{NULL, NULL}};

/* Utility functions */
struct rspamd_task *
lua_check_task(lua_State *L, int pos)
{
	void *ud = rspamd_lua_check_udata(L, pos, rspamd_task_classname);
	luaL_argcheck(L, ud != NULL, pos, "'task' expected");
	if (ud) {
		struct rspamd_task *task = *((struct rspamd_task **) ud);
		if (rspamd_task_is_valid(task)) {
			return task;
		}
		msg_err("detected use-after-free for task %p", task);
	}
	return NULL;
}

struct rspamd_task *
lua_check_task_maybe(lua_State *L, int pos)
{
	void *ud = rspamd_lua_check_udata_maybe(L, pos, rspamd_task_classname);

	if (ud) {
		struct rspamd_task *task = *((struct rspamd_task **) ud);
		if (rspamd_task_is_valid(task)) {
			return task;
		}
		msg_err("detected use-after-free for task %p", task);
	}
	return NULL;
}

static struct rspamd_image *
lua_check_image(lua_State *L)
{
	void *ud = rspamd_lua_check_udata(L, 1, rspamd_image_classname);
	luaL_argcheck(L, ud != NULL, 1, "'image' expected");
	return ud ? *((struct rspamd_image **) ud) : NULL;
}

static struct rspamd_archive *
lua_check_archive(lua_State *L)
{
	void *ud = rspamd_lua_check_udata(L, 1, rspamd_archive_classname);
	luaL_argcheck(L, ud != NULL, 1, "'archive' expected");
	return ud ? *((struct rspamd_archive **) ud) : NULL;
}

static void
lua_task_set_cached(lua_State *L, struct rspamd_task *task, const char *key,
					int pos)
{
	LUA_TRACE_POINT;
	khiter_t k;

	lua_pushvalue(L, pos);

	k = kh_get(rspamd_task_lua_cache, &task->lua_cache, (char *) key);

	if (G_UNLIKELY(k != kh_end(&task->lua_cache))) {
		/* Unref previous value */
		luaL_unref(L, LUA_REGISTRYINDEX, kh_value(&task->lua_cache, k).ref);
	}
	else {
		int r;

		k = kh_put(rspamd_task_lua_cache, &task->lua_cache, rspamd_mempool_strdup(task->task_pool, key), &r);
	}

	kh_value(&task->lua_cache, k).ref = luaL_ref(L, LUA_REGISTRYINDEX);
	kh_value(&task->lua_cache, k).id = GPOINTER_TO_UINT(task->message);
}


static gboolean
lua_task_get_cached(lua_State *L, struct rspamd_task *task, const char *key)
{
	LUA_TRACE_POINT;
	khiter_t k;

	k = kh_get(rspamd_task_lua_cache, &task->lua_cache, (char *) key);

	if (k != kh_end(&task->lua_cache) && (kh_value(&task->lua_cache, k).id == GPOINTER_TO_UINT(task->message))) {
		lua_rawgeti(L, LUA_REGISTRYINDEX, kh_value(&task->lua_cache, k).ref);

		return TRUE;
	}

	return FALSE;
}

/* Task methods */
static int
lua_task_process_message(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	gboolean enforce = FALSE;

	if (task != NULL) {
		if (task->msg.len > 0) {
			if (lua_isboolean(L, 2)) {
				enforce = lua_toboolean(L, 2);
			}

			if (rspamd_message_parse(task)) {
				if (enforce ||
					(!(task->flags & RSPAMD_TASK_FLAG_SKIP_PROCESS) &&
					 !(task->processed_stages & RSPAMD_TASK_STAGE_PROCESS_MESSAGE))) {

					lua_pushboolean(L, TRUE);
					rspamd_message_process(task);
					task->processed_stages |= RSPAMD_TASK_STAGE_PROCESS_MESSAGE;
				}
				else {
					lua_pushboolean(L, FALSE);
				}
			}
			else {
				lua_pushboolean(L, FALSE);
			}
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_cfg(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_config **pcfg;

	if (task) {
		pcfg = lua_newuserdata(L, sizeof(gpointer));
		rspamd_lua_setclass(L, rspamd_config_classname, -1);
		*pcfg = task->cfg;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_cfg(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	void *ud = rspamd_lua_check_udata(L, 2, rspamd_config_classname);

	if (task) {
		luaL_argcheck(L, ud != NULL, 1, "'config' expected");
		task->cfg = ud ? *((struct rspamd_config **) ud) : NULL;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_destroy(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {
		rspamd_task_free(task);
	}

	return 0;
}

static int
lua_task_get_message(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_text *t;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		t = lua_newuserdata(L, sizeof(*t));
		rspamd_lua_setclass(L, rspamd_text_classname, -1);
		t->flags = 0;
		t->start = task->msg.begin;
		t->len = task->msg.len;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_message(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_lua_text *t;
	struct rspamd_task *task = lua_check_task(L, 1);
	gboolean message_set = FALSE;

	if (task) {
		gsize final_len = 0;
		char *buf = NULL;

		if (lua_type(L, 2) == LUA_TTABLE) {
			/* Piecewise construct */
			unsigned int vec_len = rspamd_lua_table_size(L, 2);


			for (unsigned int i = 0; i < vec_len; i++) {
				lua_rawgeti(L, 2, i + 1);

				if (lua_type(L, -1) == LUA_TSTRING) {
					gsize l;

					(void) lua_tolstring(L, -1, &l);
					final_len += l;
				}
				else {
					t = lua_check_text(L, -1);

					if (t) {
						final_len += t->len;
					}
				}

				lua_pop(L, 1);
			}

			if (final_len > 0) {
				char *pos;

				buf = rspamd_mempool_alloc(task->task_pool, final_len);
				pos = buf;

				for (unsigned int i = 0; i < vec_len; i++) {
					lua_rawgeti(L, 2, i + 1);

					if (lua_type(L, -1) == LUA_TSTRING) {
						gsize l;
						const char *s;

						s = lua_tolstring(L, -1, &l);
						memcpy(pos, s, l);
						pos += l;
					}
					else {
						t = lua_check_text(L, -1);

						if (t) {
							memcpy(pos, t->start, t->len);
							pos += t->len;
						}
					}

					lua_pop(L, 1);
				}

				task->flags |= RSPAMD_TASK_FLAG_MESSAGE_REWRITE;
				task->msg.begin = buf;
				task->msg.len = final_len;
				message_set = TRUE;
			}
		}
		else {
			if (lua_type(L, 2) == LUA_TSTRING) {
				const char *s;

				s = lua_tolstring(L, -1, &final_len);
				buf = rspamd_mempool_alloc(task->task_pool, final_len);
				memcpy(buf, s, final_len);
			}
			else {
				t = lua_check_text(L, -1);

				if (t) {
					final_len = t->len;
					buf = rspamd_mempool_alloc(task->task_pool, final_len);
					memcpy(buf, t->start, final_len);
				}
			}

			if (buf) {
				task->msg.begin = buf;
				task->msg.len = final_len;
				task->flags |= RSPAMD_TASK_FLAG_MESSAGE_REWRITE;
				message_set = TRUE;
			}
		}

		if (message_set) {
			if (rspamd_message_parse(task)) {
				rspamd_message_process(task);
				lua_pushboolean(L, TRUE);
				lua_pushinteger(L, final_len);

				return 2;
			}
			else {
				lua_pushboolean(L, FALSE);
			}
		}
		else {
			lua_pushboolean(L, FALSE);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static void
lua_task_unmap_dtor(gpointer p)
{
	struct rspamd_task *task = (struct rspamd_task *) p;

	if (task->msg.begin) {
		munmap((gpointer) task->msg.begin, task->msg.len);
	}
}

static void
lua_task_free_dtor(gpointer p)
{
	g_free(p);
}

static int
lua_task_load_from_file(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = NULL;
	const char *fname, *err = NULL;
	struct rspamd_config *cfg = NULL;
	gboolean res = FALSE, new_task = FALSE;
	gpointer map;
	gsize sz;

	if (lua_type(L, 1) == LUA_TSTRING) {
		fname = luaL_checkstring(L, 1);
		new_task = TRUE;
	}
	else {
		/* Method */
		task = lua_check_task(L, 1);
		fname = luaL_checkstring(L, 2);
	}

	if (fname) {

		if (!task) {
			if (lua_type(L, 2) == LUA_TUSERDATA) {
				gpointer p;
				p = rspamd_lua_check_udata_maybe(L, 2, rspamd_config_classname);

				if (p) {
					cfg = *(struct rspamd_config **) p;
				}
			}
		}

		if (strcmp(fname, "-") == 0) {
			/* Read from stdin */
			int fd = STDIN_FILENO;
			GString *data = g_string_sized_new(BUFSIZ);
			char buf[BUFSIZ];
			gssize r;

			for (;;) {
				r = read(fd, buf, sizeof(buf));

				if (r == -1) {
					err = strerror(errno);
					break;
				}
				else if (r == 0) {
					break;
				}
				else {
					g_string_append_len(data, buf, r);
				}
			}

			if (!task) {
				task = rspamd_task_new(NULL, cfg, NULL, NULL, NULL, FALSE);
			}

			task->msg.begin = data->str;
			task->msg.len = data->len;
			rspamd_mempool_add_destructor(task->task_pool,
										  lua_task_free_dtor, data->str);
			if (data->len > 0) {
				task->flags &= ~RSPAMD_TASK_FLAG_EMPTY;
			}
			res = TRUE;
			g_string_free(data, FALSE); /* Buffer is still valid */
		}
		else {
			map = rspamd_file_xmap(fname, PROT_READ, &sz, TRUE);

			if (!map) {
				err = strerror(errno);
			}
			else {
				if (!task) {
					task = rspamd_task_new(NULL, cfg, NULL, NULL, NULL, FALSE);
				}

				task->msg.begin = map;
				task->msg.len = sz;

				if (sz > 0) {
					task->flags &= ~RSPAMD_TASK_FLAG_EMPTY;
				}
				rspamd_mempool_add_destructor(task->task_pool,
											  lua_task_unmap_dtor, task);
				res = TRUE;
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	lua_pushboolean(L, res);

	if (res && new_task) {
		rspamd_lua_task_push(L, task);

		return 2;
	}
	else if (!res) {
		if (err) {
			lua_pushstring(L, err);
		}
		else {
			lua_pushnil(L);
		}
		return 2;
	}
	else {
		/* No new task */
		return 1;
	}
}

static int
lua_task_load_from_string(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = NULL;
	const char *str_message;
	gsize message_len = 0;
	struct rspamd_config *cfg = NULL;
	bool new_task = false;

	if (lua_type(L, 1) == LUA_TSTRING) {
		str_message = luaL_checklstring(L, 1, &message_len);
		new_task = true;
	}
	else {
		/* Method */
		task = lua_check_task(L, 1);
		str_message = luaL_checklstring(L, 2, &message_len);
	}

	if (str_message) {

		if (!task && lua_type(L, 2) == LUA_TUSERDATA) {
			gpointer p;
			p = rspamd_lua_check_udata_maybe(L, 2, rspamd_config_classname);

			if (p) {
				cfg = *(struct rspamd_config **) p;
			}
		}

		if (!task) {
			task = rspamd_task_new(NULL, cfg, NULL, NULL, NULL, FALSE);
		}
		task->msg.begin = g_malloc(message_len);
		memcpy((char *) task->msg.begin, str_message, message_len);
		task->msg.len = message_len;
		if (message_len > 0) {
			task->flags &= ~RSPAMD_TASK_FLAG_EMPTY;
		}
		rspamd_mempool_add_destructor(task->task_pool, lua_task_free_dtor,
									  (gpointer) task->msg.begin);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	lua_pushboolean(L, true);

	if (new_task) {
		rspamd_lua_task_push(L, task);

		return 2;
	}
	else {
		return 1;
	}
}

static int
lua_task_create(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = NULL;
	struct rspamd_config *cfg = NULL;
	struct ev_loop *ev_base = NULL;

	if (lua_type(L, 1) == LUA_TUSERDATA) {
		gpointer p;
		p = rspamd_lua_check_udata_maybe(L, 1, rspamd_config_classname);

		if (p) {
			cfg = *(struct rspamd_config **) p;
		}
	}

	if (lua_type(L, 2) == LUA_TUSERDATA) {
		gpointer p;
		p = rspamd_lua_check_udata_maybe(L, 2, rspamd_ev_base_classname);

		if (p) {
			ev_base = *(struct ev_loop **) p;
		}
	}

	if (cfg == NULL) {
		return luaL_error(L, "rspamd_task.create requires cfg");
	}
	task = rspamd_task_new(NULL, cfg, NULL, NULL, ev_base, FALSE);
	task->flags |= RSPAMD_TASK_FLAG_EMPTY;

	rspamd_lua_task_push(L, task);

	return 1;
}

static int
lua_task_get_mempool(lua_State *L)
{
	LUA_TRACE_POINT;
	rspamd_mempool_t **ppool;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {
		ppool = lua_newuserdata(L, sizeof(rspamd_mempool_t *));
		rspamd_lua_setclass(L, rspamd_mempool_classname, -1);
		*ppool = task->task_pool;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_session(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_async_session **psession;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {
		psession = lua_newuserdata(L, sizeof(void *));
		rspamd_lua_setclass(L, rspamd_session_classname, -1);
		*psession = task->s;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}
	return 1;
}

static int
lua_task_set_session(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_async_session *session = lua_check_session(L, 2);
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL && session != NULL) {
		task->s = session;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}
	return 1;
}

static int
lua_task_get_ev_base(lua_State *L)
{
	LUA_TRACE_POINT;
	struct ev_loop **pbase;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {
		pbase = lua_newuserdata(L, sizeof(struct ev_loop *));
		rspamd_lua_setclass(L, rspamd_ev_base_classname, -1);
		*pbase = task->event_loop;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}
	return 1;
}

static int
lua_task_get_worker(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_worker **pworker;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {
		if (task->worker) {
			pworker = lua_newuserdata(L, sizeof(struct rspamd_worker *));
			rspamd_lua_setclass(L, rspamd_worker_classname, -1);
			*pworker = task->worker;
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}
	return 1;
}


static int
lua_task_insert_result_common(lua_State *L, struct rspamd_scan_result *result,
							  int common_args_pos)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *symbol_name;
	double weight;
	struct rspamd_symbol_result *s;
	enum rspamd_symbol_insert_flags flags = RSPAMD_SYMBOL_INSERT_DEFAULT;
	int i, top, args_start;

	if (task != NULL) {
		if (lua_isboolean(L, common_args_pos)) {
			args_start = common_args_pos + 1;

			if (lua_toboolean(L, common_args_pos)) {
				flags |= RSPAMD_SYMBOL_INSERT_ENFORCE;
			}
		}
		else {
			args_start = common_args_pos;
		}

		symbol_name = rspamd_mempool_strdup(task->task_pool,
											luaL_checkstring(L, args_start));
		weight = luaL_checknumber(L, args_start + 1);
		top = lua_gettop(L);
		s = rspamd_task_insert_result_full(task, symbol_name, weight,
										   NULL, flags, result);

		/* Get additional options */
		if (s) {
			if (s->sym == NULL) {
				/* Unknown symbol, print traceback */
				lua_pushfstring(L, "unknown symbol %s", symbol_name);
				rspamd_lua_traceback(L);

				msg_info_task("symbol insertion issue: %s", lua_tostring(L, -1));

				lua_pop(L, 1); /* Traceback string */
			}
			for (i = args_start + 2; i <= top; i++) {
				int ltype = lua_type(L, i);

				if (ltype == LUA_TSTRING) {
					gsize optlen;
					const char *opt = lua_tolstring(L, i, &optlen);

					rspamd_task_add_result_option(task, s, opt, optlen);
				}
				else if (ltype == LUA_TUSERDATA) {
					struct rspamd_lua_text *t = lua_check_text(L, i);

					if (t) {
						rspamd_task_add_result_option(task, s, t->start,
													  t->len);
					}
				}
				else if (ltype == LUA_TTABLE) {
					gsize objlen = rspamd_lua_table_size(L, i);

					for (unsigned int j = 1; j <= objlen; j++) {
						lua_rawgeti(L, i, j);

						if (lua_type(L, -1) == LUA_TSTRING) {
							gsize optlen;
							const char *opt = lua_tolstring(L, -1, &optlen);

							rspamd_task_add_result_option(task, s, opt, optlen);
						}
						else if (lua_type(L, -1) == LUA_TUSERDATA) {
							struct rspamd_lua_text *t = lua_check_text(L, -1);

							if (t) {
								rspamd_task_add_result_option(task, s, t->start,
															  t->len);
							}
							else {
								return luaL_error(L, "not rspamd_text option in a table "
													 "when adding symbol  %s: %s type",
												  s->name);
							}
						}
						else {
							const char *tname = lua_typename(L, lua_type(L, -1));
							lua_pop(L, 2);

							return luaL_error(L, "not a string option in a table "
												 "when adding symbol  %s: %s type",
											  s->name, tname);
						}

						lua_pop(L, 1);
					}
				}
				else if (ltype == LUA_TNIL) {
					/* We have received a NULL option, it is not good but not a fatal error */
					msg_info_task("nil option when adding symbol %s at pos %d",
								  s->name, i);
					continue;
				}
				else {
					const char *tname = lua_typename(L, ltype);

					return luaL_error(L, "not a string/table option "
										 "when adding symbol %s: %s type",
									  s->name, tname);
				}
			}
		}
		else if (task->settings == NULL && task->settings_elt == NULL) {
			lua_pushfstring(L, "insertion failed for %s", symbol_name);
			rspamd_lua_traceback(L);

			msg_info_task("symbol insertion issue: %s", lua_tostring(L, -1));

			lua_pop(L, 2); /* Traceback string + error string */
		}
		else {
			/* Usually denied by settings */
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_insert_result(lua_State *L)
{
	return lua_task_insert_result_common(L, NULL, 2);
}

static int
lua_task_insert_result_named(lua_State *L)
{
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *named_result = luaL_checkstring(L, 2);
	struct rspamd_scan_result *res;

	if (task && named_result) {
		res = rspamd_find_metric_result(task, named_result);

		if (res == NULL) {
			return luaL_error(L, "invalid arguments: bad named result: %s",
							  named_result);
		}

		return lua_task_insert_result_common(L, res, 3);
	}

	return luaL_error(L, "invalid arguments");
}

static int
lua_task_adjust_result(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *symbol_name;
	struct rspamd_scan_result *metric_res;
	struct rspamd_symbol_result *s = NULL;
	double weight;
	int i, top;

	if (task != NULL) {

		symbol_name = luaL_checkstring(L, 2);
		weight = luaL_checknumber(L, 3);
		top = lua_gettop(L);
		metric_res = task->result;

		if (metric_res) {
			s = rspamd_task_find_symbol_result(task, symbol_name, NULL);
		}
		else {
			return luaL_error(L, "no metric result");
		}

		if (s) {
			if (!isnan(weight)) {
				metric_res->score -= s->score;
				s->score = weight;
				metric_res->score += s->score;
			}
		}
		else {
			return luaL_error(L, "symbol not found: %s", symbol_name);
		}

		/* Get additional options */
		if (s) {
			for (i = 4; i <= top; i++) {
				if (lua_type(L, i) == LUA_TSTRING) {
					gsize optlen;
					const char *opt = lua_tolstring(L, i, &optlen);

					rspamd_task_add_result_option(task, s, opt, optlen);
				}
				else if (lua_type(L, i) == LUA_TUSERDATA) {
					struct rspamd_lua_text *t = lua_check_text(L, i);

					if (t) {
						rspamd_task_add_result_option(task, s, t->start,
													  t->len);
					}
				}
				else if (lua_type(L, i) == LUA_TTABLE) {
					gsize objlen = rspamd_lua_table_size(L, i);

					for (unsigned int j = 1; j <= objlen; j++) {
						lua_rawgeti(L, i, j);

						if (lua_type(L, -1) == LUA_TSTRING) {
							gsize optlen;
							const char *opt = lua_tolstring(L, -1, &optlen);

							rspamd_task_add_result_option(task, s, opt, optlen);
						}
						else if (lua_type(L, -1) == LUA_TUSERDATA) {
							struct rspamd_lua_text *t = lua_check_text(L, -1);

							if (t) {
								rspamd_task_add_result_option(task, s, t->start,
															  t->len);
							}
						}

						lua_pop(L, 1);
					}
				}
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_remove_result(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *symbol_name = luaL_checkstring(L, 2);
	struct rspamd_scan_result *metric_res;
	const char *named_result = luaL_optstring(L, 3, NULL);

	if (task != NULL) {
		metric_res = rspamd_find_metric_result(task, named_result);

		if (metric_res == NULL) {
			return luaL_error(L, "invalid arguments: bad named result: %s",
							  named_result);
		}

		lua_pushboolean(L, (rspamd_task_remove_symbol_result(task, symbol_name,
															 metric_res)) != NULL);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_pre_result(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *message = NULL, *module = NULL, *fl_str = NULL, *act_str = NULL,
			   *res_name = NULL;
	double score = NAN;
	struct rspamd_action *action;
	unsigned int priority = RSPAMD_PASSTHROUGH_NORMAL, flags = 0;

	if (task != NULL) {

		if (RSPAMD_TASK_IS_SKIPPED(task)) {
			/* Do not set pre-result for a skipped task */
			return 0;
		}

		if (lua_type(L, 2) == LUA_TTABLE) {
			GError *err = NULL;

			if (!rspamd_lua_parse_table_arguments(L, 2, &err,
												  RSPAMD_LUA_PARSE_ARGUMENTS_DEFAULT,
												  "*action=S;message=S;module=S;score=D;priority=i;flags=S;result=S",
												  &act_str, &message, &module, &score, &priority, &fl_str, &res_name)) {
				int ret = luaL_error(L, "invalid arguments: %s", err->message);
				g_error_free(err);

				return ret;
			}
		}
		else {
			if (lua_type(L, 2) == LUA_TSTRING) {
				act_str = lua_tostring(L, 2);
			}
			else {
				return luaL_error(L, "invalid arguments");
			}

			if (lua_type(L, 3) == LUA_TSTRING) {
				message = lua_tostring(L, 3);
			}

			if (lua_type(L, 4) == LUA_TSTRING) {
				module = lua_tostring(L, 4);
			}

			if (lua_type(L, 5) == LUA_TNUMBER) {
				score = lua_tonumber(L, 5);
			}

			if (lua_type(L, 6) == LUA_TNUMBER) {
				priority = lua_tointeger(L, 6);
			}

			if (lua_type(L, 7) == LUA_TSTRING) {
				fl_str = lua_tostring(L, 7);
			}
		}

		enum rspamd_action_type internal_type;

		if (strcmp(act_str, "accept") == 0) {
			/* Compatibility! */
			act_str = "no action";
		}
		else if (rspamd_action_from_str(act_str, &internal_type)) {
			/* Compatibility! */
			act_str = rspamd_action_to_str(internal_type);
		}

		action = rspamd_config_get_action(task->cfg, act_str);

		if (action == NULL) {
			return luaL_error(L, "unknown action %s", act_str);
		}

		if (module == NULL) {
			module = "Unknown lua";
		}

		if (message == NULL) {
			message = "unknown reason";
			flags |= RSPAMD_PASSTHROUGH_NO_SMTP_MESSAGE;
		}

		if (fl_str != NULL) {
			/*
			 * TODO: convert to a set of string and split by `,` + add table support
			 * once this legacy code is migrated to C++
			 */
			if (strstr(fl_str, "least") != NULL) {
				flags |= RSPAMD_PASSTHROUGH_LEAST;
			}
			else if (strstr(fl_str, "no_smtp_message") != NULL) {
				flags |= RSPAMD_PASSTHROUGH_NO_SMTP_MESSAGE;
			}
			else if (strstr(fl_str, "process_all") != NULL) {
				flags |= RSPAMD_PASSTHROUGH_PROCESS_ALL;
			}
		}


		rspamd_add_passthrough_result(task,
									  action,
									  priority,
									  score,
									  rspamd_mempool_strdup(task->task_pool, message),
									  rspamd_mempool_strdup(task->task_pool, module),
									  flags,
									  rspamd_find_metric_result(task, res_name));

		/* Don't classify or filter message if pre-filter sets results */

		if (res_name == NULL && !(flags & (RSPAMD_PASSTHROUGH_LEAST | RSPAMD_PASSTHROUGH_PROCESS_ALL))) {
			task->processed_stages |= (RSPAMD_TASK_STAGE_CLASSIFIERS |
									   RSPAMD_TASK_STAGE_CLASSIFIERS_PRE |
									   RSPAMD_TASK_STAGE_CLASSIFIERS_POST);
			rspamd_symcache_disable_all_symbols(task, task->cfg->cache,
												SYMBOL_TYPE_IDEMPOTENT | SYMBOL_TYPE_IGNORE_PASSTHROUGH);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_has_pre_result(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	int nret = 1;

	if (task) {
		if (task->result->passthrough_result) {
			struct rspamd_passthrough_result *pr = task->result->passthrough_result;

			lua_pushboolean(L, true);
			nret = 4;
			/* bool, action, message, module */

			if (pr->action) {
				lua_pushstring(L, rspamd_action_to_str(pr->action->action_type));
			}
			else {
				lua_pushnil(L);
			}

			if (pr->message) {
				lua_pushstring(L, pr->message);
			}
			else {
				lua_pushnil(L);
			}
			if (pr->module) {
				lua_pushstring(L, pr->module);
			}
			else {
				lua_pushnil(L);
			}
		}
		else {
			lua_pushboolean(L, false);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return nret;
}

static int
lua_task_append_message(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *category;

	if (task != NULL) {
		if (lua_type(L, 3) == LUA_TSTRING) {
			category = luaL_checkstring(L, 3);
		}
		else {
			category = "unknown";
		}

		ucl_object_insert_key(task->messages,
							  ucl_object_lua_import(L, 2),
							  category, 0,
							  true);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}


static int
lua_task_get_urls(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct lua_tree_cb_data cb;
	struct rspamd_url *u;
	static const int default_protocols_mask = PROTOCOL_HTTP | PROTOCOL_HTTPS |
											  PROTOCOL_FILE | PROTOCOL_FTP;
	gsize sz, max_urls = 0;

	if (task) {
		if (task->cfg) {
			max_urls = task->cfg->max_lua_urls;
		}

		if (task->message == NULL) {
			lua_newtable(L);

			return 1;
		}

		/*
		 * By default, include content URLs if configured (default: true).
		 * Always exclude image URLs unless explicitly requested.
		 */
		unsigned int default_flags = ~RSPAMD_URL_FLAG_IMAGE;
		if (task->cfg && !task->cfg->include_content_urls) {
			default_flags &= ~RSPAMD_URL_FLAG_CONTENT;
		}

		if (!lua_url_cbdata_fill(L, 2, &cb, default_protocols_mask,
								 default_flags,
								 max_urls)) {
			return luaL_error(L, "invalid arguments");
		}

		sz = kh_size(MESSAGE_FIELD(task, urls));
		sz = lua_url_adjust_skip_prob(task->task_timestamp,
									  MESSAGE_FIELD(task, digest), &cb, sz);

		lua_createtable(L, sz, 0);

		if (cb.sort) {
			struct rspamd_url **urls_sorted;
			int i = 0;

			urls_sorted = g_new0(struct rspamd_url *, sz);

			kh_foreach_key(MESSAGE_FIELD(task, urls), u, {
				if (i < sz) {
					urls_sorted[i] = u;
					i++;
				}
			});

			qsort(urls_sorted, i, sizeof(struct rspamd_url *), rspamd_url_cmp_qsort);

			for (int j = 0; j < i; j++) {
				lua_tree_url_callback(urls_sorted[j], urls_sorted[j], &cb);
			}

			g_free(urls_sorted);
		}
		else {
			kh_foreach_key(MESSAGE_FIELD(task, urls), u, {
				lua_tree_url_callback(u, u, &cb);
			});
		}

		lua_url_cbdata_dtor(&cb);
	}
	else {
		return luaL_error(L, "invalid arguments, no task");
	}

	return 1;
}

static int
lua_task_get_urls_filtered(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct lua_tree_cb_data cb;
	struct rspamd_url *u;
	static const int default_protocols_mask = PROTOCOL_HTTP | PROTOCOL_HTTPS |
											  PROTOCOL_FILE | PROTOCOL_FTP;
	gsize sz, max_urls = 0;

	if (task) {
		if (task->cfg) {
			max_urls = task->cfg->max_lua_urls;
		}

		if (task->message == NULL) {
			lua_newtable(L);

			return 1;
		}

		if (!lua_url_cbdata_fill_exclude_include(L, 2, &cb, default_protocols_mask, max_urls)) {
			return luaL_error(L, "invalid arguments");
		}

		sz = kh_size(MESSAGE_FIELD(task, urls));
		sz = lua_url_adjust_skip_prob(task->task_timestamp,
									  MESSAGE_FIELD(task, digest), &cb, sz);

		lua_createtable(L, sz, 0);

		if (cb.sort) {
			struct rspamd_url **urls_sorted;
			int i = 0;

			urls_sorted = g_new0(struct rspamd_url *, sz);

			kh_foreach_key(MESSAGE_FIELD(task, urls), u, {
				if (i < sz) {
					urls_sorted[i] = u;
					i++;
				}
			});

			qsort(urls_sorted, i, sizeof(struct rspamd_url *), rspamd_url_cmp_qsort);

			for (int j = 0; j < i; j++) {
				lua_tree_url_callback(urls_sorted[j], urls_sorted[j], &cb);
			}

			g_free(urls_sorted);
		}
		else {
			kh_foreach_key(MESSAGE_FIELD(task, urls), u, {
				lua_tree_url_callback(u, u, &cb);
			});
		}

		lua_url_cbdata_dtor(&cb);
	}
	else {
		return luaL_error(L, "invalid arguments, no task");
	}

	return 1;
}

static int
lua_task_has_urls(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	bool need_emails = false;
	gboolean ret = FALSE;
	gsize sz = 0;

	if (task) {
		if (task->message) {
			if (lua_gettop(L) >= 2) {
				need_emails = lua_toboolean(L, 2);
			}

			if (need_emails) {
				/* Simplified check */
				if (kh_size(MESSAGE_FIELD(task, urls)) > 0) {
					sz += kh_size(MESSAGE_FIELD(task, urls));
					ret = TRUE;
				}
			}
			else {
				/* Linear scan */
				struct rspamd_url *u;
				kh_foreach_key(MESSAGE_FIELD(task, urls), u, {
					if (u->protocol != PROTOCOL_MAILTO) {
						sz++;
						ret = TRUE;
					}
				});
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	lua_pushboolean(L, ret);
	lua_pushinteger(L, sz);

	return 2;
}

static int
lua_task_inject_part(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *type = luaL_checkstring(L, 2);
	struct rspamd_lua_text *content_text;
	const char *content = NULL;
	gsize content_len = 0;
	struct rspamd_mime_part *part, *original_part = NULL;
	struct rspamd_mime_text_part *txt_part;
	gboolean is_table = FALSE;

	/* Accept string, rspamd_text, or table of texts */
	if (lua_type(L, 3) == LUA_TTABLE) {
		is_table = TRUE;
		/* Calculate total length first and validate all entries */
		int table_len = rspamd_lua_table_size(L, 3);
		if (table_len <= 0) {
			return luaL_error(L, "empty table provided");
		}

		lua_pushnil(L);
		while (lua_next(L, 3) != 0) {
			struct rspamd_lua_text *t = lua_check_text_or_string(L, -1);
			if (!t) {
				lua_pop(L, 2); /* pop value and key */
				return luaL_error(L, "invalid entry in table (expected string or text)");
			}
			content_len += t->len;
			lua_pop(L, 1);
		}

		if (content_len == 0) {
			return luaL_error(L, "all table entries are empty");
		}
	}
	else {
		content_text = lua_check_text_or_string(L, 3);
		if (!content_text) {
			return luaL_error(L, "invalid content argument (expected string, text, or table)");
		}
		content = content_text->start;
		content_len = content_text->len;
	}

	/* Check for optional original_part parameter */
	if (lua_gettop(L) >= 4 && lua_isuserdata(L, 4)) {
		original_part = *((struct rspamd_mime_part **)
							  rspamd_lua_check_udata_maybe(L, 4, rspamd_mimepart_classname));
	}

	if (task && task->message) {
		if (g_ascii_strcasecmp(type, "text") == 0) {
			part = rspamd_mempool_alloc0(task->task_pool, sizeof(*part));
			part->part_type = RSPAMD_MIME_PART_TEXT;
			part->flags |= RSPAMD_MIME_PART_COMPUTED;

			if (original_part) {
				part->parent_part = original_part;
			}

			part->ct = rspamd_mempool_alloc0(task->task_pool, sizeof(*part->ct));

			part->ct->type.begin = "text";
			part->ct->type.len = 4;
			part->ct->subtype.begin = "plain";
			part->ct->subtype.len = 5;
			part->ct->flags = RSPAMD_CONTENT_TYPE_TEXT;
			part->ct->charset.begin = "utf-8";
			part->ct->charset.len = 5;

			part->parsed_data.begin = rspamd_mempool_alloc(task->task_pool, content_len + 1);
			part->parsed_data.len = content_len;

			if (is_table) {
				char *dst = (char *) part->parsed_data.begin;
				lua_pushnil(L);
				while (lua_next(L, 3) != 0) {
					struct rspamd_lua_text *t = lua_check_text_or_string(L, -1);
					if (t && t->len > 0) {
						memcpy(dst, t->start, t->len);
						dst += t->len;
					}
					lua_pop(L, 1);
				}
				*dst = '\0';
			}
			else {
				memcpy((char *) part->parsed_data.begin, content, content_len);
				((char *) part->parsed_data.begin)[content_len] = '\0';
			}

			part->raw_data = part->parsed_data;

			txt_part = rspamd_mempool_alloc0(task->task_pool, sizeof(*txt_part));
			txt_part->mime_part = part;
			txt_part->raw.begin = part->parsed_data.begin;
			txt_part->raw.len = content_len;
			txt_part->parsed = txt_part->raw;
			txt_part->utf_content = txt_part->raw;
			txt_part->utf_stripped_text = (UText) UTEXT_INITIALIZER;
			txt_part->real_charset = "utf-8";

			part->specific.txt = txt_part;
			g_ptr_array_add(task->message->parts, part);
			g_ptr_array_add(task->message->text_parts, txt_part);

			if (task->cfg && task->message) {
				uint16_t cur_url_order = 10000;
				rspamd_message_process_injected_text_part(task, txt_part, &cur_url_order);
			}

			lua_pushboolean(L, true);
		}
		else {
			lua_pushboolean(L, false);
		}
	}
	else {
		lua_pushboolean(L, false);
	}

	return 1;
}

struct rspamd_url_query_to_inject_cbd {
	struct rspamd_task *task;
	struct rspamd_url *url;
	GPtrArray *mpart_urls;
	uint32_t parent_flags;
};

static gboolean
inject_url_query_callback(struct rspamd_url *url, gsize start_offset,
						  gsize end_offset, gpointer ud)
{
	struct rspamd_url_query_to_inject_cbd *cbd =
		(struct rspamd_url_query_to_inject_cbd *) ud;
	struct rspamd_task *task;

	task = cbd->task;

	url->flags |= RSPAMD_URL_FLAG_QUERY;
	url->flags |= (cbd->parent_flags & RSPAMD_URL_FLAG_PROPAGATE_MASK);

	if (cbd->mpart_urls) {
		g_ptr_array_add(cbd->mpart_urls, url);
	}

	rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), url, false);

	return TRUE;
}

static void
inject_url_query(struct rspamd_task *task, struct rspamd_url *url,
				 GPtrArray *part_urls)
{
	if (url->querylen > 0) {
		struct rspamd_url_query_to_inject_cbd cbd;

		cbd.task = task;
		cbd.url = url;
		cbd.mpart_urls = part_urls;
		cbd.parent_flags = url->flags;

		rspamd_url_find_multiple(task->task_pool,
								 rspamd_url_query_unsafe(url), url->querylen,
								 RSPAMD_URL_FIND_ALL, NULL,
								 inject_url_query_callback, &cbd,
								 task->cfg ? task->cfg->lua_state : NULL);
	}

	if (part_urls) {
		g_ptr_array_add(part_urls, url);
	}
}

static int
lua_task_inject_url(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_lua_url *url = lua_check_url(L, 2);
	struct rspamd_mime_part *mpart = NULL;

	if (lua_isuserdata(L, 3)) {
		/* We also have a mime part there */
		mpart = *((struct rspamd_mime_part **)
					  rspamd_lua_check_udata_maybe(L, 3, rspamd_mimepart_classname));
	}
	if (task && task->message && url && url->url) {
		rspamd_url_set_add_or_increase(MESSAGE_FIELD(task, urls), url->url, false);

		if (mpart && mpart->urls) {
			inject_url_query(task, url->url, mpart->urls);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_get_content(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_lua_text *t;

	if (task) {
		t = lua_newuserdata(L, sizeof(*t));
		rspamd_lua_setclass(L, rspamd_text_classname, -1);
		t->len = task->msg.len;
		t->start = task->msg.begin;
		t->flags = 0;

		if (lua_is_text_binary(t)) {
			t->flags |= RSPAMD_TEXT_FLAG_BINARY;
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_filename(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (task->msg.fpath) {
			lua_pushstring(L, task->msg.fpath);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_rawbody(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_lua_text *t;

	if (task) {
		if (task->message != NULL) {


			if (MESSAGE_FIELD(task, raw_headers_content).len > 0) {
				g_assert(MESSAGE_FIELD(task, raw_headers_content).len <= task->msg.len);
				t = lua_new_text_task(L, task, task->msg.begin + MESSAGE_FIELD(task, raw_headers_content).len,
									  task->msg.len - MESSAGE_FIELD(task, raw_headers_content).len, FALSE);
			}
			else {
				t = lua_new_text_task(L, task, task->msg.begin,
									  task->msg.len, FALSE);
			}

			t->flags = 0;
		}
		else {
			/* Push body it it is there */
			if (task->msg.len > 0 && task->msg.begin != NULL) {
				lua_new_text_task(L, task, task->msg.begin, task->msg.len, FALSE);
			}
			else {
				lua_pushnil(L);
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_emails(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct lua_tree_cb_data cb;
	struct rspamd_url *u;
	gsize max_urls = 0, sz;

	if (task) {
		if (task->message) {
			if (task->cfg) {
				max_urls = task->cfg->max_lua_urls;
			}

			/*
			 * By default, include content URLs if configured (default: true).
			 * Always exclude image URLs unless explicitly requested.
			 */
			unsigned int default_flags = ~RSPAMD_URL_FLAG_IMAGE;
			if (task->cfg && !task->cfg->include_content_urls) {
				default_flags &= ~RSPAMD_URL_FLAG_CONTENT;
			}

			if (!lua_url_cbdata_fill(L, 2, &cb, PROTOCOL_MAILTO,
									 default_flags,
									 max_urls)) {
				return luaL_error(L, "invalid arguments");
			}

			sz = kh_size(MESSAGE_FIELD(task, urls));
			sz = lua_url_adjust_skip_prob(task->task_timestamp,
										  MESSAGE_FIELD(task, digest), &cb, sz);

			lua_createtable(L, sz, 0);

			kh_foreach_key(MESSAGE_FIELD(task, urls), u, {
				lua_tree_url_callback(u, u, &cb);
			});

			lua_url_cbdata_dtor(&cb);
		}
		else {
			lua_newtable(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_text_parts(lua_State *L)
{
	LUA_TRACE_POINT;
	unsigned int i;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_mime_text_part *part, **ppart;
	gboolean include_virtual = FALSE;

	if (lua_gettop(L) >= 2) {
		include_virtual = lua_toboolean(L, 2);
	}

	if (task != NULL) {

		if (task->message) {
			if (!include_virtual && lua_task_get_cached(L, task, "text_parts")) {
				return 1;
			}

			lua_newtable(L);
			int idx = 1;

			PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, part)
			{
				if (!include_virtual && (part->mime_part->flags & RSPAMD_MIME_PART_COMPUTED)) {
					continue;
				}

				ppart = lua_newuserdata(L, sizeof(struct rspamd_mime_text_part *));
				*ppart = part;
				rspamd_lua_setclass(L, rspamd_textpart_classname, -1);
				/* Make it array */
				lua_rawseti(L, -2, idx++);
			}

			if (!include_virtual) {
				lua_task_set_cached(L, task, "text_parts", -1);
			}
		}
		else {
			lua_newtable(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_parts(lua_State *L)
{
	LUA_TRACE_POINT;
	unsigned int i;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_mime_part *part, **ppart;
	gboolean include_virtual = FALSE;

	if (lua_gettop(L) >= 2) {
		include_virtual = lua_toboolean(L, 2);
	}

	if (task != NULL) {
		if (task->message) {
			lua_createtable(L, MESSAGE_FIELD(task, parts)->len, 0);
			int idx = 1;

			PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, part)
			{
				if (!include_virtual && (part->flags & RSPAMD_MIME_PART_COMPUTED)) {
					continue;
				}

				ppart = lua_newuserdata(L, sizeof(struct rspamd_mime_part *));
				*ppart = part;
				rspamd_lua_setclass(L, rspamd_mimepart_classname, -1);
				/* Make it array */
				lua_rawseti(L, -2, idx++);
			}
		}
		else {
			lua_newtable(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_request_header(lua_State *L)
{
	LUA_TRACE_POINT;
	rspamd_ftok_t *hdr;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *s;
	struct rspamd_lua_text *t;

	s = luaL_checkstring(L, 2);

	if (s && task) {
		hdr = rspamd_task_get_request_header(task, s);

		if (hdr) {
			t = lua_newuserdata(L, sizeof(*t));
			rspamd_lua_setclass(L, rspamd_text_classname, -1);
			t->start = hdr->begin;
			t->len = hdr->len;
			t->flags = 0;

			return 1;
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_request_header(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *s, *v = NULL;
	rspamd_fstring_t *buf;
	struct rspamd_lua_text *t;
	rspamd_ftok_t *hdr, *new_name;
	gsize len, vlen = 0;

	s = luaL_checklstring(L, 2, &len);

	if (s && task) {
		if (lua_type(L, 3) == LUA_TSTRING) {
			v = luaL_checklstring(L, 3, &vlen);
		}
		else if (lua_type(L, 3) == LUA_TUSERDATA) {
			t = lua_check_text(L, 3);

			if (t != NULL) {
				v = t->start;
				vlen = t->len;
			}
		}

		if (v != NULL) {
			buf = rspamd_fstring_new_init(v, vlen);
			hdr = rspamd_ftok_map(buf);
			buf = rspamd_fstring_new_init(s, len);
			new_name = rspamd_ftok_map(buf);

			rspamd_task_add_request_header(task, new_name, hdr);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}


	return 0;
}


int rspamd_lua_push_header(lua_State *L, struct rspamd_mime_header *rh,
						   enum rspamd_lua_task_header_type how)
{
	LUA_TRACE_POINT;
	switch (how) {
	case RSPAMD_TASK_HEADER_PUSH_FULL:
		/* Create new associated table for a header */
		lua_createtable(L, 0, 7);
		rspamd_lua_table_set(L, "name", rh->name);

		if (rh->value) {
			rspamd_lua_table_set(L, "value", rh->value);
		}

		if (rh->raw_len > 0) {
			lua_pushstring(L, "raw");
			lua_pushlstring(L, rh->raw_value, rh->raw_len);
			lua_settable(L, -3);
		}

		if (rh->decoded) {
			rspamd_lua_table_set(L, "decoded", rh->decoded);
		}

		lua_pushstring(L, "tab_separated");
		lua_pushboolean(L, rh->flags & RSPAMD_HEADER_TAB_SEPARATED);
		lua_settable(L, -3);
		lua_pushstring(L, "empty_separator");
		lua_pushboolean(L, rh->flags & RSPAMD_HEADER_EMPTY_SEPARATOR);
		lua_settable(L, -3);
		rspamd_lua_table_set(L, "separator", rh->separator);
		lua_pushstring(L, "order");
		lua_pushinteger(L, rh->order);
		lua_settable(L, -3);
		break;
	case RSPAMD_TASK_HEADER_PUSH_RAW:
		if (rh->value) {
			lua_pushstring(L, rh->value);
		}
		else {
			lua_pushnil(L);
		}
		break;
	case RSPAMD_TASK_HEADER_PUSH_SIMPLE:
		if (rh->decoded) {
			lua_pushstring(L, rh->decoded);
		}
		else {
			lua_pushnil(L);
		}
		break;
	case RSPAMD_TASK_HEADER_PUSH_COUNT:
	default:
		g_assert_not_reached();
		break;
	}

	return 1;
}

int rspamd_lua_push_header_array(lua_State *L,
								 const char *name,
								 struct rspamd_mime_header *rh,
								 enum rspamd_lua_task_header_type how,
								 gboolean strong)
{
	LUA_TRACE_POINT;
	struct rspamd_mime_header *cur;
	unsigned int i;
	int nret = 1;

	if (rh == NULL) {
		if (how == RSPAMD_TASK_HEADER_PUSH_HAS) {
			lua_pushboolean(L, false);
			nret = 1;
		}
		else if (how == RSPAMD_TASK_HEADER_PUSH_COUNT) {
			lua_pushnumber(L, 0);
		}
		else {
			lua_pushnil(L);
		}

		return nret;
	}

	if (how == RSPAMD_TASK_HEADER_PUSH_FULL) {
		lua_createtable(L, 0, 0);
		i = 0;

		DL_FOREACH(rh, cur)
		{
			if (!strong || strcmp(name, cur->name) == 0) {
				rspamd_lua_push_header(L, cur, how);
				lua_rawseti(L, -2, ++i);
			}
		}
	}
	else if (how == RSPAMD_TASK_HEADER_PUSH_COUNT) {
		i = 0;

		DL_FOREACH(rh, cur)
		{
			if (!strong || strcmp(name, cur->name) == 0) {
				i++;
			}
		}

		lua_pushinteger(L, i);
	}
	else if (how == RSPAMD_TASK_HEADER_PUSH_HAS) {
		nret = 1;
		bool found = false;

		if (strong) {
			/* We still have to check all headers in the chain */
			DL_FOREACH(rh, cur)
			{
				if (strcmp(name, cur->name) == 0) {
					found = true;
					break;
				}
			}
		}
		else {
			found = true;
		}

		lua_pushboolean(L, found);
	}
	else {
		DL_FOREACH(rh, cur)
		{
			if (!strong || strcmp(name, cur->name) == 0) {
				return rspamd_lua_push_header(L, cur, how);
			}
		}

		/* Not found with this case */
		lua_pushnil(L);
	}

	return nret;
}

static int
lua_task_get_header_common(lua_State *L, enum rspamd_lua_task_header_type how)
{
	LUA_TRACE_POINT;
	gboolean strong = FALSE, need_modified = FALSE;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_mime_header *rh;
	const char *name;

	name = luaL_checkstring(L, 2);

	if (name && task) {
		if (lua_gettop(L) >= 3) {
			strong = lua_toboolean(L, 3);
			if (lua_isboolean(L, 4)) {
				need_modified = lua_toboolean(L, 4);
			}
		}


		rh = rspamd_message_get_header_array(task, name, need_modified);

		return rspamd_lua_push_header_array(L, name, rh, how, strong);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}
}

static int
lua_task_get_header_full(lua_State *L)
{
	return lua_task_get_header_common(L, RSPAMD_TASK_HEADER_PUSH_FULL);
}

static int
lua_task_get_header(lua_State *L)
{
	return lua_task_get_header_common(L, RSPAMD_TASK_HEADER_PUSH_SIMPLE);
}

static int
lua_task_get_header_raw(lua_State *L)
{
	return lua_task_get_header_common(L, RSPAMD_TASK_HEADER_PUSH_RAW);
}

static int
lua_task_get_header_count(lua_State *L)
{
	return lua_task_get_header_common(L, RSPAMD_TASK_HEADER_PUSH_COUNT);
}

static int
lua_task_has_header(lua_State *L)
{
	return lua_task_get_header_common(L, RSPAMD_TASK_HEADER_PUSH_HAS);
}

static int
lua_task_get_headers(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	bool need_modified = lua_isnoneornil(L, 2) ? false : lua_toboolean(L, 2);

	if (task && task->message) {
		struct rspamd_mime_header *cur;
		int i = 1;

		lua_createtable(L, rspamd_mime_headers_count(MESSAGE_FIELD(task, raw_headers)), 0);
		LL_FOREACH2(MESSAGE_FIELD(task, headers_order), cur, ord_next)
		{
			if (need_modified && cur->modified_chain) {
				struct rspamd_mime_header *cur_modified;

				LL_FOREACH(cur->modified_chain, cur_modified)
				{
					rspamd_lua_push_header(L, cur_modified, RSPAMD_TASK_HEADER_PUSH_FULL);
					lua_rawseti(L, -2, i++);
				}
			}
			else {
				rspamd_lua_push_header(L, cur, RSPAMD_TASK_HEADER_PUSH_FULL);
				lua_rawseti(L, -2, i++);
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}


	return 1;
}

static int
lua_task_get_raw_headers(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_lua_text *t;

	if (task && task->message) {
		t = lua_newuserdata(L, sizeof(*t));
		rspamd_lua_setclass(L, rspamd_text_classname, -1);
		t->start = MESSAGE_FIELD(task, raw_headers_content).begin;
		t->len = MESSAGE_FIELD(task, raw_headers_content).len;
		t->flags = 0;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}


	return 1;
}

static int
lua_task_get_received_headers(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (!task->message) {
			/* No message - no received */
			lua_newtable(L);
			return 1;
		}

		if (!lua_task_get_cached(L, task, "received")) {

			if (rspamd_received_export_to_lua(task, L)) {
				lua_task_set_cached(L, task, "received", -1);
			}
			else {
				/* no received, preserve compatibility */
				lua_newtable(L);
				return 1;
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_queue_id(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (task->queue_id != NULL && strcmp(task->queue_id, "undef") != 0) {
			lua_pushstring(L, task->queue_id);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_uid(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		lua_pushstring(L, task->task_pool->tag.uid);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_uuid(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		lua_pushstring(L, task->task_uuid);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_resolver(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_dns_resolver **presolver;

	if (task != NULL && task->resolver != NULL) {
		presolver = lua_newuserdata(L, sizeof(void *));
		rspamd_lua_setclass(L, rspamd_resolver_classname, -1);
		*presolver = task->resolver;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_resolver(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_dns_resolver *resolver = lua_check_dns_resolver(L, 2);

	if (task != NULL && resolver != NULL) {
		task->resolver = resolver;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_inc_dns_req(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	static unsigned int warning_shown = 0;

	if (warning_shown < 100) {
		warning_shown++;
		msg_warn_task_check("task:inc_dns_req is deprecated and should not be used");
	}

	if (task != NULL) {
		/* Deprecation: already done in rspamd_dns_resolver_request */
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_get_dns_req(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {
		lua_pushinteger(L, task->dns_requests);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

enum lua_email_address_type {
	LUA_ADDRESS_ANY = 0u,
	LUA_ADDRESS_SMTP = 1,
	LUA_ADDRESS_MIME = 2,
	LUA_ADDRESS_MASK = 0x3FF,
	LUA_ADDRESS_RAW = (1u << 10),
	LUA_ADDRESS_ORIGINAL = (1u << 11),
	LUA_ADDRESS_MAX = LUA_ADDRESS_MASK,
};

/*
 * Convert element at the specified position to the type
 * for get_from/get_recipients
 */
static enum lua_email_address_type
lua_task_str_to_get_type(lua_State *L, struct rspamd_task *task, int pos, int last_pos)
{
	const char *type = NULL;
	int ret = LUA_ADDRESS_ANY;
	uint64_t h;
	gsize sz;

	/* Get what value */
	do {
		if (lua_type(L, pos) == LUA_TNUMBER) {
			ret = lua_tonumber(L, pos);

			if (ret >= LUA_ADDRESS_ANY && ret < LUA_ADDRESS_MAX) {
				return ret;
			}

			return LUA_ADDRESS_ANY;
		}
		else if (lua_type(L, pos) == LUA_TSTRING) {
			type = lua_tolstring(L, pos, &sz);

			if (type && sz > 0) {
				h = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_XXHASH64,
														type, sz, 0xdeadbabe);

				switch (h) {
				case 0xDA081341FB600389ULL: /* mime */
					ret = LUA_ADDRESS_MIME;
					break;
				case 0xEEC8A7832F8C43ACULL: /* any */
					ret = LUA_ADDRESS_ANY;
					break;
				case 0x472274D5193B2A80ULL: /* smtp */
				case 0xEFE0F586CC9F14A9ULL: /* envelope */
					ret = LUA_ADDRESS_SMTP;
					break;
				default:
					msg_err_task("invalid email type: %*s", (int) sz, type);
					break;
				}
			}
		}
		else if (lua_type(L, pos) == LUA_TTABLE) {
			for (lua_pushnil(L); lua_next(L, pos); lua_pop(L, 1)) {
				type = lua_tolstring(L, -1, &sz);

				if (type && sz > 0) {
					h = rspamd_cryptobox_fast_hash_specific(RSPAMD_CRYPTOBOX_XXHASH64,
															type, sz, 0xdeadbabe);

					switch (h) {
					case 0xDA081341FB600389ULL: /* mime */
						ret |= LUA_ADDRESS_MIME;
						break;
					case 0xEEC8A7832F8C43ACULL: /* any */
						ret |= LUA_ADDRESS_ANY;
						break;
					case 0x472274D5193B2A80ULL: /* smtp */
					case 0xEFE0F586CC9F14A9ULL: /* envelope */
						ret |= LUA_ADDRESS_SMTP;
						break;
					case 0xAF4DE083D9AD0132: /* raw */
						ret |= LUA_ADDRESS_RAW;
						break;
					case 0xC7AB6C7B7B0F5A8A: /* orig */
					case 0x1778AE905589E431: /* original */
						ret |= LUA_ADDRESS_ORIGINAL;
						break;
					default:
						msg_err_task("invalid email type: %*s", (int) sz, type);
						break;
					}
				}
			}
		}
		pos++;
	} while (pos <= last_pos);

	return ret;
}

#define EMAIL_CHECK_FLAG(fl, str)     \
	do {                              \
		if (addr->flags & (fl)) {     \
			lua_pushstring(L, (str)); \
			lua_pushboolean(L, true); \
			lua_settable(L, -3);      \
		}                             \
	} while (0)

static void
lua_push_email_address(lua_State *L, struct rspamd_email_address *addr)
{
	if (addr) {
		lua_createtable(L, 0, 5);

		if (addr->raw_len > 0) {
			lua_pushstring(L, "raw");
			lua_pushlstring(L, addr->raw, addr->raw_len);
			lua_settable(L, -3);
		}
		else {
			lua_pushstring(L, "raw");
			lua_pushstring(L, "");
			lua_settable(L, -3);
		}
		if (addr->addr_len > 0) {
			lua_pushstring(L, "addr");
			lua_pushlstring(L, addr->addr, addr->addr_len);
			lua_settable(L, -3);
		}
		else {
			lua_pushstring(L, "addr");
			lua_pushstring(L, "");
			lua_settable(L, -3);
		}
		if (addr->domain_len > 0) {
			lua_pushstring(L, "domain");
			lua_pushlstring(L, addr->domain, addr->domain_len);
			lua_settable(L, -3);
		}
		else {
			lua_pushstring(L, "domain");
			lua_pushstring(L, "");
			lua_settable(L, -3);
		}
		if (addr->user_len > 0) {
			lua_pushstring(L, "user");
			lua_pushlstring(L, addr->user, addr->user_len);
			lua_settable(L, -3);
		}
		else {
			lua_pushstring(L, "user");
			lua_pushstring(L, "");
			lua_settable(L, -3);
		}

		if (addr->name) {
			lua_pushstring(L, "name");
			lua_pushstring(L, addr->name);
			lua_settable(L, -3);
		}
		else {
			lua_pushstring(L, "name");
			lua_pushstring(L, "");
			lua_settable(L, -3);
		}

		lua_pushstring(L, "flags");
		lua_createtable(L, 0, 7);

		EMAIL_CHECK_FLAG(RSPAMD_EMAIL_ADDR_VALID, "valid");
		EMAIL_CHECK_FLAG(RSPAMD_EMAIL_ADDR_IP, "ip");
		EMAIL_CHECK_FLAG(RSPAMD_EMAIL_ADDR_BRACED, "braced");
		EMAIL_CHECK_FLAG(RSPAMD_EMAIL_ADDR_QUOTED, "quoted");
		EMAIL_CHECK_FLAG(RSPAMD_EMAIL_ADDR_EMPTY, "empty");
		EMAIL_CHECK_FLAG(RSPAMD_EMAIL_ADDR_HAS_BACKSLASH, "backslash");
		EMAIL_CHECK_FLAG(RSPAMD_EMAIL_ADDR_HAS_8BIT, "8bit");

		lua_settable(L, -3);
	}
}

void lua_push_emails_address_list(lua_State *L, GPtrArray *addrs, int flags)
{
	struct rspamd_email_address *addr;
	unsigned int i, pos = 1;

	lua_createtable(L, addrs->len, 0);

	for (i = 0; i < addrs->len; i++) {
		addr = g_ptr_array_index(addrs, i);

		if (addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL) {
			if (flags & LUA_ADDRESS_ORIGINAL) {
				lua_push_email_address(L, addr);
				lua_rawseti(L, -2, pos);
				pos++;
			}
		}
		else {
			lua_push_email_address(L, addr);
			lua_rawseti(L, -2, pos);
			pos++;
		}
	}
}

static gboolean
lua_import_email_address(lua_State *L, struct rspamd_task *task,
						 int pos,
						 struct rspamd_email_address **paddr)
{
	struct rspamd_email_address *addr;
	const char *p;
	char *dst;
	gsize len;

	g_assert(paddr != NULL);

	if (!lua_istable(L, pos)) {
		return FALSE;
	}

	addr = g_malloc0(sizeof(*addr));

	lua_pushstring(L, "name");
	lua_gettable(L, pos);

	if (lua_type(L, -1) == LUA_TSTRING) {
		p = lua_tolstring(L, -1, &len);
		dst = rspamd_mempool_alloc(task->task_pool, len + 1);
		rspamd_strlcpy(dst, p, len + 1);
		addr->name = dst;
	}

	lua_pop(L, 1);

	lua_pushstring(L, "user");
	lua_gettable(L, pos);

	if (lua_type(L, -1) == LUA_TSTRING) {
		p = lua_tolstring(L, -1, &len);
		addr->user = (const char *) rspamd_mempool_alloc(task->task_pool, len);
		memcpy((char *) addr->user, p, len);
		addr->user_len = len;
	}

	lua_pop(L, 1);

	lua_pushstring(L, "domain");
	lua_gettable(L, pos);

	if (lua_type(L, -1) == LUA_TSTRING) {
		p = lua_tolstring(L, -1, &len);
		addr->domain = (const char *) rspamd_mempool_alloc(task->task_pool, len);
		memcpy((char *) addr->domain, p, len);
		addr->domain_len = len;
	}

	lua_pop(L, 1);

	lua_pushstring(L, "addr");
	lua_gettable(L, pos);

	if (lua_type(L, -1) == LUA_TSTRING) {
		p = lua_tolstring(L, -1, &len);
		addr->addr = (const char *) rspamd_mempool_alloc(task->task_pool, len);
		memcpy((char *) addr->addr, p, len);
		addr->addr_len = len;
	}
	else {
		/* Construct addr */
		len = addr->domain_len + addr->user_len + 1;
		addr->addr = (const char *) rspamd_mempool_alloc(task->task_pool, len);
		addr->addr_len = rspamd_snprintf((char *) addr->addr, len, "%*s@%*s",
										 (int) addr->user_len, addr->user,
										 (int) addr->domain_len, addr->domain);
	}

	lua_pop(L, 1);

	lua_pushstring(L, "raw");
	lua_gettable(L, pos);

	if (lua_type(L, -1) == LUA_TSTRING) {
		char *cpy;
		p = lua_tolstring(L, -1, &len);
		cpy = rspamd_mempool_alloc(task->task_pool, len + 1);
		memcpy(cpy, p, len);
		cpy[len] = '\0';
		addr->raw_len = len;
		addr->raw = cpy;
	}
	else {
		/* Construct raw addr */
		len = addr->addr_len + 3;

		if (addr->name) {
			len += strlen(addr->name) + 1;
			dst = rspamd_mempool_alloc(task->task_pool, len + 1);

			addr->raw_len = rspamd_snprintf(dst, len, "%s <%*s>",
											addr->name,
											(int) addr->addr_len, addr->addr);
		}
		else {
			dst = rspamd_mempool_alloc(task->task_pool, len + 1);

			addr->raw_len = rspamd_snprintf(dst, len, "<%*s@%*s>",
											(int) addr->user_len, addr->user,
											(int) addr->domain_len, addr->domain);
		}

		addr->raw = dst;
	}

	lua_pop(L, 1);
	addr->flags = RSPAMD_EMAIL_ADDR_VALID;

	*paddr = addr;

	return TRUE;
}

static int
lua_task_get_recipients(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	GPtrArray *ptrs = NULL;
	int what = 0;

	if (task) {
		if (lua_gettop(L) == 2) {
			/* Get what value */
			what = lua_task_str_to_get_type(L, task, 2, lua_gettop(L));
		}

		switch (what & LUA_ADDRESS_MASK) {
		case LUA_ADDRESS_SMTP:
			/* Here we check merely envelope rcpt */
			ptrs = task->rcpt_envelope;
			break;
		case LUA_ADDRESS_MIME:
			/* Here we check merely mime rcpt */
			ptrs = MESSAGE_FIELD_CHECK(task, rcpt_mime);
			break;
		case LUA_ADDRESS_ANY:
		default:
			if (task->rcpt_envelope) {
				ptrs = task->rcpt_envelope;
			}
			else {
				ptrs = MESSAGE_FIELD_CHECK(task, rcpt_mime);
			}
			break;
		}
		if (ptrs) {
			lua_push_emails_address_list(L, ptrs, what & ~LUA_ADDRESS_MASK);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_recipients(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	GPtrArray *ptrs = NULL;
	struct rspamd_email_address *addr = NULL;
	int what = 0, pos = 3;
	const char *how = "add";
	gboolean need_update_digest = FALSE;

	if (task && lua_gettop(L) >= 3) {

		/* Get what value */
		what = lua_task_str_to_get_type(L, task, 2, -1);

		if (lua_isstring(L, 4)) {
			how = lua_tostring(L, 4);
		}

		switch (what) {
		case LUA_ADDRESS_SMTP:
			/* Here we check merely envelope rcpt */
			if (task->rcpt_envelope) {
				ptrs = task->rcpt_envelope;
			}
			else {
				ptrs = g_ptr_array_new();
				task->rcpt_envelope = ptrs;
			}
			break;
		case LUA_ADDRESS_MIME:
			/* Here we check merely mime rcpt */
			ptrs = MESSAGE_FIELD_CHECK(task, rcpt_mime);
			need_update_digest = TRUE;
			break;
		case LUA_ADDRESS_ANY:
		default:
			if (task->rcpt_envelope) {
				if (task->rcpt_envelope) {
					ptrs = task->rcpt_envelope;
				}
				else {
					ptrs = g_ptr_array_new();
					task->rcpt_envelope = ptrs;
				}
			}
			else {
				ptrs = MESSAGE_FIELD_CHECK(task, rcpt_mime);
				need_update_digest = TRUE;
			}
			break;
		}
		if (ptrs) {
			unsigned int i, flags_existing = RSPAMD_EMAIL_ADDR_ORIGINAL, flags_add = 0;
			struct rspamd_email_address *tmp;

			if (strcmp(how, "alias") == 0) {
				flags_add |= RSPAMD_EMAIL_ADDR_ALIASED;
			}
			else if (strcmp(how, "rewrite") == 0) {
				/* Clear old addresses */
				PTR_ARRAY_FOREACH(ptrs, i, tmp)
				{
					rspamd_email_address_free(addr);
				}

				g_ptr_array_set_size(ptrs, 0);
			}

			PTR_ARRAY_FOREACH(ptrs, i, tmp)
			{
				tmp->flags |= flags_existing;
			}

			lua_pushvalue(L, pos);

			for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
				if (lua_import_email_address(L, task, lua_gettop(L), &addr)) {

					if (need_update_digest) {
						rspamd_message_update_digest(task->message,
													 addr->addr, addr->addr_len);
					}

					addr->flags |= flags_add;
					g_ptr_array_add(ptrs, addr);
				}
			}

			lua_pop(L, 1);
			lua_pushboolean(L, true);
		}
		else {
			lua_pushboolean(L, false);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}


#define CHECK_EMAIL_ADDR(addr)                           \
	do {                                                 \
		if (addr == NULL) {                              \
			ret = 0;                                     \
		}                                                \
		else {                                           \
			ret = addr->flags & RSPAMD_EMAIL_ADDR_VALID; \
		}                                                \
	} while (0)

#define CHECK_EMAIL_ADDR_LIST(addr) \
	do {                            \
		if (addr == NULL) {         \
			ret = 0;                \
		}                           \
		else {                      \
			ret = addr->len > 0;    \
			nrcpt = addr->len;      \
		}                           \
	} while (0)

static int
lua_task_has_from(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	int what = 0, nrcpt = 0;
	gboolean ret = FALSE;

	if (task) {
		if (lua_gettop(L) == 2) {
			/* Get what value */
			what = lua_task_str_to_get_type(L, task, 2, lua_gettop(L));
		}

		switch (what & LUA_ADDRESS_MASK) {
		case LUA_ADDRESS_SMTP:
			/* Here we check merely envelope rcpt */
			CHECK_EMAIL_ADDR(task->from_envelope);
			break;
		case LUA_ADDRESS_MIME:
			/* Here we check merely mime rcpt */
			CHECK_EMAIL_ADDR_LIST(MESSAGE_FIELD_CHECK(task, from_mime));
			break;
		case LUA_ADDRESS_ANY:
		default:
			CHECK_EMAIL_ADDR(task->from_envelope);

			if (!ret) {
				CHECK_EMAIL_ADDR_LIST(MESSAGE_FIELD_CHECK(task, from_mime));
			}
			break;
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	lua_pushboolean(L, ret);
	(void) nrcpt; /* Silence warning */

	return 1;
}

static inline int
rspamd_check_real_recipients_array_size(GPtrArray *ar)
{
	int ret = 0, i;
	struct rspamd_email_address *addr;

	PTR_ARRAY_FOREACH(ar, i, addr)
	{
		if (!(addr->flags & RSPAMD_EMAIL_ADDR_ORIGINAL)) {
			ret++;
		}
	}

	return ret;
}

static int
lua_task_has_recipients(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	int what = 0, nrcpt = 0;
	gboolean ret = FALSE;

	if (task) {
		if (lua_gettop(L) == 2) {
			/* Get what value */
			what = lua_task_str_to_get_type(L, task, 2, lua_gettop(L));
		}

		switch (what & LUA_ADDRESS_MASK) {
		case LUA_ADDRESS_SMTP:
			/* Here we check merely envelope rcpt */
			nrcpt = rspamd_check_real_recipients_array_size(task->rcpt_envelope);
			ret = nrcpt > 0;
			break;
		case LUA_ADDRESS_MIME:
			/* Here we check merely mime rcpt */
			nrcpt = rspamd_check_real_recipients_array_size(MESSAGE_FIELD_CHECK(task, rcpt_mime));
			ret = nrcpt > 0;
			break;
		case LUA_ADDRESS_ANY:
		default:
			nrcpt = rspamd_check_real_recipients_array_size(task->rcpt_envelope);
			ret = nrcpt > 0;

			if (!ret) {
				nrcpt = rspamd_check_real_recipients_array_size(MESSAGE_FIELD_CHECK(task, rcpt_mime));
				ret = nrcpt > 0;
			}
			break;
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	lua_pushboolean(L, ret);
	lua_pushinteger(L, nrcpt);

	return 2;
}

static int
lua_task_get_from(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	GPtrArray *addrs = NULL;
	struct rspamd_email_address *addr = NULL;
	int what = 0;

	if (task) {
		if (lua_gettop(L) == 2) {
			/* Get what value */
			what = lua_task_str_to_get_type(L, task, 2, lua_gettop(L));
		}

		switch (what & LUA_ADDRESS_MASK) {
		case LUA_ADDRESS_SMTP:
			/* Here we check merely envelope rcpt */
			addr = task->from_envelope;
			break;
		case LUA_ADDRESS_MIME:
			/* Here we check merely mime rcpt */
			addrs = MESSAGE_FIELD_CHECK(task, from_mime);
			break;
		case LUA_ADDRESS_ANY:
		default:
			if (task->from_envelope) {
				addr = task->from_envelope;
			}
			else {
				addrs = MESSAGE_FIELD_CHECK(task, from_mime);
			}
			break;
		}

		if (addrs && addrs->len > 0) {
			lua_push_emails_address_list(L, addrs, what & ~LUA_ADDRESS_MASK);
		}
		else if (addr) {
			/* Create table to preserve compatibility */
			if (addr->addr) {
				lua_createtable(L, 1, 0);
				if (what & LUA_ADDRESS_ORIGINAL) {
					if (task->from_envelope_orig) {
						lua_push_email_address(L, task->from_envelope_orig);
					}
					else {
						lua_push_email_address(L, addr);
					}
				}
				else {
					lua_push_email_address(L, addr);
				}

				lua_rawseti(L, -2, 1);
			}
			else {
				lua_pushnil(L);
			}
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_from(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *how = "rewrite";
	GPtrArray *addrs = NULL;
	struct rspamd_email_address **paddr = NULL, *addr;
	gboolean need_update_digest = FALSE;
	int what = 0;

	if (task && lua_gettop(L) >= 3) {
		what = lua_task_str_to_get_type(L, task, 2, -1);

		if (lua_isstring(L, 4)) {
			how = lua_tostring(L, 4);
		}

		switch (what & LUA_ADDRESS_MASK) {
		case LUA_ADDRESS_SMTP:
			/* Here we check merely envelope rcpt */
			paddr = &task->from_envelope;
			break;
		case LUA_ADDRESS_MIME:
			/* Here we check merely mime rcpt */
			addrs = MESSAGE_FIELD_CHECK(task, from_mime);
			need_update_digest = TRUE;
			break;
		case LUA_ADDRESS_ANY:
		default:
			if (task->from_envelope) {
				paddr = &task->from_envelope;
			}
			else {
				addrs = MESSAGE_FIELD_CHECK(task, from_mime);
				need_update_digest = TRUE;
			}
			break;
		}

		if (addrs) {
			if (lua_import_email_address(L, task, 3, &addr)) {
				unsigned int i, flags_add = RSPAMD_EMAIL_ADDR_ORIGINAL;
				struct rspamd_email_address *tmp;

				if (strcmp(how, "alias") == 0) {
					flags_add |= RSPAMD_EMAIL_ADDR_ALIASED;
				}

				PTR_ARRAY_FOREACH(addrs, i, tmp)
				{
					tmp->flags |= flags_add;
				}

				if (need_update_digest) {
					rspamd_message_update_digest(task->message,
												 addr->addr, addr->addr_len);
				}

				g_ptr_array_add(addrs, addr);
				lua_pushboolean(L, true);
			}
			else {
				lua_pushboolean(L, false);
			}
		}
		else if (paddr) {
			/* SMTP from case */
			if (lua_import_email_address(L, task, 3, &addr)) {
				task->from_envelope_orig = *paddr;
				task->from_envelope = addr;
				lua_pushboolean(L, true);
			}
			else {
				lua_pushboolean(L, false);
			}
		}
		else {
			lua_pushboolean(L, false);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_principal_recipient(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *r;

	if (task) {
		r = rspamd_task_get_principal_recipient(task);
		if (r != NULL) {
			lua_pushstring(L, r);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_reply_sender(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_mime_header *rh;

	if (task) {

		rh = rspamd_message_get_header_array(task, "Reply-To", FALSE);

		if (rh) {
			GPtrArray *addrs;

			addrs = rspamd_email_address_from_mime(task->task_pool, rh->decoded,
												   strlen(rh->decoded), NULL, -1);

			if (addrs == NULL || addrs->len == 0) {
				lua_pushnil(L);
			}
			else {
				struct rspamd_email_address *addr;

				addr = (struct rspamd_email_address *) g_ptr_array_index(addrs, 0);
				lua_pushlstring(L, addr->addr, addr->addr_len);
			}
		}
		else if (MESSAGE_FIELD_CHECK(task, from_mime) &&
				 MESSAGE_FIELD(task, from_mime)->len >= 1) {
			struct rspamd_email_address *addr;

			addr = (struct rspamd_email_address *) g_ptr_array_index(
				MESSAGE_FIELD(task, from_mime), 0);

			lua_pushlstring(L, addr->addr, addr->addr_len);
		}
		else if (task->from_envelope) {
			lua_pushlstring(L, task->from_envelope->addr,
							task->from_envelope->addr_len);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_mail_esmtp_args(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		/* First try to get ESMTP args from task */
		GHashTable *mail_args = rspamd_task_get_mail_esmtp_args(task);

		if (mail_args) {
			GHashTableIter iter;
			gpointer key, value;

			lua_createtable(L, 0, 0);

			g_hash_table_iter_init(&iter, mail_args);
			while (g_hash_table_iter_next(&iter, &key, &value)) {
				rspamd_ftok_t *k = (rspamd_ftok_t *) key;
				rspamd_ftok_t *v = (rspamd_ftok_t *) value;

				lua_pushlstring(L, k->begin, k->len);
				lua_pushlstring(L, v->begin, v->len);
				lua_settable(L, -3);
			}
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_rcpt_esmtp_args(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	int idx = -1;
	gboolean all_rcpts = TRUE;

	if (task) {
		/* Check if idx was specified */
		if (lua_gettop(L) >= 2 && lua_type(L, 2) == LUA_TNUMBER) {
			idx = lua_tointeger(L, 2);
			all_rcpts = FALSE;
		}

		/* First try to get ESMTP args from task */
		GPtrArray *rcpt_args = rspamd_task_get_rcpt_esmtp_args(task);

		if (rcpt_args) {
			if (all_rcpts) {
				/* Return all recipients' args */
				lua_createtable(L, 0, 0);

				for (unsigned int i = 0; i < rcpt_args->len; i++) {
					GHashTable *args = g_ptr_array_index(rcpt_args, i);
					if (args) {
						GHashTableIter iter;
						gpointer key, value;

						lua_createtable(L, 0, 0);

						g_hash_table_iter_init(&iter, args);
						while (g_hash_table_iter_next(&iter, &key, &value)) {
							rspamd_ftok_t *k = (rspamd_ftok_t *) key;
							rspamd_ftok_t *v = (rspamd_ftok_t *) value;

							lua_pushlstring(L, k->begin, k->len);
							lua_pushlstring(L, v->begin, v->len);
							lua_settable(L, -3);
						}

						lua_rawseti(L, -2, i + 1);
					}
				}
			}
			else {
				/* Return specific recipient's args */
				if (idx >= 0 && idx < (int) rcpt_args->len) {
					GHashTable *args = g_ptr_array_index(rcpt_args, idx);
					if (args) {
						GHashTableIter iter;
						gpointer key, value;

						lua_createtable(L, 0, 0);

						g_hash_table_iter_init(&iter, args);
						while (g_hash_table_iter_next(&iter, &key, &value)) {
							rspamd_ftok_t *k = (rspamd_ftok_t *) key;
							rspamd_ftok_t *v = (rspamd_ftok_t *) value;

							lua_pushlstring(L, k->begin, k->len);
							lua_pushlstring(L, v->begin, v->len);
							lua_settable(L, -3);
						}
					}
					else {
						lua_pushnil(L);
					}
				}
				else {
					lua_pushnil(L);
				}
			}
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_user(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (task->auth_user != NULL) {
			lua_pushstring(L, task->auth_user);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_user(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *new_user;

	if (task) {

		if (lua_type(L, 2) == LUA_TSTRING) {
			new_user = lua_tostring(L, 2);

			if (task->auth_user) {
				/* Push old user */
				lua_pushstring(L, task->auth_user);
			}
			else {
				lua_pushnil(L);
			}

			task->auth_user = rspamd_mempool_strdup(task->task_pool, new_user);
		}
		else {
			/* Reset user */
			if (task->auth_user) {
				/* Push old user */
				lua_pushstring(L, task->auth_user);
			}
			else {
				lua_pushnil(L);
			}

			task->auth_user = NULL;
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_from_ip(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (task->from_addr) {
			rspamd_lua_ip_push(L, task->from_addr);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_from_ip(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	rspamd_inet_addr_t *addr = NULL;

	if (!task) {
		return luaL_error(L, "no task");
	}
	else {
		if (lua_type(L, 2) == LUA_TSTRING) {
			gsize len;
			const char *ip_str = lua_tolstring(L, 2, &len);

			if (!rspamd_parse_inet_address(&addr,
										   ip_str,
										   len,
										   RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) {
				return luaL_error(L, "invalid IP string: %s", ip_str);
			}
			else {
				if (task->from_addr) {
					rspamd_inet_address_free(task->from_addr);
				}

				task->from_addr = addr;
			}
		}
		else if (lua_type(L, 2) == LUA_TUSERDATA) {
			struct rspamd_lua_ip *ip = lua_check_ip(L, 2);

			if (ip && ip->addr) {
				if (task->from_addr) {
					rspamd_inet_address_free(task->from_addr);
				}

				task->from_addr = rspamd_inet_address_copy(ip->addr, NULL);
			}
			else {
				return luaL_error(L, "invalid IP object");
			}
		}
		else {
			return luaL_error(L, "invalid IP argument type: %s", lua_typename(L, lua_type(L, 2)));
		}
	}

	return 0;
}

static int
lua_task_get_from_ip_num(lua_State *L)
{
	LUA_TRACE_POINT;
	msg_err("this function is deprecated and should no longer be used");
	lua_pushnil(L);
	return 1;
}

static int
lua_task_get_client_ip(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (task->client_addr) {
			rspamd_lua_ip_push(L, task->client_addr);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_helo(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (task->helo != NULL) {
			lua_pushstring(L, task->helo);
			return 1;
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_subject(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (MESSAGE_FIELD_CHECK(task, subject) != NULL) {
			lua_pushstring(L, MESSAGE_FIELD(task, subject));
			return 1;
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_helo(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *new_helo;

	if (task) {
		new_helo = luaL_checkstring(L, 2);
		if (new_helo) {
			task->helo = rspamd_mempool_strdup(task->task_pool, new_helo);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_get_hostname(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (task->hostname != NULL) {
			/* Check whether it looks like an IP address */
			if (*task->hostname == '[') {
				/*
				 * From the milter documentation:
				 *  If the reverse lookup fails or if none of the IP
				 *  addresses of the resolved host name matches the
				 *  original IP address, hostname will contain the
				 *  message sender's IP address enclosed in square
				 *  brackets (e.g. `[a.b.c.d]')
				 */
				lua_pushnil(L);
			}
			else {
				lua_pushstring(L, task->hostname);
			}
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_hostname(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *new_hostname;

	if (task) {
		new_hostname = luaL_checkstring(L, 2);
		if (new_hostname) {
			task->hostname = rspamd_mempool_strdup(task->task_pool,
												   new_hostname);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_get_images(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	unsigned int nelt = 0, i;
	struct rspamd_mime_part *part;
	struct rspamd_image **pimg;

	if (task) {
		if (task->message) {
			if (!lua_task_get_cached(L, task, "images")) {
				lua_createtable(L, MESSAGE_FIELD(task, parts)->len, 0);

				PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, part)
				{
					if (part->part_type == RSPAMD_MIME_PART_IMAGE) {
						pimg = lua_newuserdata(L, sizeof(struct rspamd_image *));
						rspamd_lua_setclass(L, rspamd_image_classname, -1);
						*pimg = part->specific.img;
						lua_rawseti(L, -2, ++nelt);
					}
				}

				lua_task_set_cached(L, task, "images", -1);
			}
		}
		else {
			lua_newtable(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_archives(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	unsigned int nelt = 0, i;
	struct rspamd_mime_part *part;
	struct rspamd_archive **parch;

	if (task) {
		if (task->message) {
			if (!lua_task_get_cached(L, task, "archives")) {
				lua_createtable(L, MESSAGE_FIELD(task, parts)->len, 0);

				PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, parts), i, part)
				{
					if (part->part_type == RSPAMD_MIME_PART_ARCHIVE) {
						parch = lua_newuserdata(L, sizeof(struct rspamd_archive *));
						rspamd_lua_setclass(L, rspamd_archive_classname, -1);
						*parch = part->specific.arch;
						lua_rawseti(L, -2, ++nelt);
					}
				}

				lua_task_set_cached(L, task, "archives", -1);
			}
		}
		else {
			lua_newtable(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_dkim_results(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	unsigned int nelt = 0, i;
	struct rspamd_dkim_check_result **pres, **cur;

	if (task) {
		if (!lua_task_get_cached(L, task, "dkim_results")) {
			pres = rspamd_mempool_get_variable(task->task_pool,
											   RSPAMD_MEMPOOL_DKIM_CHECK_RESULTS);

			if (pres == NULL) {
				lua_newtable(L);
			}
			else {
				for (cur = pres; *cur != NULL; cur++) {
					nelt++;
				}

				lua_createtable(L, nelt, 0);

				for (i = 0; i < nelt; i++) {
					struct rspamd_dkim_check_result *res = pres[i];
					const char *result_str = "unknown";

					lua_createtable(L, 0, 4);

					switch (res->rcode) {
					case DKIM_CONTINUE:
						result_str = "allow";
						break;
					case DKIM_REJECT:
						result_str = "reject";
						break;
					case DKIM_TRYAGAIN:
						result_str = "tempfail";
						break;
					case DKIM_NOTFOUND:
						result_str = "not found";
						break;
					case DKIM_RECORD_ERROR:
						result_str = "bad record";
						break;
					case DKIM_PERM_ERROR:
						result_str = "permanent error";
						break;
					default:
						break;
					}

					rspamd_lua_table_set(L, "result", result_str);

					if (res->domain) {
						rspamd_lua_table_set(L, "domain", res->domain);
					}

					if (res->selector) {
						rspamd_lua_table_set(L, "selector", res->selector);
					}

					if (res->short_b) {
						rspamd_lua_table_set(L, "bhash", res->short_b);
					}

					if (res->fail_reason) {
						rspamd_lua_table_set(L, "fail_reason", res->fail_reason);
					}

					lua_rawseti(L, -2, i + 1);
				}
			}

			lua_task_set_cached(L, task, "dkim_results", -1);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static inline gboolean
lua_push_symbol_result(lua_State *L,
					   struct rspamd_task *task,
					   const char *symbol,
					   struct rspamd_symbol_result *symbol_result,
					   struct rspamd_scan_result *metric_res,
					   gboolean add_metric,
					   gboolean add_name)
{

	struct rspamd_symbol_result *s = NULL;
	struct rspamd_symbol_option *opt;
	struct rspamd_symbols_group *sym_group;
	unsigned int i;
	int j = 1, table_fields_cnt = 5;

	if (!metric_res) {
		metric_res = task->result;
	}

	if (!symbol_result) {
		s = rspamd_task_find_symbol_result(task, symbol, metric_res);
	}
	else {
		s = symbol_result;
	}

	if (s && !(s->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
		if (add_metric) {
			table_fields_cnt++;
		}
		if (add_name) {
			table_fields_cnt++;
		}

		lua_createtable(L, 0, table_fields_cnt);

		if (add_name) {
			lua_pushstring(L, "name");
			lua_pushstring(L, symbol);
			lua_settable(L, -3);
		}

		lua_pushstring(L, "score");
		lua_pushnumber(L, s->score);
		lua_settable(L, -3);

		/* Dynamic weight of the symbol */
		if (s->sym != NULL && s->sym->score != 0) {
			lua_pushstring(L, "weight");
			lua_pushnumber(L, s->score / s->sym->score);
		}
		else {
			lua_pushstring(L, "weight");
			lua_pushnumber(L, 0.0);
		}
		lua_settable(L, -3);

		if (s->sym && s->sym->gr) {
			lua_pushstring(L, "group");
			lua_pushstring(L, s->sym->gr->name);
			lua_settable(L, -3);

			lua_pushstring(L, "groups");
			lua_createtable(L, s->sym->groups->len, 0);

			PTR_ARRAY_FOREACH(s->sym->groups, i, sym_group)
			{
				lua_pushstring(L, sym_group->name);
				lua_rawseti(L, -2, i + 1);
			}

			lua_settable(L, -3);
		}
		else {
			lua_pushstring(L, "group");
			lua_pushstring(L, "ungrouped");
			lua_settable(L, -3);
		}

		if (s->options) {
			lua_pushstring(L, "options");
			lua_createtable(L, kh_size(s->options), 0);

			DL_FOREACH(s->opts_head, opt)
			{
				lua_pushlstring(L, opt->option, opt->optlen);
				lua_rawseti(L, -2, j++);
			}

			lua_settable(L, -3);
		}

		return TRUE;
	}

	return FALSE;
}

static int
lua_task_get_symbol(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *symbol;
	gboolean found = FALSE;

	symbol = luaL_checkstring(L, 2);

	if (task && symbol) {
		struct rspamd_scan_result *sres = NULL;

		if (lua_isstring(L, 3)) {
			sres = rspamd_find_metric_result(task, lua_tostring(L, 3));

			if (sres == NULL) {
				return luaL_error(L, "invalid scan result: %s",
								  lua_tostring(L, 3));
			}
		}

		/* Always push as a table for compatibility :( */
		lua_createtable(L, 1, 0);

		if ((found = lua_push_symbol_result(L, task, symbol,
											NULL, sres, TRUE, FALSE))) {
			lua_rawseti(L, -2, 1);
		}
		else {
			/* Pop table */
			lua_pop(L, 1);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	if (!found) {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_task_has_symbol(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_symbol_result *s;
	const char *symbol;
	gboolean found = FALSE;

	symbol = luaL_checkstring(L, 2);

	if (task && symbol) {
		if (lua_isstring(L, 3)) {
			s = rspamd_task_find_symbol_result(task, symbol,
											   rspamd_find_metric_result(task, lua_tostring(L, 3)));

			if (s && !(s->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
				found = TRUE;
			}
		}
		else {
			s = rspamd_task_find_symbol_result(task, symbol, NULL);

			if (s && !(s->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
				found = TRUE;
			}
		}
		lua_pushboolean(L, found);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_enable_symbol(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *symbol;
	gboolean found = FALSE;

	symbol = luaL_checkstring(L, 2);

	if (task && symbol) {
		found = rspamd_symcache_enable_symbol(task, task->cfg->cache, symbol);
		lua_pushboolean(L, found);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_disable_symbol(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *symbol;
	gboolean found = FALSE;

	symbol = luaL_checkstring(L, 2);

	if (task && symbol) {
		found = rspamd_symcache_disable_symbol(task, task->cfg->cache, symbol);
		lua_pushboolean(L, found);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_symbols(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_scan_result *mres;
	int i = 1;
	struct rspamd_symbol_result *s;

	if (task) {
		mres = task->result;

		if (lua_isstring(L, 2)) {
			mres = rspamd_find_metric_result(task, lua_tostring(L, 2));
		}

		if (mres) {
			lua_createtable(L, kh_size(mres->symbols), 0);
			lua_createtable(L, kh_size(mres->symbols), 0);

			kh_foreach_value(mres->symbols, s, {
				if (!(s->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
					lua_pushstring(L, s->name);
					lua_rawseti(L, -3, i);
					lua_pushnumber(L, s->score);
					lua_rawseti(L, -2, i);
					i++;
				}
			});
		}
		else {
			lua_createtable(L, 0, 0);
			lua_createtable(L, 0, 0);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 2;
}

static int
lua_task_get_symbols_all(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_scan_result *mres;
	struct rspamd_symbol_result *s;
	gboolean found = FALSE;
	int i = 1;

	if (task) {
		mres = task->result;

		if (lua_isstring(L, 2)) {
			mres = rspamd_find_metric_result(task, lua_tostring(L, 2));
		}

		if (mres) {
			found = TRUE;
			lua_createtable(L, kh_size(mres->symbols), 0);

			kh_foreach_value(mres->symbols, s, {
				if (!(s->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
					lua_push_symbol_result(L, task, s->name, s, mres, FALSE, TRUE);
					lua_rawseti(L, -2, i++);
				}
			});
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	if (!found) {
		lua_pushnil(L);
	}

	return 1;
}


static int
lua_task_get_symbols_numeric(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_scan_result *mres;
	int i = 1, id;
	struct rspamd_symbol_result *s;

	if (task) {
		mres = task->result;

		if (lua_isstring(L, 2)) {
			mres = rspamd_find_metric_result(task, lua_tostring(L, 2));
		}

		if (mres) {
			lua_createtable(L, kh_size(mres->symbols), 0);
			lua_createtable(L, kh_size(mres->symbols), 0);

			lua_createtable(L, kh_size(mres->symbols), 0);

			kh_foreach_value(mres->symbols, s, {
				if (!(s->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {
					id = rspamd_symcache_find_symbol(task->cfg->cache,
													 s->name);
					lua_pushinteger(L, id);
					lua_rawseti(L, -3, i);
					lua_pushnumber(L, s->score);
					lua_rawseti(L, -2, i);
					i++;
				}
			});
		}
		else {
			lua_createtable(L, 0, 0);
			lua_createtable(L, 0, 0);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 2;
}

static int
lua_task_get_groups(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	gboolean need_private;
	struct rspamd_scan_result *mres;
	struct rspamd_symbols_group *gr;
	double gr_score;

	if (task) {
		mres = task->result;

		if (lua_isboolean(L, 2)) {
			need_private = lua_toboolean(L, 2);
		}
		else {
			need_private = !(task->cfg->public_groups_only);
		}

		if (lua_isstring(L, 3)) {
			mres = rspamd_find_metric_result(task, lua_tostring(L, 3));
		}

		if (mres == NULL) {
			lua_pushnil(L);

			return 1;
		}

		lua_createtable(L, 0, kh_size(mres->sym_groups));

		kh_foreach(mres->sym_groups, gr, gr_score, {
			if (!(gr->flags & RSPAMD_SYMBOL_GROUP_PUBLIC)) {
				if (!need_private) {
					continue;
				}
			}

			lua_pushnumber(L, gr_score);
			lua_setfield(L, -2, gr->name);
		});
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

struct tokens_foreach_cbdata {
	struct rspamd_task *task;
	lua_State *L;
	int idx;
	gboolean normalize;
};

static void
tokens_foreach_cb(struct rspamd_symcache_item *item, gpointer ud)
{
	struct tokens_foreach_cbdata *cbd = ud;
	struct rspamd_symbol_result *s;
	int flags;
	const char *sym;

	sym = rspamd_symcache_item_name(item);
	flags = rspamd_symcache_item_flags(item);

	if (flags & SYMBOL_TYPE_NOSTAT) {
		return;
	}

	if ((s = rspamd_task_find_symbol_result(cbd->task, sym, NULL)) != NULL) {
		if (s->flags & RSPAMD_SYMBOL_RESULT_IGNORED) {
			lua_pushnumber(cbd->L, 0.0);
		}
		else {
			if (cbd->normalize) {
				lua_pushnumber(cbd->L, tanh(s->score));
			}
			else {
				lua_pushnumber(cbd->L, s->score);
			}
		}
	}
	else {
		lua_pushnumber(cbd->L, 0.0);
	}

	lua_rawseti(cbd->L, -2, cbd->idx++);
}

static int
lua_task_get_symbols_tokens(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct tokens_foreach_cbdata cbd;

	if (task) {
		cbd.task = task;
		cbd.L = L;
		cbd.idx = 1;
		cbd.normalize = TRUE;

		if (lua_type(L, 2) == LUA_TBOOLEAN) {
			cbd.normalize = lua_toboolean(L, 2);
		}
		else {
			cbd.normalize = TRUE;
		}

		lua_createtable(L,
						rspamd_symcache_stats_symbols_count(task->cfg->cache), 0);
		rspamd_symcache_foreach(task->cfg->cache, tokens_foreach_cb, &cbd);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	/* Return type is table created */
	return 1;
}

static int
lua_task_process_ann_tokens(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	int offset = luaL_checkinteger(L, 4);
	double min_score = 0.0;

	if (task && lua_istable(L, 2) && lua_istable(L, 3)) {
		unsigned int symlen = rspamd_lua_table_size(L, 2);
		if (lua_isnumber(L, 5)) {
			min_score = lua_tonumber(L, 5);
		}

		for (unsigned int i = 1; i <= symlen; i++, offset++) {
			const char *sym;
			struct rspamd_symbol_result *sres;

			lua_rawgeti(L, 2, i);
			sym = lua_tostring(L, -1);

			/*
			 * TODO: this cycle involves one hash lookup per symbol in a profile
			 * Basically, in a common case that would be a table of all symbols
			 * So we need to do N_symbols hash lookups which is not optimal
			 * The optimal solution is to convert [sym1, sym2, ... symn] profile
			 * to a set {sym1 = true, sym2 = true, ...} and then for each
			 * resulting symbol check this table.
			 *
			 * That would lead to N_results lookups which is usually MUCH smaller
			 */
			sres = rspamd_task_find_symbol_result(task, sym, NULL);

			if (sres && !(sres->flags & RSPAMD_SYMBOL_RESULT_IGNORED)) {

				if (!isnan(sres->score) && !isinf(sres->score) &&
					(!sres->sym ||
					 !(rspamd_symcache_item_flags(sres->sym->cache_item) & SYMBOL_TYPE_NOSTAT))) {

					double norm_score;

					if (sres->sym && !isnan(sres->sym->score)) {
						if (sres->sym->score == 0) {

							if (sres->score == 0) {
								/* Binary symbol */
								norm_score = 1.0;
							}
							else {
								norm_score = fabs(tanh(sres->score));
							}
						}
						else {
							/* Get dynamic weight */
							norm_score = fabs(sres->score / sres->sym->score);

							if (norm_score > 1.0) {
								/* Multiple hits, we assume them as a single one */
								norm_score = 1.0;
							}
						}
					}
					else {
						norm_score = fabs(tanh(sres->score));
					}

					lua_pushnumber(L, MAX(min_score, norm_score));
					lua_rawseti(L, 3, offset + 1);
				}
			}

			lua_pop(L, 1); /* Symbol name */
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

enum lua_date_type {
	DATE_CONNECT = 0,
	DATE_MESSAGE,
	DATE_INVALID
};

static enum lua_date_type
lua_task_detect_date_type(struct rspamd_task *task,
						  lua_State *L, int idx, gboolean *gmt)
{
	enum lua_date_type type = DATE_CONNECT;

	if (lua_type(L, idx) == LUA_TNUMBER) {
		int num = lua_tonumber(L, idx);
		if (num >= DATE_CONNECT && num < DATE_INVALID) {
			return num;
		}
	}
	else if (lua_type(L, idx) == LUA_TTABLE) {
		const char *str;

		lua_pushvalue(L, idx);
		lua_pushstring(L, "format");
		lua_gettable(L, -2);

		str = lua_tostring(L, -1);

		if (str) {
			if (g_ascii_strcasecmp(str, "message") == 0) {
				type = DATE_MESSAGE;
			}
		}
		else {
			msg_warn_task("date format has not been specified");
		}

		lua_pop(L, 1);

		lua_pushstring(L, "gmt");
		lua_gettable(L, -2);

		if (lua_type(L, -1) == LUA_TBOOLEAN) {
			*gmt = lua_toboolean(L, -1);
		}

		/* Value and table */
		lua_pop(L, 2);
	}

	return type;
}

static int
lua_task_get_date(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_mime_header *h;
	double tim;
	enum lua_date_type type = DATE_CONNECT;
	gboolean gmt = TRUE;

	if (task != NULL) {
		if (lua_gettop(L) > 1) {
			type = lua_task_detect_date_type(task, L, 2, &gmt);
		}
		/* Get GMT date and store it to time_t */
		if (type == DATE_CONNECT) {
			tim = task->task_timestamp;

			if (!gmt) {
				struct tm t;
				time_t tt;

				tt = tim;
				rspamd_localtime(tt, &t);
#if !defined(__sun)
				t.tm_gmtoff = 0;
#endif
				t.tm_isdst = 0;
				/* Preserve fractional part as Lua is aware of it */
				tim = mktime(&t) + (tim - tt);
			}
		}
		else {
			h = rspamd_message_get_header_array(task, "Date", FALSE);

			if (h) {
				time_t tt;
				struct tm t;
				GError *err = NULL;

				tt = rspamd_parse_smtp_date(h->decoded, strlen(h->decoded),
											&err);

				if (err == NULL) {
					if (!gmt) {
						rspamd_localtime(tt, &t);
#if !defined(__sun)
						t.tm_gmtoff = 0;
#endif
						t.tm_isdst = 0;
						tim = mktime(&t);
					}
					else {
						tim = tt;
					}
				}
				else {
					g_error_free(err);
					tim = 0.0;
				}
			}
			else {
				tim = 0.0;
			}
		}

		lua_pushnumber(L, tim);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_message_id(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {
		if (MESSAGE_FIELD_CHECK(task, message_id) != NULL) {
			lua_pushstring(L, MESSAGE_FIELD(task, message_id));
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_timeval(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct timeval tv;

	if (task != NULL) {
		if (lua_isboolean(L, 2) && !!lua_toboolean(L, 2)) {
			lua_pushnumber(L, task->task_timestamp);
		}
		else {
			double_to_tv(task->task_timestamp, &tv);
			lua_createtable(L, 0, 2);
			lua_pushstring(L, "tv_sec");
			lua_pushinteger(L, (lua_Integer) tv.tv_sec);
			lua_settable(L, -3);
			lua_pushstring(L, "tv_usec");
			lua_pushinteger(L, (lua_Integer) tv.tv_usec);
			lua_settable(L, -3);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_scan_time(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	gboolean set = TRUE;

	if (task != NULL) {
		if (lua_isboolean(L, 2)) {
			set = lua_toboolean(L, 2);
		}

		rspamd_task_set_finish_time(task);
		double diff = task->time_real_finish - task->task_timestamp;
		lua_pushnumber(L, diff);
		lua_pushnumber(L, diff);

		if (!set) {
			/* Reset to nan to allow further calcs in rspamd_task_set_finish_time */
			task->time_real_finish = NAN;
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 2;
}

static int
lua_task_get_size(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {
		lua_pushinteger(L, task->msg.len);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

/**
* - `no_log`: do not log task summary
* - `no_stat`: do not include task into scanned stats
* - `pass_all`: check all filters for task
* - `extended_urls`: output extended info about urls
* - `skip`: skip task processing
*/

#define LUA_TASK_FLAG_WRITE(flag, set)                                          \
	do {                                                                        \
		task->flags = (set) ? (task->flags | (flag)) : (task->flags & ~(flag)); \
	} while (0)

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

#define LUA_TASK_FLAG_READ(flag)                      \
	do {                                              \
		lua_pushboolean(L, !!(task->flags & (flag))); \
	} while (0)

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

#define LUA_TASK_PROTOCOL_FLAG_READ(flag)                      \
	do {                                                       \
		lua_pushboolean(L, !!(task->protocol_flags & (flag))); \
	} while (0)

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

static int
lua_task_set_flag(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *flag = luaL_checkstring(L, 2);
	gboolean set = TRUE, found = FALSE;

	if (lua_gettop(L) >= 3) {
		set = lua_toboolean(L, 3);
	}

	if (task != NULL && flag != NULL) {
		LUA_TASK_SET_FLAG(flag, "pass_all", RSPAMD_TASK_FLAG_PASS_ALL, set);
		LUA_TASK_SET_FLAG(flag, "no_log", RSPAMD_TASK_FLAG_NO_LOG, set);
		LUA_TASK_SET_FLAG(flag, "no_stat", RSPAMD_TASK_FLAG_NO_STAT, set);
		LUA_TASK_SET_FLAG(flag, "skip", RSPAMD_TASK_FLAG_SKIP, set);
		LUA_TASK_SET_FLAG(flag, "learn_spam", RSPAMD_TASK_FLAG_LEARN_SPAM, set);
		LUA_TASK_SET_FLAG(flag, "learn_ham", RSPAMD_TASK_FLAG_LEARN_HAM, set);
		LUA_TASK_SET_FLAG(flag, "broken_headers",
						  RSPAMD_TASK_FLAG_BROKEN_HEADERS, set);
		LUA_TASK_SET_FLAG(flag, "greylisted", RSPAMD_TASK_FLAG_GREYLISTED, set);
		LUA_TASK_SET_FLAG(flag, "skip_process", RSPAMD_TASK_FLAG_SKIP_PROCESS, set);
		LUA_TASK_SET_FLAG(flag, "message_rewrite", RSPAMD_TASK_FLAG_MESSAGE_REWRITE, set);

		if (!found) {
			msg_warn_task("unknown flag requested: %s", flag);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_has_flag(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *flag = luaL_checkstring(L, 2);
	gboolean found = FALSE;

	if (task != NULL && flag != NULL) {
		LUA_TASK_GET_FLAG(flag, "pass_all", RSPAMD_TASK_FLAG_PASS_ALL);
		LUA_TASK_GET_FLAG(flag, "no_log", RSPAMD_TASK_FLAG_NO_LOG);
		LUA_TASK_GET_FLAG(flag, "no_stat", RSPAMD_TASK_FLAG_NO_STAT);
		LUA_TASK_GET_FLAG(flag, "skip", RSPAMD_TASK_FLAG_SKIP);
		LUA_TASK_GET_FLAG(flag, "learn_spam", RSPAMD_TASK_FLAG_LEARN_SPAM);
		LUA_TASK_GET_FLAG(flag, "learn_ham", RSPAMD_TASK_FLAG_LEARN_HAM);
		LUA_TASK_GET_FLAG(flag, "greylisted", RSPAMD_TASK_FLAG_GREYLISTED);
		LUA_TASK_GET_FLAG(flag, "broken_headers",
						  RSPAMD_TASK_FLAG_BROKEN_HEADERS);
		LUA_TASK_GET_FLAG(flag, "skip_process",
						  RSPAMD_TASK_FLAG_SKIP_PROCESS);
		LUA_TASK_GET_FLAG(flag, "bad_unicode",
						  RSPAMD_TASK_FLAG_BAD_UNICODE);
		LUA_TASK_GET_FLAG(flag, "mime",
						  RSPAMD_TASK_FLAG_MIME);
		LUA_TASK_GET_FLAG(flag, "message_rewrite",
						  RSPAMD_TASK_FLAG_MESSAGE_REWRITE);
		LUA_TASK_GET_PROTOCOL_FLAG(flag, "milter",
								   RSPAMD_TASK_PROTOCOL_FLAG_MILTER);

		if (!found) {
			msg_warn_task("unknown flag requested: %s", flag);
			lua_pushboolean(L, 0);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_flags(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	int idx = 1;
	unsigned int flags, bit, i;

	if (task) {
		lua_createtable(L, 8, 0);

		flags = task->flags;

		for (i = 0; i <= RSPAMD_TASK_FLAG_MAX_SHIFT; i++) {
			bit = (1U << i);

			if (flags & bit) {
				switch (bit) {
				case RSPAMD_TASK_FLAG_PASS_ALL:
					lua_pushstring(L, "pass_all");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_NO_LOG:
					lua_pushstring(L, "no_log");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_NO_STAT:
					lua_pushstring(L, "no_stat");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_SKIP:
					lua_pushstring(L, "skip");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_BROKEN_HEADERS:
					lua_pushstring(L, "broken_headers");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_LEARN_SPAM:
					lua_pushstring(L, "learn_spam");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_LEARN_HAM:
					lua_pushstring(L, "learn_ham");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_GREYLISTED:
					lua_pushstring(L, "greylisted");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_SKIP_PROCESS:
					lua_pushstring(L, "skip_process");
					lua_rawseti(L, -2, idx++);
					break;
				case RSPAMD_TASK_FLAG_MESSAGE_REWRITE:
					lua_pushstring(L, "message_rewrite");
					lua_rawseti(L, -2, idx++);
					break;
				default:
					break;
				}
			}
		}

		if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_MILTER) {
			lua_pushstring(L, "milter");
			lua_rawseti(L, -2, idx++);
		}
		if (task->protocol_flags & RSPAMD_TASK_PROTOCOL_FLAG_BODY_BLOCK) {
			lua_pushstring(L, "body_block");
			lua_rawseti(L, -2, idx++);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_digest(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	char hexbuf[sizeof(MESSAGE_FIELD(task, digest)) * 2 + 1];
	int r;

	if (task) {
		if (task->message) {
			r = rspamd_encode_hex_buf(MESSAGE_FIELD(task, digest),
									  sizeof(MESSAGE_FIELD(task, digest)),
									  hexbuf, sizeof(hexbuf) - 1);

			if (r > 0) {
				hexbuf[r] = '\0';
				lua_pushstring(L, hexbuf);
			}
			else {
				lua_pushnil(L);
			}
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_learn(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	gboolean is_spam = FALSE;
	const char *clname = NULL;
	GError *err = NULL;
	int ret = 1;

	if (task == NULL) {
		return luaL_error(L, "invalid arguments");
	}

	is_spam = lua_toboolean(L, 2);
	if (lua_gettop(L) > 2) {
		clname = luaL_checkstring(L, 3);
	}

	if (!rspamd_learn_task_spam(task, is_spam, clname, &err)) {
		lua_pushboolean(L, FALSE);
		if (err != NULL) {
			lua_pushstring(L, err->message);
			ret = 2;
		}
	}
	else {
		lua_pushboolean(L, TRUE);
	}

	return ret;
}

static int
lua_task_set_settings(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	ucl_object_t *settings;
	const ucl_object_t *act, *metric_elt, *vars, *cur;
	ucl_object_iter_t it = NULL;
	struct rspamd_scan_result *mres;
	unsigned int i;

	settings = ucl_object_lua_import(L, 2);

	if (settings != NULL && task != NULL) {

		if (task->settings) {
			/* Replace existing settings (will be handled by merge in the future) */
			msg_info_task("replacing existing settings with new ones");
			ucl_object_unref(task->settings);
			task->settings = NULL;
		}

		metric_elt = ucl_object_lookup(settings, DEFAULT_METRIC);

		if (metric_elt) {
			task->settings = ucl_object_ref(metric_elt);
			ucl_object_unref(settings);
		}
		else {
			task->settings = settings;
		}

		act = ucl_object_lookup(task->settings, "actions");

		if (act && ucl_object_type(act) == UCL_OBJECT) {
			/* Adjust desired actions */
			mres = task->result;

			it = NULL;

			while ((cur = ucl_object_iterate(act, &it, true)) != NULL) {
				const char *act_name = ucl_object_key(cur);
				struct rspamd_action_config *action_config = NULL;
				double act_score;
				enum rspamd_action_type act_type;

				if (!rspamd_action_from_str(act_name, &act_type)) {
					act_type = -1;
				}

				for (i = 0; i < mres->nactions; i++) {
					struct rspamd_action_config *cur_act = &mres->actions_config[i];

					if (cur_act->action->action_type == METRIC_ACTION_CUSTOM &&
						act_type == -1) {
						/* Compare by name */
						if (g_ascii_strcasecmp(act_name, cur_act->action->name) == 0) {
							action_config = cur_act;
							break;
						}
					}
					else {
						if (cur_act->action->action_type == act_type) {
							action_config = cur_act;
							break;
						}
					}
				}

				if (!action_config) {
					act_score = ucl_object_todouble(cur);
					if (!isnan(act_score)) {
						struct rspamd_action *new_act;

						new_act = rspamd_config_get_action(task->cfg, act_name);

						if (new_act == NULL) {
							/* New action! */
							msg_info_task("added new action %s with threshold %.2f "
										  "due to settings",
										  act_name,
										  act_score);
							new_act = rspamd_mempool_alloc0(task->task_pool,
															sizeof(*new_act));
							new_act->name = rspamd_mempool_strdup(task->task_pool, act_name);
							new_act->action_type = METRIC_ACTION_CUSTOM;
							new_act->threshold = act_score;
						}
						else {
							/* A disabled action that is enabled */
							msg_info_task("enabled disabled action %s with threshold %.2f "
										  "due to settings",
										  act_name,
										  act_score);
						}

						/* Insert it to the mres structure */
						gsize new_actions_cnt = mres->nactions + 1;
						struct rspamd_action_config *old_actions = mres->actions_config;

						mres->actions_config = rspamd_mempool_alloc(task->task_pool,
																	sizeof(struct rspamd_action_config) * new_actions_cnt);
						memcpy(mres->actions_config, old_actions,
							   sizeof(struct rspamd_action_config) * mres->nactions);
						mres->actions_config[mres->nactions].action = new_act;
						mres->actions_config[mres->nactions].cur_limit = act_score;
						mres->nactions++;
					}
					/* Disabled/missing action is disabled one more time, not an error */
				}
				else {
					/* Found the existing configured action */
					if (ucl_object_type(cur) == UCL_NULL) {
						/* Disable action completely */
						action_config->flags |= RSPAMD_ACTION_RESULT_DISABLED;
						msg_info_task("disabled action %s due to settings",
									  action_config->action->name);
					}
					else {
						act_score = ucl_object_todouble(cur);
						if (isnan(act_score)) {
							msg_info_task("disabled action %s threshold (was %.2f) due to settings",
										  action_config->action->name,
										  action_config->cur_limit);
							action_config->flags |= RSPAMD_ACTION_RESULT_NO_THRESHOLD;
						}
						else {
							action_config->cur_limit = act_score;
							msg_debug_task("adjusted action %s: %.2f -> %.2f",
										   act_name,
										   action_config->cur_limit,
										   act_score);
						}
					}
				}
			}
		}

		vars = ucl_object_lookup(task->settings, "variables");
		if (vars && ucl_object_type(vars) == UCL_OBJECT) {
			/* Set memory pool variables */
			it = NULL;

			while ((cur = ucl_object_iterate(vars, &it, true)) != NULL) {
				if (ucl_object_type(cur) == UCL_STRING) {
					rspamd_mempool_set_variable(task->task_pool,
												ucl_object_key(cur), rspamd_mempool_strdup(task->task_pool, ucl_object_tostring(cur)), NULL);
				}
			}
		}

		rspamd_symcache_process_settings(task, task->cfg->cache);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_merge_and_apply_settings(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task == NULL || !lua_istable(L, 2)) {
		return luaL_error(L, "invalid arguments");
	}

	struct rspamd_settings_merge_ctx *ctx =
		rspamd_settings_merge_ctx_create(task->task_pool, task->cfg);

	/* Iterate the layers table */
	for (lua_pushnil(L); lua_next(L, 2); lua_pop(L, 1)) {
		if (!lua_istable(L, -1)) {
			continue;
		}

		lua_getfield(L, -1, "level");
		int level = lua_tointeger(L, -1);
		lua_pop(L, 1);

		lua_getfield(L, -1, "name");
		const char *name = lua_tostring(L, -1);
		lua_pop(L, 1);

		lua_getfield(L, -1, "settings_id");
		uint32_t settings_id = lua_tointeger(L, -1);
		lua_pop(L, 1);

		lua_getfield(L, -1, "apply");
		if (lua_istable(L, -1)) {
			ucl_object_t *apply_obj = ucl_object_lua_import(L, lua_gettop(L));
			if (apply_obj) {
				rspamd_settings_merge_add_layer(ctx,
												(enum rspamd_settings_layer) level,
												name ? name : "unknown",
												settings_id,
												apply_obj);
				ucl_object_unref(apply_obj);
			}
		}
		lua_pop(L, 1); /* pop apply */
	}

	ucl_object_t *merged = rspamd_settings_merge_finalize(ctx);

	if (merged) {
		/* Apply via set_settings path */
		if (task->settings) {
			ucl_object_unref(task->settings);
		}
		task->settings = merged;

		/* Apply actions overrides */
		const ucl_object_t *act = ucl_object_lookup(task->settings, "actions");
		if (act && ucl_object_type(act) == UCL_OBJECT) {
			struct rspamd_scan_result *mres = task->result;
			ucl_object_iter_t it = NULL;
			const ucl_object_t *cur;

			while ((cur = ucl_object_iterate(act, &it, true)) != NULL) {
				const char *act_name = ucl_object_key(cur);
				enum rspamd_action_type act_type;

				if (!rspamd_action_from_str(act_name, &act_type)) {
					act_type = -1;
				}

				for (unsigned int i = 0; i < mres->nactions; i++) {
					struct rspamd_action_config *cur_act = &mres->actions_config[i];
					gboolean matched = FALSE;

					if (cur_act->action->action_type == METRIC_ACTION_CUSTOM && act_type == -1) {
						matched = g_ascii_strcasecmp(act_name, cur_act->action->name) == 0;
					}
					else {
						matched = cur_act->action->action_type == act_type;
					}

					if (matched) {
						if (ucl_object_type(cur) == UCL_NULL) {
							cur_act->flags |= RSPAMD_ACTION_RESULT_DISABLED;
						}
						else {
							double act_score = ucl_object_todouble(cur);
							if (isnan(act_score)) {
								cur_act->flags |= RSPAMD_ACTION_RESULT_NO_THRESHOLD;
							}
							else {
								cur_act->cur_limit = act_score;
							}
						}
						break;
					}
				}
			}
		}

		/* Apply variables */
		const ucl_object_t *vars = ucl_object_lookup(task->settings, "variables");
		if (vars && ucl_object_type(vars) == UCL_OBJECT) {
			ucl_object_iter_t it = NULL;
			const ucl_object_t *cur;

			while ((cur = ucl_object_iterate(vars, &it, true)) != NULL) {
				if (ucl_object_type(cur) == UCL_STRING) {
					rspamd_mempool_set_variable(task->task_pool,
												ucl_object_key(cur),
												rspamd_mempool_strdup(task->task_pool,
																	  ucl_object_tostring(cur)),
												NULL);
				}
			}
		}

		rspamd_symcache_process_settings(task, task->cfg->cache);
		lua_pushboolean(L, 1);
	}
	else {
		lua_pushboolean(L, 0);
	}

	return 1;
}

static int
lua_task_set_milter_reply(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	ucl_object_t *reply, *prev;

	reply = ucl_object_lua_import(L, 2);

	if (reply != NULL && task != NULL) {
		prev = rspamd_mempool_get_variable(task->task_pool,
										   RSPAMD_MEMPOOL_MILTER_REPLY);

		if (prev) {
			/*
			 * We need to be very special about the add_headers part
			 * If we want to insert some existing object, such as
			 * add_headers = {
			 *   hdr = {value = val1, order = 1},
			 * }
			 *
			 * and new header has something similar:
			 * add_headers = {
			 *   hdr = {value = val2, order = 1},
			 * }
			 *
			 * then we need to convert it to an array...
			 *
			 * add_headers = {
			 *   hdr = [{value = val1, order = 1}, {value = val2, order = 1}],
			 * }
			 *
			 * UCL itself cannot do it directly. So the trick is to extract the
			 * original object, pack it into an array and then insert it back.
			 *
			 * I wish there was a simpler way to do it...
			 */
			const ucl_object_t *add_hdrs = ucl_object_lookup(prev, "add_headers");
			const ucl_object_t *nadd_hdrs = ucl_object_lookup(reply, "add_headers");

			if (add_hdrs && nadd_hdrs) {
				ucl_object_iter_t it = NULL;
				const ucl_object_t *cur;

				while ((cur = ucl_object_iterate(nadd_hdrs, &it, true)) != NULL) {
					gsize klen;
					const char *key = ucl_object_keyl(cur, &klen);
					const ucl_object_t *existing;

					existing = ucl_object_lookup_len(add_hdrs, key, klen);

					if (existing && ucl_object_type(existing) != UCL_ARRAY) {
						ucl_object_t *ar = ucl_object_typed_new(UCL_ARRAY);

						ucl_array_append(ar, ucl_object_ref(existing));
						/* Avoid double refcount */
						key = ucl_object_keyl(existing, &klen);
						ucl_object_delete_keyl((ucl_object_t *) add_hdrs, key, klen);
						ucl_object_insert_key((ucl_object_t *) add_hdrs,
											  ar, key, klen, false);
					}
				}
			}

			if (!ucl_object_merge(prev, reply, false)) {
				msg_err_task("internal error: cannot merge two objects when setting milter reply!");
			}
			ucl_object_unref(reply);
		}
		else {
			rspamd_mempool_set_variable(task->task_pool,
										RSPAMD_MEMPOOL_MILTER_REPLY,
										reply,
										(rspamd_mempool_destruct_t) ucl_object_unref);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_get_settings(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {

		if (task->settings) {
			return ucl_object_push_lua(L, task->settings, true);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_lookup_settings(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *key = NULL;
	const ucl_object_t *elt;

	if (task != NULL) {

		if (lua_isstring(L, 2)) {
			key = lua_tostring(L, 2);
		}

		if (task->settings) {
			if (key == NULL) {
				return ucl_object_push_lua(L, task->settings, true);
			}
			else {
				elt = ucl_object_lookup(task->settings, key);

				if (elt) {
					return ucl_object_push_lua(L, elt, true);
				}
				else {
					lua_pushnil(L);
				}
			}
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_settings_id(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task != NULL) {

		if (task->settings_elt) {
			lua_pushinteger(L, task->settings_elt->id);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_settings_id(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	uint32_t id = lua_tointeger(L, 2);

	if (task != NULL && id != 0) {

		struct rspamd_config_settings_elt *selt =
			rspamd_config_find_settings_id_ref(task->cfg, id);

		if (selt == NULL) {
			return luaL_error(L, "settings id %f is unknown", (lua_Number) id);
		}
		if (task->settings_elt) {
			/* Overwrite existing settings from Lua */
			REF_RELEASE(task->settings_elt);
			lua_pushboolean(L, true);
		}
		else {
			lua_pushboolean(L, false);
		}

		task->settings_elt = selt;
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_cache_get(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *key = luaL_checkstring(L, 2);

	if (task && key) {
		if (!lua_task_get_cached(L, task, key)) {
			lua_pushnil(L);
		}
	}
	else {
		luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_cache_set(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *key = luaL_checkstring(L, 2);

	if (task && key && lua_gettop(L) >= 3) {
		lua_task_set_cached(L, task, key, 3);
	}
	else {
		luaL_error(L, "invalid arguments");
	}

	return 0;
}

struct lua_file_cbdata {
	char *fname;
	int fd;
	gboolean keep;
};

static void
lua_tmp_file_dtor(gpointer p)
{
	struct lua_file_cbdata *cbdata = p;

	if (!cbdata->keep) {
		unlink(cbdata->fname);
	}

	close(cbdata->fd);
}

static int
lua_task_store_in_file(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	gboolean force_new = FALSE, keep = FALSE;
	char fpath[PATH_MAX];
	const char *tmpmask = NULL, *fname = NULL;
	unsigned int mode = 00600;
	int fd;
	struct lua_file_cbdata *cbdata;
	GError *err = NULL;

	if (task) {
		if (lua_istable(L, 2)) {
			if (!rspamd_lua_parse_table_arguments(L, 2, &err,
												  RSPAMD_LUA_PARSE_ARGUMENTS_DEFAULT,
												  "filename=S;tmpmask=S;mode=I;force_new=B;keep=B",
												  &fname, &tmpmask, &mode, &force_new, &keep)) {
				msg_err_task("cannot get parameters list: %e", err);

				if (err) {
					g_error_free(err);
				}

				return luaL_error(L, "invalid arguments");
			}
		}
		else if (lua_isnumber(L, 2)) {
			mode = lua_tointeger(L, 2);
		}

		if (!force_new && (task->flags & RSPAMD_TASK_FLAG_FILE) &&
			task->msg.fpath) {
			lua_pushstring(L, task->msg.fpath);
		}
		else {
			if (fname == NULL) {
				if (tmpmask == NULL) {
					rspamd_snprintf(fpath, sizeof(fpath), "%s%c%s",
									task->cfg->temp_dir,
									G_DIR_SEPARATOR, "rmsg-XXXXXXXXXX");
				}
				else {
					rspamd_snprintf(fpath, sizeof(fpath), "%s", tmpmask);
				}

				fd = g_mkstemp_full(fpath, O_WRONLY | O_CREAT | O_EXCL, mode);
				fname = fpath;

				if (fd != -1) {
					fchmod(fd, mode);
				}
			}
			else {
				fd = rspamd_file_xopen(fname, O_WRONLY | O_CREAT | O_EXCL,
									   (unsigned int) mode, FALSE);
			}

			if (fd == -1) {
				msg_err_task("cannot save file: %s", strerror(errno));
				lua_pushnil(L);
			}
			else {
				if (write(fd, task->msg.begin, task->msg.len) == -1) {
					msg_err_task("cannot write file %s: %s", fpath,
								 strerror(errno));
					unlink(fname);
					close(fd);
					lua_pushnil(L);

					return 1;
				}

				cbdata = rspamd_mempool_alloc(task->task_pool, sizeof(*cbdata));
				cbdata->fd = fd;
				cbdata->fname = rspamd_mempool_strdup(task->task_pool, fname);
				cbdata->keep = keep;
				lua_pushstring(L, cbdata->fname);
				rspamd_mempool_add_destructor(task->task_pool,
											  lua_tmp_file_dtor, cbdata);
			}
		}
	}
	else {
		luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_process_regexp(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_lua_regexp *re = NULL;
	gboolean strong = FALSE;
	const char *type_str = NULL, *header_str = NULL, *selector_str = NULL;
	GError *err = NULL;
	int ret = 0;
	enum rspamd_re_type type = RSPAMD_RE_BODY;

	/*
	 * Two calling conventions are supported:
	 * 1) Table form: task:process_regexp({ re=..., type=..., header=?, selector=?, strong=? })
	 * 2) Positional: task:process_regexp(re, type, header_or_selector, strong)
	 */
	if (task != NULL) {
		if (lua_type(L, 2) == LUA_TTABLE) {
			/* Table-based API */
			size_t header_len = 0, selector_len = 0;
			if (!rspamd_lua_parse_table_arguments(L, 2, &err,
												  RSPAMD_LUA_PARSE_ARGUMENTS_DEFAULT,
												  "*re=U{regexp};*type=S;header=V;selector=V;strong=B",
												  &re, &type_str,
												  &header_len, &header_str,
												  &selector_len, &selector_str,
												  &strong)) {
				msg_err_task("cannot get parameters list: %e", err);

				if (err) {
					g_error_free(err);
				}

				return luaL_error(L, "invalid arguments");
			}
			else {
				type = rspamd_re_cache_type_from_string(type_str);

				if ((type == RSPAMD_RE_HEADER || type == RSPAMD_RE_RAWHEADER || type == RSPAMD_RE_MIMEHEADER) && header_str == NULL) {
					msg_err_task(
						"header argument is mandatory for header/rawheader regexps");
				}
				else {
					const char *type_data = NULL;
					size_t type_len = 0;

					if (type == RSPAMD_RE_HEADER || type == RSPAMD_RE_RAWHEADER || type == RSPAMD_RE_MIMEHEADER) {
						type_data = header_str;
						type_len = header_str ? (strlen(header_str) + 1) : 0;
					}
					else if (type == RSPAMD_RE_SELECTOR) {
						type_data = selector_str;
						type_len = selector_str ? (strlen(selector_str) + 1) : 0;
					}

					ret = rspamd_re_cache_process(task, re->re, type,
												  (gpointer) type_data, type_len, strong);
				}
			}
		}
		else {
			/* Positional API: (re, type, header_or_selector, strong) */
			re = lua_check_regexp(L, 2);
			type_str = luaL_checkstring(L, 3);
			type = rspamd_re_cache_type_from_string(type_str);
			const char *type_data = NULL;
			size_t type_len = 0;

			if (lua_type(L, 4) == LUA_TSTRING) {
				type_data = lua_tostring(L, 4);
				type_len = strlen(type_data) + 1;
			}
			if (lua_type(L, 5) == LUA_TBOOLEAN) {
				strong = lua_toboolean(L, 5);
			}

			/* For header/rawheader/mimeheader, arg4 is header; for selector, arg4 is selector name */
			if ((type == RSPAMD_RE_HEADER || type == RSPAMD_RE_RAWHEADER || type == RSPAMD_RE_MIMEHEADER) && type_data == NULL) {
				msg_err_task("header argument is mandatory for header/rawheader regexps");
			}

			ret = rspamd_re_cache_process(task, re->re, type,
										  (gpointer) type_data, type_len, strong);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	lua_pushinteger(L, ret);

	return 1;
}

static int
lua_task_get_metric_result(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_scan_result *metric_res;
	struct rspamd_action *action;

	if (task) {
		metric_res = task->result;

		if (lua_isstring(L, 2)) {
			metric_res = rspamd_find_metric_result(task, lua_tostring(L, 2));

			if (metric_res == NULL) {
				lua_pushnil(L);

				return 1;
			}
		}

		/* Fields added:
		 * - `score`: current score
		 * - `action`: current action as a string
		 * - `nnegative`: number of negative rules matched
		 * - `npositive`: number of positive rules matched
		 * - `positive_score`: total score for positive rules
		 * - `negative_score`: total score for negative rules
		 * - `passthrough`: set to true if message has a passthrough result
		 */
		lua_createtable(L, 0, 7);

		lua_pushstring(L, "score");
		lua_pushnumber(L, metric_res->score);
		lua_settable(L, -3);

		action = rspamd_check_action_metric(task, NULL, metric_res);

		if (action) {
			lua_pushstring(L, "action");
			lua_pushstring(L, action->name);
			lua_settable(L, -3);
		}

		lua_pushstring(L, "nnegative");
		lua_pushnumber(L, metric_res->nnegative);
		lua_settable(L, -3);

		lua_pushstring(L, "npositive");
		lua_pushnumber(L, metric_res->npositive);
		lua_settable(L, -3);

		lua_pushstring(L, "positive_score");
		lua_pushnumber(L, metric_res->positive_score);
		lua_settable(L, -3);

		lua_pushstring(L, "negative_score");
		lua_pushnumber(L, metric_res->negative_score);
		lua_settable(L, -3);

		lua_pushstring(L, "passthrough");
		lua_pushboolean(L, !!(metric_res->passthrough_result != NULL));
		lua_settable(L, -3);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_metric_score(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	double rs;
	struct rspamd_scan_result *metric_res;

	if (task) {
		metric_res = task->result;

		if (lua_isstring(L, 2)) {
			metric_res = rspamd_find_metric_result(task, lua_tostring(L, 2));
		}

		if (metric_res != NULL) {
			lua_createtable(L, 2, 0);
			lua_pushnumber(L, isnan(metric_res->score) ? 0.0 : metric_res->score);
			rs = rspamd_task_get_required_score(task, metric_res);
			lua_rawseti(L, -2, 1);
			lua_pushnumber(L, rs);
			lua_rawseti(L, -2, 2);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_metric_action(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_action *action;

	if (task) {
		struct rspamd_scan_result *mres = task->result;

		if (lua_isstring(L, 2)) {
			mres = rspamd_find_metric_result(task, lua_tostring(L, 2));
		}

		if (mres == NULL) {
			lua_pushnil(L);

			return 1;
		}

		action = rspamd_check_action_metric(task, NULL, mres);
		lua_pushstring(L, action->name);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_metric_threshold(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *act_name = luaL_checkstring(L, 2);

	if (task && act_name && task->result) {
		struct rspamd_action *action = rspamd_config_get_action(task->cfg, act_name);

		if (action == NULL) {
			lua_pushnil(L);
		}
		else {
			struct rspamd_action_config *act_config =
				rspamd_find_action_config_for_action(task->result, action);

			if (act_config) {
				lua_pushnumber(L, act_config->cur_limit);
			}
			else {
				lua_pushnil(L);
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_metric_score(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_scan_result *metric_res;
	double nscore;

	if (lua_isnumber(L, 2)) {
		nscore = luaL_checknumber(L, 2);
	}
	else {
		nscore = luaL_checknumber(L, 3);
	}

	if (task) {
		metric_res = task->result;

		if (lua_isstring(L, 4)) {
			metric_res = rspamd_find_metric_result(task, lua_tostring(L, 4));
		}

		if (metric_res != NULL) {
			msg_debug_task("set metric score from %.2f to %.2f",
						   metric_res->score, nscore);
			metric_res->score = nscore;
			lua_pushboolean(L, true);
		}
		else {
			lua_pushboolean(L, false);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_disable_action(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *action_name;
	struct rspamd_action_config *action_res;

	action_name = luaL_checkstring(L, 2);

	if (task && action_name) {

		for (unsigned int i = 0; i < task->result->nactions; i++) {
			action_res = &task->result->actions_config[i];

			if (strcmp(action_name, action_res->action->name) == 0) {
				if (isnan(action_res->cur_limit)) {
					lua_pushboolean(L, false);
				}
				else {
					action_res->cur_limit = NAN;
					lua_pushboolean(L, true);
				}

				break;
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_newlines_type(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		if (task->message) {
			switch (MESSAGE_FIELD(task, nlines_type)) {
			case RSPAMD_TASK_NEWLINES_CR:
				lua_pushstring(L, "cr");
				break;
			case RSPAMD_TASK_NEWLINES_LF:
				lua_pushstring(L, "lf");
				break;
			case RSPAMD_TASK_NEWLINES_CRLF:
			default:
				lua_pushstring(L, "crlf");
				break;
			}
		}
		else {
			lua_pushstring(L, "crlf");
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static void
lua_push_stat_token(lua_State *L, rspamd_token_t *tok)
{
	char numbuf[64];

	/* Table values
	 * - `data`: 64 bit number encoded as a string
	 * - `t1`: the first token (if any)
	 * - `t2`: the second token (if any)
	 * - `win`: window index
	 * - `flag`: table of strings:
	 *    - `text`: text token
	 *    - `meta`: meta token
	 *    - `lua`: lua meta token
	 *    - `exception`: exception
	 *    - `subject`: subject token
	 *    - `unigram`: unigram token
	 */
	lua_createtable(L, 0, 5);

	rspamd_snprintf(numbuf, sizeof(numbuf), "%uL", tok->data);
	lua_pushstring(L, "data");
	lua_pushstring(L, numbuf);
	lua_settable(L, -3);

	if (tok->t1) {
		lua_pushstring(L, "t1");
		lua_pushlstring(L, tok->t1->stemmed.begin, tok->t1->stemmed.len);
		lua_settable(L, -3);
	}

	if (tok->t2) {
		lua_pushstring(L, "t2");
		lua_pushlstring(L, tok->t2->stemmed.begin, tok->t2->stemmed.len);
		lua_settable(L, -3);
	}

	lua_pushstring(L, "win");
	lua_pushinteger(L, tok->window_idx);
	lua_settable(L, -3);

	lua_pushstring(L, "flags");
	lua_createtable(L, 0, 5);

	/* Flags */
	{
		if (tok->flags & RSPAMD_STAT_TOKEN_FLAG_TEXT) {
			lua_pushstring(L, "text");
			lua_pushboolean(L, true);
			lua_settable(L, -3);
		}
		if (tok->flags & RSPAMD_STAT_TOKEN_FLAG_META) {
			lua_pushstring(L, "meta");
			lua_pushboolean(L, true);
			lua_settable(L, -3);
		}
		if (tok->flags & RSPAMD_STAT_TOKEN_FLAG_LUA_META) {
			lua_pushstring(L, "lua");
			lua_pushboolean(L, true);
			lua_settable(L, -3);
		}
		if (tok->flags & RSPAMD_STAT_TOKEN_FLAG_EXCEPTION) {
			lua_pushstring(L, "exception");
			lua_pushboolean(L, true);
			lua_settable(L, -3);
		}
		if (tok->flags & RSPAMD_STAT_TOKEN_FLAG_HEADER) {
			lua_pushstring(L, "header");
			lua_pushboolean(L, true);
			lua_settable(L, -3);
		}
	}
	lua_settable(L, -3);
}

static int
lua_task_get_stat_tokens(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	unsigned int i;
	rspamd_token_t *tok;

	if (task) {
		if (!task->tokens) {
			rspamd_stat_process_tokenize(NULL, task);
		}

		if (!task->tokens) {
			lua_pushnil(L);
		}
		else {
			lua_createtable(L, task->tokens->len, 0);

			PTR_ARRAY_FOREACH(task->tokens, i, tok)
			{
				lua_push_stat_token(L, tok);
				lua_rawseti(L, -2, i + 1);
			}
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_set_metric_subject(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *subject;

	subject = luaL_checkstring(L, 2);

	if (task && subject) {
		rspamd_mempool_set_variable(task->task_pool, "metric_subject",
									rspamd_mempool_strdup(task->task_pool, subject), NULL);
		lua_pushboolean(L, true);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_protocol_reply(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	unsigned int flags = 0;
	ucl_object_t *obj;

	if (!task) {
		return luaL_error(L, "invalid arguments");
	}

	if (!(task->processed_stages & (RSPAMD_TASK_STAGE_POST_FILTERS >> 1))) {
		return luaL_error(L, "must not be called before post-filters");
	}

	if (lua_istable(L, 2)) {
		for (lua_pushnil(L); lua_next(L, 2); lua_pop(L, 1)) {
			if (lua_isstring(L, -1)) {
				const char *str = lua_tostring(L, -1);

				if (strcmp(str, "default") == 0) {
					flags |= RSPAMD_PROTOCOL_DEFAULT;
				}
				else if (strcmp(str, "basic") == 0) {
					flags |= RSPAMD_PROTOCOL_BASIC;
				}
				else if (strcmp(str, "metrics") == 0) {
					flags |= RSPAMD_PROTOCOL_METRICS;
				}
				else if (strcmp(str, "messages") == 0) {
					flags |= RSPAMD_PROTOCOL_MESSAGES;
				}
				else if (strcmp(str, "rmilter") == 0) {
					flags |= RSPAMD_PROTOCOL_RMILTER;
				}
				else if (strcmp(str, "dkim") == 0) {
					flags |= RSPAMD_PROTOCOL_DKIM;
				}
				else if (strcmp(str, "extra") == 0) {
					flags |= RSPAMD_PROTOCOL_EXTRA;
				}
				else {
					msg_err_task("invalid protocol flag: %s", str);
				}
			}
		}
	}
	else {
		flags = RSPAMD_PROTOCOL_DEFAULT;
	}

	obj = rspamd_protocol_write_ucl(task, flags);

	if (obj) {
		ucl_object_push_lua(L, obj, true);
	}
	else {
		lua_pushnil(L);
	}

	return 1;
}

static int
lua_task_headers_foreach(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	enum rspamd_lua_task_header_type how = RSPAMD_TASK_HEADER_PUSH_SIMPLE;
	struct rspamd_lua_regexp *re = NULL;
	struct rspamd_mime_header *hdr, *cur;
	int old_top;

	if (task && lua_isfunction(L, 2)) {
		if (task->message) {
			if (lua_istable(L, 3)) {
				lua_pushstring(L, "full");
				lua_gettable(L, 3);

				if (lua_isboolean(L, -1) && lua_toboolean(L, -1)) {
					how = RSPAMD_TASK_HEADER_PUSH_FULL;
				}

				lua_pop(L, 1);

				lua_pushstring(L, "raw");
				lua_gettable(L, 3);

				if (lua_isboolean(L, -1) && lua_toboolean(L, -1)) {
					how = RSPAMD_TASK_HEADER_PUSH_RAW;
				}

				lua_pop(L, 1);

				lua_pushstring(L, "regexp");
				lua_gettable(L, 3);

				if (lua_isuserdata(L, -1)) {
					RSPAMD_LUA_CHECK_UDATA_PTR_OR_RETURN(L, -1, rspamd_regexp_classname,
														 struct rspamd_lua_regexp, re);
				}

				lua_pop(L, 1);
			}

			if (MESSAGE_FIELD(task, headers_order)) {
				hdr = MESSAGE_FIELD(task, headers_order);

				LL_FOREACH2(hdr, cur, ord_next)
				{
					if (re && re->re) {
						if (!rspamd_regexp_match(re->re, cur->name,
												 strlen(cur->name), FALSE)) {
							continue;
						}
					}

					old_top = lua_gettop(L);
					lua_pushvalue(L, 2);
					lua_pushstring(L, cur->name);
					rspamd_lua_push_header(L, cur, how);

					if (lua_pcall(L, 2, LUA_MULTRET, 0) != 0) {
						msg_err("call to header_foreach failed: %s",
								lua_tostring(L, -1));
						lua_settop(L, old_top);
						break;
					}
					else {
						if (lua_gettop(L) > old_top) {
							if (lua_isboolean(L, old_top + 1)) {
								if (lua_toboolean(L, old_top + 1)) {
									lua_settop(L, old_top);
									break;
								}
							}
						}
					}

					lua_settop(L, old_top);
				}
			}
		} /* if (task->message) */
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_modify_header(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *hname = luaL_checkstring(L, 2);

	if (hname && task && lua_type(L, 3) == LUA_TTABLE) {
		if (task->message) {
			ucl_object_t *mods = ucl_object_lua_import(L, 3);

			rspamd_message_set_modified_header(task,
											   MESSAGE_FIELD(task, raw_headers),
											   hname,
											   mods,
											   &(MESSAGE_FIELD(task, headers_order)));
			ucl_object_unref(mods);

			lua_pushboolean(L, true);
		}
		else {
			lua_pushboolean(L, false);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_get_meta_words(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	enum rspamd_lua_words_type how = RSPAMD_LUA_WORDS_STEM;

	if (task == NULL) {
		return luaL_error(L, "invalid arguments");
	}

	if (!task->meta_words.a) {
		lua_createtable(L, 0, 0);
	}
	else {
		if (lua_type(L, 2) == LUA_TSTRING) {
			const char *how_str = lua_tostring(L, 2);

			if (strcmp(how_str, "stem") == 0) {
				how = RSPAMD_LUA_WORDS_STEM;
			}
			else if (strcmp(how_str, "norm") == 0) {
				how = RSPAMD_LUA_WORDS_NORM;
			}
			else if (strcmp(how_str, "raw") == 0) {
				how = RSPAMD_LUA_WORDS_RAW;
			}
			else if (strcmp(how_str, "full") == 0) {
				how = RSPAMD_LUA_WORDS_FULL;
			}
			else {
				return luaL_error(L, "unknown words type: %s", how_str);
			}
		}

		return rspamd_lua_push_words_kvec(L, &task->meta_words, how);
	}

	return 1;
}

static unsigned int
lua_lookup_words_array(lua_State *L,
					   int cbpos,
					   struct rspamd_task *task,
					   struct rspamd_lua_map *map,
					   GArray *words)
{
	rspamd_stat_token_t *tok;
	unsigned int i, nmatched = 0;
	int err_idx;
	gboolean matched;
	const char *key;
	gsize keylen;

	for (i = 0; i < words->len; i++) {
		tok = &g_array_index(words, rspamd_stat_token_t, i);

		matched = FALSE;

		if (tok->normalized.len == 0) {
			continue;
		}

		key = tok->normalized.begin;
		keylen = tok->normalized.len;

		switch (map->type) {
		case RSPAMD_LUA_MAP_SET:
		case RSPAMD_LUA_MAP_HASH:
			/* We know that tok->normalized is zero terminated in fact */
			if (rspamd_match_hash_map(map->data.hash, key, keylen)) {
				matched = TRUE;
			}
			break;
		case RSPAMD_LUA_MAP_REGEXP:
		case RSPAMD_LUA_MAP_REGEXP_MULTIPLE:
			if (rspamd_match_regexp_map_single(map->data.re_map, key,
											   keylen)) {
				matched = TRUE;
			}
			break;
		default:
			g_assert_not_reached();
			break;
		}

		if (matched) {
			nmatched++;

			lua_pushcfunction(L, &rspamd_lua_traceback);
			err_idx = lua_gettop(L);
			lua_pushvalue(L, cbpos); /* Function */
			rspamd_lua_push_full_word(L, tok);

			if (lua_pcall(L, 1, 0, err_idx) != 0) {
				msg_err_task("cannot call callback function for lookup words: %s",
							 lua_tostring(L, -1));
			}

			lua_settop(L, err_idx - 1);
		}
	}

	return nmatched;
}

static unsigned int
lua_lookup_words_kvec(lua_State *L,
					  int cbpos,
					  struct rspamd_task *task,
					  struct rspamd_lua_map *map,
					  rspamd_words_t *words)
{
	rspamd_word_t *tok;
	unsigned int i, nmatched = 0;
	int err_idx;
	gboolean matched;
	const char *key;
	gsize keylen;

	if (!words || !words->a) {
		return 0;
	}

	for (i = 0; i < kv_size(*words); i++) {
		tok = &kv_A(*words, i);

		matched = FALSE;

		if (tok->normalized.len == 0) {
			continue;
		}

		key = tok->normalized.begin;
		keylen = tok->normalized.len;

		switch (map->type) {
		case RSPAMD_LUA_MAP_SET:
		case RSPAMD_LUA_MAP_HASH:
			/* We know that tok->normalized is zero terminated in fact */
			if (rspamd_match_hash_map(map->data.hash, key, keylen)) {
				matched = TRUE;
			}
			break;
		case RSPAMD_LUA_MAP_REGEXP:
		case RSPAMD_LUA_MAP_REGEXP_MULTIPLE:
			if (rspamd_match_regexp_map_single(map->data.re_map, key,
											   keylen)) {
				matched = TRUE;
			}
			break;
		default:
			g_assert_not_reached();
			break;
		}

		if (matched) {
			nmatched++;

			lua_pushcfunction(L, &rspamd_lua_traceback);
			err_idx = lua_gettop(L);
			lua_pushvalue(L, cbpos); /* Function */
			rspamd_lua_push_full_word(L, tok);

			if (lua_pcall(L, 1, 0, err_idx) != 0) {
				msg_err_task("cannot call callback function for lookup words: %s",
							 lua_tostring(L, -1));
			}

			lua_settop(L, err_idx - 1);
		}
	}

	return nmatched;
}

static int
lua_task_lookup_words(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	struct rspamd_lua_map *map = lua_check_map(L, 2);
	struct rspamd_mime_text_part *tp;

	unsigned int i, matches = 0;

	if (task == NULL || map == NULL || task->message == NULL || lua_type(L, 3) != LUA_TFUNCTION) {
		return luaL_error(L, "invalid arguments");
	}

	if (map->type != RSPAMD_LUA_MAP_SET &&
		map->type != RSPAMD_LUA_MAP_REGEXP &&
		map->type != RSPAMD_LUA_MAP_HASH &&
		map->type != RSPAMD_LUA_MAP_REGEXP_MULTIPLE) {
		return luaL_error(L, "invalid map type");
	}

	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, tp)
	{
		if (tp->utf_words.a) {
			matches += lua_lookup_words_kvec(L, 3, task, map, &tp->utf_words);
		}
	}

	if (task->meta_words.a) {
		matches += lua_lookup_words_kvec(L, 3, task, map, &task->meta_words);
	}

	lua_pushinteger(L, matches);

	return 1;
}

static int
lua_task_topointer(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		/* XXX: this might cause issues on arm64 and LuaJIT */
		lua_pushlightuserdata(L, task);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_task_add_named_result(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);
	const char *name = luaL_checkstring(L, 2);
	int cbref;

	if (task && name && lua_isfunction(L, 3)) {
		lua_pushvalue(L, 3);
		cbref = luaL_ref(L, LUA_REGISTRYINDEX);
		rspamd_create_metric_result(task, name, cbref);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 0;
}

static int
lua_task_get_all_named_results(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_task *task = lua_check_task(L, 1);

	if (task) {
		int n = 0;
		struct rspamd_scan_result *res;

		DL_COUNT(task->result, res, n);
		lua_createtable(L, n, 0);
		n = 1;

		DL_FOREACH(task->result, res)
		{
			if (res->name != NULL) {
				lua_pushstring(L, res->name);
			}
			else {
				lua_pushstring(L, DEFAULT_METRIC);
			}

			lua_rawseti(L, -2, n++);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}


/* Image functions */
static int
lua_image_get_width(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_image *img = lua_check_image(L);

	if (img != NULL) {
		lua_pushinteger(L, img->width);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_image_get_height(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_image *img = lua_check_image(L);

	if (img != NULL) {
		lua_pushinteger(L, img->height);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_image_get_type(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_image *img = lua_check_image(L);

	if (img != NULL) {
		lua_pushstring(L, rspamd_image_type_str(img->type));
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_image_get_size(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_image *img = lua_check_image(L);

	if (img != NULL) {
		lua_pushinteger(L, img->data->len);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_image_get_filename(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_image *img = lua_check_image(L);

	if (img != NULL) {
		if (img->filename != NULL) {
			lua_pushlstring(L, img->filename->begin, img->filename->len);
		}
		else {
			lua_pushnil(L);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

/* Archive methods */
static int
lua_archive_get_type(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_archive *arch = lua_check_archive(L);

	if (arch != NULL) {
		lua_pushstring(L, rspamd_archive_type_str(arch->type));
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_archive_get_files(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_archive *arch = lua_check_archive(L);
	unsigned int i, max_files = 0;
	struct rspamd_archive_file *f;

	if (arch != NULL) {
		if (lua_isnumber(L, 2)) {
			max_files = lua_tointeger(L, 2);
			max_files = MIN(arch->files->len, max_files);
		}
		else {
			max_files = arch->files->len;
		}

		lua_createtable(L, max_files, 0);

		for (i = 0; i < max_files; i++) {
			f = g_ptr_array_index(arch->files, i);

			lua_pushlstring(L, f->fname->str, f->fname->len);
			lua_rawseti(L, -2, i + 1);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_archive_get_files_full(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_archive *arch = lua_check_archive(L);
	unsigned int i, max_files = 0;
	struct rspamd_archive_file *f;

	if (arch != NULL) {
		if (lua_isnumber(L, 2)) {
			max_files = lua_tointeger(L, 2);
			max_files = MIN(arch->files->len, max_files);
		}
		else {
			max_files = arch->files->len;
		}

		lua_createtable(L, max_files, 0);

		for (i = 0; i < max_files; i++) {
			f = g_ptr_array_index(arch->files, i);

			lua_createtable(L, 0, 4);

			lua_pushstring(L, "name");
			lua_pushlstring(L, f->fname->str, f->fname->len);
			lua_settable(L, -3);

			lua_pushstring(L, "compressed_size");
			lua_pushinteger(L, f->compressed_size);
			lua_settable(L, -3);

			lua_pushstring(L, "uncompressed_size");
			lua_pushinteger(L, f->uncompressed_size);
			lua_settable(L, -3);

			lua_pushstring(L, "encrypted");
			lua_pushboolean(L, (f->flags & RSPAMD_ARCHIVE_FILE_ENCRYPTED) ? true : false);
			lua_settable(L, -3);

			lua_rawseti(L, -2, i + 1);
		}
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_archive_is_encrypted(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_archive *arch = lua_check_archive(L);

	if (arch != NULL) {
		lua_pushboolean(L, (arch->flags & RSPAMD_ARCHIVE_ENCRYPTED) ? true : false);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_archive_is_obfuscated(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_archive *arch = lua_check_archive(L);

	if (arch != NULL) {
		lua_pushboolean(L,
						(arch->flags & RSPAMD_ARCHIVE_HAS_OBFUSCATED_FILES) ? true : false);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_archive_is_unreadable(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_archive *arch = lua_check_archive(L);

	if (arch != NULL) {
		lua_pushboolean(L, (arch->flags & RSPAMD_ARCHIVE_CANNOT_READ) ? true : false);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_archive_get_size(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_archive *arch = lua_check_archive(L);

	if (arch != NULL) {
		lua_pushinteger(L, arch->size);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

static int
lua_archive_get_filename(lua_State *L)
{
	LUA_TRACE_POINT;
	struct rspamd_archive *arch = lua_check_archive(L);

	if (arch != NULL) {
		lua_pushlstring(L, arch->archive_name->begin, arch->archive_name->len);
	}
	else {
		return luaL_error(L, "invalid arguments");
	}

	return 1;
}

struct rspamd_task_timer_cbdata {
	lua_State *L;
	struct rspamd_task *task;
	struct rspamd_symcache_dynamic_item *item;
	struct rspamd_async_event *async_ev;
	int cbref;
	ev_timer ev;
};

static void
lua_timer_fin(gpointer arg)
{
	struct rspamd_task_timer_cbdata *cbdata = (struct rspamd_task_timer_cbdata *) arg;

	ev_timer_stop(cbdata->task->event_loop, &cbdata->ev);
	luaL_unref(cbdata->L, LUA_REGISTRYINDEX, cbdata->cbref);
}

static void
lua_task_timer_cb(struct ev_loop *loop, struct ev_timer *t, int events)
{
	struct rspamd_task_timer_cbdata *cbdata = (struct rspamd_task_timer_cbdata *) t->data;
	lua_State *L;
	bool schedule_more = false;

	L = cbdata->L;

	lua_pushcfunction(L, &rspamd_lua_traceback);
	int err_idx = lua_gettop(L);
	lua_rawgeti(L, LUA_REGISTRYINDEX, cbdata->cbref);
	rspamd_lua_task_push(L, cbdata->task);

	if (lua_pcall(L, 1, 1, err_idx) != 0) {
		msg_err("call to periodic "
				"script failed: %s",
				lua_tostring(L, -1));
	}
	else {
		if (lua_isnumber(L, -1)) {
			schedule_more = true;
			ev_timer_set(&cbdata->ev, lua_tonumber(L, -1), 0.0);
		}
	}

	if (schedule_more) {
		ev_timer_again(loop, t);
	}
	else {
		/* Cleanup */
		if (cbdata->item) {
			rspamd_symcache_item_async_dec_check(cbdata->task, cbdata->item, "timer");
			cbdata->item = NULL;
		}
		rspamd_session_remove_event(cbdata->task->s, lua_timer_fin, cbdata);
	}
}

static int
lua_task_add_timer(lua_State *L)
{
	struct ev_loop *ev_base;
	struct rspamd_task *task;

	task = lua_check_task(L, 1);
	ev_base = task->event_loop;
	if (!lua_isfunction(L, 3)) {
		return luaL_error(L, "invalid arguments: callback expected");
	}

	if (!lua_isnumber(L, 2)) {
		return luaL_error(L, "invalid arguments: timeout expected");
	}

	struct rspamd_task_timer_cbdata *cbdata = rspamd_mempool_alloc(task->task_pool, sizeof(*cbdata));
	cbdata->L = L;
	lua_pushvalue(L, 3);
	cbdata->ev.data = cbdata;
	cbdata->cbref = luaL_ref(L, LUA_REGISTRYINDEX);
	cbdata->task = task;
	cbdata->item = rspamd_symcache_get_cur_item(task);

	if (cbdata->item) {
		cbdata->async_ev = rspamd_session_add_event_full(task->s, lua_timer_fin, cbdata, "timer",
														 rspamd_symcache_dyn_item_name(cbdata->task, cbdata->item));
		rspamd_symcache_item_async_inc(task, cbdata->item, "timer");
	}
	else {
		cbdata->async_ev = rspamd_session_add_event(task->s, lua_timer_fin, cbdata, "timer");
	}

	ev_timer_init(&cbdata->ev, lua_task_timer_cb, lua_tonumber(L, 2), 0.0);
	ev_timer_start(ev_base, &cbdata->ev);

	return 0;
}

static int
lua_task_rewrite_html_urls(lua_State *L)
{
	struct rspamd_task *task = lua_check_task(L, 1);

	if (!lua_isfunction(L, 2)) {
		return luaL_error(L, "invalid arguments: function expected");
	}

	if (!task || !MESSAGE_FIELD_CHECK(task, text_parts)) {
		lua_pushnil(L);
		return 1;
	}

	/* Create function reference */
	lua_pushvalue(L, 2);
	int func_ref = luaL_ref(L, LUA_REGISTRYINDEX);

	/* Create result table */
	lua_newtable(L);
	int results = 0;
	unsigned int i;
	void *part;

	/* Iterate through text parts */
	PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, part)
	{
		struct rspamd_mime_text_part *text_part = (struct rspamd_mime_text_part *) part;

		/* Only process HTML parts */
		if (!IS_TEXT_PART_HTML(text_part) || !text_part->html) {
			continue;
		}

		/* Skip if no UTF-8 content available */
		if (!text_part->utf_raw_content || text_part->utf_raw_content->len == 0) {
			continue;
		}

		char *output_html = NULL;
		gsize output_len = 0;

		/* Process URL rewriting using C wrapper on UTF-8 buffer */
		int ret = rspamd_html_url_rewrite(
			task,
			L,
			text_part->html,
			func_ref,
			text_part->mime_part->part_number,
			(const char *) text_part->utf_raw_content->data,
			text_part->utf_raw_content->len,
			&output_html,
			&output_len);

		if (ret == 0 && output_html) {
			/* Store result in table: table[part_number] = rewritten_html */
			lua_pushinteger(L, text_part->mime_part->part_number);

			/* Create rspamd_text userdata for the rewritten content */
			struct rspamd_lua_text *t = (struct rspamd_lua_text *) lua_newuserdata(L, sizeof(struct rspamd_lua_text));
			rspamd_lua_setclass(L, rspamd_text_classname, -1);
			t->flags = 0;
			t->start = output_html;
			t->len = output_len;

			lua_settable(L, -3);
			results++;
		}
	}

	/* Unreference the function */
	luaL_unref(L, LUA_REGISTRYINDEX, func_ref);

	if (results == 0) {
		lua_pop(L, 1);
		lua_pushnil(L);
	}

	return 1;
}

/* lua_task_get_html_urls is implemented in lua_html_url_rewrite.cxx */

/* Init part */

static int
lua_load_task(lua_State *L)
{
	lua_newtable(L);
	luaL_register(L, NULL, tasklib_f);

	return 1;
}

static void
luaopen_archive(lua_State *L)
{
	rspamd_lua_new_class(L, rspamd_archive_classname, archivelib_m);
	lua_pop(L, 1);
}

void luaopen_task(lua_State *L)
{
	rspamd_lua_new_class(L, rspamd_task_classname, tasklib_m);
	lua_pop(L, 1);

	rspamd_lua_add_preload(L, "rspamd_task", lua_load_task);

	luaopen_archive(L);
}

void luaopen_image(lua_State *L)
{
	rspamd_lua_new_class(L, rspamd_image_classname, imagelib_m);
	lua_pop(L, 1);
}

void rspamd_lua_task_push(lua_State *L, struct rspamd_task *task)
{
	struct rspamd_task **ptask;

	ptask = lua_newuserdata(L, sizeof(gpointer));
	rspamd_lua_setclass(L, rspamd_task_classname, -1);
	*ptask = task;
}
