/*
 * Copyright 2024 Vsevolod Stakhov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "config.h"
#include "milter.h"
#include "milter_internal.h"
#include "email_addr.h"
#include "addr.h"
#include "unix-std.h"
#include "logger.h"
#include "ottery.h"
#include "libserver/http/http_connection.h"
#include "libserver/http/http_private.h"
#include "libserver/protocol_internal.h"
#include "libserver/cfg_file_private.h"
#include "libmime/scan_result.h"
#include "libserver/worker_util.h"
#include "utlist.h"

#define msg_err_milter(...) rspamd_default_log_function(G_LOG_LEVEL_CRITICAL,          \
														"milter", priv->pool->tag.uid, \
														RSPAMD_LOG_FUNC,               \
														__VA_ARGS__)
#define msg_warn_milter(...) rspamd_default_log_function(G_LOG_LEVEL_WARNING,           \
														 "milter", priv->pool->tag.uid, \
														 RSPAMD_LOG_FUNC,               \
														 __VA_ARGS__)
#define msg_info_milter(...) rspamd_default_log_function(G_LOG_LEVEL_INFO,              \
														 "milter", priv->pool->tag.uid, \
														 RSPAMD_LOG_FUNC,               \
														 __VA_ARGS__)
#define msg_debug_milter(...) rspamd_conditional_debug_fast(NULL, NULL,                                          \
															rspamd_milter_log_id, "milter", priv->pool->tag.uid, \
															RSPAMD_LOG_FUNC,                                     \
															__VA_ARGS__)

INIT_LOG_MODULE(milter)

static const struct rspamd_milter_context *milter_ctx = NULL;

static gboolean rspamd_milter_handle_session(
	struct rspamd_milter_session *session,
	struct rspamd_milter_private *priv);
static inline void rspamd_milter_plan_io(struct rspamd_milter_session *session,
										 struct rspamd_milter_private *priv, gshort what);

static GQuark
rspamd_milter_quark(void)
{
	return g_quark_from_static_string("milter");
}

static void
rspamd_milter_obuf_free(struct rspamd_milter_outbuf *obuf)
{
	if (obuf) {
		if (obuf->buf) {
			rspamd_fstring_free(obuf->buf);
		}

		g_free(obuf);
	}
}

#define RSPAMD_MILTER_RESET_COMMON (1 << 0)
#define RSPAMD_MILTER_RESET_IO (1 << 1)
#define RSPAMD_MILTER_RESET_ADDR (1 << 2)
#define RSPAMD_MILTER_RESET_MACRO (1 << 3)
#define RSPAMD_MILTER_RESET_ALL (RSPAMD_MILTER_RESET_COMMON | \
								 RSPAMD_MILTER_RESET_IO |     \
								 RSPAMD_MILTER_RESET_ADDR |   \
								 RSPAMD_MILTER_RESET_MACRO)
#define RSPAMD_MILTER_RESET_QUIT_NC (RSPAMD_MILTER_RESET_COMMON | \
									 RSPAMD_MILTER_RESET_ADDR |   \
									 RSPAMD_MILTER_RESET_MACRO)
#define RSPAMD_MILTER_RESET_ABORT (RSPAMD_MILTER_RESET_COMMON)

static void
rspamd_milter_session_reset(struct rspamd_milter_session *session,
							unsigned int how)
{
	struct rspamd_milter_outbuf *obuf, *obuf_tmp;
	struct rspamd_milter_private *priv = session->priv;
	struct rspamd_email_address *cur;
	unsigned int i;

	if (how & RSPAMD_MILTER_RESET_IO) {
		msg_debug_milter("cleanup IO on abort");

		DL_FOREACH_SAFE(priv->out_chain, obuf, obuf_tmp)
		{
			rspamd_milter_obuf_free(obuf);
		}

		priv->out_chain = NULL;

		if (priv->parser.buf) {
			priv->parser.buf->len = 0;
		}
	}

	if (how & RSPAMD_MILTER_RESET_COMMON) {
		msg_debug_milter("cleanup common data on abort");

		if (session->message) {
			session->message->len = 0;
			msg_debug_milter("cleanup message on abort");
		}

		if (session->rcpts) {
			PTR_ARRAY_FOREACH(session->rcpts, i, cur)
			{
				rspamd_email_address_free(cur);
			}

			msg_debug_milter("cleanup %d recipients on abort",
							 (int) session->rcpts->len);

			g_ptr_array_free(session->rcpts, TRUE);
			session->rcpts = NULL;
		}

		if (session->from) {
			msg_debug_milter("cleanup from");
			rspamd_email_address_free(session->from);
			session->from = NULL;
		}

		if (session->mail_esmtp_args) {
			msg_debug_milter("cleanup mail esmtp args");
			g_hash_table_unref(session->mail_esmtp_args);
			session->mail_esmtp_args = NULL;
		}

		if (session->rcpt_esmtp_args) {
			GHashTable *args;

			msg_debug_milter("cleanup rcpt esmtp args");
			PTR_ARRAY_FOREACH(session->rcpt_esmtp_args, i, args)
			{
				if (args) {
					g_hash_table_unref(args);
				}
			}

			g_ptr_array_free(session->rcpt_esmtp_args, TRUE);
			session->rcpt_esmtp_args = NULL;
		}

		if (priv->headers) {
			msg_debug_milter("cleanup headers");
			char *k;
			GArray *ar;

			kh_foreach(priv->headers, k, ar, {
				g_free(k);
				g_array_free(ar, TRUE);
			});

			kh_clear(milter_headers_hash_t, priv->headers);
		}

		priv->cur_hdr = 0;
	}

	if (how & RSPAMD_MILTER_RESET_ADDR) {
		if (session->addr) {
			msg_debug_milter("cleanup addr");
			rspamd_inet_address_free(session->addr);
			session->addr = NULL;
		}
		if (session->hostname) {
			msg_debug_milter("cleanup hostname");
			session->hostname->len = 0;
		}
	}

	if (how & RSPAMD_MILTER_RESET_MACRO) {
		if (session->macros) {
			msg_debug_milter("cleanup macros");
			g_hash_table_unref(session->macros);
			session->macros = NULL;
		}
	}
}

static void
rspamd_milter_session_dtor(struct rspamd_milter_session *session)
{
	struct rspamd_milter_private *priv;

	if (session) {
		priv = session->priv;
		msg_debug_milter("destroying milter session");

		rspamd_ev_watcher_stop(priv->event_loop, &priv->ev);
		rspamd_milter_session_reset(session, RSPAMD_MILTER_RESET_ALL);
		close(priv->fd);

		if (priv->parser.buf) {
			rspamd_fstring_free(priv->parser.buf);
		}

		if (session->message) {
			rspamd_fstring_free(session->message);
		}

		if (session->helo) {
			rspamd_fstring_free(session->helo);
		}

		if (session->hostname) {
			rspamd_fstring_free(session->hostname);
		}

		if (priv->headers) {
			char *k;
			GArray *ar;

			kh_foreach(priv->headers, k, ar, {
				g_free(k);
				g_array_free(ar, TRUE);
			});

			kh_destroy(milter_headers_hash_t, priv->headers);
		}

		if (milter_ctx->sessions_cache) {
			rspamd_worker_session_cache_remove(milter_ctx->sessions_cache,
											   session);
		}

		rspamd_mempool_delete(priv->pool);
		g_free(priv);
		g_free(session);
	}
}

static void
rspamd_milter_on_protocol_error(struct rspamd_milter_session *session,
								struct rspamd_milter_private *priv, GError *err)
{
	msg_debug_milter("protocol error: %e", err);
	priv->state = RSPAMD_MILTER_WANNA_DIE;
	REF_RETAIN(session);
	priv->err_cb(priv->fd, session, priv->ud, err);
	REF_RELEASE(session);
	g_error_free(err);

	rspamd_milter_plan_io(session, priv, EV_WRITE);
}

static void
rspamd_milter_on_protocol_ping(struct rspamd_milter_session *session,
							   struct rspamd_milter_private *priv)
{
	GError *err = NULL;
	static const char reply[] = "HTTP/1.1 200 OK\r\n"
								"Connection: close\r\n"
								"Server: rspamd/2.7 (milter mode)\r\n"
								"Content-Length: 6\r\n"
								"Content-Type: text/plain\r\n"
								"\r\n"
								"pong\r\n";

	if (write(priv->fd, reply, sizeof(reply)) == -1) {
		int serrno = errno;
		msg_err_milter("cannot write pong reply: %s", strerror(serrno));
		g_set_error(&err, rspamd_milter_quark(), serrno, "ping command IO error: %s",
					strerror(serrno));
		priv->state = RSPAMD_MILTER_WANNA_DIE;
		REF_RETAIN(session);
		priv->err_cb(priv->fd, session, priv->ud, err);
		REF_RELEASE(session);
		g_error_free(err);
	}
	else {
		priv->state = RSPAMD_MILTER_PONG_AND_DIE;
		rspamd_milter_plan_io(session, priv, EV_WRITE);
	}
}

static int
rspamd_milter_http_on_url(http_parser *parser, const char *at, size_t length)
{
	GString *url = (GString *) parser->data;

	g_string_append_len(url, at, length);

	return 0;
}

static void
rspamd_milter_io_handler(int fd, gshort what, void *ud)
{
	struct rspamd_milter_session *session = ud;
	struct rspamd_milter_private *priv;
	GError *err;

	priv = session->priv;

	if (what == EV_TIMEOUT) {
		msg_debug_milter("connection timed out");
		err = g_error_new(rspamd_milter_quark(), ETIMEDOUT, "connection "
															"timed out");
		rspamd_milter_on_protocol_error(session, priv, err);
	}
	else {
		rspamd_milter_handle_session(session, priv);
	}
}

static inline void
rspamd_milter_plan_io(struct rspamd_milter_session *session,
					  struct rspamd_milter_private *priv, gshort what)
{
	rspamd_ev_watcher_reschedule(priv->event_loop, &priv->ev, what);
}


#define READ_INT_32(pos, var)               \
	do {                                    \
		memcpy(&(var), (pos), sizeof(var)); \
		(pos) += sizeof(var);               \
		(var) = ntohl(var);                 \
	} while (0)
#define READ_INT_16(pos, var)               \
	do {                                    \
		memcpy(&(var), (pos), sizeof(var)); \
		(pos) += sizeof(var);               \
		(var) = ntohs(var);                 \
	} while (0)

/**
 * Parse ESMTP arguments from MAIL/RCPT commands
 * Arguments are null-terminated strings after the email address
 * Format: KEY=VALUE or KEY (without value)
 */
