/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "imap-common.h"
#include "array.h"
#include "buffer.h"
#include "str.h"
#include "str-sanitize.h"
#include "imap-resp-code.h"
#include "imap-parser.h"
#include "imap-sync.h"
#include "imap-utf7.h"
#include "imap-util.h"
#include "mail-storage.h"
#include "mail-namespace.h"
#include "imap-commands-util.h"

struct mail_namespace *
client_find_namespace_full(struct client_command_context *cmd,
			   const char **mailbox, const char **client_error_r)
{
	struct client *client = cmd->client;
	struct mail_namespace *namespaces = client->user->namespaces;
	struct mail_namespace *ns;
	string_t *utf8_name;

	utf8_name = t_str_new(64);
	if (cmd->utf8) {
		if (!uni_utf8_str_is_valid(*mailbox)) {
			*client_error_r = "NO Mailbox name is not valid UTF-8";
			return NULL;
		}
		str_append(utf8_name, *mailbox);
	} else if (imap_utf7_to_utf8(*mailbox, utf8_name) < 0) {
		*client_error_r = "NO Mailbox name is not valid mUTF-7";
		return NULL;
	}

	ns = mail_namespace_find(namespaces, str_c(utf8_name));
	if ((ns->flags & NAMESPACE_FLAG_AUTOCREATED) != 0 &&
	    ns->prefix_len == 0) {
		/* this matched only the autocreated prefix="" namespace.
		   give a nice human-readable error message */
		*client_error_r = t_strdup_printf(
			"NO Client tried to access nonexistent namespace. "
			"(Mailbox name should probably be prefixed with: %s)",
			mail_namespace_find_inbox(namespaces)->prefix);
		return NULL;
	}

	if ((client->set->parsed_workarounds &
	     		WORKAROUND_TB_EXTRA_MAILBOX_SEP) != 0 &&
	    str_len(utf8_name) > 0 &&
	    str_c(utf8_name)[str_len(utf8_name)-1] == mail_namespace_get_sep(ns)) {
		/* drop the extra trailing hierarchy separator */
		str_truncate(utf8_name, str_len(utf8_name)-1);
	}

	*mailbox = str_c(utf8_name);
	return ns;
}

struct mail_namespace *
client_find_namespace(struct client_command_context *cmd, const char **mailbox)
{
	struct mail_namespace *ns;
	const char *client_error;

	ns = client_find_namespace_full(cmd, mailbox, &client_error);
	if (ns == NULL)
		client_send_tagline(cmd, client_error);
	return ns;
}

bool client_verify_open_mailbox(struct client_command_context *cmd)
{
	if (cmd->client->mailbox != NULL) {
		event_add_str(cmd->global_event, "mailbox",
			      mailbox_get_vname(cmd->client->mailbox));
		return TRUE;
	} else {
		client_send_tagline(cmd, "BAD No mailbox selected.");
		return FALSE;
	}
}

void imap_client_close_mailbox(struct client *client)
{
	struct mailbox *box;

	i_assert(client->mailbox != NULL);

	if (array_is_created(&client->fetch_failed_uids))
		array_clear(&client->fetch_failed_uids);
	client_search_updates_free(client);
	array_free(&client->search_saved_uidset);

	box = client->mailbox;
	client->mailbox = NULL;

	mailbox_free(&box);
	client_update_mailbox_flags(client, NULL);
}

int client_open_save_dest_box(struct client_command_context *cmd,
			      const char *name, struct mailbox **destbox_r)
{
	struct mail_namespace *ns;
	struct mailbox *box;
	const char *error_string;
	enum mail_error error;

	ns = client_find_namespace(cmd, &name);
	if (ns == NULL)
		return -1;

	if (cmd->client->mailbox != NULL &&
	    mailbox_equals(cmd->client->mailbox, ns, name)) {
		*destbox_r = cmd->client->mailbox;
		return 0;
	}
	box = mailbox_alloc(ns->list, name, MAILBOX_FLAG_SAVEONLY);
	if (mailbox_open(box) < 0) {
		error_string = mailbox_get_last_error(box, &error);
		if (error == MAIL_ERROR_NOTFOUND) {
			client_send_tagline(cmd,  t_strdup_printf(
				"NO [TRYCREATE] %s", error_string));
		} else {
			client_send_box_error(cmd, box);
		}
		mailbox_free(&box);
		return -1;
	}
	if (mailbox_enable(box, client_enabled_mailbox_features(cmd->client)) < 0) {
		client_send_box_error(cmd, box);
		mailbox_free(&box);
		return -1;
	}
	*destbox_r = box;
	return 0;
}

