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

#include "lib.h"
#include "mail-storage.h"

#include "imap-metadata.h"

struct imap_metadata_transaction {
	struct mailbox *box;
	struct mailbox_transaction_context *trans;

	enum mail_error error;
	char *error_string;

	bool server:1;
	bool validated_only:1;
};

bool imap_metadata_verify_entry_name(const char *name,
				     const char **client_error_r)
{
	unsigned int i;
	bool ok;

	if (name[0] != '/') {
		*client_error_r = "Entry name must begin with '/'";
		return FALSE;
	}
	for (i = 0; name[i] != '\0'; i++) {
		switch (name[i]) {
		case '/':
			if (i > 0 && name[i-1] == '/') {
				*client_error_r = "Entry name can't contain consecutive '/'";
				return FALSE;
			}
			if (name[i+1] == '\0') {
				*client_error_r = "Entry name can't end with '/'";
				return FALSE;
			}
			break;
		case '*':
			*client_error_r = "Entry name can't contain '*'";
			return FALSE;
		case '%':
			*client_error_r = "Entry name can't contain '%'";
			return FALSE;
		default:
			if (name[i] <= 0x19) {
				*client_error_r = "Entry name can't contain control chars";
				return FALSE;
			}
			break;
		}
	}
	T_BEGIN {
		const char *prefix, *p = strchr(name+1, '/');

		prefix = p == NULL ? name : t_strdup_until(name, p);
		ok = strcasecmp(prefix, IMAP_METADATA_PRIVATE_PREFIX) == 0 ||
			strcasecmp(prefix, IMAP_METADATA_SHARED_PREFIX) == 0;
	} T_END;
	if (!ok) {
		*client_error_r = "Entry name must begin with /private or /shared";
		return FALSE;
	}
	return TRUE;
}

static void
imap_metadata_transaction_set_error(struct imap_metadata_transaction *imtrans,
				    enum mail_error error, const char *string)
{
	i_free(imtrans->error_string);
	imtrans->error_string = i_strdup(string);
	imtrans->error = error;
}

static bool
imap_metadata_entry2key(struct imap_metadata_transaction *imtrans,
			const char *entry, enum mail_attribute_type *type_r,
			const char **key_r)
{
	const char *key_prefix = (imtrans->server ?
		MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT_SERVER : NULL);

	/* names are case-insensitive so we'll always lowercase them */
	entry = t_str_lcase(entry);

	if (str_begins(entry, IMAP_METADATA_PRIVATE_PREFIX, key_r))
		*type_r = MAIL_ATTRIBUTE_TYPE_PRIVATE;
	else if (str_begins(entry, IMAP_METADATA_SHARED_PREFIX, key_r))
		*type_r = MAIL_ATTRIBUTE_TYPE_SHARED;
	else
		i_unreached();
	if ((*key_r)[0] == '\0') {
		/* /private or /shared prefix has no value itself */
	} else {
		i_assert((*key_r)[0] == '/');
		*key_r += 1;
	}

	if (imtrans->validated_only)
		*type_r |= MAIL_ATTRIBUTE_TYPE_FLAG_VALIDATED;

	if (str_begins_with(*key_r, MAILBOX_ATTRIBUTE_PREFIX_DOVECOT_PVT)) {
		/* Dovecot's internal attribute (mailbox or server).
		   don't allow accessing this. */
		return FALSE;
	}
	/* Add the server-prefix (after checking for the above internal
	   attribute). */
	if (key_prefix != NULL)
		*key_r = t_strconcat(key_prefix, *key_r, NULL);
	return TRUE;
}

static int
imap_metadata_get_mailbox_transaction(struct imap_metadata_transaction *imtrans)
{
	if (imtrans->trans != NULL)
		return 0;

	if (imtrans->box == NULL || mailbox_open(imtrans->box) < 0)
		return -1;
	imtrans->trans = mailbox_transaction_begin(imtrans->box,
			MAILBOX_TRANSACTION_FLAG_EXTERNAL, __func__);
	return 0;
}

int imap_metadata_set(struct imap_metadata_transaction *imtrans,
	const char *entry, const struct mail_attribute_value *value)
{
	enum mail_attribute_type type;
	const char *key;

	if (!imap_metadata_entry2key(imtrans, entry, &type, &key)) {
		imap_metadata_transaction_set_error(imtrans, MAIL_ERROR_PARAMS,
			"Internal mailbox attributes cannot be accessed");
		return -1;
	}

	if (imap_metadata_get_mailbox_transaction(imtrans) < 0)
		return -1;
	return (value->value == NULL && value->value_stream == NULL ?
		mailbox_attribute_unset(imtrans->trans, type, key) :
		mailbox_attribute_set(imtrans->trans, type, key, value));
}

int imap_metadata_unset(struct imap_metadata_transaction *imtrans,
			const char *entry)
{
	struct mail_attribute_value value;

	i_zero(&value);
	return imap_metadata_set(imtrans, entry, &value);
}