static GHashTable *
rspamd_milter_parse_esmtp_args(const unsigned char *pos,
							   const unsigned char *end,
							   rspamd_mempool_t *pool)
{
	GHashTable *args = NULL;
	const unsigned char *arg_start, *arg_end, *eq;
	rspamd_fstring_t *key, *value;
	rspamd_ftok_t *key_tok, *value_tok;

	while (pos < end) {
		/* Each argument is null-terminated */
		arg_end = memchr(pos, '\0', end - pos);

		if (!arg_end || arg_end == pos) {
			/* No more arguments or empty argument */
			break;
		}

		arg_start = pos;

		/* Look for KEY=VALUE separator */
		eq = memchr(arg_start, '=', arg_end - arg_start);

		if (!args) {
			/* Lazy initialization */
			args = g_hash_table_new_full(rspamd_ftok_icase_hash,
										 rspamd_ftok_icase_equal,
										 rspamd_fstring_mapped_ftok_free,
										 rspamd_fstring_mapped_ftok_free);
		}

		if (eq && eq > arg_start) {
			/* KEY=VALUE format */
			key = rspamd_fstring_new_init(arg_start, eq - arg_start);
			value = rspamd_fstring_new_init(eq + 1, arg_end - eq - 1);
		}
		else {
			/* KEY only format (no value) */
			key = rspamd_fstring_new_init(arg_start, arg_end - arg_start);
			value = rspamd_fstring_new_init("", 0);
		}

		key_tok = rspamd_ftok_map(key);
		value_tok = rspamd_ftok_map(value);

		g_hash_table_replace(args, key_tok, value_tok);

		pos = arg_end + 1;
	}

	return args;
}

static gboolean
rspamd_milter_process_command(struct rspamd_milter_session *session,
							  struct rspamd_milter_private *priv)
{
	GError *err;
	rspamd_fstring_t *buf;
	const unsigned char *pos, *end, *zero;
	unsigned int cmdlen;
	uint32_t version, actions, protocol;

	buf = priv->parser.buf;
	pos = buf->str + priv->parser.cmd_start;
	cmdlen = priv->parser.datalen;
	end = pos + cmdlen;

	switch (priv->parser.cur_cmd) {
	case RSPAMD_MILTER_CMD_ABORT:
		msg_debug_milter("got abort command");
		rspamd_milter_session_reset(session, RSPAMD_MILTER_RESET_ABORT);
		break;
	case RSPAMD_MILTER_CMD_BODY:
		if (!session->message) {
			session->message = rspamd_fstring_sized_new(
				RSPAMD_MILTER_MESSAGE_CHUNK);
		}

		msg_debug_milter("got body chunk: %d bytes", (int) cmdlen);
		session->message = rspamd_fstring_append(session->message,
												 pos, cmdlen);
		break;
	case RSPAMD_MILTER_CMD_CONNECT:
		msg_debug_milter("got connect command");

		/*
		 * char hostname[]: Hostname, NUL terminated
		 * char family: Protocol family
		 * uint16 port: Port number (SMFIA_INET or SMFIA_INET6 only)
		 * char address[]: IP address (ASCII) or unix socket path, NUL terminated
		 */
		zero = memchr(pos, '\0', cmdlen);

		if (zero == NULL || zero > (end - sizeof(uint16_t) + 1)) {
			err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
															 "connect command (no name)");
			rspamd_milter_on_protocol_error(session, priv, err);

			return FALSE;
		}
		else {
			unsigned char proto;
			uint16_t port;
			char ip6_str[INET6_ADDRSTRLEN + 3];
			gsize r;

			/*
			 * Important notice: Postfix do NOT use this command to pass
			 * client's info (e.g. hostname is not really here)
			 * Sendmail will pass it here
			 */
			if (session->hostname == NULL) {
				session->hostname = rspamd_fstring_new_init(pos, zero - pos);
				msg_debug_milter("got hostname on connect phase: %V",
								 session->hostname);
			}
			else {
				session->hostname = rspamd_fstring_assign(session->hostname,
														  pos, zero - pos);
				msg_debug_milter("rewrote hostname on connect phase: %V",
								 session->hostname);
			}

			pos = zero + 1;
			proto = *pos++;

			if (proto == RSPAMD_MILTER_CONN_UNKNOWN) {
				/* We have no information about host */
				msg_debug_milter("unknown connect address");
			}
			else {
				READ_INT_16(pos, port);

				if (pos >= end) {
					/* No IP somehow */
					msg_debug_milter("unknown connect IP/socket");
				}
				else {
					zero = memchr(pos, '\0', end - pos);

					if (zero == NULL) {
						err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
																		 "connect command (no zero terminated IP)");
						rspamd_milter_on_protocol_error(session, priv, err);

						return FALSE;
					}

					switch (proto) {
					case RSPAMD_MILTER_CONN_UNIX:
						session->addr = rspamd_inet_address_new(AF_UNIX,
																pos);
						break;

					case RSPAMD_MILTER_CONN_INET:
						session->addr = rspamd_inet_address_new(AF_INET, NULL);

						if (!rspamd_parse_inet_address_ip(pos, zero - pos,
														  session->addr)) {
							err = g_error_new(rspamd_milter_quark(), EINVAL,
											  "invalid connect command (bad IPv4)");
							rspamd_milter_on_protocol_error(session, priv,
															err);

							return FALSE;
						}

						rspamd_inet_address_set_port(session->addr, port);
						break;

					case RSPAMD_MILTER_CONN_INET6:
						session->addr = rspamd_inet_address_new(AF_INET6, NULL);

						if (zero - pos > sizeof("IPv6:") &&
							rspamd_lc_cmp(pos, "IPv6:",
										  sizeof("IPv6:") - 1) == 0) {
							/* Kill sendmail please */
							pos += sizeof("IPv6:") - 1;

							if (*pos != '[') {
								/* Add explicit braces */
								r = rspamd_snprintf(ip6_str, sizeof(ip6_str),
													"[%*s]", (int) (zero - pos), pos);
							}
							else {
								r = rspamd_strlcpy(ip6_str, pos, sizeof(ip6_str));
							}
						}
						else {
							r = rspamd_strlcpy(ip6_str, pos, sizeof(ip6_str));
						}

						if (!rspamd_parse_inet_address_ip(ip6_str, r,
														  session->addr)) {
							err = g_error_new(rspamd_milter_quark(), EINVAL,
											  "invalid connect command (bad IPv6)");
							rspamd_milter_on_protocol_error(session, priv,
															err);

							return FALSE;
						}

						rspamd_inet_address_set_port(session->addr, port);
						break;

					default:
						err = g_error_new(rspamd_milter_quark(), EINVAL,
										  "invalid connect command (bad protocol: %c)",
										  proto);
						rspamd_milter_on_protocol_error(session, priv,
														err);

						return FALSE;
					}
				}
			}