const char *imap_client_command_get_reason(struct client_command_context *cmd)
{
	return cmd->args[0] == '\0' ? cmd->name :
		t_strdup_printf("%s %s", cmd->name, cmd->human_args);
}

const char *
imap_get_error_string(const char *error_string, enum mail_error error)
{
	const char *resp_code = NULL;

	switch (error) {
	case MAIL_ERROR_NONE:
		break;
	case MAIL_ERROR_TEMP:
	case MAIL_ERROR_LOOKUP_ABORTED: /* BUG: shouldn't be visible here */
		resp_code = IMAP_RESP_CODE_SERVERBUG;
		break;
	case MAIL_ERROR_UNAVAILABLE:
		resp_code = IMAP_RESP_CODE_UNAVAILABLE;
		break;
	case MAIL_ERROR_NOTPOSSIBLE:
	case MAIL_ERROR_PARAMS:
		resp_code = IMAP_RESP_CODE_CANNOT;
		break;
	case MAIL_ERROR_PERM:
		resp_code = IMAP_RESP_CODE_NOPERM;
		break;
	case MAIL_ERROR_NOQUOTA:
		resp_code = IMAP_RESP_CODE_OVERQUOTA;
		break;
	case MAIL_ERROR_NOTFOUND:
		resp_code = IMAP_RESP_CODE_NONEXISTENT;
		break;
	case MAIL_ERROR_EXISTS:
		resp_code = IMAP_RESP_CODE_ALREADYEXISTS;
		break;
	case MAIL_ERROR_EXPUNGED:
		resp_code = IMAP_RESP_CODE_EXPUNGEISSUED;
		break;
	case MAIL_ERROR_INUSE:
		resp_code = IMAP_RESP_CODE_INUSE;
		break;
	case MAIL_ERROR_CONVERSION:
	case MAIL_ERROR_INVALIDDATA:
		break;
	case MAIL_ERROR_LIMIT:
		resp_code = IMAP_RESP_CODE_LIMIT;
		break;
	case MAIL_ERROR_TOOBIG:
		resp_code = IMAP_RESP_CODE_TOOBIG;
		break;
	case MAIL_ERROR_INTERRUPTED:
		break;
	}
	if (resp_code == NULL || *error_string == '[')
		return t_strconcat("NO ", error_string, NULL);
	else
		return t_strdup_printf("NO [%s] %s", resp_code, error_string);
}

void client_send_error(struct client_command_context *cmd,
		       const char *error_string, enum mail_error error)
{
	client_send_tagline(cmd, imap_get_error_string(error_string, error));
	client_disconnect_if_inconsistent(cmd->client);
}

void client_send_list_error(struct client_command_context *cmd,
			    struct mailbox_list *list)
{
	const char *error_string;
	enum mail_error error;

	error_string = mailbox_list_get_last_error(list, &error);
	client_send_tagline(cmd, imap_get_error_string(error_string, error));
}

void client_disconnect_if_inconsistent(struct client *client)
{
	if (client->mailbox != NULL &&
	    mailbox_is_inconsistent(client->mailbox)) {
		/* we can't do forced CLOSE, so have to disconnect */
		client_disconnect_with_error(client,
			"IMAP session state is inconsistent, please relogin.");
	}
}

void client_send_box_error(struct client_command_context *cmd,
			   struct mailbox *box)
{
	client_send_storage_error(cmd, mailbox_get_storage(box));
}

void client_send_storage_error(struct client_command_context *cmd,
			       struct mail_storage *storage)
{
	const char *error_string;
	enum mail_error error;

	error_string = mail_storage_get_last_error(storage, &error);
	client_send_error(cmd, error_string, error);
}

void client_send_untagged_storage_error(struct client *client,
					struct mail_storage *storage)
{
	const char *error_string;
	enum mail_error error;

	error_string = mail_storage_get_last_error(storage, &error);
	client_send_line(client, t_strconcat("* NO ", error_string, NULL));

	client_disconnect_if_inconsistent(client);
}

