/*
	Copyright (c) 2010 by Dennis Schridde

	This file is part of dovecot-metadata.

	dovecot-metadata is free software: you can redistribute it and/or modify
	it under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	dovecot-metadata is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Lesser General Public License for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with dovecot-metadata.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "metadata-global.h"

#include "imap-common.h"
#include "imap-client.h"
#include "imap-quote.h"
#include "imap-utf7.h"
#if DOVECOT_PREREQ(2,2)
#include "mailbox-list-iter.h"
#endif

#include <stdbool.h>
#include <stdlib.h>

#include "str-ext.h"
#include "imap-arg-ext.h"
#include "dict-ext.h"
#include "metadata-entry.h"
#include "metadata-backend.h"
#include "metadata-mail-user-module-private.h"

/* The IMAP Metadata plugin is an implementation of RFC 5464 */

#ifdef DOVECOT_ABI_VERSION
const char *imap_metadata_plugin_version = DOVECOT_ABI_VERSION;
#else
const char *imap_metadata_plugin_version = DOVECOT_VERSION;
#endif
const char *imap_metadata_plugin_dependencies[] = { "metadata", NULL };

static struct module *imap_metadata_module;
static void (*next_hook_client_created)(struct client **client);


static const char *
entry_scopes[ENTRY_SCOPE_MAX] = {
	"private/", /* ENTRY_SCOPE_PRIVATE */
	"shared/" /* ENTRY_SCOPE_SHARED */
};

static const char *
entry_types[ENTRY_TYPE_MAX] = {
	"vendor/", /* ENTRY_TYPE_VENDOR */
	"", /* ENTRY_TYPE_RFC */
};

static const char **
entry_subtypes_rfc[ENTRY_SUBJECT_MAX] = {
	(const char*[]){ // server
		"comment",
		"admin",
		NULL
	},
	(const char*[]){ // mailbox
		"comment",
		NULL
	}
};


enum getmetadata_option {
	GETMETADATA_OPTION_MAXSIZE,
	GETMEDADATA_OPTION_DEPTH
};

struct option_definition {
	const char *name;
	int num_values;
	enum getmetadata_option option;
};

static
struct option_definition
getmetadata_options[] = {
	{"maxsize", 1, GETMETADATA_OPTION_MAXSIZE},
	{"depth", 1, GETMEDADATA_OPTION_DEPTH},
	{NULL, 0, 0}
};


static
struct option_definition *
parse_getmetadata_option(const char *option) {
	struct option_definition *optdef = getmetadata_options;
	while (optdef->name != NULL) {
		if (strcasecmp(optdef->name, option) == 0) {
			return optdef;
		}
		optdef++;
	}
	return NULL;
}


static int
parse_getmetadata_depth(const char *value) {
	if (!str_is_numeric(value, '\0')) {
		if (strcasecmp(value, "infinity"))
			return METADATA_ITERATE_DEPTH_INF;

		return -1;
	}

	char *end = NULL;
	long int val = strtol(value, &end, 10);

	if (end == value)
		return -2;
	if (val == LONG_MAX)
		return -3;
	if (val < 0)
		return -4;

	return val;
}


static int
parse_getmetadata_maxsize(const char *value) {
	if (!str_is_numeric(value, '\0'))
		return -1;

	char *end = NULL;
	long int val = strtol(value, &end, 10);

	if (end == value)
		return -2;
	if (val == LONG_MAX)
		return -3;
	if (val < 0)
		return -4;

	return val;
}


/* validates that the whole entry name conforms to section 3.2 of the RFC */
static ATTR_NONNULL(1)
bool
is_valid_rfc5464_entry_name(const char *name) {
	const char *lastslash = NULL;

	if (name == NULL || *name != '/') {
		return false;
	}

	for (const char *c = name; *c != '\0'; c++) {
		// Must not be a command character
		if (*c >= 0x00 && *c <= 0x19) {
			return false;
		}
		// Must be ASCII
		else if (*c > 0x7f) {
			return false;
		}

		switch (*c) {
			case '/':
				// Two consecutive slashes, or a slash at the end are an error
				if (lastslash == c-1 || *(c+1) == '\0') {
					return false;
				}
				lastslash = c;
				break;
			case '*':
			case '%':
				return false;
			default:
				break;
		}
	}

	return true;
}


