/*
	Copyright (c) 2008 by Intevation GmbH / Bernhard Herzog <bh@intevation.de>
	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 "str.h"
#include "unichar.h"
#include "imap-common.h"
#include "imap-client.h"
#include "imap-quote.h"
#include "mailbox-list.h"
#if DOVECOT_PREREQ(2,2)
#include "mailbox-list-iter.h"
#endif

#include <string.h>

#include "str-ext.h"
#include "metadata-entry-private.h"
#include "metadata-backend.h"

/* The IMAP Annotatemore plugin is a partial implementation of draft-daboo-imap-annotatemore-08 */

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

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

enum attribute_properties {
	ATTR_INVALID = 0x0001,
	ATTR_PRIVATE = 0x0002,
	ATTR_PUBLIC  = 0x0004,
	ATTR_BOTH    = ATTR_PUBLIC | ATTR_PRIVATE,
};


static bool validate_entry_name(struct client_command_context *cmd,
				const char *entry)
{
	if (entry == NULL) {
		client_send_command_error(cmd, "Missing entry name.");
		return FALSE;
	}

	if (entry[0] != '/' && entry[0] != '*' && entry[0] != '%') {
		client_send_command_error(cmd,
				    "Entry name must start with slash or be a glob.");
		return FALSE;
	}

	return TRUE;
}


static enum attribute_properties
validate_attribute_name(struct client_command_context *cmd,
			const char *attribute)
{
	if (attribute == NULL) {
		client_send_command_error(cmd,
				    "Missing or NIL attribute name.");
		return ATTR_INVALID;
	}

	if (strcmp(attribute, "value.shared") == 0) {
		return ATTR_PUBLIC;
	} else if (strcmp(attribute, "value.priv") == 0) {
		return ATTR_PRIVATE;
	} else if (strcmp(attribute, "value") == 0) {
		return ATTR_BOTH;
	} else if (strchr(attribute, '*')) {
		return ATTR_BOTH;
	} else if (strchr(attribute, '%')) {
		client_send_command_error(cmd, "'%' globs not supported in attribute.");
		return ATTR_INVALID;
	} else {
		client_send_command_error(cmd,
				    "Only 'value.shared' and 'value.priv' attributes"
				    " are supported '.'");
		return ATTR_INVALID;
	}
}


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
	}
};


/* validates that the whole entry name conforms to the specs */
static ATTR_NONNULL(1)
bool
is_valid_annotatemore_entry_name(const char *name) {
	const char *lastslash = NULL;

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

	// Must be UTF-8
	if (!uni_utf8_str_is_valid(name)) {
		return false;
	}

	for (const char *c = name; *c != '\0'; c++) {
		// Must not be a command character
		/* NOTE This is stricter than the spec requires, but makes our backend saner */
		if (*c >= 0x00 && *c <= 0x19) {
			return false;
		}

		switch (*c) {
			case '/':
				// Two consecutive slashes, or a slash at the end are an error
				/* NOTE This is stricter than the spec requires, but makes our backend saner */
				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 the specs */
static ATTR_NONNULL(1)
bool
is_valid_annotatemore_vendor_name(const char *name) {
	int num_components = 2; // "vendor/" already includes the slash of component No2

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

	/* NOTE This is stricter than the spec requires, but makes our backend saner */
	return num_components >= 3;
}


static ATTR_NONNULL(1)
bool is_valid_annotatemore_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) {
			found_subtype = true;
		}
	}

	return found_subtype;
}


static ATTR_NONNULL(2)
const char *
backend_name(enum metadata_entry_scope scope, const char *name) {
	if (name == NULL) {
		return NULL;
	}

	// Skip leading '/'
	if (*name == '/') {
		name++;
	}

	string_t *backend_name = t_str_new(128);
	str_append(backend_name, "/");
	str_append(backend_name, entry_scopes[scope]);
	str_append(backend_name, name);

	return str_c(backend_name);
}


