/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "str.h"
#include "strfuncs.h"
#include "message-date.h"
#include "smtp-address.h"
#include "smtp-params.h"

#include "smtp-server-private.h"

static void
smtp_server_transaction_update_event(struct smtp_server_transaction *trans)
{
	struct event *event = trans->event;

	event_add_str(event, "transaction_id", trans->id);
	event_add_str(event, "session", trans->id);
	event_add_str(event, "mail_from",
		      smtp_address_encode(trans->mail_from));
	event_add_str(event, "mail_from_raw",
		      smtp_address_encode_raw(trans->mail_from));
	smtp_params_mail_add_to_event(&trans->params, event);
	event_set_append_log_prefix(event,
				    t_strdup_printf("trans <%s>: ", trans->id));
}

struct smtp_server_transaction *
smtp_server_transaction_create(struct smtp_server_connection *conn,
			       const struct smtp_server_cmd_mail *mail_data)
{
	struct smtp_server_transaction *trans;
	pool_t pool;

	/* create new transaction */
	pool = pool_alloconly_create("smtp server transaction", 4096);
	trans = p_new(pool, struct smtp_server_transaction, 1);
	trans->pool = pool;
	trans->conn = conn;

	/* generate transaction ID */
	if (conn->transaction_seq++ == 0)
		trans->id = conn->session_id;
	else {
		trans->id = p_strdup_printf(pool, "%s:T%u", conn->session_id,
					    conn->transaction_seq);
	}

	trans->flags = mail_data->flags;
	trans->mail_from = smtp_address_clone(trans->pool, mail_data->path);
	smtp_params_mail_copy(pool, &trans->params, &mail_data->params);
	trans->timestamp = mail_data->timestamp;

	if (conn->next_trans_event == NULL)
		trans->event = event_create(conn->event);
	else {
		trans->event = conn->next_trans_event;
		conn->next_trans_event = NULL;
	}
	smtp_server_transaction_update_event(trans);

	struct event_passthrough *e =
		event_create_passthrough(trans->event)->
		set_name("smtp_server_transaction_started");

	e_debug(e->event(), "Start");

	if (conn->callbacks != NULL &&
	    conn->callbacks->conn_trans_start != NULL)
		conn->callbacks->conn_trans_start(conn->context, trans);

	return trans;
}

void smtp_server_transaction_free(struct smtp_server_transaction **_trans)
{
	struct smtp_server_transaction *trans = *_trans;
	struct smtp_server_connection *conn = trans->conn;
	struct smtp_server_recipient **rcpts;
	unsigned int rcpts_total, rcpts_aborted, rcpts_failed;
	unsigned int rcpts_count, i;

	*_trans = NULL;

	if (conn->callbacks != NULL &&
	    conn->callbacks->conn_trans_free != NULL)
		conn->callbacks->conn_trans_free(conn->context, trans);

	rcpts_count = 0;
	if (array_is_created(&trans->rcpt_to))
		rcpts = array_get_modifiable(&trans->rcpt_to, &rcpts_count);

	rcpts_aborted = rcpts_count + conn->state.pending_rcpt_cmds;
	rcpts_failed =  conn->state.denied_rcpt_cmds;
	rcpts_total = rcpts_aborted + rcpts_failed;

	for (i = 0; i < rcpts_count; i++)
		smtp_server_recipient_destroy(&rcpts[i]);

	if (!trans->finished) {
		struct event_passthrough *e =
			event_create_passthrough(trans->event)->
			set_name("smtp_server_transaction_finished")->
			add_int("recipients", rcpts_total)->
			add_int("recipients_denied", rcpts_failed)->
			add_int("recipients_aborted", rcpts_aborted)->
			add_int("recipients_failed", rcpts_failed)->
			add_int("recipients_succeeded", 0);
		e->add_int("status_code", 9000);
		e->add_str("enhanced_code", "9.0.0");
		e->add_str("error", "Aborted");

		e_debug(e->event(), "Aborted");
	}

	event_unref(&trans->event);
	pool_unref(&trans->pool);
}

struct smtp_server_recipient *
smtp_server_transaction_find_rcpt_duplicate(
	struct smtp_server_transaction *trans,
	struct smtp_server_recipient *rcpt)
{
	struct smtp_server_recipient *drcpt;

