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

#include "lib.h"
#include "str.h"
#include "array.h"
#include "istream.h"
#include "istream-chain.h"

#include "smtp-command-parser.h"
#include "smtp-server-private.h"

/* DATA/BDAT/B... commands */

struct cmd_data_context {
	struct istream *main_input;
	struct istream *chunk_input;
	uoff_t chunk_size;

	bool chunking:1;
	bool client_input:1;
	bool chunk_first:1;
	bool chunk_last:1;
};

static void
smtp_server_cmd_data_size_limit_exceeded(struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_command *command = cmd->cmd;

	smtp_server_command_fail(command, 552, "5.2.3",
				 "Message size exceeds administrative limit");
}

bool smtp_server_cmd_data_check_size(struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	const struct smtp_server_settings *set = &conn->set;

	i_assert(conn->state.state == SMTP_SERVER_STATE_DATA);

	if (conn->state.data_input == NULL)
		return TRUE;
	if (set->max_message_size == 0)
		return TRUE;
	if (conn->state.data_input->v_offset <= set->max_message_size)
		return TRUE;

	smtp_server_cmd_data_size_limit_exceeded(cmd);
	return FALSE;
}

bool smtp_server_connection_data_check_state(struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;
	struct cmd_data_context *data_cmd = command->data;

	if (conn->state.data_chunks > 0 && conn->state.data_failed) {
		// FIXME: should it even reply anything? RFC is unclear.
		smtp_server_command_fail(command, 503, "5.5.0",
			"Previous data chunk failed, issue RSET first");
		return FALSE;
	}

	/* check valid MAIL */
	if (conn->state.trans == NULL
		&& conn->state.pending_mail_cmds == 0) {
		smtp_server_command_fail(command,
			503, "5.5.0", "MAIL needed first");
		return FALSE;
	}
	if (conn->state.trans != NULL &&
	    (conn->state.trans->params.body.type ==
		SMTP_PARAM_MAIL_BODY_TYPE_BINARYMIME) &&
	    !data_cmd->chunking) {
		/* RFC 3030, Section 3:
		   BINARYMIME cannot be used with the DATA command. If a DATA
		   command is issued after a MAIL command containing the
		   body-value of "BINARYMIME", a 503 "Bad sequence of commands"
		   response MUST be sent. The resulting state from this error
		   condition is indeterminate and the transaction MUST be reset
		   with the RSET command. */
		smtp_server_command_fail(command,
			503, "5.5.0", "DATA cannot be used with BINARYMIME");
		return FALSE;
	}

	/* Can only decide whether we have valid recipients once there are no
	   pending RCPT commands */
	if (conn->state.pending_rcpt_cmds > 0)
		return TRUE;

	/* special handling for LMTP */
	if (conn->set.protocol == SMTP_PROTOCOL_LMTP) {
		/* check valid RCPT (at least one) */
		if (conn->state.trans == NULL ||
		    !smtp_server_transaction_has_rcpt(conn->state.trans)) {
			if (data_cmd->chunk_size > 0 && data_cmd->chunk_last) {
				/* RFC 2033, Section 4.3:
				   If there were no previously successful RCPT
				   commands in the mail transaction, then the
				   BDAT LAST command returns zero replies.
				 */
				smtp_server_command_abort(&command);
			} else {
				/* RFC 2033, Section 4.2:
				   The additional restriction is that when there
				   have been no successful RCPT commands in the
				   mail transaction, the DATA command MUST fail
				   with a 503 reply code.
				*/
				smtp_server_command_fail(command,
					503, "5.5.0", "No valid recipients");
			}
			return FALSE;
		}

	} else {
		/* check valid RCPT (at least one) */
		if (conn->state.trans == NULL ||
			!smtp_server_transaction_has_rcpt(conn->state.trans)) {
			smtp_server_command_fail(command,
				554, "5.5.0", "No valid recipients");
			return FALSE;
		}
	}
	return TRUE;
}

static void
cmd_data_destroy(struct smtp_server_cmd_ctx *cmd,
		 struct cmd_data_context *data_cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;

	i_assert(data_cmd != NULL);

	if (data_cmd->main_input == conn->state.data_input &&
	    (data_cmd->chunk_last ||
	     !smtp_server_command_replied_success(command))) {
		/* clean up */
		i_stream_destroy(&conn->state.data_input);
		i_stream_destroy(&conn->state.data_chain_input);
		conn->state.data_chain = NULL;
	}

	i_stream_unref(&data_cmd->chunk_input);
}

