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

#include "imap-common.h"
#include "mail-storage-private.h"
#include "str.h"
#include "istream.h"
#include "istream-sized.h"
#include "ostream.h"
#include "mailbox-list-iter.h"
#include "imap-utf7.h"
#include "imap-quote.h"
#include "imap-metadata.h"

struct imap_getmetadata_context {
	struct client_command_context *cmd;

	struct mailbox *box;
	struct imap_metadata_transaction *trans;
	struct mailbox_list_iterate_context *list_iter;

	ARRAY_TYPE(const_string) entries;
	uint32_t maxsize;
	uoff_t largest_seen_size;
	unsigned int depth;

	struct istream *cur_stream;

	struct imap_metadata_iter *iter;
	string_t *iter_entry_prefix;

	string_t *delayed_errors;
	string_t *last_error_str;
	enum mail_error last_error;

	unsigned int entry_idx;
	bool first_entry_sent:1;
	bool iterating_boxes:1;
};

static bool
cmd_getmetadata_mailbox_iter_next(struct imap_getmetadata_context *ctx);

static bool
cmd_getmetadata_parse_options(struct imap_getmetadata_context *ctx,
			      const struct imap_arg *options)
{
	const char *value;

	while (!IMAP_ARG_IS_EOL(options)) {
		if (imap_arg_atom_equals(options, "MAXSIZE")) {
			options++;
			if (!imap_arg_get_atom(options, &value) ||
			    str_to_uint32(value, &ctx->maxsize) < 0) {
				client_send_command_error(ctx->cmd,
					"Invalid value for MAXSIZE option");
				return FALSE;
			}
		} else if (imap_arg_atom_equals(options, "DEPTH")) {
			options++;
			if (!imap_arg_get_atom(options, &value)) {
				client_send_command_error(ctx->cmd,
					"Invalid value for DEPTH option");
				return FALSE;
			}
			if (strcmp(value, "0") == 0)
				ctx->depth = 0;
			else if (strcmp(value, "1") == 0)
				ctx->depth = 1;
			else if (strcmp(value, "infinity") == 0)
				ctx->depth = UINT_MAX;
			else {
				client_send_command_error(ctx->cmd,
					"Invalid value for DEPTH option");
				return FALSE;
			}
		} else {
			client_send_command_error(ctx->cmd, "Unknown option");
			return FALSE;
		}
		options++;
	}
	return TRUE;
}

static bool
imap_metadata_parse_entry_names(struct imap_getmetadata_context *ctx,
				const struct imap_arg *entries)
{
	const char *value, *client_error;

	p_array_init(&ctx->entries, ctx->cmd->pool, 4);
	for (; !IMAP_ARG_IS_EOL(entries); entries++) {
		if (!imap_arg_get_astring(entries, &value)) {
			client_send_command_error(ctx->cmd, "Entry isn't astring");
			return FALSE;
		}
		if (!imap_metadata_verify_entry_name(value, &client_error)) {
			client_send_command_error(ctx->cmd, client_error);
			return FALSE;
		}

		/* names are case-insensitive so we'll always lowercase them */
		value = p_strdup(ctx->cmd->pool, t_str_lcase(value));
		array_push_back(&ctx->entries, &value);
	}
	return TRUE;
}

static string_t *
metadata_add_entry(struct imap_getmetadata_context *ctx, const char *entry)
{
	enum imap_quote_flags qflags = (ctx->cmd->utf8 ?
					IMAP_QUOTE_FLAG_UTF8 : 0);
	string_t *str;

	str = t_str_new(64);
	if (!ctx->first_entry_sent) {
		string_t *mailbox_mutf7 = t_str_new(64);

		ctx->first_entry_sent = TRUE;
		str_append(str, "* METADATA ");
		if (ctx->box == NULL) {
			/* server metadata reply */
			str_append(str, "\"\"");
		} else if (ctx->cmd->utf8) {
			imap_append_astring(str, mailbox_get_vname(ctx->box), qflags);
		} else {
			if (imap_utf8_to_utf7(mailbox_get_vname(ctx->box), mailbox_mutf7) < 0)
				i_unreached();
			imap_append_astring(str, str_c(mailbox_mutf7), 0);
		}
		str_append(str, " (");

		/* nothing can be sent until untagged METADATA is finished */
		ctx->cmd->client->output_cmd_lock = ctx->cmd;
	} else {
		str_append_c(str, ' ');
	}
	imap_append_astring(str, entry, qflags);
	return str;
}