/* 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) {
	if (**name == '\0')
		return ENTRY_TYPE_NONE;

	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_annotatemore_subtype_name(*name, subject))
						return ENTRY_TYPE_MAX;
					break;
				case ENTRY_TYPE_VENDOR:
					if (!is_valid_annotatemore_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(3)
struct metadata_entry *
parse_entry(struct mailbox *box, enum metadata_entry_scope scope, const char *name, const char *value) {
	if (!is_valid_annotatemore_entry_name(name)) {
		return NULL;
	}

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

	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, backend_name(scope, name), value);
}


static void send_annotation_line(struct client_command_context *cmd,
				 struct mailbox *box,
				 const char *entry_name,
				 const char *value,
				 bool private)
{
	if (value == NULL) {
		value = "NIL";
	}

	const char *mailbox_name = mailbox_get_vname(box);
	string_t *str = t_str_new(128);
	str_append(str, "* ANNOTATION ");
#if DOVECOT_PREREQ(2,2)
	imap_append_string(str, mailbox_name);
#else
	imap_quote_append_string(str, mailbox_name, false);
#endif
	str_append(str, " ");
#if DOVECOT_PREREQ(2,2)
	imap_append_string(str, entry_name);
#else
	imap_quote_append_string(str, entry_name, false);
#endif
	str_append(str, " (");
	// FIXME: Should be imap_append_printf(), if that would exist
	str_append_printf(str, "\"value.%s\"", private ? "priv" : "shared");
	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, ")");

	client_send_line(cmd->client, str_c(str));
}


static void get_and_send_annotation(struct client_command_context *cmd,
				    struct mailbox *box,
				    const char *entry_name,
				    enum attribute_properties scope)
{
	if (strchr(entry_name, '*')) {
		int entrylastchar = strlen(entry_name);
		if (entrylastchar > 0)
			entrylastchar--;

		/* We do not support more than one glob, and at no other location than the end */
		if (strchr_num(entry_name, '*') == 1 && entry_name[entrylastchar] == '*') {
			const char *entrypattern = t_strdup_until(entry_name, &entry_name[entrylastchar]);

			if ((scope & ATTR_PUBLIC) != 0) {
				struct metadata_entry *entry = metadata_entry_alloc(box, backend_name(ENTRY_SCOPE_SHARED, entrypattern), NULL);
				if (entry == NULL) {
					client_send_tagline(cmd, "NO Allocating entry failed.");
					return;
				}
				if (entry == NULL) {
					client_send_tagline(cmd, "NO Allocating entry failed.");
					return;
				}

				struct metadata_iterate_context *iter = metadata_iterate_init(box, entry, METADATA_ITERATE_DEPTH_INF);
				while (metadata_iterate(iter, entry)) {
					const char *name = metadata_entry_get_name(entry) + strlen(entry_scopes[ENTRY_SCOPE_SHARED]);
					const char *value = metadata_entry_get_value(entry);

					send_annotation_line(cmd, box, name, value, FALSE);
				}
				if (metadata_iterate_deinit(&iter) < 0) {
					client_send_tagline(cmd, "NO Iterating metadata failed.");
					return;
				}
			}

			if ((scope & ATTR_PRIVATE) != 0) {
				struct metadata_entry *entry = metadata_entry_alloc(box, backend_name(ENTRY_SCOPE_PRIVATE, entrypattern), NULL);

				struct metadata_iterate_context *iter = metadata_iterate_init(box, entry, METADATA_ITERATE_DEPTH_INF);
				while (metadata_iterate(iter, entry)) {
					const char *name = metadata_entry_get_name(entry) + strlen(entry_scopes[ENTRY_SCOPE_PRIVATE]);
					const char *value = metadata_entry_get_value(entry);

					send_annotation_line(cmd, box, name, value, TRUE);
				}
				if (metadata_iterate_deinit(&iter) < 0) {
					client_send_tagline(cmd, "NO Iterating metadata failed.");
					return;
				}
			}
		} else {
			client_send_command_error(cmd, "'*' globs only supported at end of entry pattern.");
			return;
		}
	} else if (strchr(entry_name, '%')) {
		client_send_command_error(cmd, "'%' globs not supported in entry.");
		return;
	} else {
		if ((scope & ATTR_PUBLIC) != 0) {
			struct metadata_entry *entry = parse_entry(box, ENTRY_SCOPE_SHARED, entry_name, NULL);
			if (entry == NULL) {
				const char *estr = t_strdup_printf("NO Parsing entry '%s' failed.", entry_name);
				client_send_tagline(cmd, estr);
				return;
			}

			int success = metadata_get_entry(entry, cmd->client->user);
			if (success < 0) {
				client_send_tagline(cmd, "NO Getting entry failed.");
				return;
			}
			else if (success > 0) {
				const char *name = metadata_entry_get_name(entry) + strlen(entry_scopes[ENTRY_SCOPE_SHARED]);
				const char *value = metadata_entry_get_value(entry);

				send_annotation_line(cmd, box, name, value, FALSE);
			}
		}

		if ((scope & ATTR_PRIVATE) != 0) {
			struct metadata_entry *entry = parse_entry(box, ENTRY_SCOPE_PRIVATE, entry_name, NULL);
			if (entry == NULL) {
				const char *estr = t_strdup_printf("NO Parsing entry '%s' failed.", entry_name);
				client_send_tagline(cmd, estr);
				return;
			}

			int success = metadata_get_entry(entry, cmd->client->user);
			if (success < 0) {
				client_send_tagline(cmd, "NO Getting entry failed.");
				return;
			}
			else if (success > 0) {
				const char *name = metadata_entry_get_name(entry) + strlen(entry_scopes[ENTRY_SCOPE_PRIVATE]);
				const char *value = metadata_entry_get_value(entry);

				send_annotation_line(cmd, box, name, value, TRUE);
			}
		}
	}
}