static void
cmd_data_replied_one(struct smtp_server_cmd_ctx *cmd,
		     struct cmd_data_context *data_cmd ATTR_UNUSED)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_transaction *trans = conn->state.trans;
	struct smtp_server_recipient *rcpt;

	if (trans == NULL || !array_is_created(&trans->rcpt_to))
		return;

	array_foreach_elem(&trans->rcpt_to, rcpt)
		smtp_server_recipient_data_replied(rcpt);
}

static void
cmd_data_replied(struct smtp_server_cmd_ctx *cmd,
		 struct cmd_data_context *data_cmd ATTR_UNUSED)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;

        i_assert(conn->state.pending_data_cmds > 0);
        conn->state.pending_data_cmds--;

	smtp_server_command_input_lock(cmd);
	if (!smtp_server_command_replied_success(command))
		smtp_server_command_input_unlock(cmd);
}

static void
cmd_data_completed(struct smtp_server_cmd_ctx *cmd,
		   struct cmd_data_context *data_cmd)
{
	struct smtp_server_connection *conn = cmd->conn;

	i_assert(data_cmd != NULL);
	i_stream_unref(&data_cmd->chunk_input);

	i_assert(conn->state.trans != NULL);
	smtp_server_transaction_finished(conn->state.trans, cmd);

	/* reset state */
	smtp_server_connection_reset_state(conn);
}

static void
cmd_data_chunk_replied(struct smtp_server_cmd_ctx *cmd,
		       struct cmd_data_context *data_cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;

	i_assert(data_cmd != NULL);

        i_assert(conn->state.pending_data_cmds > 0);
        conn->state.pending_data_cmds--;

	i_assert(smtp_server_command_is_replied(command));
	if (!smtp_server_command_replied_success(command) &&
	    conn->state.pending_data_cmds == 0)
		conn->state.data_failed = TRUE;
}

static void
cmd_data_chunk_completed(struct smtp_server_cmd_ctx *cmd,
			 struct cmd_data_context *data_cmd ATTR_UNUSED)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;

	if (!smtp_server_command_replied_success(command))
		conn->state.data_failed = TRUE;
}

static void
cmd_data_chunk_finish(struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_command *command = cmd->cmd;
	struct cmd_data_context *data_cmd = command->data;

	smtp_server_command_input_lock(cmd);
	i_stream_unref(&data_cmd->chunk_input);

	/* re-check transaction state (for BDAT/B... command) */
	if (!smtp_server_connection_data_check_state(cmd))
		return;

	smtp_server_reply(cmd, 250, "2.0.0",
		"Added %"PRIuUOFF_T" octets", data_cmd->chunk_size);
}

static void cmd_data_input_error(struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;
	struct cmd_data_context *data_cmd = command->data;
	struct istream *data_input = conn->state.data_input;
	const char *error;

	conn->state.data_failed = TRUE;

	if (!data_cmd->client_input) {
		if (!smtp_server_command_is_replied(command)) {
			smtp_server_command_fail(command,
				400, "4.0.0", "Failed to add data");
		}
		return;
	}

	error = i_stream_get_disconnect_reason(data_input);
	e_debug(conn->event, "Connection lost during data transfer: %s", error);
	smtp_server_connection_close(&conn, error);
}