static void
cmd_getmetadata_send_nil_reply(struct imap_getmetadata_context *ctx,
			       const char *entry)
{
	string_t *str;

	/* client requested a specific entry that didn't exist.
	   we must return it as NIL. */
	str = metadata_add_entry(ctx, entry);
	str_append(str, " NIL");
	o_stream_nsend(ctx->cmd->client->output, str_data(str), str_len(str));
}

static void
cmd_getmetadata_handle_error_str(struct imap_getmetadata_context *ctx,
				 const char *error_string,
				 enum mail_error error)
{
	if (str_len(ctx->last_error_str) > 0) {
		str_append(ctx->delayed_errors, "* NO ");
		str_append_str(ctx->delayed_errors, ctx->last_error_str);
		str_append(ctx->delayed_errors, "\r\n");
		str_truncate(ctx->last_error_str, 0);
	}
	str_append(ctx->last_error_str, error_string);
	ctx->last_error = error;
}

static bool
cmd_getmetadata_handle_error(struct imap_getmetadata_context *ctx)
{
	const char *error_string;
	enum mail_error error;

	error_string = imap_metadata_transaction_get_last_error(ctx->trans, &error);
	if (error == MAIL_ERROR_NOTPOSSIBLE && ctx->depth > 0) {
		/* Using DEPTH to iterate children with imap_metadata=no.
		   Don't return an error, since some of the entries could be
		   returned successfully. */
		return FALSE;
	}

	/* When the acl plugin or imapc are involved, the decision to consider
	   (iterate on) a mailbox is taken in a very distant moment from the
	   one that decides the permission ACLs getmetadata. The denial of
	   this access shows as either MAIL_ERROR_PERM or MAIL_ERROR_NOTFOUND,
	   so we simply suppress them after the fact. This happens when we
	   have a bare "l" (lookup) ACL right without another supporting ACL
	   right beside it. */
	if (ctx->iterating_boxes && (error == MAIL_ERROR_PERM ||
				     error == MAIL_ERROR_NOTFOUND))
		return TRUE;

	cmd_getmetadata_handle_error_str(ctx, error_string, error);
	return TRUE;
}

static void cmd_getmetadata_send_entry(struct imap_getmetadata_context *ctx,
				       const char *entry, bool require_reply)
{
	struct client *client = ctx->cmd->client;
	struct mail_attribute_value value;
	uoff_t value_len;
	string_t *str;

	if (imap_metadata_get_stream(ctx->trans, entry, &value) < 0) {
		if (cmd_getmetadata_handle_error(ctx))
			return;
	}

	if (value.value != NULL)
		value_len = strlen(value.value);
	else if (value.value_stream != NULL) {
		if (i_stream_get_size(value.value_stream, TRUE, &value_len) < 0) {
			e_error(client->event,
				"GETMETADATA %s: i_stream_get_size(%s) failed: %s", entry,
				i_stream_get_name(value.value_stream),
				i_stream_get_error(value.value_stream));
			i_stream_unref(&value.value_stream);
			cmd_getmetadata_handle_error_str(ctx,
				MAIL_ERRSTR_CRITICAL_MSG, MAIL_ERROR_TEMP);
			return;
		}
	} else {
		/* skip nonexistent entries */
		if (require_reply)
			cmd_getmetadata_send_nil_reply(ctx, entry);
		return;
	}

	if (value_len > ctx->maxsize) {
		/* value length is larger than specified MAXSIZE,
		   skip this entry */
		if (ctx->largest_seen_size < value_len)
			ctx->largest_seen_size = value_len;
		i_stream_unref(&value.value_stream);
		return;
	}

	str = metadata_add_entry(ctx, entry);
	if (value.value != NULL) {
		str_printfa(str, " {%"PRIuUOFF_T"}\r\n%s", value_len, value.value);
		o_stream_nsend(client->output, str_data(str), str_len(str));
	} else {
		str_printfa(str, " ~{%"PRIuUOFF_T"}\r\n", value_len);
		o_stream_nsend(client->output, str_data(str), str_len(str));

		ctx->cur_stream = i_stream_create_sized(value.value_stream, value_len);
		i_stream_unref(&value.value_stream);
	}
}