			msg_info_milter("got connection from %s",
							rspamd_inet_address_to_string_pretty(session->addr));
		}
		break;
	case RSPAMD_MILTER_CMD_MACRO:
		msg_debug_milter("got macro command");
		/*
		 * Format is
		 * 1 byte - command associated (we don't care about it)
		 * 0-terminated name
		 * 0-terminated value
		 * ...
		 */
		if (session->macros == NULL) {
			session->macros = g_hash_table_new_full(rspamd_ftok_icase_hash,
													rspamd_ftok_icase_equal,
													rspamd_fstring_mapped_ftok_free,
													rspamd_fstring_mapped_ftok_free);
		}

		/* Ignore one byte */
		pos++;

		while (pos < end) {
			zero = memchr(pos, '\0', cmdlen);

			if (zero == NULL || zero >= end) {
				err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
																 "macro command (no name)");
				rspamd_milter_on_protocol_error(session, priv, err);

				return FALSE;
			}
			else {
				rspamd_fstring_t *name, *value;
				rspamd_ftok_t *name_tok, *value_tok;
				const unsigned char *zero_val;

				zero_val = memchr(zero + 1, '\0', end - zero - 1);

				if (zero_val != NULL && end > zero_val) {
					name = rspamd_fstring_new_init(pos, zero - pos);
					value = rspamd_fstring_new_init(zero + 1,
													zero_val - zero - 1);
					name_tok = rspamd_ftok_map(name);
					value_tok = rspamd_ftok_map(value);

					g_hash_table_replace(session->macros, name_tok, value_tok);
					msg_debug_milter("got macro: %T -> %T",
									 name_tok, value_tok);

					cmdlen -= zero_val - pos;
					pos = zero_val + 1;
				}
				else {
					err = g_error_new(rspamd_milter_quark(), EINVAL,
									  "invalid macro command (bad value)");
					rspamd_milter_on_protocol_error(session, priv, err);

					return FALSE;
				}
			}
		}
		break;
	case RSPAMD_MILTER_CMD_BODYEOB:
		msg_debug_milter("got eob command");
		REF_RETAIN(session);
		priv->fin_cb(priv->fd, session, priv->ud);
		REF_RELEASE(session);
		break;
	case RSPAMD_MILTER_CMD_HELO:
		msg_debug_milter("got helo command");

		if (end > pos && *(end - 1) == '\0') {
			if (session->helo == NULL) {
				session->helo = rspamd_fstring_new_init(pos, cmdlen - 1);
			}
			else {
				session->helo = rspamd_fstring_assign(session->helo,
													  pos, cmdlen - 1);
			}
		}
		else if (end > pos) {
			/* Should not happen */
			if (session->helo == NULL) {
				session->helo = rspamd_fstring_new_init(pos, cmdlen);
			}
			else {
				session->helo = rspamd_fstring_assign(session->helo,
													  pos, cmdlen);
			}
		}

		msg_debug_milter("got helo value: %V", session->helo);

		break;
	case RSPAMD_MILTER_CMD_QUIT_NC:
		/* We need to reset session and start over */
		msg_debug_milter("got quit_nc command");
		rspamd_milter_session_reset(session, RSPAMD_MILTER_RESET_QUIT_NC);
		break;
	case RSPAMD_MILTER_CMD_HEADER:
		msg_debug_milter("got header command");
		if (!session->message) {
			session->message = rspamd_fstring_sized_new(
				RSPAMD_MILTER_MESSAGE_CHUNK);
		}
		zero = memchr(pos, '\0', cmdlen);

		if (zero == NULL) {
			err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
															 "header command (no name)");
			rspamd_milter_on_protocol_error(session, priv, err);

			return FALSE;
		}
		else {
			if (end > zero && *(end - 1) == '\0') {
				khiter_t k;
				int res;

				k = kh_get(milter_headers_hash_t, priv->headers, (char *) pos);

				if (k == kh_end(priv->headers)) {
					GArray *ar;

					k = kh_put(milter_headers_hash_t, priv->headers,
							   g_strdup(pos), &res);
					ar = g_array_new(FALSE, FALSE, sizeof(int));
					g_array_append_val(ar, priv->cur_hdr);
					kh_value(priv->headers, k) = ar;
				}
				else {
					g_array_append_val(kh_value(priv->headers, k),
									   priv->cur_hdr);
				}

				rspamd_printf_fstring(&session->message, "%*s: %*s\r\n",
									  (int) (zero - pos), pos,
									  (int) (end - zero - 2), zero + 1);
				priv->cur_hdr++;
			}
			else {
				err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
																 "header command (bad value)");
				rspamd_milter_on_protocol_error(session, priv, err);

				return FALSE;
			}
		}
		break;
	case RSPAMD_MILTER_CMD_MAIL:
		msg_debug_milter("mail command");

		while (pos < end) {
			struct rspamd_email_address *addr;
			char *cpy;

			zero = memchr(pos, '\0', end - pos);

			if (zero && zero > pos) {
				cpy = rspamd_mempool_alloc(priv->pool, zero - pos);
				memcpy(cpy, pos, zero - pos);
				msg_debug_milter("got mail: %*s", (int) (zero - pos), cpy);
				addr = rspamd_email_address_from_smtp(cpy, zero - pos);

				if (addr) {
					session->from = addr;
				}

				/* Parse ESMTP arguments */
				pos = zero + 1;
				if (pos < end) {
					session->mail_esmtp_args = rspamd_milter_parse_esmtp_args(pos, end, priv->pool);

					if (session->mail_esmtp_args) {
						GHashTableIter iter;
						gpointer key, value;

						g_hash_table_iter_init(&iter, session->mail_esmtp_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;
							msg_debug_milter("mail esmtp arg: %T=%T", k, v);
						}
					}
				}
				break;
			}
			else {
				msg_debug_milter("got weird from: %*s", (int) (end - pos),
								 pos);
				/* That actually should not happen */
				cpy = rspamd_mempool_alloc(priv->pool, end - pos);
				memcpy(cpy, pos, end - pos);
				addr = rspamd_email_address_from_smtp(cpy, end - pos);

				if (addr) {
					session->from = addr;
				}

				break;
			}
		}
		break;
	case RSPAMD_MILTER_CMD_EOH:
		msg_debug_milter("got eoh command");

		if (!session->message) {
			session->message = rspamd_fstring_sized_new(
				RSPAMD_MILTER_MESSAGE_CHUNK);
		}

		session->message = rspamd_fstring_append(session->message,
												 "\r\n", 2);
		break;
	case RSPAMD_MILTER_CMD_OPTNEG:
		if (cmdlen != sizeof(uint32_t) * 3) {
			err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
															 "optneg command");
			rspamd_milter_on_protocol_error(session, priv, err);

			return FALSE;
		}

		READ_INT_32(pos, version);
		READ_INT_32(pos, actions);
		READ_INT_32(pos, protocol);

		msg_debug_milter("optneg: version: %d, actions: %d, protocol: %d",
						 version, actions, protocol);

		if (version < RSPAMD_MILTER_PROTO_VER) {
			msg_warn_milter("MTA specifies too old protocol: %d, "
							"aborting connection",
							version);

			err = g_error_new(rspamd_milter_quark(), EINVAL, "invalid "
															 "protocol version: %d",
							  version);
			rspamd_milter_on_protocol_error(session, priv, err);

			return FALSE;
		}

		version = RSPAMD_MILTER_PROTO_VER;
		actions |= RSPAMD_MILTER_ACTIONS_MASK;
		protocol = RSPAMD_MILTER_FLAG_NOREPLY_MASK;

		return rspamd_milter_send_action(session, RSPAMD_MILTER_OPTNEG,
										 version, actions, protocol);
		break;
	case RSPAMD_MILTER_CMD_QUIT:
		if (priv->out_chain) {
			msg_debug_milter("quit command, refcount: %d, "
							 "some output buffers left - draining",
							 session->ref.refcount);

			priv->state = RSPAMD_MILTER_WRITE_AND_DIE;
		}
		else {
			msg_debug_milter("quit command, refcount: %d",
							 session->ref.refcount);

			priv->state = RSPAMD_MILTER_WANNA_DIE;
			REF_RETAIN(session);
			priv->fin_cb(priv->fd, session, priv->ud);
			REF_RELEASE(session);
			return FALSE;
		}
		break;
	case RSPAMD_MILTER_CMD_RCPT:
		msg_debug_milter("rcpt command");

		while (pos < end) {
			struct rspamd_email_address *addr;
			char *cpy;
			GHashTable *esmtp_args = NULL;

			zero = memchr(pos, '\0', end - pos);

			if (zero && zero > pos) {
				gsize addr_len = (gsize) (zero - pos);

				/* Allocate exact length for address plus NUL terminator */
				cpy = rspamd_mempool_alloc(priv->pool, addr_len + 1);
				memcpy(cpy, pos, addr_len);
				cpy[addr_len] = '\0';

				msg_debug_milter("got rcpt: %*s", (int) addr_len, cpy);
				addr = rspamd_email_address_from_smtp(cpy, addr_len);

				if (addr) {
					if (!session->rcpts) {
						session->rcpts = g_ptr_array_sized_new(1);
					}

					g_ptr_array_add(session->rcpts, addr);

					/* Parse ESMTP arguments for this recipient only */
					const unsigned char *args_start = zero + 1;
					const unsigned char *scan = args_start;

					if (args_start < end) {
						/* Walk over NUL-terminated tokens until a double-NUL or end-of-buffer */
						while (scan < end) {
							const unsigned char *z = memchr(scan, '\0', end - scan);

							if (z == NULL) {
								/* Malformed (no further NUL), treat rest as arguments */
								scan = end;
								break;
							}

							/* Advance past this token */
							scan = z + 1;

							/* Double-NUL terminates the list; consume the terminator */
							if (scan < end && *scan == '\0') {
								scan++;
								break;
							}
						}

						if (args_start < scan) {
							esmtp_args = rspamd_milter_parse_esmtp_args(args_start, scan, priv->pool);
						}

						if (!session->rcpt_esmtp_args) {
							session->rcpt_esmtp_args = g_ptr_array_sized_new(1);
						}

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

							g_ptr_array_add(session->rcpt_esmtp_args, esmtp_args);

							g_hash_table_iter_init(&iter, esmtp_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;
								msg_debug_milter("rcpt esmtp arg: %T=%T", k, v);
							}
						}
						else {
							/* Add NULL placeholder to keep indices aligned with rcpts array */
							g_ptr_array_add(session->rcpt_esmtp_args, NULL);
						}

						/* Advance cursor to after the args (or end) and stop processing */
						pos = scan;
					}
					else {
						/* No ESMTP args, add NULL placeholder */
						if (!session->rcpt_esmtp_args) {
							session->rcpt_esmtp_args = g_ptr_array_sized_new(1);
						}
						g_ptr_array_add(session->rcpt_esmtp_args, NULL);
						pos = zero + 1;
					}

					/* Only one recipient per RCPT command is expected */
					break;
				}
				else {
					/* Address parsing failed, move past address NUL and continue */
					pos = zero + 1;
				}
			}
			else {
				gsize weird_len = (gsize) (end - pos);
				cpy = rspamd_mempool_alloc(priv->pool, weird_len + 1);
				memcpy(cpy, pos, weird_len);
				cpy[weird_len] = '\0';

				msg_debug_milter("got weird rcpt: %*s", (int) weird_len,
								 cpy);
				/* That actually should not happen */
				addr = rspamd_email_address_from_smtp(cpy, weird_len);

				if (addr) {
					if (!session->rcpts) {
						session->rcpts = g_ptr_array_sized_new(1);
					}

					g_ptr_array_add(session->rcpts, addr);

					/* No ESMTP args in this case, add NULL placeholder */
					if (!session->rcpt_esmtp_args) {
						session->rcpt_esmtp_args = g_ptr_array_sized_new(1);
					}
					g_ptr_array_add(session->rcpt_esmtp_args, NULL);
				}

				break;
			}
		}
		break;
	case RSPAMD_MILTER_CMD_DATA:
		if (!session->message) {
			session->message = rspamd_fstring_sized_new(
				RSPAMD_MILTER_MESSAGE_CHUNK);
		}
		msg_debug_milter("got data command");
		/* We do not need reply as specified */
		break;
	default:
		msg_debug_milter("got bad command: %c", priv->parser.cur_cmd);
		break;
	}

	return TRUE;
}