/* validates that the part after /vendor conforms to section 3.2 of the RFC */
static ATTR_NONNULL(1)
bool
is_valid_rfc5464_vendor_name(const char *name) {
	int num_components = 3; // "vendor/" already includes the slash of component No3

	for (const char *c = name; *c != '\0'; c++) {
		if (*c == '/') {
			num_components++;
		}
	}

	return num_components >= 4;
}


/* validates that a non-vendor entry conforms to section 3.2 of the RFC */
static ATTR_NONNULL(1)
bool
is_valid_rfc5464_subtype_name(const char *name, enum metadata_entry_subject subject) {
	bool found_subtype = false;

	i_assert(subject > 0 && subject < ENTRY_SUBJECT_MAX);

	for (const char **subtype = entry_subtypes_rfc[subject]; *subtype != NULL; subtype++) {
		size_t subtype_len = strlen(*subtype);

		if (strncasecmp(name, *subtype, subtype_len) == 0
			&& name[subtype_len] == '\0') {
			found_subtype = true;
		}
	}

	return found_subtype;
}


/* sets entry->scope and returns remaining string */
static ATTR_NONNULL(1)
enum metadata_entry_scope
parse_entry_scope(const char **name) {
	for (int scope = 0; scope < ENTRY_SCOPE_MAX; scope++) {
		size_t scope_len = strlen(entry_scopes[scope]);

		if (strncasecmp(*name, entry_scopes[scope], scope_len) == 0) {
			*name += scope_len;
			return scope;
		}
	}

	return ENTRY_SCOPE_MAX;
}


/* sets entry->type and returns remaining string */
static ATTR_NONNULL(1)
enum metadata_entry_type
parse_entry_type(const char **name, enum metadata_entry_subject subject) {
	for (int type = 0; type < ENTRY_TYPE_MAX; type++) {
		size_t type_len = strlen(entry_types[type]);

		if (strncasecmp(*name, entry_types[type], type_len) == 0) {
			*name += type_len;

			switch (type) {
				case ENTRY_TYPE_RFC:
					if (!is_valid_rfc5464_subtype_name(*name, subject))
						return ENTRY_TYPE_MAX;
					break;
				case ENTRY_TYPE_VENDOR:
					if (!is_valid_rfc5464_vendor_name(*name))
						return ENTRY_TYPE_MAX;
					break;
			}

			return type;
		}
	}

	return ENTRY_TYPE_MAX;
}


/* fill entry with data parsed from entry->full_name */
static ATTR_NONNULL(2)
struct metadata_entry *
parse_entry(struct mailbox *box, const char *name, const char *value) {
	if (!is_valid_rfc5464_entry_name(name)) {
		return NULL;
	}

	// Skip leading '/'
	const char *name_tmp = name+1;

	enum metadata_entry_scope scope = parse_entry_scope(&name_tmp);
	if (scope >= ENTRY_SCOPE_MAX)
		return NULL;

	enum metadata_entry_type type = parse_entry_type(&name_tmp, box ? ENTRY_SUBJECT_MAILBOX : ENTRY_SUBJECT_SERVER);
	if (type >= ENTRY_TYPE_MAX)
		return NULL;

	return metadata_entry_alloc(box, name, value);
}