static bool
cmd_getmetadata_stream_continue(struct imap_getmetadata_context *ctx)
{
	enum ostream_send_istream_result res;

	o_stream_set_max_buffer_size(ctx->cmd->client->output, 0);
	res = o_stream_send_istream(ctx->cmd->client->output, ctx->cur_stream);
	o_stream_set_max_buffer_size(ctx->cmd->client->output, SIZE_MAX);

	switch (res) {
	case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
		return TRUE;
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
		i_unreached();
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
		return FALSE;
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
		e_error(ctx->cmd->client->event, "read(%s) failed: %s",
			i_stream_get_name(ctx->cur_stream),
			i_stream_get_error(ctx->cur_stream));
		client_disconnect(ctx->cmd->client,
				  "Internal GETMETADATA failure");
		return TRUE;
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
		/* client disconnected */
		return TRUE;
	}
	i_unreached();
}

static int
cmd_getmetadata_send_entry_tree(struct imap_getmetadata_context *ctx,
					   const char *entry)
{
	struct client *client = ctx->cmd->client;

	if (o_stream_get_buffer_used_size(client->output) >=
	    CLIENT_OUTPUT_OPTIMAL_SIZE) {
		if (o_stream_flush(client->output) <= 0) {
			o_stream_set_flush_pending(client->output, TRUE);
			return 0;
		}
	}

	if (ctx->iter != NULL) {
		const char *subentry;

		/* DEPTH iteration */
		do {
			subentry = imap_metadata_iter_next(ctx->iter);
			if (subentry == NULL) {
				/* iteration finished, get to the next entry */
				if (imap_metadata_iter_deinit(&ctx->iter) < 0) {
					if (!cmd_getmetadata_handle_error(ctx))
						i_unreached();
				}
				return -1;
			}
		} while (ctx->depth == 1 && strchr(subentry, '/') != NULL);
		entry = t_strconcat(str_c(ctx->iter_entry_prefix), subentry, NULL);
	}
	/* send NIL only on depth 0 query */
	cmd_getmetadata_send_entry(ctx, entry, ctx->depth == 0);

	if (ctx->cur_stream != NULL) {
		if (!cmd_getmetadata_stream_continue(ctx))
			return 0;
		i_stream_unref(&ctx->cur_stream);
	}

	if (ctx->iter != NULL) {
		/* already iterating the entry */
		return 1;
	} else if (ctx->depth == 0) {
		/* no iteration for the entry */
		return -1;
	} else {
		/* we just sent the entry root. iterate its children. */
		str_truncate(ctx->iter_entry_prefix, 0);
		str_append(ctx->iter_entry_prefix, entry);
		str_append_c(ctx->iter_entry_prefix, '/');

		ctx->iter = imap_metadata_iter_init(ctx->trans, entry);
		return 1;
	}
}

static void cmd_getmetadata_iter_deinit(struct imap_getmetadata_context *ctx)
{
	if (ctx->iter != NULL)
		(void)imap_metadata_iter_deinit(&ctx->iter);
	if (ctx->trans != NULL)
		(void)imap_metadata_transaction_commit(&ctx->trans, NULL, NULL);
	if (ctx->box != NULL)
		mailbox_free(&ctx->box);
	ctx->first_entry_sent = FALSE;
	ctx->entry_idx = 0;
}