static gboolean
rspamd_milter_is_valid_cmd(unsigned char c)
{
	switch (c) {
	case RSPAMD_MILTER_CMD_ABORT:
	case RSPAMD_MILTER_CMD_BODY:
	case RSPAMD_MILTER_CMD_CONNECT:
	case RSPAMD_MILTER_CMD_MACRO:
	case RSPAMD_MILTER_CMD_BODYEOB:
	case RSPAMD_MILTER_CMD_HELO:
	case RSPAMD_MILTER_CMD_QUIT_NC:
	case RSPAMD_MILTER_CMD_HEADER:
	case RSPAMD_MILTER_CMD_MAIL:
	case RSPAMD_MILTER_CMD_EOH:
	case RSPAMD_MILTER_CMD_OPTNEG:
	case RSPAMD_MILTER_CMD_QUIT:
	case RSPAMD_MILTER_CMD_RCPT:
	case RSPAMD_MILTER_CMD_DATA:
	case RSPAMD_MILTER_CMD_UNKNOWN:
		return TRUE;
	default:
		break;
	}

	return FALSE;
}

static gboolean
rspamd_milter_consume_input(struct rspamd_milter_session *session,
							struct rspamd_milter_private *priv)
{
	const unsigned char *p, *end;
	GError *err;

	p = priv->parser.buf->str + priv->parser.pos;
	end = priv->parser.buf->str + priv->parser.buf->len;

	while (p < end) {
		msg_debug_milter("offset: %d, state: %d",
						 (int) (p - (const unsigned char *) priv->parser.buf->str),
						 priv->parser.state);

		switch (priv->parser.state) {
		case st_len_1:
			/* The first length byte in big endian order */
			priv->parser.datalen = 0;
			priv->parser.datalen |= ((gsize) *p) << 24;
			priv->parser.state = st_len_2;
			p++;
			break;
		case st_len_2:
			/* The second length byte in big endian order */
			priv->parser.datalen |= ((gsize) *p) << 16;
			priv->parser.state = st_len_3;
			p++;
			break;
		case st_len_3:
			/* The third length byte in big endian order */
			priv->parser.datalen |= ((gsize) *p) << 8;
			priv->parser.state = st_len_4;
			p++;
			break;
		case st_len_4:
			/* The fourth length byte in big endian order */
			priv->parser.datalen |= ((gsize) *p);
			priv->parser.state = st_read_cmd;
			p++;
			break;
		case st_read_cmd:
			priv->parser.cur_cmd = *p;
			priv->parser.state = st_read_data;

			if (priv->parser.datalen < 1) {
				err = g_error_new(rspamd_milter_quark(), EINVAL,
								  "Command length is too short");
				rspamd_milter_on_protocol_error(session, priv, err);

				return FALSE;
			}
			else {
				/* Eat command itself */
				priv->parser.datalen--;
			}

			p++;
			priv->parser.cmd_start = p - (const unsigned char *) priv->parser.buf->str;
			break;
		case st_read_data:
			/* We might need some more data in buffer for further steps */
			if (priv->parser.datalen >
				RSPAMD_MILTER_MESSAGE_CHUNK * 2) {
				/* Check if we have HTTP input instead of milter */
				if (priv->parser.buf->len > sizeof("GET") &&
					memcmp(priv->parser.buf->str, "GET", 3) == 0) {
					struct http_parser http_parser;
					struct http_parser_settings http_callbacks;
					GString *url = g_string_new(NULL);

					/* Hack, hack, hack */
					/*
					 * This code is assumed to read `/ping` command and
					 * handle it to monitor port's availability since
					 * milter protocol is stupid and does not allow to do that
					 * This code also assumes that HTTP request can be read
					 * as as single data chunk which is not true in some cases
					 * In general, don't use it for anything but ping checks
					 */
					memset(&http_callbacks, 0, sizeof(http_callbacks));
					http_parser.data = url;
					http_parser_init(&http_parser, HTTP_REQUEST);
					http_callbacks.on_url = rspamd_milter_http_on_url;
					http_parser_execute(&http_parser, &http_callbacks,
										priv->parser.buf->str, priv->parser.buf->len);

					if (url->len == sizeof("/ping") - 1 &&
						rspamd_lc_cmp(url->str, "/ping", url->len) == 0) {
						rspamd_milter_on_protocol_ping(session, priv);
						g_string_free(url, TRUE);

						return TRUE;
					}
					else {
						err = g_error_new(rspamd_milter_quark(), EINVAL,
										  "HTTP GET request is not supported in milter mode, url: %s",
										  url->str);
					}

					g_string_free(url, TRUE);
				}
				else if (priv->parser.buf->len > sizeof("POST") &&
						 memcmp(priv->parser.buf->str, "POST", 4) == 0) {
					err = g_error_new(rspamd_milter_quark(), EINVAL,
									  "HTTP POST request is not supported in milter mode");
				}
				else {
					err = g_error_new(rspamd_milter_quark(), E2BIG,
									  "Command length is too big: %zd",
									  priv->parser.datalen);
				}

				rspamd_milter_on_protocol_error(session, priv, err);

				return FALSE;
			}
			if (!rspamd_milter_is_valid_cmd(priv->parser.cur_cmd)) {
				err = g_error_new(rspamd_milter_quark(), E2BIG,
								  "Unvalid command: %c",
								  priv->parser.cur_cmd);
				rspamd_milter_on_protocol_error(session, priv, err);

				return FALSE;
			}
			if (priv->parser.buf->allocated < priv->parser.datalen) {
				priv->parser.pos = p - (const unsigned char *) priv->parser.buf->str;
				priv->parser.buf = rspamd_fstring_grow(priv->parser.buf,
													   priv->parser.buf->len + priv->parser.datalen);
				/* This can realloc buffer */
				rspamd_milter_plan_io(session, priv, EV_READ);
				goto end;
			}
			else {
				/* We may have the full command available */
				if (p + priv->parser.datalen <= end) {
					/* We can process command */
					if (!rspamd_milter_process_command(session, priv)) {
						return FALSE;
					}

					p += priv->parser.datalen;
					priv->parser.state = st_len_1;
					priv->parser.cur_cmd = '\0';
					priv->parser.cmd_start = 0;
				}
				else {
					/* Need to read more */
					priv->parser.pos = p - (const unsigned char *) priv->parser.buf->str;
					rspamd_milter_plan_io(session, priv, EV_READ);
					goto end;
				}
			}
			break;
		}
	}

	/* Leftover */
	switch (priv->parser.state) {
	case st_read_data:
		if (p + priv->parser.datalen <= end) {
			if (!rspamd_milter_process_command(session, priv)) {
				return FALSE;
			}

			priv->parser.state = st_len_1;
			priv->parser.cur_cmd = '\0';
			priv->parser.cmd_start = 0;
		}
		break;
	default:
		/* No need to do anything */
		break;
	}

	if (p == end) {
		priv->parser.buf->len = 0;
		priv->parser.pos = 0;
		priv->parser.cmd_start = 0;
	}

	if (priv->out_chain) {
		rspamd_milter_plan_io(session, priv, EV_READ | EV_WRITE);
	}
	else {
		rspamd_milter_plan_io(session, priv, EV_READ);
	}
end:

	return TRUE;
}

static gboolean
rspamd_milter_handle_session(struct rspamd_milter_session *session,
							 struct rspamd_milter_private *priv)
{
	struct rspamd_milter_outbuf *obuf, *obuf_tmp;
	gssize r, to_write;
	GError *err;

	g_assert(session != NULL);

	switch (priv->state) {
	case RSPAMD_MILTER_READ_MORE:
		if (priv->parser.buf->len >= priv->parser.buf->allocated) {
			priv->parser.buf = rspamd_fstring_grow(priv->parser.buf,
												   priv->parser.buf->len * 2);
		}

		r = read(priv->fd, priv->parser.buf->str + priv->parser.buf->len,
				 priv->parser.buf->allocated - priv->parser.buf->len);

		msg_debug_milter("read %z bytes, %z remain, %z allocated",
						 r, priv->parser.buf->len, priv->parser.buf->allocated);

		if (r == -1) {
			if (errno == EAGAIN || errno == EINTR) {
				rspamd_milter_plan_io(session, priv, EV_READ);

				return TRUE;
			}
			else {
				/* Fatal IO error */
				err = g_error_new(rspamd_milter_quark(), errno,
								  "IO read error: %s", strerror(errno));
				REF_RETAIN(session);
				priv->err_cb(priv->fd, session, priv->ud, err);
				REF_RELEASE(session);
				g_error_free(err);

				REF_RELEASE(session);

				return FALSE;
			}
		}
		else if (r == 0) {
			err = g_error_new(rspamd_milter_quark(), ECONNRESET,
							  "Unexpected EOF");
			REF_RETAIN(session);
			priv->err_cb(priv->fd, session, priv->ud, err);
			REF_RELEASE(session);
			g_error_free(err);

			REF_RELEASE(session);

			return FALSE;
		}
		else {
			priv->parser.buf->len += r;

			return rspamd_milter_consume_input(session, priv);
		}

		break;
	case RSPAMD_MILTER_WRITE_REPLY:
	case RSPAMD_MILTER_WRITE_AND_DIE:
		if (priv->out_chain == NULL) {
			if (priv->state == RSPAMD_MILTER_WRITE_AND_DIE) {
				/* Finished writing, let's die finally */
				msg_debug_milter("output drained, terminating, refcount: %d",
								 session->ref.refcount);

				/* Session should be destroyed by fin_cb... */
				REF_RETAIN(session);
				priv->fin_cb(priv->fd, session, priv->ud);
				REF_RELEASE(session);

				return FALSE;
			}
			else {
				/* We have written everything, so we can read something */
				priv->state = RSPAMD_MILTER_READ_MORE;
				rspamd_milter_plan_io(session, priv, EV_READ);
			}
		}
		else {
			DL_FOREACH_SAFE(priv->out_chain, obuf, obuf_tmp)
			{
				to_write = obuf->buf->len - obuf->pos;

				g_assert(to_write > 0);

				r = write(priv->fd, obuf->buf->str + obuf->pos, to_write);

				if (r == -1) {
					if (errno == EAGAIN || errno == EINTR) {
						rspamd_milter_plan_io(session, priv, EV_WRITE);
					}
					else {
						/* Fatal IO error */
						err = g_error_new(rspamd_milter_quark(), errno,
										  "IO write error: %s", strerror(errno));
						REF_RETAIN(session);
						priv->err_cb(priv->fd, session, priv->ud, err);
						REF_RELEASE(session);
						g_error_free(err);

						REF_RELEASE(session);

						return FALSE;
					}
				}
				else if (r == 0) {
					err = g_error_new(rspamd_milter_quark(), ECONNRESET,
									  "Unexpected EOF");
					REF_RETAIN(session);
					priv->err_cb(priv->fd, session, priv->ud, err);
					REF_RELEASE(session);
					g_error_free(err);

					REF_RELEASE(session);

					return FALSE;
				}
				else {
					if (r == to_write) {
						/* We have done with this buf */
						DL_DELETE(priv->out_chain, obuf);
						rspamd_milter_obuf_free(obuf);
					}
					else {
						/* We need to plan another write */
						obuf->pos += r;
						rspamd_milter_plan_io(session, priv, EV_WRITE);

						return TRUE;
					}
				}
			}

			/* Here we have written everything, so we can plan reading */
			priv->state = RSPAMD_MILTER_READ_MORE;
			rspamd_milter_plan_io(session, priv, EV_READ);
		}
		break;
	case RSPAMD_MILTER_WANNA_DIE:
		/* We are here after processing everything, so release session */
		REF_RELEASE(session);
		return FALSE;
		break;
	case RSPAMD_MILTER_PONG_AND_DIE:
		err = g_error_new(rspamd_milter_quark(), 0,
						  "ping command");
		REF_RETAIN(session);
		priv->err_cb(priv->fd, session, priv->ud, err);
		REF_RELEASE(session);
		g_error_free(err);
		REF_RELEASE(session);
		return FALSE;
		break;
	}