static int cmd_data_do_handle_input(struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	const struct smtp_server_callbacks *callbacks = conn->callbacks;
	struct smtp_server_command *command = cmd->cmd;
	struct cmd_data_context *data_cmd = command->data;
	int ret;

	i_assert(data_cmd != NULL);

	i_assert(callbacks != NULL &&
		 callbacks->conn_cmd_data_continue != NULL);
	struct event_reason *reason =
		smtp_server_connection_reason_begin(conn, "cmd_data");
	ret = callbacks->conn_cmd_data_continue(conn->context,
		cmd, conn->state.trans);
	event_reason_end(&reason);
	if (ret >= 0) {
		if (!smtp_server_cmd_data_check_size(cmd)) {
			return -1;
		} else if (!i_stream_have_bytes_left(conn->state.data_input)) {
			e_debug(cmd->event, "End of data");
			smtp_server_transaction_received(
				conn->state.trans,
				conn->state.data_input->v_offset);
			smtp_server_command_input_lock(cmd);
			smtp_server_connection_timeout_stop(conn);
		} else if (!data_cmd->chunk_last &&
			!i_stream_have_bytes_left(data_cmd->chunk_input)) {
			e_debug(cmd->event, "End of chunk");
			cmd_data_chunk_finish(cmd);
		} else if (i_stream_get_data_size(
			conn->state.data_input) > 0) {
			e_debug(cmd->event, "Not all client data read");
			smtp_server_connection_timeout_stop(cmd->conn);
		} else {
			smtp_server_connection_timeout_start(cmd->conn);
		}
	} else {
		if (conn->state.data_input->stream_errno != 0) {
			cmd_data_input_error(cmd);
			return -1;
		}
		/* command is waiting for external event or it failed */
		i_assert(smtp_server_command_is_replied(command));
	}

	return 1;
}

static int cmd_data_handle_input(struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;
	int ret;

	if (!smtp_server_cmd_data_check_size(cmd))
		return -1;

	smtp_server_connection_ref(conn);
	smtp_server_command_ref(command);

	/* continue reading from client */
	ret = cmd_data_do_handle_input(cmd);

	smtp_server_command_unref(&command);
	smtp_server_connection_unref(&conn);

	return ret;
}

static void cmd_data_input(struct smtp_server_cmd_ctx *cmd)
{
	smtp_server_connection_timeout_reset(cmd->conn);
	(void)cmd_data_handle_input(cmd);
}

static void
cmd_data_next(struct smtp_server_cmd_ctx *cmd,
	      struct cmd_data_context *data_cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_transaction *trans = conn->state.trans;
	const struct smtp_server_callbacks *callbacks = conn->callbacks;
	struct smtp_server_command *command = cmd->cmd;

	/* this command is next to send a reply */

	i_assert(data_cmd != NULL);
	i_assert(trans != NULL);

	/* DATA command stops the pipeline, so if it is next to reply, nothing
	   else can be pending. */
	i_assert(conn->state.pending_mail_cmds == 0 &&
		 conn->state.pending_rcpt_cmds == 0);

	e_debug(cmd->event, "Command is next to be replied");

	smtp_server_transaction_data_command(trans, cmd);

	/* check whether we have had successful mail and rcpt commands */
	if (!smtp_server_connection_data_check_state(cmd))
		return;

	if (data_cmd->chunk_last) {
		/* LMTP 'DATA' and 'BDAT LAST' commands need to send more than
		   one reply per recipient */
		if (HAS_ALL_BITS(trans->flags,
				 SMTP_SERVER_TRANSACTION_FLAG_REPLY_PER_RCPT)) {
			smtp_server_command_set_reply_count(command,
				array_count(&trans->rcpt_to));
		}
	}

	smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_DATA, NULL);

	/* chain data streams in the correct order */
	if (conn->state.data_chain != NULL) {
		i_assert(data_cmd->chunk_input != NULL);
		i_stream_chain_append(conn->state.data_chain,
				      data_cmd->chunk_input);
		if (data_cmd->chunk_last) {
			e_debug(cmd->event, "Seen the last chunk");
			i_stream_chain_append_eof(conn->state.data_chain);
		}
	}

	if (data_cmd->chunk_first) {
		struct smtp_server_command *cmd_temp = command;

		e_debug(cmd->event, "First chunk");

		smtp_server_command_ref(cmd_temp);
		i_assert(callbacks != NULL &&
			 callbacks->conn_cmd_data_begin != NULL);
		i_assert(conn->state.data_input != NULL);
		struct event_reason *reason =
			smtp_server_connection_reason_begin(conn, "cmd_data");
		int ret = callbacks->conn_cmd_data_begin(conn->context,
			cmd, conn->state.trans, conn->state.data_input);
		event_reason_end(&reason);
		if (ret < 0) {
			i_assert(smtp_server_command_is_replied(cmd_temp));
			/* command failed */
			smtp_server_command_unref(&cmd_temp);
			return;
		}
		if (!smtp_server_command_unref(&cmd_temp))
			return;
	}

	if (smtp_server_command_is_replied(command)) {
		smtp_server_command_input_unlock(cmd);
	} else {
		if (data_cmd->client_input) {
			/* using input from client connection;
			   capture I/O event */
			smtp_server_connection_timeout_start(conn);
			smtp_server_command_input_capture(cmd, cmd_data_input);
		}

		(void)cmd_data_handle_input(cmd);
	}
}