int imap_metadata_get(struct imap_metadata_transaction *imtrans,
		      const char *entry, struct mail_attribute_value *value_r)
{
	enum mail_attribute_type type;
	const char *key;

	i_zero(value_r);
	if (!imap_metadata_entry2key(imtrans, entry, &type, &key))
		return 0;
	return mailbox_attribute_get(imtrans->box, type, key, value_r);
}

int imap_metadata_get_stream(struct imap_metadata_transaction *imtrans,
		      const char *entry, struct mail_attribute_value *value_r)
{
	enum mail_attribute_type type;
	const char *key;

	i_zero(value_r);
	if (!imap_metadata_entry2key(imtrans, entry, &type, &key))
		return 0;
	return mailbox_attribute_get_stream(imtrans->box, type, key, value_r);
}

struct imap_metadata_iter {
	struct mailbox_attribute_iter *iter;
};

struct imap_metadata_iter *
imap_metadata_iter_init(struct imap_metadata_transaction *imtrans,
			const char *entry)
{
	struct imap_metadata_iter *iter;
	enum mail_attribute_type type;
	const char *key;

	iter = i_new(struct imap_metadata_iter, 1);
	if (imap_metadata_entry2key(imtrans, entry, &type, &key)) {
		const char *prefix =
			key[0] == '\0' ? "" : t_strconcat(key, "/", NULL);
		iter->iter = mailbox_attribute_iter_init(imtrans->box, type,
							 prefix);
	}
	return iter;
}

const char *imap_metadata_iter_next(struct imap_metadata_iter *iter)
{
	if (iter->iter == NULL)
		return NULL;
	return mailbox_attribute_iter_next(iter->iter);
}

int imap_metadata_iter_deinit(struct imap_metadata_iter **_iter)
{
	struct imap_metadata_iter *iter = *_iter;
	int ret;

	*_iter = NULL;

	if (iter->iter == NULL)
		ret = 0;
	else
		ret = mailbox_attribute_iter_deinit(&iter->iter);
	i_free(iter);
	return ret;
}

struct imap_metadata_transaction *
imap_metadata_transaction_begin(struct mailbox *box)
{
	struct imap_metadata_transaction *imtrans;

	imtrans = i_new(struct imap_metadata_transaction, 1);
	imtrans->box = box;
	return imtrans;
}

struct imap_metadata_transaction *
imap_metadata_transaction_begin_server(struct mail_user *user)
{
	struct mail_namespace *ns;
	struct mailbox *box;
	struct imap_metadata_transaction *imtrans;

	ns = mail_namespace_find_inbox(user->namespaces);
	/* Server metadata shouldn't depend on INBOX's ACLs, so ignore them. */
	box = mailbox_alloc(ns->list, "INBOX", MAILBOX_FLAG_IGNORE_ACLS |
			    MAILBOX_FLAG_ATTRIBUTE_SESSION);
	imtrans = imap_metadata_transaction_begin(box);
	imtrans->server = TRUE;
	return imtrans;
}

void imap_metadata_transaction_validated_only(struct imap_metadata_transaction *imtrans,
					      bool set)
{
	imtrans->validated_only = set;
}

static void
imap_metadata_transaction_finish(struct imap_metadata_transaction **_imtrans)
{
	struct imap_metadata_transaction *imtrans = *_imtrans;

	if (imtrans->server)
		mailbox_free(&imtrans->box);

	i_free(imtrans->error_string);
	i_free(imtrans);
	*_imtrans = NULL;
}

int imap_metadata_transaction_commit(
	struct imap_metadata_transaction **_imtrans,
	enum mail_error *error_code_r, const char **client_error_r)
{
	struct imap_metadata_transaction *imtrans = *_imtrans;
	int ret = 0;

	if (imtrans->trans != NULL) {
		const char *error = NULL;
		ret = mailbox_transaction_commit(&imtrans->trans);
		if (ret < 0)
			error = mailbox_get_last_error(imtrans->box, error_code_r);
		if (client_error_r != NULL)
			*client_error_r = error;
	}
	imap_metadata_transaction_finish(_imtrans);
	return ret;
}

void imap_metadata_transaction_rollback(
	struct imap_metadata_transaction **_imtrans)
{
	struct imap_metadata_transaction *imtrans = *_imtrans;

	if (imtrans->trans != NULL)
		mailbox_transaction_rollback(&imtrans->trans);
	imap_metadata_transaction_finish(_imtrans);
}

const char *
imap_metadata_transaction_get_last_error(
	struct imap_metadata_transaction *imtrans,
	enum mail_error *error_code_r)
{
	if  (imtrans->error != MAIL_ERROR_NONE) {
		if (error_code_r != NULL)
			*error_code_r = imtrans->error;
		return imtrans->error_string;
	}
	i_assert(imtrans->box != NULL);
	return mailbox_get_last_error(imtrans->box, error_code_r);
}