	return TRUE;
}


gboolean
rspamd_milter_handle_socket(int fd, ev_tstamp timeout,
							rspamd_mempool_t *pool,
							struct ev_loop *ev_base, rspamd_milter_finish finish_cb,
							rspamd_milter_error error_cb, void *ud)
{
	struct rspamd_milter_session *session;
	struct rspamd_milter_private *priv;
	int nfd = dup(fd);

	if (nfd == -1) {
		GError *err = g_error_new(rspamd_milter_quark(), errno,
								  "dup failed: %s", strerror(errno));
		error_cb(fd, NULL, ud, err);

		return FALSE;
	}

	g_assert(finish_cb != NULL);
	g_assert(error_cb != NULL);
	g_assert(milter_ctx != NULL);

	session = g_malloc0(sizeof(*session));
	priv = g_malloc0(sizeof(*priv));
	priv->fd = nfd;
	priv->ud = ud;
	priv->fin_cb = finish_cb;
	priv->err_cb = error_cb;
	priv->parser.state = st_len_1;
	priv->parser.buf = rspamd_fstring_sized_new(RSPAMD_MILTER_MESSAGE_CHUNK + 5);
	priv->event_loop = ev_base;
	priv->state = RSPAMD_MILTER_READ_MORE;
	priv->pool = rspamd_mempool_new_short_lived("milter");
	priv->discard_on_reject = milter_ctx->discard_on_reject;
	priv->quarantine_on_reject = milter_ctx->quarantine_on_reject;
	priv->ev.timeout = timeout;

	rspamd_ev_watcher_init(&priv->ev, priv->fd, EV_READ | EV_WRITE,
						   rspamd_milter_io_handler, session);

	if (pool) {
		/* Copy tag */
		memcpy(priv->pool->tag.uid, pool->tag.uid, sizeof(pool->tag.uid));
	}

	priv->headers = kh_init(milter_headers_hash_t);
	kh_resize(milter_headers_hash_t, priv->headers, 32);

	session->priv = priv;
	REF_INIT_RETAIN(session, rspamd_milter_session_dtor);

	if (milter_ctx->sessions_cache) {
		rspamd_worker_session_cache_add(milter_ctx->sessions_cache,
										priv->pool->tag.uid, &session->ref.refcount, session);
	}

	return rspamd_milter_handle_session(session, priv);
}

gboolean
rspamd_milter_set_reply(struct rspamd_milter_session *session,
						rspamd_fstring_t *rcode,
						rspamd_fstring_t *xcode,
						rspamd_fstring_t *reply)
{
	GString *buf;
	gboolean ret;

	buf = g_string_sized_new(xcode->len + rcode->len + reply->len + 2);
	rspamd_printf_gstring(buf, "%V %V %V", rcode, xcode, reply);
	ret = rspamd_milter_send_action(session, RSPAMD_MILTER_REPLYCODE,
									buf);
	g_string_free(buf, TRUE);

	return ret;
}

#define SET_COMMAND(cmd, sz, reply, pos)                           \
	do {                                                           \
		uint32_t _len;                                             \
		_len = (sz) + 1;                                           \
		(reply) = rspamd_fstring_sized_new(sizeof(_len) + _len);   \
		(reply)->len = sizeof(_len) + _len;                        \
		_len = htonl(_len);                                        \
		memcpy((reply)->str, &_len, sizeof(_len));                 \
		(reply)->str[sizeof(_len)] = (cmd);                        \
		(pos) = (unsigned char *) (reply)->str + sizeof(_len) + 1; \
	} while (0)

gboolean
rspamd_milter_send_action(struct rspamd_milter_session *session,
						  enum rspamd_milter_reply act, ...)
{
	uint32_t ver, actions, protocol, idx;
	va_list ap;
	unsigned char cmd, *pos;
	rspamd_fstring_t *reply = NULL;
	gsize len;
	GString *name, *value;
	const char *body_str;
	struct rspamd_milter_outbuf *obuf;
	struct rspamd_milter_private *priv = session->priv;
	const rspamd_fstring_t *reason;

	va_start(ap, act);
	cmd = act;

	switch (act) {
	case RSPAMD_MILTER_ACCEPT:
	case RSPAMD_MILTER_CONTINUE:
	case RSPAMD_MILTER_DISCARD:
	case RSPAMD_MILTER_PROGRESS:
	case RSPAMD_MILTER_REJECT:
	case RSPAMD_MILTER_TEMPFAIL:
		/* No additional arguments */
		msg_debug_milter("send %c command", cmd);
		SET_COMMAND(cmd, 0, reply, pos);
		break;
	case RSPAMD_MILTER_QUARANTINE:
		reason = va_arg(ap, const rspamd_fstring_t *);

		if (reason != NULL) {
			len = reason->len;
			msg_debug_milter("send quarantine action %*s", (int) len, reason->str);
			SET_COMMAND(cmd, len + 1, reply, pos);
			memcpy(pos, reason->str, len);
			pos[len] = '\0';
		}
		else {
			msg_debug_milter("send quarantine action with no specific string");
			SET_COMMAND(cmd, 1, reply, pos);
			*pos = '\0';
		}

		break;
	case RSPAMD_MILTER_ADDHEADER:
		name = va_arg(ap, GString *);
		value = va_arg(ap, GString *);

		/* Name and value must be zero terminated */
		msg_debug_milter("add header command - \"%v\"=\"%v\"", name, value);
		SET_COMMAND(cmd, name->len + value->len + 2, reply, pos);
		memcpy(pos, name->str, name->len + 1);
		pos += name->len + 1;
		memcpy(pos, value->str, value->len + 1);
		break;
	case RSPAMD_MILTER_CHGHEADER:
	case RSPAMD_MILTER_INSHEADER:
		idx = va_arg(ap, uint32_t);
		name = va_arg(ap, GString *);
		value = va_arg(ap, GString *);

		msg_debug_milter("change/insert header command pos = %d- \"%v\"=\"%v\"",
						 idx, name, value);
		/* Name and value must be zero terminated */
		SET_COMMAND(cmd, name->len + value->len + 2 + sizeof(uint32_t),
					reply, pos);
		idx = htonl(idx);
		memcpy(pos, &idx, sizeof(idx));
		pos += sizeof(idx);
		memcpy(pos, name->str, name->len + 1);
		pos += name->len + 1;
		memcpy(pos, value->str, value->len + 1);
		break;
	case RSPAMD_MILTER_REPLBODY:
		len = va_arg(ap, gsize);
		body_str = va_arg(ap, const char *);
		msg_debug_milter("want to change body; size = %uz",
						 len);
		SET_COMMAND(cmd, len, reply, pos);
		memcpy(pos, body_str, len);
		break;
	case RSPAMD_MILTER_REPLYCODE:
	case RSPAMD_MILTER_ADDRCPT:
	case RSPAMD_MILTER_DELRCPT:
	case RSPAMD_MILTER_CHGFROM:
		/* Single GString * argument */
		value = va_arg(ap, GString *);
		msg_debug_milter("command %c; value=%v", cmd, value);
		SET_COMMAND(cmd, value->len + 1, reply, pos);
		memcpy(pos, value->str, value->len + 1);
		break;
	case RSPAMD_MILTER_OPTNEG:
		ver = va_arg(ap, uint32_t);
		actions = va_arg(ap, uint32_t);
		protocol = va_arg(ap, uint32_t);

		msg_debug_milter("optneg reply: ver=%d, actions=%d, protocol=%d",
						 ver, actions, protocol);
		ver = htonl(ver);
		actions = htonl(actions);
		protocol = htonl(protocol);
		SET_COMMAND(cmd, sizeof(uint32_t) * 3, reply, pos);
		memcpy(pos, &ver, sizeof(ver));
		pos += sizeof(ver);
		memcpy(pos, &actions, sizeof(actions));
		pos += sizeof(actions);
		memcpy(pos, &protocol, sizeof(protocol));
		break;
	default:
		msg_err_milter("invalid command: %c", cmd);
		break;
	}

	va_end(ap);