static void
cmd_data_start_input(struct smtp_server_cmd_ctx *cmd,
		     struct cmd_data_context *data_cmd, struct istream *input)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;

	i_assert(data_cmd != NULL);

	if (input != NULL) {
		i_assert(conn->state.data_input == NULL);
		conn->state.data_input = input;
		i_stream_ref(input);
	}
	data_cmd->main_input = conn->state.data_input;

	if (data_cmd->client_input)
		smtp_server_command_input_lock(cmd);

	if (data_cmd->chunk_last) {
		smtp_server_command_add_hook(
			command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
			cmd_data_completed, data_cmd);
	} else {
		smtp_server_command_add_hook(
			command, SMTP_SERVER_COMMAND_HOOK_COMPLETED,
			cmd_data_chunk_completed, data_cmd);
	}

	if (conn->state.pending_mail_cmds == 0 &&
		conn->state.pending_rcpt_cmds == 0) {
		cmd_data_next(cmd, data_cmd);
	} else {
		smtp_server_command_add_hook(
			command, SMTP_SERVER_COMMAND_HOOK_NEXT,
			cmd_data_next, data_cmd);
	}
}

/* DATA command */

static void
cmd_data_start(struct smtp_server_cmd_ctx *cmd,
	       struct cmd_data_context *data_cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_transaction *trans = conn->state.trans;
	struct istream *dot_input;

	/* called when all previous commands were finished */
	i_assert(conn->state.pending_mail_cmds == 0 &&
		conn->state.pending_rcpt_cmds == 0);

	if (trans != NULL)
		smtp_server_transaction_data_command(trans, cmd);

	/* check whether we have had successful mail and rcpt commands */
	if (!smtp_server_connection_data_check_state(cmd))
		return;

	/* don't allow classic DATA when CHUNKING sequence was started before */
	if (conn->state.data_chunks > 0) {
		smtp_server_command_fail(cmd->cmd,
			503, "5.5.0", "Bad sequence of commands");
		return;
	}

	smtp_server_connection_set_state(conn, SMTP_SERVER_STATE_DATA, NULL);

	/* confirm initial success to client */
	smtp_server_connection_reply_immediate(conn, 354, "OK");

	/* start reading message data from client */
	dot_input = smtp_command_parse_data_with_dot(conn->smtp_parser);
	cmd_data_start_input(cmd, data_cmd, dot_input);
	i_stream_unref(&dot_input);
}

void smtp_server_cmd_data(struct smtp_server_cmd_ctx *cmd,
			  const char *params)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;
	struct cmd_data_context *data_cmd;

	/* data = "DATA" CRLF */
	if (*params != '\0') {
		smtp_server_reply(cmd,
			501, "5.5.4", "Invalid parameters");
		return;
	}

	smtp_server_command_input_lock(cmd);
	smtp_server_command_pipeline_block(cmd);

	data_cmd = p_new(cmd->pool, struct cmd_data_context, 1);
	data_cmd->chunk_first = TRUE;
	data_cmd->chunk_last = TRUE;
	data_cmd->client_input = TRUE;
	command->data = data_cmd;

	smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_NEXT,
				     cmd_data_start, data_cmd);
	smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED_ONE,
				     cmd_data_replied_one, data_cmd);
	smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED,
				     cmd_data_replied, data_cmd);
	smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_DESTROY,
				     cmd_data_destroy, data_cmd);

	conn->state.pending_data_cmds++;
}

/* BDAT/B... commands */

void smtp_server_connection_data_chunk_init(struct smtp_server_cmd_ctx *cmd)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_command *command = cmd->cmd;
	struct cmd_data_context *data_cmd;

	data_cmd = p_new(cmd->pool, struct cmd_data_context, 1);
	data_cmd->chunking = TRUE;
	data_cmd->chunk_first = (conn->state.data_chunks++ == 0);
	command->data = data_cmd;

	smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_REPLIED,
				     cmd_data_chunk_replied, data_cmd);
	smtp_server_command_add_hook(command, SMTP_SERVER_COMMAND_HOOK_DESTROY,
				     cmd_data_destroy, data_cmd);

	conn->state.pending_data_cmds++;

	if (!conn->state.data_failed && conn->state.data_chain == NULL) {
		i_assert(data_cmd->chunk_first);
		i_assert(conn->state.data_chain_input == NULL);
		conn->state.data_chain_input =
			i_stream_create_chain(&conn->state.data_chain,
					      IO_BLOCK_SIZE);
	}
}

