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

#include "imap-common.h"
#include "ioloop.h"
#include "istream.h"
#include "istream-seekable.h"
#include "ostream.h"
#include "str.h"
#include "imap-metadata.h"
#include "mail-storage-private.h"

#define METADATA_MAX_INMEM_SIZE (1024*128)

struct imap_setmetadata_context {
	struct client_command_context *cmd;
	struct imap_parser *parser;

	struct mailbox *box;
	struct imap_metadata_transaction *trans;

	char *entry_name;
	uoff_t entry_value_len;
	struct istream *input;
	bool failed;
	bool cmd_error_sent;
	bool storage_failure;
};

static void cmd_setmetadata_deinit(struct imap_setmetadata_context *ctx)
{
	o_stream_set_flush_callback(ctx->cmd->client->output,
				    client_output, ctx->cmd->client);

	ctx->cmd->client->input_lock = NULL;
	imap_parser_unref(&ctx->parser);
	if (ctx->trans != NULL)
		imap_metadata_transaction_rollback(&ctx->trans);
	if (ctx->box != NULL && ctx->box != ctx->cmd->client->mailbox)
		mailbox_free(&ctx->box);
	i_free(ctx->entry_name);
}

static int
cmd_setmetadata_parse_entryvalue(struct imap_setmetadata_context *ctx,
				 const char **entry_r,
				 const struct imap_arg **value_r)
{
	const struct imap_arg *args;
	const char *name, *client_error;
	enum imap_parser_error parse_error;
	int ret;

	/* parse the entry name */
	ret = imap_parser_read_args(ctx->parser, 1,
				    IMAP_PARSE_FLAG_INSIDE_LIST, &args);
	if (ret >= 0) {
		if (ret == 0) {
			/* ')' found */
			*entry_r = NULL;
			return 1;
		}
		if (!imap_arg_get_astring(args, &name)) {
			client_send_command_error(ctx->cmd,
						  "Entry name isn't astring");
			return -1;
		}

		ret = imap_parser_read_args(ctx->parser, 2,
					    IMAP_PARSE_FLAG_INSIDE_LIST |
					    IMAP_PARSE_FLAG_LITERAL_SIZE |
					    IMAP_PARSE_FLAG_LITERAL8, &args);
	}
	if (ret < 0) {
		if (ret == -2)
			return 0;
		client_error = imap_parser_get_error(ctx->parser, &parse_error);
		switch (parse_error) {
		case IMAP_PARSE_ERROR_NONE:
			i_unreached();
		case IMAP_PARSE_ERROR_LITERAL_TOO_BIG:
			client_disconnect_with_error(ctx->cmd->client,
						     client_error);
			break;
		default:
			client_send_command_error(ctx->cmd, client_error);
			break;
		}
		return -1;
	}
	if (args[1].type == IMAP_ARG_EOL) {
		client_send_command_error(ctx->cmd, "Entry value missing");
		return -1;
	}
	if (args[1].type == IMAP_ARG_LIST) {
		client_send_command_error(ctx->cmd, "Entry value can't be a list");
		return -1;
	}
	if (!ctx->cmd_error_sent &&
	    !imap_metadata_verify_entry_name(name, &client_error)) {
		client_send_command_error(ctx->cmd, client_error);
		ctx->cmd_error_sent = TRUE;
	}
	if (ctx->cmd_error_sent) {
		ctx->cmd->param_error = FALSE;
		ctx->cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT;

		ctx->failed = TRUE;
		if (args[1].type == IMAP_ARG_LITERAL_SIZE) {
			/* client won't see "+ OK", so we can abort
			   immediately */
			ctx->cmd->client->input_skip_line = FALSE;
			return -1;
		}
	}

	/* entry names are case-insensitive. handle this by using only
	   lowercase names. */
	*entry_r = t_str_lcase(name);
	*value_r = &args[1];
	return 1;
}