bool client_parse_mail_flags(struct client_command_context *cmd,
			     const struct imap_arg *args,
			     enum mail_flags *flags_r,
			     const char *const **keywords_r)
{
	const char *atom;
	enum mail_flags flag;
	ARRAY(const char *) keywords;

	*flags_r = 0;
	*keywords_r = NULL;
	p_array_init(&keywords, cmd->pool, 16);

	while (!IMAP_ARG_IS_EOL(args)) {
		if (!imap_arg_get_atom(args, &atom)) {
			client_send_command_error(cmd,
				"Flags list contains non-atoms.");
			return FALSE;
		}

		if (*atom == '\\') {
			/* system flag */
			atom = t_str_ucase(atom);
			flag = imap_parse_system_flag(atom);
			if (flag != 0 && flag != MAIL_RECENT)
				*flags_r |= flag;
			else {
				client_send_command_error(cmd, t_strconcat(
					"Invalid system flag ", atom, NULL));
				return FALSE;
			}
		} else {
			/* keyword validity checks are done by lib-storage */
			array_push_back(&keywords, &atom);
		}

		args++;
	}

	if (array_count(&keywords) == 0)
		*keywords_r = NULL;
	else {
		array_append_zero(&keywords); /* NULL-terminate */
		*keywords_r = array_front(&keywords);
	}
	return TRUE;
}

void client_send_mailbox_flags(struct client *client, bool selecting)
{
	struct mailbox_status status;
	unsigned int count = array_count(client->keywords.names);
	const char *const *keywords;
	string_t *str;

	if (!selecting && count == client->keywords.announce_count) {
		/* no changes to keywords and we're not selecting a mailbox */
		return;
	}

	client->keywords.announce_count = count;
	mailbox_get_open_status(client->mailbox, STATUS_PERMANENT_FLAGS,
				&status);

	keywords = count == 0 ? NULL :
		array_front(client->keywords.names);
	str = t_str_new(128);
	str_append(str, "* FLAGS (");
	imap_write_flags(str, status.flags, keywords);
	str_append_c(str, ')');
	client_send_line(client, str_c(str));

	bool readonly = mailbox_is_readonly(client->mailbox);
	if (!selecting && (status.allow_new_keywords || readonly)) {
		/* If not in a SELEXT/EXAMINE command, update is optional.

		   If the mailbox is readonly, nothing can really change,
		   so no update is needed.

		   Also, if '*' is present, all possibly new keywords
		   are already included in that, so there is no need to
		   update either. */
	 	return;
	}

	if (!status.permanent_keywords)
		keywords = NULL;

	str_truncate(str, 0);
	str_append(str, "* OK [PERMANENTFLAGS (");
	imap_write_flags(str, status.permanent_flags, keywords);
	if (status.allow_new_keywords) {
		if (status.permanent_flags != 0 || keywords != NULL)
			str_append_c(str, ' ');
		str_append(str, "\\*");
	}
	str_append(str, ")] ");

	if (readonly)
		str_append(str, "Read-only mailbox.");
	else
		str_append(str, "Flags permitted.");
	client_send_line(client, str_c(str));
}

void client_update_mailbox_flags(struct client *client,
				 const ARRAY_TYPE(keywords) *keywords)
{
	client->keywords.names = keywords;
	client->keywords.announce_count = 0;
}

const char *const *
client_get_keyword_names(struct client *client, ARRAY_TYPE(keywords) *dest,
			 const ARRAY_TYPE(keyword_indexes) *src)
{
	unsigned int kw_index;
	const char *const *all_names;
	unsigned int all_count;

	client_send_mailbox_flags(client, FALSE);

	/* convert indexes to names */
	all_names = array_get(client->keywords.names, &all_count);
	array_clear(dest);
	array_foreach_elem(src, kw_index) {
		i_assert(kw_index < all_count);
		array_push_back(dest, &all_names[kw_index]);
	}

	array_append_zero(dest);
	return array_front(dest);
}

void msgset_generator_init(struct msgset_generator_context *ctx, string_t *str)
{
	i_zero(ctx);
	ctx->str = str;
	ctx->last_uid = (uint32_t)-1;
}

void msgset_generator_next(struct msgset_generator_context *ctx, uint32_t uid)
{
	i_assert(uid > 0);

	if (uid-1 != ctx->last_uid) {
		if (ctx->first_uid == 0)
			;
		else if (ctx->first_uid == ctx->last_uid)
			str_printfa(ctx->str, "%u,", ctx->first_uid);
		else {
			str_printfa(ctx->str, "%u:%u,",
				    ctx->first_uid, ctx->last_uid);
		}
		ctx->first_uid = uid;
	}
	ctx->last_uid = uid;
}

void msgset_generator_finish(struct msgset_generator_context *ctx)
{
	if (ctx->first_uid == ctx->last_uid)
		str_printfa(ctx->str, "%u", ctx->first_uid);
	else
		str_printfa(ctx->str, "%u:%u", ctx->first_uid, ctx->last_uid);
}