static int
get_and_send_entry(struct client_command_context *cmd, struct mailbox *box, const char *name, int depth, int maxsize, int *longentries) {
	if (strlen(name) == 0) {
		client_send_command_error(cmd, "Entry name must not be empty. (RFC 5464 Section 3.2)");
		return -1;
	}

	if (str_has_wildcards(name)) {
		const char *estr = t_strdup_printf("Wildcards in entry name '%s' not allowed. (RFC 5464 Section 3.2)", name);
		client_send_command_error(cmd, estr);
		return -1;
	}

	if (depth == 0) {
		struct metadata_entry *entry = parse_entry(box, name, NULL);
		if (entry == NULL) {
			const char *estr = t_strdup_printf("Invalid entry name '%s'. (RFC 5464 Section 3.2)", name);
			client_send_command_error(cmd, estr);
			return -1;
		}

		int success = metadata_get_entry(entry, cmd->client->user);
		if (success < 0) {
			i_assert(0);
			client_send_tagline(cmd, "NO Getting entry failed.");
			return -1;
		}
		else if (success > 0) {
			string_t *str = t_str_new(128);
			str_append(str, "* METADATA ");

			const char *mailbox_name = mailbox_get_vname(box);
			string_t *mailbox_name_utf7 = NULL;
			if (imap_utf8_to_utf7(mailbox_name, mailbox_name_utf7) < 0) {
				i_panic("Mailbox name not UTF-8: %s", name);
				return -1;
			}

#if DOVECOT_PREREQ(2,2)
			imap_append_string(str, str_c(mailbox_name_utf7));
#else
			imap_quote_append_string(str, str_c(mailbox_name_utf7), false);
#endif

			const char *name = metadata_entry_get_name(entry);
			const char *value = metadata_entry_get_value(entry);
			if (value == NULL) {
				value = "NIL";
			}

			str_append(str, " (");
#if DOVECOT_PREREQ(2,2)
			imap_append_string(str, name);
#else
			imap_quote_append_string(str, name, false);
#endif
			str_append(str, " ");
#if DOVECOT_PREREQ(2,2)
			imap_append_string(str, value);
#else
			imap_quote_append_string(str, value, false);
#endif
			str_append(str, ")");

#if DOVECOT_PREREQ(2,2)
			return client_send_line_next(cmd->client, str_c(str));
#else
			return client_send_line(cmd->client, str_c(str));
#endif
		}

		return 0;
	}

	string_t *str = t_str_new(128);
	str_append(str, "* METADATA ");

	const char *mailbox_name = mailbox_get_vname(box);
	string_t *mailbox_name_utf7 = t_str_new(64);
	if (imap_utf8_to_utf7(mailbox_name, mailbox_name_utf7) < 0) {
		i_panic("Mailbox name not UTF-8: %s", name);
		return -1;
	}

#if DOVECOT_PREREQ(2,2)
	imap_append_string(str, str_c(mailbox_name_utf7));
#else
	imap_quote_append_string(str, str_c(mailbox_name_utf7), false);
#endif

	int num_entries = 0;

	struct metadata_entry *entry = metadata_entry_alloc(box, name, NULL);
	if (entry == NULL) {
		const char *estr = t_strdup_printf("Invalid entry name '%s'. (RFC 5464 Section 3.2)", name);
		client_send_command_error(cmd, estr);
		return -1;
	}

	str_append(str, " (");

	struct metadata_iterate_context *iter = metadata_iterate_init(box, entry, depth);
	while (metadata_iterate(iter, entry)) {
		const char *name = metadata_entry_get_name(entry);
		const char *value = metadata_entry_get_value(entry);
		if (value == NULL) {
			value = "NIL";
		}

		/* only respect maxsize if it is not 'undefined' */
		if (maxsize > 0) {
			size_t val_len = strlen(value);
			if (val_len > maxsize && val_len > *longentries) {
				*longentries = val_len;
				continue;
			}
		}

#if DOVECOT_PREREQ(2,2)
		imap_append_string(str, name);
#else
		imap_quote_append_string(str, name, false);
#endif
		str_append(str, " ");
#if DOVECOT_PREREQ(2,2)
		imap_append_string(str, name);
#else
		imap_quote_append_string(str, name, false);
#endif

		num_entries++;
	}
	if (metadata_iterate_deinit(&iter) < 0) {
		client_send_tagline(cmd, "NO Iterating metadata failed.");
		return -1;
	}

	str_append(str, ")");

	if (num_entries > 0) {
#if DOVECOT_PREREQ(2,2)
		return client_send_line_next(cmd->client, str_c(str));
#else
		return client_send_line(cmd->client, str_c(str));
#endif
	}

	return 0;
}