static int
cmd_setmetadata_entry_read_stream(struct imap_setmetadata_context *ctx)
{
	const unsigned char *data;
	size_t size;
	struct mail_attribute_value value;
	int ret;

	while ((ret = i_stream_read_more(ctx->input, &data, &size)) > 0)
		i_stream_skip(ctx->input, size);
	if (ret == 0)
		return 0;

	if (ctx->input->v_offset != ctx->entry_value_len) {
		/* client disconnected */
		i_assert(ctx->input->eof);
		return -1;
	}

	/* finished reading the value */
	i_stream_seek(ctx->input, 0);

	if (ctx->failed) {
		i_stream_unref(&ctx->input);
		return 1;
	}

	i_zero(&value);
	value.value_stream = ctx->input;
	if (imap_metadata_set(ctx->trans, ctx->entry_name, &value) < 0) {
		/* delay reporting the failure so we'll finish
		   reading the command input */
		ctx->storage_failure = TRUE;
		ctx->failed = TRUE;
	}
	i_stream_unref(&ctx->input);
	return 1;
}

static int
cmd_setmetadata_entry(struct imap_setmetadata_context *ctx,
		      const char *entry_name,
		      const struct imap_arg *entry_value)
{
	struct istream *inputs[2];
	struct mail_attribute_value value;
	string_t *path;
	int ret;

	switch (entry_value->type) {
	case IMAP_ARG_NIL:
	case IMAP_ARG_ATOM:
	case IMAP_ARG_STRING:
		/* we have the value already */
		if (ctx->failed)
			return 1;
		i_zero(&value);
		/* NOTE: The RFC doesn't allow atoms as value, but since
		   Dovecot has traditionally supported it this is kept for
		   backwards compatibility just in case some client is
		   using it. */
		if (entry_value->type == IMAP_ARG_NIL)
			;
		else if (!imap_arg_get_atom(entry_value, &value.value))
			value.value = imap_arg_as_nstring(entry_value);
		ret = imap_metadata_set(ctx->trans, entry_name, &value);
		if (ret < 0) {
			/* delay reporting the failure so we'll finish
			   reading the command input */
			ctx->storage_failure = TRUE;
			ctx->failed = TRUE;
		}
		return 1;
	case IMAP_ARG_LITERAL_SIZE:
		o_stream_nsend(ctx->cmd->client->output, "+ OK\r\n", 6);
		o_stream_uncork(ctx->cmd->client->output);
		o_stream_cork(ctx->cmd->client->output);
		/* fall through */
	case IMAP_ARG_LITERAL_SIZE_NONSYNC:
		i_free(ctx->entry_name);
		ctx->entry_name = i_strdup(entry_name);
		ctx->entry_value_len = imap_arg_as_literal_size(entry_value);

		inputs[0] = i_stream_create_limit(ctx->cmd->client->input,
						  ctx->entry_value_len);
		inputs[1] = NULL;

		path = t_str_new(128);
		mail_user_set_get_temp_prefix(path, ctx->cmd->client->user->set);
		ctx->input = i_stream_create_seekable_path(inputs,
					METADATA_MAX_INMEM_SIZE, str_c(path));
		i_stream_set_name(ctx->input, i_stream_get_name(inputs[0]));
		i_stream_unref(&inputs[0]);
		return cmd_setmetadata_entry_read_stream(ctx);
	case IMAP_ARG_LITERAL:
	case IMAP_ARG_LIST:
	case IMAP_ARG_EOL:
		break;
	}
	i_unreached();
}

static bool cmd_setmetadata_continue(struct client_command_context *cmd)
{
	struct imap_setmetadata_context *ctx = cmd->context;
	const char *entry, *client_error;
	enum mail_error error;
	const struct imap_arg *value;
	int ret;

	if (cmd->cancel) {
		cmd_setmetadata_deinit(ctx);
		return TRUE;
	}

	if (ctx->input != NULL) {
		if ((ret = cmd_setmetadata_entry_read_stream(ctx)) == 0)
			return FALSE;
		if (ret < 0) {
			cmd_setmetadata_deinit(ctx);
			return TRUE;
		}
	}

	while ((ret = cmd_setmetadata_parse_entryvalue(ctx, &entry, &value)) > 0 &&
	       entry != NULL) {
		ret = ctx->failed ? 1 :
			cmd_setmetadata_entry(ctx, entry, value);
		imap_parser_reset(ctx->parser);
		if (ret <= 0)
			break;
	}
	if (ret == 0)
		return 0;

	if (ret < 0 || ctx->cmd_error_sent) {
		/* already sent the error to client */ ;
	} else if (ctx->storage_failure) {
		if (ctx->box == NULL)
			client_disconnect_if_inconsistent(cmd->client);
		client_error = imap_metadata_transaction_get_last_error
			(ctx->trans, &error);
		client_send_tagline(cmd,
			imap_get_error_string(client_error, error));
	} else if (imap_metadata_transaction_commit(&ctx->trans,
						&error, &client_error) < 0) {
		if (ctx->box == NULL)
			client_disconnect_if_inconsistent(cmd->client);
		client_send_tagline(cmd,
			imap_get_error_string(client_error, error));
	} else {
		client_send_tagline(cmd, "OK Setmetadata completed.");
	}
	cmd_setmetadata_deinit(ctx);
	return TRUE;
}