	if (reply) {
		obuf = g_malloc(sizeof(*obuf));
		obuf->buf = reply;
		obuf->pos = 0;
		DL_APPEND(priv->out_chain, obuf);
		priv->state = RSPAMD_MILTER_WRITE_REPLY;
		rspamd_milter_plan_io(session, priv, EV_WRITE);

		return TRUE;
	}

	return FALSE;
}

gboolean
rspamd_milter_add_header(struct rspamd_milter_session *session,
						 GString *name, GString *value)
{
	return rspamd_milter_send_action(session, RSPAMD_MILTER_ADDHEADER,
									 name, value);
}

gboolean
rspamd_milter_del_header(struct rspamd_milter_session *session,
						 GString *name)
{
	GString value;
	uint32_t idx = 1;

	value.str = (char *) "";
	value.len = 0;

	return rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
									 idx, name, &value);
}

void rspamd_milter_session_unref(struct rspamd_milter_session *session)
{
	REF_RELEASE(session);
}

struct rspamd_milter_session *
rspamd_milter_session_ref(struct rspamd_milter_session *session)
{
	REF_RETAIN(session);

	return session;
}

#define IF_MACRO(lit)                                    \
	RSPAMD_FTOK_ASSIGN(&srch, (lit));                    \
	found = g_hash_table_lookup(session->macros, &srch); \
	if (found)

static void
rspamd_milter_macro_http(struct rspamd_milter_session *session,
						 struct rspamd_http_message *msg)
{
	rspamd_ftok_t *found, srch;
	struct rspamd_milter_private *priv = session->priv;

	/*
	 * We assume postfix macros here, sendmail ones might be slightly
	 * different
	 */

	if (!session->macros) {
		return;
	}

	/*
	 * When we get a queue-id we try to pass it to the backend, where possible
	 * We also need that for logging consistency
	 */
	IF_MACRO("{i}")
	{
		rspamd_http_message_add_header_len(msg, QUEUE_ID_HEADER,
										   found->begin, found->len);
	}
	else
	{
		IF_MACRO("i")
		{
			rspamd_http_message_add_header_len(msg, QUEUE_ID_HEADER,
											   found->begin, found->len);
		}
	}

	IF_MACRO("{v}")
	{
		rspamd_http_message_add_header_len(msg, USER_AGENT_HEADER,
										   found->begin, found->len);
	}
	else
	{
		IF_MACRO("v")
		{
			rspamd_http_message_add_header_len(msg, USER_AGENT_HEADER,
											   found->begin, found->len);
		}
	}

	IF_MACRO("{cipher}")
	{
		rspamd_http_message_add_header_len(msg, TLS_CIPHER_HEADER,
										   found->begin, found->len);
	}

	IF_MACRO("{tls_version}")
	{
		rspamd_http_message_add_header_len(msg, TLS_VERSION_HEADER,
										   found->begin, found->len);
	}

	IF_MACRO("{auth_authen}")
	{
		rspamd_http_message_add_header_len(msg, USER_HEADER,
										   found->begin, found->len);
	}

	IF_MACRO("{rcpt_mailer}")
	{
		rspamd_http_message_add_header_len(msg, MAILER_HEADER,
										   found->begin, found->len);
	}

	if (milter_ctx->client_ca_name) {
		IF_MACRO("{cert_issuer}")
		{
			rspamd_http_message_add_header_len(msg, CERT_ISSUER_HEADER,
											   found->begin, found->len);

			if (found->len == strlen(milter_ctx->client_ca_name) &&
				rspamd_cryptobox_memcmp(found->begin,
										milter_ctx->client_ca_name, found->len) == 0) {
				msg_debug_milter("process certificate issued by %T", found);
				IF_MACRO("{cert_subject}")
				{
					rspamd_http_message_add_header_len(msg, USER_HEADER,
													   found->begin, found->len);
				}
			}
			else {
				msg_debug_milter("skip certificate issued by %T", found);
			}
		}
	}
	else {
		IF_MACRO("{cert_issuer}")
		{
			rspamd_http_message_add_header_len(msg, CERT_ISSUER_HEADER,
											   found->begin, found->len);
		}
	}

	if (!session->hostname || session->hostname->len == 0) {
		IF_MACRO("{client_name}")
		{
			if (!(found->len == sizeof("unknown") - 1 &&
				  memcmp(found->begin, "unknown",
						 sizeof("unknown") - 1) == 0)) {
				rspamd_http_message_add_header_len(msg, HOSTNAME_HEADER,
												   found->begin, found->len);
			}
			else {
				msg_debug_milter("skip unknown hostname from being added");
			}
		}
	}

	IF_MACRO("{daemon_name}")
	{
		/* Postfix style */
		rspamd_http_message_add_header_len(msg, MTA_NAME_HEADER,
										   found->begin, found->len);
	}
	else
	{
		/* Sendmail style */
		IF_MACRO("{j}")
		{
			rspamd_http_message_add_header_len(msg, MTA_NAME_HEADER,
											   found->begin, found->len);
		}
		else
		{
			IF_MACRO("j")
			{
				rspamd_http_message_add_header_len(msg, MTA_NAME_HEADER,
												   found->begin, found->len);
			}
		}
	}
}

struct rspamd_http_message *
rspamd_milter_to_http(struct rspamd_milter_session *session)
{
	struct rspamd_http_message *msg;
	unsigned int i;
	struct rspamd_email_address *rcpt;
	struct rspamd_milter_private *priv = session->priv;

	g_assert(session != NULL);

	msg = rspamd_http_new_message(HTTP_REQUEST);

	msg->url = rspamd_fstring_assign(msg->url, "/" MSG_CMD_CHECK_V2,
									 sizeof("/" MSG_CMD_CHECK_V2) - 1);

	if (session->message) {
		rspamd_http_message_set_body_from_fstring_steal(msg, session->message);
		session->message = NULL;
	}

	if (session->hostname && RSPAMD_FSTRING_LEN(session->hostname) > 0) {
		if (!(session->hostname->len == sizeof("unknown") - 1 &&
			  memcmp(RSPAMD_FSTRING_DATA(session->hostname), "unknown",
					 sizeof("unknown") - 1) == 0)) {
			rspamd_http_message_add_header_fstr(msg, HOSTNAME_HEADER,
												session->hostname);
		}
		else {
			msg_debug_milter("skip unknown hostname from being added");
		}
	}

	if (session->helo && session->helo->len > 0) {
		rspamd_http_message_add_header_fstr(msg, HELO_HEADER,
											session->helo);
	}

	if (session->from) {
		rspamd_http_message_add_header_len(msg, FROM_HEADER,
										   session->from->raw, session->from->raw_len);
	}

	if (session->rcpts) {
		PTR_ARRAY_FOREACH(session->rcpts, i, rcpt)
		{
			rspamd_http_message_add_header_len(msg, RCPT_HEADER,
											   rcpt->raw, rcpt->raw_len);
		}
	}

	if (session->addr) {
		if (rspamd_inet_address_get_af(session->addr) != AF_UNIX) {
			rspamd_http_message_add_header(msg, IP_ADDR_HEADER,
										   rspamd_inet_address_to_string_pretty(session->addr));
		}
		else {
			rspamd_http_message_add_header(msg, IP_ADDR_HEADER,
										   rspamd_inet_address_to_string(session->addr));
		}
	}

	rspamd_milter_macro_http(session, msg);
	rspamd_http_message_add_header(msg, FLAGS_HEADER, "milter,body_block");

	/* Add ESMTP arguments as HTTP headers */
	if (session->mail_esmtp_args) {
		GHashTableIter iter;
		gpointer key, value;
		GString *hdr_val;

		g_hash_table_iter_init(&iter, session->mail_esmtp_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;

			/* Use X-Rspamd-Mail-Esmtp-Arg- prefix for mail ESMTP args */
			hdr_val = g_string_sized_new(k->len + v->len + 1);
			g_string_append_len(hdr_val, k->begin, k->len);

			if (v->len > 0) {
				g_string_append_c(hdr_val, '=');
				g_string_append_len(hdr_val, v->begin, v->len);
			}

			rspamd_http_message_add_header(msg, "X-Rspamd-Mail-Esmtp-Args", hdr_val->str);
			g_string_free(hdr_val, TRUE);
		}
	}

	if (session->rcpt_esmtp_args) {
		GHashTable *rcpt_args;
		GString *hdr_val;
		unsigned int idx;

		PTR_ARRAY_FOREACH(session->rcpt_esmtp_args, idx, rcpt_args)
		{
			if (rcpt_args) {
				GHashTableIter iter;
				gpointer key, value;

				g_hash_table_iter_init(&iter, rcpt_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;

					/* Use X-Rspamd-Rcpt-Esmtp-Arg- prefix for rcpt ESMTP args */
					/* Format: rcpt_index:key=value */
					hdr_val = g_string_sized_new(k->len + v->len + 16);
					g_string_append_printf(hdr_val, "%u:", idx);
					g_string_append_len(hdr_val, k->begin, k->len);

					if (v->len > 0) {
						g_string_append_c(hdr_val, '=');
						g_string_append_len(hdr_val, v->begin, v->len);
					}

					rspamd_http_message_add_header(msg, "X-Rspamd-Rcpt-Esmtp-Args", hdr_val->str);
					g_string_free(hdr_val, TRUE);
				}
			}
		}
	}

	return msg;
}

struct rspamd_http_message *
rspamd_milter_to_http_with_task(struct rspamd_milter_session *session,
								struct rspamd_task *task)
{
	struct rspamd_http_message *msg;

	/* First create the HTTP message using the existing function */
	msg = rspamd_milter_to_http(session);

	/* Then store ESMTP arguments in the task */
	if (task && session) {
		if (session->mail_esmtp_args) {
			rspamd_task_set_mail_esmtp_args(task, session->mail_esmtp_args);
		}
		if (session->rcpt_esmtp_args) {
			rspamd_task_set_rcpt_esmtp_args(task, session->rcpt_esmtp_args);
		}
	}