static bool extract_single_value(struct client_command_context *cmd,
				 const struct imap_arg *attribute,
				 const char **value_r)
{
	const struct imap_arg *attrlist = NULL;
	unsigned int attrcount = 0;

	if (!imap_arg_get_list_full(attribute, &attrlist, &attrcount)) {
		// Actually this should never happen, since we first test args[1].type == IMAP_ARG_LIST ! */
		i_error("metadata: got attributes of non-list type after confirming they were of correct type!");
		client_send_command_error(cmd, "Attributes must be of list type.");
		return FALSE;
	}

	if (attrcount == 1) {
		if (!imap_arg_get_astring(&attrlist[0], value_r)) {
			client_send_command_error(cmd,
					"Name/attribute must be of string type.");
			return FALSE;
		}
		return TRUE;
	} else {
		client_send_tagline(cmd,
				    "NO Lists of entries not yet implemented.");
		return FALSE;
	}
}


static bool cmd_getannotation(struct client_command_context *cmd)
{
	const char *mailbox_name;
	const char *entry_name;
	const char *attribute;
	enum attribute_properties attribute_properties;

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

	if (!imap_arg_get_astring(&args[0], &mailbox_name)) {
		client_send_command_error(cmd,
				"Mailbox name must be of string type.");
		return TRUE;
	}
	if (mailbox_name == NULL) {
		client_send_command_error(cmd,
				    "Missing mailbox name.");
		return TRUE;
	}

	if (*mailbox_name == '\0') {
		client_send_tagline(cmd,
				    "NO Server annotations not yet"
				    " implemented.");
		return TRUE;
	}

	if (args[1].type == IMAP_ARG_LIST) {
		if (!extract_single_value(cmd, &args[1], &entry_name))
			return TRUE;
	} else {
		if (!imap_arg_get_astring(&args[1], &entry_name)) {
			client_send_command_error(cmd,
					"Entry name must be of string type.");
			return TRUE;
		}
	}

	if (!validate_entry_name(cmd, entry_name))
		return TRUE;

	if (args[2].type == IMAP_ARG_LIST) {
		if (!extract_single_value(cmd, &args[2], &attribute))
			return TRUE;
	} else {
		if (!imap_arg_get_astring(&args[2], &attribute)) {
			client_send_command_error(cmd,
					"Attribute must be of string type.");
			return TRUE;
		}
	}

	attribute_properties = validate_attribute_name(cmd, attribute);
	if (attribute_properties & ATTR_INVALID) {
		client_send_command_error(cmd,
				"Invalid attribute.");
		return TRUE;
	}

	if (str_has_wildcards(mailbox_name)) {
		i_debug("metadata: Mailbox '%s' contains 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;
				}

				get_and_send_annotation(cmd, box, entry_name, attribute_properties);

				mailbox_free(&box);
			}
			if (mailbox_list_iter_deinit(&ctx) < 0) {
				client_send_tagline(cmd, "NO Iterating mailboxes failed.");
			}
		}
	} else {
		i_debug("metadata: Mailbox '%s' does not contain wildcards", mailbox_name);
#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;
		}

		i_debug("metadata: Found mailbox '%s' in namespace '%s'", mailbox_name, ns->prefix);

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

		get_and_send_annotation(cmd, box, entry_name, attribute_properties);

		mailbox_free(&box);
	}

	client_send_tagline(cmd, "OK Completed.");

	return TRUE;
}