	i_assert(array_is_created(&trans->rcpt_to));
	array_foreach_elem(&trans->rcpt_to, drcpt) {
		if (drcpt == rcpt)
			continue;
		if (smtp_address_equals(drcpt->path, rcpt->path) &&
		    smtp_params_rcpt_equal(&drcpt->params, &rcpt->params))
			return drcpt;
	}
	return NULL;
}

void smtp_server_transaction_add_rcpt(struct smtp_server_transaction *trans,
				      struct smtp_server_recipient *rcpt)
{
	if (!array_is_created(&trans->rcpt_to))
		p_array_init(&trans->rcpt_to, trans->pool, 8);

	rcpt->trans = trans;
	rcpt->index = array_count(&trans->rcpt_to);

	array_push_back(&trans->rcpt_to, &rcpt);
}

bool smtp_server_transaction_has_rcpt(struct smtp_server_transaction *trans)
{
	return (array_is_created(&trans->rcpt_to) &&
		array_count(&trans->rcpt_to) > 0);
}

unsigned int
smtp_server_transaction_rcpt_count(struct smtp_server_transaction *trans)
{
	if (!array_is_created(&trans->rcpt_to))
		return 0;
	return array_count(&trans->rcpt_to);
}

void smtp_server_transaction_data_command(struct smtp_server_transaction *trans,
					  struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_recipient *rcpt;

	trans->cmd = cmd;

	if (!array_is_created(&trans->rcpt_to))
		return;
	array_foreach_elem(&trans->rcpt_to, rcpt)
		smtp_server_recipient_data_command(rcpt, cmd);
}

void smtp_server_transaction_received(struct smtp_server_transaction *trans,
				      uoff_t data_size)
{
	event_add_int(trans->event, "data_size", data_size);
}

void smtp_server_transaction_reset(struct smtp_server_transaction *trans)
{
	struct smtp_server_connection *conn = trans->conn;
	struct smtp_server_recipient *const *rcpts = NULL;
	unsigned int rcpts_total, rcpts_failed, rcpts_aborted;
	unsigned int rcpts_count, i;

	i_assert(!trans->finished);
	trans->finished = TRUE;

	rcpts_count = 0;
	if (array_is_created(&trans->rcpt_to))
		rcpts = array_get(&trans->rcpt_to, &rcpts_count);

	rcpts_aborted = rcpts_count + conn->state.pending_rcpt_cmds;
	rcpts_failed = conn->state.denied_rcpt_cmds;
	rcpts_total = rcpts_aborted + rcpts_failed;

	for (i = 0; i < rcpts_count; i++)
		smtp_server_recipient_reset(rcpts[i]);

	struct event_passthrough *e =
		event_create_passthrough(trans->event)->
		set_name("smtp_server_transaction_finished")->
		add_int("recipients", rcpts_total)->
		add_int("recipients_denied", rcpts_failed)->
		add_int("recipients_aborted", rcpts_aborted)->
		add_int("recipients_failed", rcpts_failed)->
		add_int("recipients_succeeded", 0)->
		add_str("is_reset", "yes");
	e_debug(e->event(), "Finished");
}