	return msg;
}

void *
rspamd_milter_update_userdata(struct rspamd_milter_session *session,
							  void *ud)
{
	struct rspamd_milter_private *priv = session->priv;
	void *prev_ud;

	prev_ud = priv->ud;
	priv->ud = ud;

	return prev_ud;
}

static void
rspamd_milter_remove_header_safe(struct rspamd_milter_session *session,
								 const char *key, int nhdr)
{
	int i;
	GString *hname, *hvalue;
	struct rspamd_milter_private *priv = session->priv;
	khiter_t k;
	GArray *ar;

	k = kh_get(milter_headers_hash_t, priv->headers, (char *) key);

	if (k != kh_end(priv->headers)) {
		ar = kh_val(priv->headers, k);

		hname = g_string_new(key);
		hvalue = g_string_new("");

		if (nhdr > 0) {
			if (ar->len >= nhdr) {
				rspamd_milter_send_action(session,
										  RSPAMD_MILTER_CHGHEADER,
										  nhdr, hname, hvalue);
				priv->cur_hdr--;
			}
		}
		else if (nhdr == 0) {
			/* We need to clear all headers */
			for (i = ar->len; i > 0; i--) {
				rspamd_milter_send_action(session,
										  RSPAMD_MILTER_CHGHEADER,
										  i, hname, hvalue);
				priv->cur_hdr--;
			}
		}
		else {
			/* Remove from the end */
			if (nhdr >= -(ar->len)) {
				rspamd_milter_send_action(session,
										  RSPAMD_MILTER_CHGHEADER,
										  ar->len + nhdr + 1, hname, hvalue);
				priv->cur_hdr--;
			}
		}

		g_string_free(hname, TRUE);
		g_string_free(hvalue, TRUE);

		if (priv->cur_hdr < 0) {
			msg_err_milter("negative header count after removing %s", key);
			priv->cur_hdr = 0;
		}
	}
}

static void
rspamd_milter_extract_single_header(struct rspamd_milter_session *session,
									const char *hdr, const ucl_object_t *obj)
{
	GString *hname, *hvalue;
	struct rspamd_milter_private *priv = session->priv;
	int idx = -1;
	const ucl_object_t *val;

	val = ucl_object_lookup(obj, "value");

	if (val && ucl_object_type(val) == UCL_STRING) {
		const ucl_object_t *idx_obj;
		gboolean has_idx = FALSE;

		idx_obj = ucl_object_lookup_any(obj, "order",
										"index", NULL);

		if (idx_obj && (ucl_object_type(idx_obj) == UCL_INT || ucl_object_type(idx_obj) == UCL_FLOAT)) {
			idx = ucl_object_toint(idx_obj);
			has_idx = TRUE;
		}

		hname = g_string_new(hdr);
		hvalue = g_string_new(ucl_object_tostring(val));

		if (has_idx) {
			if (idx >= 0) {
				rspamd_milter_send_action(session,
										  RSPAMD_MILTER_INSHEADER,
										  idx,
										  hname, hvalue);
			}
			else {
				/* Calculate negative offset */

				if (idx == -1) {
					rspamd_milter_send_action(session,
											  RSPAMD_MILTER_ADDHEADER,
											  hname, hvalue);
				}
				else if (-idx <= priv->cur_hdr) {
					/*
					 * Note: We should account MTA's own "Received:" field
					 * which wasn't passed by Milter's header command.
					 */
					rspamd_milter_send_action(session,
											  RSPAMD_MILTER_INSHEADER,
											  priv->cur_hdr + idx + 2,
											  hname, hvalue);
				}
				else {
					rspamd_milter_send_action(session,
											  RSPAMD_MILTER_INSHEADER,
											  0,
											  hname, hvalue);
				}
			}
		}
		else {
			rspamd_milter_send_action(session,
									  RSPAMD_MILTER_ADDHEADER,
									  hname, hvalue);
		}

		priv->cur_hdr++;

		g_string_free(hname, TRUE);
		g_string_free(hvalue, TRUE);
	}
}

/*
 * Returns `TRUE` if action has been processed internally by this function
 */
static gboolean
rspamd_milter_process_milter_block(struct rspamd_milter_session *session,
								   const ucl_object_t *obj, struct rspamd_action *action)
{
	const ucl_object_t *elt, *cur;
	ucl_object_iter_t it;
	struct rspamd_milter_private *priv = session->priv;
	GString *hname, *hvalue;

	if (obj && ucl_object_type(obj) == UCL_OBJECT) {
	elt = ucl_object_lookup(obj, "remove_headers");
	/*
	 * remove_headers:  {"name": 1, ... }
	 * -or-
	 * remove_headers:  {"name": [1, 2, ...], ... }
	 * where number is the header's position starting from '1'
	 */
	if (elt && ucl_object_type(elt) == UCL_OBJECT) {
		it = NULL;

		while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
			if (ucl_object_type(cur) == UCL_INT) {
				rspamd_milter_remove_header_safe(session,
												 ucl_object_key(cur),
												 ucl_object_toint(cur));
			}
			else if (ucl_object_type(cur) == UCL_ARRAY) {
				/* Multiple positions for the same header name */
				ucl_object_iter_t *array_it;
				const ucl_object_t *array_elt;

				array_it = ucl_object_iterate_new(cur);

				while ((array_elt = ucl_object_iterate_safe(array_it,
															true)) != NULL) {
					if (ucl_object_type(array_elt) == UCL_INT) {
						rspamd_milter_remove_header_safe(session,
														 ucl_object_key(cur),
														 ucl_object_toint(array_elt));
					}
				}

				ucl_object_iterate_free(array_it);
			}
		}
	}

		elt = ucl_object_lookup(obj, "add_headers");
		/*
		 * add_headers: {"name": "value", ... }
		 * name could have multiple values
		 * -or- (since 1.7)
		 * {"name": {"value": "val", "order": 0}, ... }
		 */
		if (elt && ucl_object_type(elt) == UCL_OBJECT) {
			it = NULL;

			while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {

				const char *key_name = ucl_object_key(cur);

				if (ucl_object_type(cur) == UCL_STRING) {
					/*
					 * Legacy support of {"name": "value", ... } with
					 * multiple names under the same name
					 */
					ucl_object_iter_t *elt_it;
					const ucl_object_t *cur_elt;

					elt_it = ucl_object_iterate_new(cur);
					while ((cur_elt = ucl_object_iterate_safe(elt_it, false)) != NULL) {
						if (ucl_object_type(cur_elt) == UCL_STRING) {
							hname = g_string_new(key_name);
							hvalue = g_string_new(ucl_object_tostring(cur_elt));

							rspamd_milter_send_action(session,
													  RSPAMD_MILTER_ADDHEADER,
													  hname, hvalue);
							g_string_free(hname, TRUE);
							g_string_free(hvalue, TRUE);
						}
						else {
							msg_warn_milter("legacy header with name %s, that has not a string value: %s",
											key_name, ucl_object_type_to_string(cur_elt->type));
						}
					}
					ucl_object_iterate_free(elt_it);
				}
				else {
					if (ucl_object_type(cur) == UCL_OBJECT) {
						rspamd_milter_extract_single_header(session,
															key_name, cur);
					}
					else if (ucl_object_type(cur) == UCL_ARRAY) {
						/* Multiple values for the same key */
						ucl_object_iter_t *array_it;
						const ucl_object_t *array_elt;

						array_it = ucl_object_iterate_new(cur);

						while ((array_elt = ucl_object_iterate_safe(array_it,
																	true)) != NULL) {
							rspamd_milter_extract_single_header(session,
																key_name, array_elt);
						}

						ucl_object_iterate_free(array_it);
					}
					else {
						msg_warn_milter("non-legacy header with name %s, that has unsupported value type: %s",
										key_name, ucl_object_type_to_string(cur->type));
					}
				}
			}
		}

		elt = ucl_object_lookup(obj, "change_from");

		if (elt && ucl_object_type(elt) == UCL_STRING) {
			hvalue = g_string_new(ucl_object_tostring(elt));
			rspamd_milter_send_action(session,
									  RSPAMD_MILTER_CHGFROM,
									  hvalue);
			g_string_free(hvalue, TRUE);
		}

		elt = ucl_object_lookup(obj, "add_rcpt");

		if (elt && ucl_object_type(elt) == UCL_ARRAY) {
			it = NULL;

			while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
				hvalue = g_string_new(ucl_object_tostring(cur));
				rspamd_milter_send_action(session,
										  RSPAMD_MILTER_ADDRCPT,
										  hvalue);
				g_string_free(hvalue, TRUE);
			}
		}

		elt = ucl_object_lookup(obj, "del_rcpt");

		if (elt && ucl_object_type(elt) == UCL_ARRAY) {
			it = NULL;

			while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
				hvalue = g_string_new(ucl_object_tostring(cur));
				rspamd_milter_send_action(session,
										  RSPAMD_MILTER_DELRCPT,
										  hvalue);
				g_string_free(hvalue, TRUE);
			}
		}

		elt = ucl_object_lookup(obj, "reject");

		if (elt && ucl_object_type(elt) == UCL_STRING) {
			if (strcmp(ucl_object_tostring(elt), "discard") == 0) {
				priv->discard_on_reject = TRUE;
				msg_info_milter("discard message instead of rejection");
			}
			else if (strcmp(ucl_object_tostring(elt), "quarantine") == 0) {
				priv->quarantine_on_reject = TRUE;
				msg_info_milter("quarantine message instead of rejection");
			}
			else {
				priv->discard_on_reject = FALSE;
				priv->quarantine_on_reject = FALSE;
			}
		}

		elt = ucl_object_lookup(obj, "no_action");

		if (elt && ucl_object_type(elt) == UCL_BOOLEAN) {
			priv->no_action = ucl_object_toboolean(elt);
		}
	}

	if (action->action_type == METRIC_ACTION_ADD_HEADER) {
		elt = ucl_object_lookup(obj, "spam_header");

		if (elt) {
			if (ucl_object_type(elt) == UCL_STRING) {
				rspamd_milter_remove_header_safe(session,
												 milter_ctx->spam_header,
												 0);

				hname = g_string_new(milter_ctx->spam_header);
				hvalue = g_string_new(ucl_object_tostring(elt));
				rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
										  (uint32_t) 1, hname, hvalue);
				g_string_free(hname, TRUE);
				g_string_free(hvalue, TRUE);
				rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);

				return TRUE;
			}
			else if (ucl_object_type(elt) == UCL_OBJECT) {
				it = NULL;

				while ((cur = ucl_object_iterate(elt, &it, true)) != NULL) {
					rspamd_milter_remove_header_safe(session,
													 ucl_object_key(cur),
													 0);

					hname = g_string_new(ucl_object_key(cur));
					hvalue = g_string_new(ucl_object_tostring(cur));
					rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
											  (uint32_t) 1, hname, hvalue);
					g_string_free(hname, TRUE);
					g_string_free(hvalue, TRUE);
				}

				rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);

				return TRUE;
			}
		}
	}

	return FALSE;
}