static bool pair_extract_value(struct client_command_context *cmd,
			       const struct imap_arg *attributes,
			       const char **value_r,
			       bool *private_r)
{
	const struct imap_arg *pairs;
	unsigned int count;

	if (!imap_arg_get_list_full(attributes, &pairs, &count)) {
		client_send_command_error(cmd,
				    "Attributes parameter must be a list"
				    " of attribute value pairs.");
		return FALSE;
	}

	if (count % 2 != 0) {
		client_send_command_error(cmd,
				    "List of attribute value pairs"
				    " must have an even number of elements");
		return FALSE;
	}

	if (count == 0) {
		client_send_command_error(cmd,
				    "List of attribute value pairs"
				    " is empty");
		return FALSE;
	}

	if (count == 2) {
		enum attribute_properties properties;
		const char *tmp;
		if (!imap_arg_get_astring(&pairs[0], &tmp)) {
			client_send_command_error(cmd,
					"Attribute must be of string type.");
			return FALSE;
		}
		properties = validate_attribute_name(cmd, tmp);
		if ((properties & ATTR_INVALID) != 0)
			return FALSE;
		if ((properties & ATTR_BOTH) == ATTR_BOTH) {
			client_send_command_error(cmd,
					    "Attribute must end in .priv"
					    " or .shared for SETANNOTATION");
			return FALSE;
		}

		if (!imap_arg_get_nstring(&pairs[1], value_r)) {
			client_send_command_error(cmd,
					"Value must be of string type.");
			return FALSE;
		}
		*private_r = ((properties & ATTR_PRIVATE) != 0);
		return TRUE;
	}

	client_send_tagline(cmd, "NO only setting single attributes supported");
	return FALSE;
}


static bool cmd_setannotation(struct client_command_context *cmd)
{
	const char *mailbox_name;
	const char *entry_name;
	const char *value;
	bool private;

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

	if (!imap_arg_get_astring(&args[0], &mailbox_name)) {
		client_send_command_error(cmd,
				"Mailbox name must be of string type.");
		return TRUE;
	}
	if (mailbox_name == NULL) {
		client_send_command_error(cmd,
				    "Missing mailbox name.");
		return TRUE;
	}
	if (*mailbox_name == '\0') {
		client_send_tagline(cmd,
				    "NO Server annotations not yet"
				    " implemented.");
		return TRUE;
	}

	if (args[1].type == IMAP_ARG_LIST) {
		client_send_tagline(cmd,
				    "NO Lists of entries not yet implemented.");
		return TRUE;
	}
	if (!imap_arg_get_astring(&args[1], &entry_name)) {
		client_send_command_error(cmd,
				"Entry name must be of string type.");
		return TRUE;
	}
	if (entry_name == NULL) {
		client_send_tagline(cmd, "NO Entry name is NULL.");
		return true;
	}

	if (!validate_entry_name(cmd, entry_name))
		return TRUE;

	if (!pair_extract_value(cmd, &args[2], &value, &private))
		return TRUE;

#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;
	}

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

	struct metadata_entry *entry = parse_entry(box, private ? ENTRY_SCOPE_PRIVATE : ENTRY_SCOPE_SHARED, entry_name, value);
	if (entry == NULL) {
		const char *estr = t_strdup_printf("NO Parsing entry '%s' failed.", entry_name);
		client_send_tagline(cmd, estr);
		mailbox_free(&box);
		return TRUE;
	}

	int ret = metadata_set_entry(entry, cmd->client->user);
	if (ret < 0) {
		const char *estr = t_strdup_printf("NO Setting entry '%s' failed: %s.", entry_name, metadata_error_tostring(-ret));
		client_send_tagline(cmd, estr);
		mailbox_free(&box);
		return TRUE;
	}

	client_send_tagline(cmd, "OK Completed.");

	mailbox_free(&box);

	return TRUE;
}


static void imap_annotatemore_client_created(struct client **client)
{
	if (mail_user_is_plugin_loaded((*client)->user, imap_annotatemore_module))
		str_append((*client)->capability_string, " ANNOTATEMORE");

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


void imap_annotatemore_plugin_init(struct module *module)
{
	command_register("GETANNOTATION", cmd_getannotation, 0);
	command_register("SETANNOTATION", cmd_setannotation, 0);

	imap_annotatemore_module = module;
	next_hook_client_created = hook_client_created;
	hook_client_created = imap_annotatemore_client_created;
}


void imap_annotatemore_plugin_deinit(void)
{
	command_unregister("SETANNOTATION");
	command_unregister("GETANNOTATION");

	hook_client_created = next_hook_client_created;
}