static bool
cmd_getmetadata(struct client_command_context *cmd) {
	int maxsize = 0, depth = 0;

	const struct imap_arg *args;
	if (!client_read_args(cmd, 0, 0, &args))
		return false;

	if (args[0].type == IMAP_ARG_LIST) { // options
		const struct imap_arg *arglist = NULL;
		if (!imap_arg_get_list(&args[0], &arglist)) {
			client_send_command_error(cmd, "Cannot read options - list expected. (RFC 5464 Section 4.2)");
			return true;
		}

		while (!IMAP_ARG_IS_EOL(arglist)) {
			const char *option = NULL;
			if (!imap_arg_get_astring(arglist, &option)){
				client_send_command_error(cmd, "Cannot read option - string expected. (RFC 5464 Section 4.2)");
				return true;
			}

			struct option_definition *optdef = parse_getmetadata_option(option);
			if (optdef == NULL) {
				const char *estr = t_strdup_printf("Unknown option: '%s'. (RFC 5464 Section 4.2)", option);
				client_send_command_error(cmd, estr);
				return true;
			}

			arglist++;

			const char *values[optdef->num_values];
			memset(values, 0, sizeof(*values) * optdef->num_values);

			for (int i = 0; i < optdef->num_values; i++) {
				if (!imap_arg_get_astring(&arglist[i], &values[i])){
					const char *estr = t_strdup_printf(
						"Cannot read value %d/%d of '%s' - string expected. (RFC 5464 Section 4.2)",
						i, optdef->num_values, option
					);
					client_send_command_error(cmd, estr);
					return true;
				}
			}

			switch (optdef->option) {
				case GETMEDADATA_OPTION_DEPTH:
					depth = parse_getmetadata_depth(values[0]);
					if (depth < 0) {
						client_send_command_error(cmd, "Value 1/1 of DEPTH is not numeric and positive or \"infinity\". (RFC 5464 Section 4.2.2)");
						return true;
					}
					break;
				case GETMETADATA_OPTION_MAXSIZE:
					maxsize = parse_getmetadata_maxsize(values[0]);
					if (maxsize < 0) {
						client_send_command_error(cmd, "Value 1/1 of MAXSIZE is not numeric and positive. (RFC 5464 Section 4.2.1)");
						return true;
					}
					break;
			}

			arglist += optdef->num_values;
		}

		args++;
	}

	const char *mailbox_name = NULL;
	if (!imap_arg_get_astring(&args[0], &mailbox_name)){
		client_send_command_error(cmd, "Cannot read mailbox name - string expected. (RFC 5464 Section 4.2)");
		return true;
	}

	if (mailbox_name == NULL) {
		client_send_tagline(cmd, "NO Mailbox name is NULL.");
		return true;
	}

	string_t *mailbox_name_utf8 = t_str_new(strlen(mailbox_name));
	if (imap_utf7_to_utf8(mailbox_name, mailbox_name_utf8) == 0) {
		mailbox_name = str_c(mailbox_name_utf8);
	}

	const char **entry_names = NULL;
	if (!imap_arg_get_astringlist(&args[1], &entry_names)) {
		client_send_command_error(cmd, "Cannot read entries - string or list of strings expected. (RFC 5464 Section 4.2)");
		return true;
	}

	int warn_longentries = 0;

	if (str_has_wildcards(mailbox_name)) {
		for (const struct mail_namespace *ns = cmd->client->user->namespaces; ns != NULL; ns = ns->next) {
			const struct mailbox_info *info = NULL;

			struct mailbox_list_iterate_context *ctx = mailbox_list_iter_init(ns->list, mailbox_name, 0);
			while ((info = mailbox_list_iter_next(ctx)) != NULL) {
#if DOVECOT_PREREQ(2,2)
				i_debug("metadata: Getting info for mailbox '%s'", info->vname);

				struct mailbox *box = mailbox_alloc(ns->list, info->vname, MAILBOX_FLAG_READONLY);
#else
				i_debug("metadata: Getting info for mailbox '%s'", info->name);

				struct mailbox *box = mailbox_alloc(ns->list, info->name, MAILBOX_FLAG_READONLY);
#endif
				if (box == NULL) {
					client_send_tagline(cmd, "NO Allocating mailbox failed.");
					return true;
				}

				enum mailbox_existence existence;
				if (mailbox_exists(box, false, &existence) < 0) {
					client_send_tagline(cmd, "NO Checking mailbox existence failed.");
					return true;
				}

				if (existence == MAILBOX_EXISTENCE_NONE) {
					client_send_tagline(cmd, "NO Mailbox does not exist.");
					return true;
				}

				const char **entry_name = entry_names;
				while (*entry_name != NULL) {
					if (get_and_send_entry(cmd, box, *entry_name, depth, maxsize, &warn_longentries) < 0) {
						/* get_and_send_entry outputs the response for the client, already */
						mailbox_free(&box);
						return true;
					}

					entry_name++;
				}

				mailbox_free(&box);
			}

			if (mailbox_list_iter_deinit(&ctx) < 0) {
				client_send_tagline(cmd, "NO Iterating mailboxes failed.");
			}
		}
	}
	else {
		struct mailbox *box = NULL;
		/* empty mailbox_name -> box=NULL -> server scope */
		if (*mailbox_name != '\0') {
#if DOVECOT_PREREQ(2,1)
			struct mail_namespace *ns = mail_namespace_find(cmd->client->user->namespaces, mailbox_name);
#else
			struct mail_namespace *ns = mail_namespace_find(cmd->client->user->namespaces, &mailbox_name);
#endif
			if (ns == NULL) {
				client_send_tagline(cmd, "NO Mailbox not found.");
				return true;
			}

			box = mailbox_alloc(ns->list, mailbox_name, MAILBOX_FLAG_READONLY);
			if (box == NULL) {
				client_send_tagline(cmd, "NO Allocating mailbox failed.");
				return true;
			}

			enum mailbox_existence existence;
			if (mailbox_exists(box, false, &existence) < 0) {
				client_send_tagline(cmd, "NO Checking mailbox existence failed.");
				return true;
			}

			if (existence == MAILBOX_EXISTENCE_NONE) {
				client_send_tagline(cmd, "NO Mailbox does not exist.");
				return true;
			}
		}

		/* FIXME Enforce RFC 5464 Section 3.3 - Private versus Shared and Access Control! */

		const char **entry_name = entry_names;
		while (*entry_name != NULL) {
			if (get_and_send_entry(cmd, box, *entry_name, depth, maxsize, &warn_longentries) < 0) {
				/* get_and_send_entry outputs the response for the client, already */
				if (box != NULL) {
					/* mailbox_free calls mailbox_close, which cannot deal with NULL boxes */
					mailbox_free(&box);
				}
				return true;
			}

			entry_name++;
		}

		if (box != NULL) {
			/* mailbox_free calls mailbox_close, which cannot deal with NULL boxes */
			mailbox_free(&box);
		}
	}

	free(entry_names);

	const char *response;
	if (warn_longentries > 0) {
		response = t_strdup_printf("OK [METADATA LONGENTRIES %d] Completed.", warn_longentries);
	}
	else {
		response = "OK Completed.";
	}
	client_send_tagline(cmd, response);

	return true;
}