static bool
cmd_setmetadata_start(struct imap_setmetadata_context *ctx)
{
	struct client_command_context *cmd = ctx->cmd;
	struct client *client = cmd->client;

	imap_metadata_transaction_validated_only(ctx->trans,
		!cmd->client->set->imap_metadata);
	/* we support large literals, so read the values from client
	   asynchronously the same way as APPEND does. */
	client->input_lock = cmd;
	ctx->parser = imap_parser_create(client->input, client->output,
					 client->set->imap_max_line_length,
					 NULL);
	if (client->set->imap_literal_minus)
		imap_parser_enable_literal_minus(ctx->parser);
	o_stream_unset_flush_callback(client->output);

	cmd->func = cmd_setmetadata_continue;
	cmd->context = ctx;
	return cmd_setmetadata_continue(cmd);
}

static bool
cmd_setmetadata_server(struct imap_setmetadata_context *ctx)
{
	ctx->trans = imap_metadata_transaction_begin_server(ctx->cmd->client->user);
	return cmd_setmetadata_start(ctx);
}

static bool
cmd_setmetadata_mailbox(struct imap_setmetadata_context *ctx,
	const char *mailbox)
{
	struct client_command_context *cmd = ctx->cmd;
	struct client *client = cmd->client;
	struct mail_namespace *ns;

	ns = client_find_namespace(cmd, &mailbox);
	if (ns == NULL)
		return TRUE;

	if (client->mailbox != NULL && !client->mailbox_examined &&
	    mailbox_equals(client->mailbox, ns, mailbox))
		ctx->box = client->mailbox;
	else {
		ctx->box = mailbox_alloc(ns->list, mailbox,
					 MAILBOX_FLAG_ATTRIBUTE_SESSION);
		enum mailbox_existence existence;
		if (mailbox_exists(ctx->box, TRUE, &existence) < 0) {
			client_send_box_error(cmd, ctx->box);
			mailbox_free(&ctx->box);
			return TRUE;
		} else if (existence == MAILBOX_EXISTENCE_NONE) {
			const char *err = t_strdup_printf(MAIL_ERRSTR_MAILBOX_NOT_FOUND,
							  mailbox_get_vname(ctx->box));
			mail_storage_set_error(ctx->box->storage, MAIL_ERROR_NOTFOUND, err);
			client_send_box_error(cmd, ctx->box);
			mailbox_free(&ctx->box);
			return TRUE;
		}
	}
	event_add_str(ctx->cmd->global_event, "mailbox",
		      mailbox_get_vname(ctx->box));
	ctx->trans = imap_metadata_transaction_begin(ctx->box);
	return cmd_setmetadata_start(ctx);
}

bool cmd_setmetadata(struct client_command_context *cmd)
{
	struct imap_setmetadata_context *ctx;
	const struct imap_arg *args;
	const char *mailbox;
	int ret;

	ret = imap_parser_read_args(cmd->parser, 2,
				    IMAP_PARSE_FLAG_STOP_AT_LIST, &args);
	if (ret == -1) {
		client_send_command_error(cmd, NULL);
		return TRUE;
	}
	if (ret == -2)
		return FALSE;
	if (!imap_arg_get_astring(&args[0], &mailbox) ||
	    args[1].type != IMAP_ARG_LIST) {
		client_send_command_error(cmd, "Invalid arguments.");
		return TRUE;
	}

	ctx = p_new(cmd->pool, struct imap_setmetadata_context, 1);
	ctx->cmd = cmd;
	ctx->cmd->context = ctx;

	if (mailbox[0] == '\0') {
		/* server attribute */
		return cmd_setmetadata_server(ctx);
	}

	return cmd_setmetadata_mailbox(ctx, mailbox);
}