void smtp_server_transaction_finished(struct smtp_server_transaction *trans,
				      struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_connection *conn = trans->conn;
	struct smtp_server_recipient *const *rcpts = NULL;
	const struct smtp_server_reply *trans_reply = NULL;
	unsigned int rcpts_total, rcpts_denied, rcpts_failed, rcpts_succeeded;
	unsigned int rcpts_count, i;

	i_assert(conn->state.pending_rcpt_cmds == 0);
	i_assert(!trans->finished);
	trans->finished = TRUE;

	rcpts_count = 0;
	if (array_is_created(&trans->rcpt_to))
		rcpts = array_get(&trans->rcpt_to, &rcpts_count);

	rcpts_succeeded = 0;
	rcpts_denied = conn->state.denied_rcpt_cmds;
	rcpts_failed = conn->state.denied_rcpt_cmds;
	rcpts_total = rcpts_count + conn->state.denied_rcpt_cmds;
	for (i = 0; i < rcpts_count; i++) {
		struct smtp_server_reply *reply;

		if ((trans->flags &
		     SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT) != 0)
			reply = smtp_server_command_get_reply(cmd->cmd, i);
		else
			reply = smtp_server_command_get_reply(cmd->cmd, 0);
		smtp_server_recipient_finished(rcpts[i], reply);

		if (smtp_server_reply_is_success(reply))
			rcpts_succeeded++;
		else {
			rcpts_failed++;
			if (trans_reply == NULL)
				trans_reply = reply;
		}
	}

	if (trans_reply == NULL) {
		/* record first success reply in transaction */
		trans_reply = smtp_server_command_get_reply(cmd->cmd, 0);
	}

	struct event_passthrough *e =
		event_create_passthrough(trans->event)->
		set_name("smtp_server_transaction_finished")->
		add_int("recipients", rcpts_total)->
		add_int("recipients_denied", rcpts_denied)->
		add_int("recipients_aborted", 0)->
		add_int("recipients_failed", rcpts_failed)->
		add_int("recipients_succeeded", rcpts_succeeded);
	smtp_server_reply_add_to_event(trans_reply, e);

	e_debug(e->event(), "Finished");
}

void smtp_server_transaction_fail_data(struct smtp_server_transaction *trans,
	struct smtp_server_cmd_ctx *data_cmd,
	unsigned int status, const char *enh_code,
	const char *fmt, va_list args)
{
	struct smtp_server_recipient *const *rcpts;
	const char *msg;
	unsigned int count, i;

	msg = t_strdup_vprintf(fmt, args);
	rcpts = array_get(&trans->rcpt_to, &count);
	for (i = 0; i < count; i++) {
		smtp_server_reply_index(data_cmd, i,
			status, enh_code, "<%s> %s",
			smtp_address_encode(rcpts[i]->path), msg);
	}
}

void smtp_server_transaction_write_trace_record(
	string_t *str, struct smtp_server_transaction *trans,
	enum smtp_server_trace_rcpt_to_address rcpt_to_address)
{
	struct smtp_server_connection *conn = trans->conn;
	const struct smtp_server_helo_data *helo_data = &conn->helo;
	const char *host, *secstr, *rcpt_to = NULL;

	if (array_count(&trans->rcpt_to) == 1) {
		struct smtp_server_recipient *const *rcpts =
			array_front(&trans->rcpt_to);

		switch (rcpt_to_address) {
		case SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_NONE:
			break;
		case SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_FINAL:
			rcpt_to = smtp_address_encode(rcpts[0]->path);
			break;
		case SMTP_SERVER_TRACE_RCPT_TO_ADDRESS_ORIGINAL:
			rcpt_to = smtp_address_encode(
				smtp_server_recipient_get_original(rcpts[0]));
			break;
		}
	}

	/* from */
	str_append(str, "Received: from ");
	if (helo_data->domain_valid)
		str_append(str, helo_data->domain);
	else
		str_append(str, "unknown");
	host = "";
	if (conn->conn.remote_ip.family != 0)
		host = net_ip2addr(&conn->conn.remote_ip);
	if (host[0] != '\0') {
		str_append(str, " ([");
		str_append(str, host);
		str_append(str, "])");
	}
	/* (using) */
	secstr = smtp_server_connection_get_security_string(conn);
	if (secstr != NULL) {
		str_append(str, "\r\n\t(using ");
		str_append(str, secstr);
		str_append(str, ")");
	}
	/* by, with */
	str_append(str, "\r\n\tby ");
	str_append(str, conn->set.hostname);
	str_append(str, " with ");
	str_append(str, smtp_server_connection_get_protocol_name(conn));
	/* id */
	str_append(str, "\r\n\tid ");
	str_append(str, trans->id);
	/* (envelope-from) */
	str_append(str, "\r\n\t(envelope-from <");
	smtp_address_write(str, trans->mail_from);
	str_append(str, ">)");
	/* for */
	if (rcpt_to != NULL) {
		str_append(str, "\r\n\tfor <");
		str_append(str, rcpt_to);
		str_append(str, ">");
	}
	str_append(str, "; ");
	/* date */
	str_append(str, message_date_create(trans->timestamp.tv_sec));
	str_printfa(str, "\r\n");
}