static void
setmetadata_helper(struct client_command_context *cmd, const struct imap_arg *args, struct mailbox *box) {
	bool warn_maxsize = false, warn_toomany = false, warn_noprivate = false;

	const struct imap_arg *arglist = NULL;
	if (!imap_arg_get_list(&args[1], &arglist)) {
		client_send_command_error(cmd, "Cannot read entries - list expected. (RFC 5464 Section 4.3)");
		return;
	}

	while (!IMAP_ARG_IS_EOL(&arglist[0])) {
		const char *name = NULL;
		if (!imap_arg_get_astring(&arglist[0], &name)){
			client_send_command_error(cmd, "Cannot read entry name - string expected. (RFC 5464 Section 4.3)");
			return;
		}

		if (name == NULL) {
			client_send_tagline(cmd, "NO Entry name is NULL.");
			return;
		}

		if (strlen(name) == 0) {
			client_send_command_error(cmd, "Entry name must not be empty. (RFC 5464 Section 3.2)");
			return;
		}

		const char *value;
		if (!imap_arg_get_nstring(&arglist[1], &value)){
			client_send_command_error(cmd, "Cannot read value - string or NIL expected. (RFC 5464 Section 4.3)");
			return;
		}

		struct metadata_entry *entry = parse_entry(box, name, value);
		if (entry == NULL) {
			const char *estr = t_strdup_printf("Invalid entry name '%s'. (RFC 5464 Section 3.2)", name);
			client_send_command_error(cmd, estr);
			return;
		}

		int ret = metadata_set_entry(entry, cmd->client->user);
		if (ret == -METADATA_ERROR_TOOLARGE) {
			warn_maxsize = true;
		}
		else if (ret == -METADATA_ERROR_TOOMANY) {
			warn_toomany = true;
		}
		else if (ret < 0) {
			client_send_tagline(cmd, "NO Setting entry failed.");
			return;
		}

		/* skip this name/value pair */
		arglist += 2;
	}

	const char *response;
	if (warn_maxsize) {
		struct metadata_mail_user *muser = METADATA_USER_CONTEXT(cmd->client->user);
		if (muser == NULL) {
			i_error("metadata: Found NULL user - can't set metadata");
			client_send_tagline(cmd, "NO Internal error.");
			return;
		}

		response = t_strdup_printf("OK [METADATA MAXSIZE %d] Completed.", muser->set->maxsize);
	}
	else if (warn_toomany) {
		response = t_strdup_printf("OK [METADATA TOOMANY] Completed.");
	}
	else if (warn_noprivate) {
		response = t_strdup_printf("OK [METADATA NOPRIVATE] Completed.");
	}
	else {
		response = "OK Completed.";
	}
	client_send_tagline(cmd, response);
}