int smtp_server_connection_data_chunk_add(struct smtp_server_cmd_ctx *cmd,
	struct istream *chunk, uoff_t chunk_size, bool chunk_last,
	bool client_input)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct smtp_server_transaction *trans = conn->state.trans;
	const struct smtp_server_settings *set = &conn->set;
	struct smtp_server_command *command = cmd->cmd;
	struct cmd_data_context *data_cmd = command->data;

	i_assert(chunk != NULL);
	i_assert(data_cmd != NULL);

	if (trans != NULL)
		smtp_server_transaction_data_command(trans, cmd);

	if (!smtp_server_connection_data_check_state(cmd))
		return -1;

	/* check message size increase early */
	if (UOFF_T_MAX - conn->state.data_size < chunk_size ||
	    (set->max_message_size > 0 &&
	     conn->state.data_size + chunk_size > set->max_message_size)) {
		smtp_server_cmd_data_size_limit_exceeded(cmd);
		return -1;
	}
	conn->state.data_size += chunk_size;

	if (chunk_last) {
		smtp_server_command_remove_hook(
			command, SMTP_SERVER_COMMAND_HOOK_REPLIED,
			cmd_data_chunk_replied);
		smtp_server_command_add_hook(
			command, SMTP_SERVER_COMMAND_HOOK_REPLIED,
			cmd_data_replied, data_cmd);
		smtp_server_command_pipeline_block(cmd);
	}

	data_cmd->chunk_input = chunk;
	data_cmd->chunk_size = chunk_size;
	data_cmd->chunk_last = chunk_last;
	data_cmd->client_input = client_input;
	i_stream_ref(chunk);

	cmd_data_start_input(cmd, data_cmd, conn->state.data_chain_input);
	i_stream_unref(&conn->state.data_chain_input);
	return 0;
}

/* BDAT command */

void smtp_server_cmd_bdat(struct smtp_server_cmd_ctx *cmd,
			  const char *params)
{
	struct smtp_server_connection *conn = cmd->conn;
	struct istream *input = NULL;
	uoff_t size = 0;
	const char *const *argv;
	bool chunk_last = FALSE;
	int ret = 1;

	if ((conn->set.capabilities & SMTP_CAPABILITY_CHUNKING) == 0) {
		smtp_server_reply(cmd,
			502, "5.5.1", "Unsupported command");
		return;
	}

	smtp_server_connection_data_chunk_init(cmd);

	/* bdat-cmd   = "BDAT" SP chunk-size [ SP end-marker ] CR LF
	   chunk-size = 1*DIGIT
	   end-marker = "LAST"
	 */
	argv = t_strsplit(params, " ");
	if (argv[0] == NULL || str_to_uoff(argv[0], &size) < 0 ||
	    size > INTMAX_MAX) {
		smtp_server_reply(cmd,
			501, "5.5.4", "Invalid chunk size parameter");
		size = 0;
		ret = -1;
	} else if (argv[1] != NULL) {
		if (argv[2] != NULL) {
			smtp_server_reply(cmd,
				501, "5.5.4", "Invalid parameters");
			ret = -1;
		} else if (strcasecmp(argv[1], "LAST") != 0) {
			smtp_server_reply(cmd,
				501, "5.5.4", "Invalid end marker parameter");
			ret = -1;
		} else {
			chunk_last = TRUE;
		}
	}

	if (ret > 0 || (size > 0 && !conn->disconnected)) {
		/* Read/skip data even in case of error, as long as size is
		   known and connection is still usable. */
		input = smtp_command_parse_data_with_size(conn->smtp_parser,
							  size);
	}

	if (ret < 0) {
		i_stream_unref(&input);
		return;
	}

	(void)smtp_server_connection_data_chunk_add(cmd,
		input, size, chunk_last, TRUE);
	i_stream_unref(&input);
}