static void cmd_getmetadata_deinit(struct imap_getmetadata_context *ctx)
{
	struct client_command_context *cmd = ctx->cmd;

	cmd_getmetadata_iter_deinit(ctx);
	cmd->client->output_cmd_lock = NULL;

	if (ctx->list_iter != NULL &&
	    mailbox_list_iter_deinit(&ctx->list_iter) < 0)
		client_send_list_error(cmd, cmd->client->user->namespaces->list);
	else if (ctx->last_error != 0) {
		i_assert(str_len(ctx->last_error_str) > 0);
		const char *tagline =
			imap_get_error_string(str_c(ctx->last_error_str),
					      ctx->last_error);
		client_send_tagline(cmd, tagline);
	} else if (ctx->largest_seen_size != 0) {
		client_send_tagline(cmd, t_strdup_printf(
			"OK [METADATA LONGENTRIES %"PRIuUOFF_T"] "
			"Getmetadata completed.", ctx->largest_seen_size));
	} else {
		client_send_tagline(cmd, "OK Getmetadata completed.");
	}
}

static bool cmd_getmetadata_continue(struct client_command_context *cmd)
{
	struct imap_getmetadata_context *ctx = cmd->context;
	const char *const *entries;
	unsigned int count;
	int ret;

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

	if (ctx->cur_stream != NULL) {
		if (!cmd_getmetadata_stream_continue(ctx))
			return FALSE;
		i_stream_unref(&ctx->cur_stream);
	}

	entries = array_get(&ctx->entries, &count);
	for (; ctx->entry_idx < count; ctx->entry_idx++) {
		do {
			T_BEGIN {
				ret = cmd_getmetadata_send_entry_tree(ctx, entries[ctx->entry_idx]);
			} T_END;
			if (ret == 0)
				return FALSE;
		} while (ret > 0);
	}
	if (ctx->first_entry_sent)
		o_stream_nsend_str(cmd->client->output, ")\r\n");

	if (str_len(ctx->delayed_errors) > 0) {
		o_stream_nsend(cmd->client->output,
			       str_data(ctx->delayed_errors),
			       str_len(ctx->delayed_errors));
		str_truncate(ctx->delayed_errors, 0);
	}

	cmd_getmetadata_iter_deinit(ctx);
	if (ctx->list_iter != NULL)
		return cmd_getmetadata_mailbox_iter_next(ctx);
	cmd_getmetadata_deinit(ctx);
	return TRUE;
}

static bool
cmd_getmetadata_start(struct imap_getmetadata_context *ctx)
{
	struct client_command_context *cmd = ctx->cmd;

	if (ctx->depth > 0)
		ctx->iter_entry_prefix = str_new(cmd->pool, 128);
	imap_metadata_transaction_validated_only(ctx->trans,
		!cmd->client->set->imap_metadata);

	if (!cmd_getmetadata_continue(cmd)) {
		cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
		cmd->func = cmd_getmetadata_continue;
		return FALSE;
	}
	return TRUE;
}

static bool
cmd_getmetadata_server(struct imap_getmetadata_context *ctx)
{
	ctx->trans = imap_metadata_transaction_begin_server(ctx->cmd->client->user);
	return cmd_getmetadata_start(ctx);
}

static int
cmd_getmetadata_try_mailbox(struct imap_getmetadata_context *ctx,
			    struct mail_namespace *ns, const char *mailbox)
{
	ctx->box = mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_READONLY);
	event_add_str(ctx->cmd->global_event, "mailbox",
		      mailbox_get_vname(ctx->box));

	enum mailbox_existence existence;
	if (mailbox_exists(ctx->box, TRUE, &existence) < 0) {
		return -1;
	} 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);
		return -1;
	}

	ctx->trans = imap_metadata_transaction_begin(ctx->box);
	return cmd_getmetadata_start(ctx) ? 1 : 0;
}