static bool
cmd_setmetadata(struct client_command_context *cmd) {
	const struct imap_arg *args;
	if (!client_read_args(cmd, 0, 0, &args))
		return false;

	const char *mailbox_name = NULL;
	if (!imap_arg_get_astring(&args[0], &mailbox_name)){
		client_send_command_error(cmd, "Cannot read mailbox name - string expected. (RFC 5464 Section 4.3)");
		return true;
	}

	if (mailbox_name == NULL) {
		client_send_tagline(cmd, "NO Mailbox name is NULL.");
		return true;
	}

	string_t *mailbox_name_utf8 = t_str_new(strlen(mailbox_name));
	if (imap_utf7_to_utf8(mailbox_name, mailbox_name_utf8) == 0) {
		mailbox_name = str_c(mailbox_name_utf8);
	}

	struct mailbox *box = NULL;
	/* empty name -> box=NULL -> server scope */
	if (*mailbox_name != '\0') {
#if DOVECOT_PREREQ(2,1)
		struct mail_namespace *ns = mail_namespace_find(cmd->client->user->namespaces, mailbox_name);
#else
		struct mail_namespace *ns = mail_namespace_find(cmd->client->user->namespaces, &mailbox_name);
#endif
		if (ns == NULL) {
			client_send_tagline(cmd, "NO Mailbox not found.");
			return true;
		}

		box = mailbox_alloc(ns->list, mailbox_name, MAILBOX_FLAG_READONLY);
		if (box == NULL) {
			client_send_tagline(cmd, "NO Allocating mailbox failed.");
			return true;
		}

		enum mailbox_existence existence;
		if (mailbox_exists(box, false, &existence) < 0) {
			client_send_tagline(cmd, "NO Checking mailbox existence failed.");
			return true;
		}

		if (existence == MAILBOX_EXISTENCE_NONE) {
			client_send_tagline(cmd, "NO Mailbox does not exist.");
			return true;
		}
	}

	setmetadata_helper(cmd, args, box);

	if (box != NULL) {
		/* mailbox_free calls mailbox_close, which cannot deal with NULL boxes */
		mailbox_free(&box);
	}

	return true;
}


static void imap_metadata_client_created(struct client **client)
{
	if (mail_user_is_plugin_loaded((*client)->user, imap_metadata_module))
		str_append((*client)->capability_string, " METADATA");

	if (next_hook_client_created != NULL)
		next_hook_client_created(client);
}


void imap_metadata_plugin_init(struct module *module)
{
	command_register("GETMETADATA", cmd_getmetadata, 0);
	command_register("SETMETADATA", cmd_setmetadata, 0);

	imap_metadata_module = module;
	next_hook_client_created = hook_client_created;
	hook_client_created = imap_metadata_client_created;
}


void imap_metadata_plugin_deinit(void)
{
	command_unregister("SETMETADATA");
	command_unregister("GETMETADATA");

	hook_client_created = next_hook_client_created;
}