void rspamd_milter_send_task_results(struct rspamd_milter_session *session,
									 const ucl_object_t *results,
									 const char *new_body,
									 gsize bodylen)
{
	const ucl_object_t *elt;
	struct rspamd_milter_private *priv = session->priv;
	const char *str_action;
	struct rspamd_action *action;
	rspamd_fstring_t *xcode = NULL, *rcode = NULL, *reply = NULL;
	GString *hname, *hvalue;
	gboolean processed = FALSE;

	if (results == NULL) {
		msg_err_milter("cannot find scan results, tempfail");
		rspamd_milter_send_action(session, RSPAMD_MILTER_TEMPFAIL);

		goto cleanup;
	}

	elt = ucl_object_lookup(results, "action");

	if (!elt) {
		msg_err_milter("cannot find action in results, tempfail");
		rspamd_milter_send_action(session, RSPAMD_MILTER_TEMPFAIL);

		goto cleanup;
	}

	str_action = ucl_object_tostring(elt);
	action = rspamd_config_get_action(milter_ctx->cfg, str_action);

	if (action == NULL) {
		msg_err_milter("action %s has not been registered", str_action);
		rspamd_milter_send_action(session, RSPAMD_MILTER_TEMPFAIL);

		goto cleanup;
	}

	elt = ucl_object_lookup(results, "messages");
	if (elt) {
		const ucl_object_t *smtp_res;
		const char *msg;
		gsize len = 0;

		smtp_res = ucl_object_lookup(elt, "smtp_message");

		if (smtp_res) {
			msg = ucl_object_tolstring(smtp_res, &len);
			reply = rspamd_fstring_new_init(msg, len);
		}
	}

	/* Deal with milter headers */
	elt = ucl_object_lookup(results, "milter");

	if (elt) {
		processed = rspamd_milter_process_milter_block(session, elt, action);
	}

	/* DKIM-Signature */
	elt = ucl_object_lookup(results, "dkim-signature");

	if (elt) {
		hname = g_string_new(RSPAMD_MILTER_DKIM_HEADER);

		if (ucl_object_type(elt) == UCL_STRING) {
			hvalue = g_string_new(ucl_object_tostring(elt));

			rspamd_milter_send_action(session, RSPAMD_MILTER_INSHEADER,
									  1, hname, hvalue);

			g_string_free(hvalue, TRUE);
		}
		else {
			ucl_object_iter_t it;
			const ucl_object_t *cur;
			int i = 1;

			it = ucl_object_iterate_new(elt);

			while ((cur = ucl_object_iterate_safe(it, true)) != NULL) {
				hvalue = g_string_new(ucl_object_tostring(cur));

				rspamd_milter_send_action(session, RSPAMD_MILTER_INSHEADER,
										  i++, hname, hvalue);

				g_string_free(hvalue, TRUE);
			}

			ucl_object_iterate_free(it);
		}

		g_string_free(hname, TRUE);
	}

	if (processed) {
		goto cleanup;
	}

	if (new_body) {
		rspamd_milter_send_action(session, RSPAMD_MILTER_REPLBODY,
								  bodylen, new_body);
	}

	if (priv->no_action) {
		msg_info_milter("do not apply action %s, no_action is set",
						str_action);
		hname = g_string_new(RSPAMD_MILTER_ACTION_HEADER);
		hvalue = g_string_new(str_action);

		rspamd_milter_send_action(session, RSPAMD_MILTER_ADDHEADER,
								  hname, hvalue);
		g_string_free(hname, TRUE);
		g_string_free(hvalue, TRUE);
		rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);

		goto cleanup;
	}

	switch (action->action_type) {
	case METRIC_ACTION_REJECT:
		if (priv->discard_on_reject) {
			rspamd_milter_send_action(session, RSPAMD_MILTER_DISCARD);
		}
		else if (priv->quarantine_on_reject) {
			if (!reply) {
				if (milter_ctx->quarantine_message == NULL) {
					reply = rspamd_fstring_new_init(
						RSPAMD_MILTER_QUARANTINE_MESSAGE,
						sizeof(RSPAMD_MILTER_QUARANTINE_MESSAGE) - 1);
				}
				else {
					reply = rspamd_fstring_new_init(milter_ctx->quarantine_message,
													strlen(milter_ctx->quarantine_message));
				}
			}

			/* A caller-supplied reply is used as the quarantine reason */
			rspamd_milter_send_action(session, RSPAMD_MILTER_QUARANTINE, reply);
			/* Quarantine also requires accept action, all hail Sendmail */
			rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
		}
		else {
			rcode = rspamd_fstring_new_init(RSPAMD_MILTER_RCODE_REJECT,
											sizeof(RSPAMD_MILTER_RCODE_REJECT) - 1);
			xcode = rspamd_fstring_new_init(RSPAMD_MILTER_XCODE_REJECT,
											sizeof(RSPAMD_MILTER_XCODE_REJECT) - 1);

			if (!reply) {
				if (milter_ctx->reject_message == NULL) {
					reply = rspamd_fstring_new_init(
						RSPAMD_MILTER_REJECT_MESSAGE,
						sizeof(RSPAMD_MILTER_REJECT_MESSAGE) - 1);
				}
				else {
					reply = rspamd_fstring_new_init(milter_ctx->reject_message,
													strlen(milter_ctx->reject_message));
				}
			}

			rspamd_milter_set_reply(session, rcode, xcode, reply);
		}
		break;
	case METRIC_ACTION_SOFT_REJECT:
		rcode = rspamd_fstring_new_init(RSPAMD_MILTER_RCODE_TEMPFAIL,
										sizeof(RSPAMD_MILTER_RCODE_TEMPFAIL) - 1);
		xcode = rspamd_fstring_new_init(RSPAMD_MILTER_XCODE_TEMPFAIL,
										sizeof(RSPAMD_MILTER_XCODE_TEMPFAIL) - 1);

		if (!reply) {
			if (milter_ctx->tempfail_message == NULL) {
				reply = rspamd_fstring_new_init(
					RSPAMD_MILTER_TEMPFAIL_MESSAGE,
					sizeof(RSPAMD_MILTER_TEMPFAIL_MESSAGE) - 1);
			}
			else {
				reply = rspamd_fstring_new_init(milter_ctx->tempfail_message,
												strlen(milter_ctx->tempfail_message));
			}
		}

		rspamd_milter_set_reply(session, rcode, xcode, reply);
		break;

	case METRIC_ACTION_REWRITE_SUBJECT:
		elt = ucl_object_lookup(results, "subject");

		if (elt) {
			hname = g_string_new("Subject");
			hvalue = g_string_new(ucl_object_tostring(elt));

			rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
									  (uint32_t) 1, hname, hvalue);
			g_string_free(hname, TRUE);
			g_string_free(hvalue, TRUE);
		}

		rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
		break;

	case METRIC_ACTION_ADD_HEADER:
		/* Remove existing headers */
		rspamd_milter_remove_header_safe(session,
										 milter_ctx->spam_header,
										 0);

		hname = g_string_new(milter_ctx->spam_header);
		hvalue = g_string_new("Yes");
		rspamd_milter_send_action(session, RSPAMD_MILTER_CHGHEADER,
								  (uint32_t) 1, hname, hvalue);
		g_string_free(hname, TRUE);
		g_string_free(hvalue, TRUE);
		rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
		break;

	case METRIC_ACTION_QUARANTINE:
		if (!reply) {
			if (milter_ctx->quarantine_message == NULL) {
				reply = rspamd_fstring_new_init(
					RSPAMD_MILTER_QUARANTINE_MESSAGE,
					sizeof(RSPAMD_MILTER_QUARANTINE_MESSAGE) - 1);
			}
			else {
				reply = rspamd_fstring_new_init(milter_ctx->quarantine_message,
												strlen(milter_ctx->quarantine_message));
			}
		}

		/* A caller-supplied reply is used as the quarantine reason */
		rspamd_milter_send_action(session, RSPAMD_MILTER_QUARANTINE, reply);
		/* Quarantine also requires accept action, all hail Sendmail */
		rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
		break;
	case METRIC_ACTION_DISCARD:
		rspamd_milter_send_action(session, RSPAMD_MILTER_DISCARD);
		break;
	case METRIC_ACTION_GREYLIST:
	case METRIC_ACTION_NOACTION:
	default:
		rspamd_milter_send_action(session, RSPAMD_MILTER_ACCEPT);
		break;
	}

cleanup:
	rspamd_fstring_free(rcode);
	rspamd_fstring_free(xcode);
	rspamd_fstring_free(reply);

	rspamd_milter_session_reset(session, RSPAMD_MILTER_RESET_ABORT);
}

void rspamd_milter_init_library(const struct rspamd_milter_context *ctx)
{
	milter_ctx = ctx;
}

rspamd_mempool_t *
rspamd_milter_get_session_pool(struct rspamd_milter_session *session)
{
	struct rspamd_milter_private *priv = session->priv;

	return priv->pool;
}