static bool
cmd_getmetadata_mailbox(struct imap_getmetadata_context *ctx,
			struct mail_namespace *ns, const char *mailbox)
{
	int ret;

	ret = cmd_getmetadata_try_mailbox(ctx, ns, mailbox);
	if (ret < 0) {
		client_send_box_error(ctx->cmd, ctx->box);
		mailbox_free(&ctx->box);
	}
	return ret != 0;
}

static bool
cmd_getmetadata_mailbox_iter_next(struct imap_getmetadata_context *ctx)
{
	const struct mailbox_info *info;
	int ret;

	while ((info = mailbox_list_iter_next(ctx->list_iter)) != NULL) {
		if ((info->flags & (MAILBOX_NOSELECT | MAILBOX_NONEXISTENT)) != 0)
			continue;
		ret = cmd_getmetadata_try_mailbox(ctx, info->ns, info->vname);
		if (ret > 0) {
			/* we'll already recursively went through
			   all the mailboxes (FIXME: ugly and potentially
			   stack consuming) */
			return TRUE;
		} else if (ret == 0) {
			/* need to send more data later */
			return FALSE;
		}
		T_BEGIN {
			client_send_line(ctx->cmd->client, t_strdup_printf(
				"* NO Failed to open mailbox %s: %s",
				info->vname, mailbox_get_last_error(ctx->box, NULL)));
		} T_END;
		mailbox_free(&ctx->box);
	}
	cmd_getmetadata_deinit(ctx);
	return TRUE;
}

bool cmd_getmetadata(struct client_command_context *cmd)
{
	struct imap_getmetadata_context *ctx;
	struct mail_namespace *ns;
	const struct imap_arg *args, *options, *entries;
	const char *mailbox, *entry_name;

	if (!client_read_args(cmd, 0, 0, &args))
		return FALSE;

	ctx = p_new(cmd->pool, struct imap_getmetadata_context, 1);
	ctx->cmd = cmd;
	ctx->maxsize = (uint32_t)-1;
	ctx->cmd->context = ctx;
	ctx->delayed_errors = str_new(cmd->pool, 128);
	ctx->last_error_str = str_new(cmd->pool, 128);

	if (imap_arg_get_list(&args[0], &options)) {
		if (!cmd_getmetadata_parse_options(ctx, options))
			return TRUE;
		args++;
	}
	if (!imap_arg_get_astring(&args[0], &mailbox)) {
		client_send_command_error(cmd, "Invalid arguments.");
		return TRUE;
	}
	if (!imap_arg_get_list(&args[1], &entries)) {
		if (!imap_arg_get_astring(&args[1], &entry_name) ||
		    !IMAP_ARG_IS_EOL(&args[2])) {
			client_send_command_error(cmd, "Invalid arguments.");
			return TRUE;
		}
		entries = args+1;
	}
	if (!imap_metadata_parse_entry_names(ctx, entries))
		return TRUE;

	if (mailbox[0] == '\0') {
		/* server attribute */
		return cmd_getmetadata_server(ctx);
	} else if (strchr(mailbox, '*') == NULL &&
		   strchr(mailbox, '%') == NULL) {
		/* mailbox attribute */
		ns = client_find_namespace(cmd, &mailbox);
		if (ns == NULL)
			return TRUE;
		return cmd_getmetadata_mailbox(ctx, ns, mailbox);
	} else {
		/* wildcards in mailbox name. this isn't supported by RFC 5464,
		   but it was in the earlier drafts and is already used by
		   some software (Horde). */
		const char *patterns[2];
		patterns[0] = mailbox; patterns[1] = NULL;

		ctx->iterating_boxes = TRUE;
		ctx->list_iter =
			mailbox_list_iter_init_namespaces(
				cmd->client->user->namespaces,
				patterns, MAIL_NAMESPACE_TYPE_MASK_ALL, 0);
		return cmd_getmetadata_mailbox_iter_next(ctx);
	}
}
