/* Copyright (C) 2007 Timo Sirainen, LGPLv2.1 */

/*
   export DOVECOT=~/src/dovecot-1.1.0
   gcc -fPIC -g -shared -Wall -I$DOVECOT -I$DOVECOT/src/lib \
     -I$DOVECOT/src/lib-storage -I$DOVECOT/src/lib-mail \
     -I$DOVECOT/src/lib-imap -I$DOVECOT/src/lib-index -DHAVE_CONFIG_H \
     listescape-plugin.c -o listescape_plugin.so
*/

#include "lib.h"
#include "array.h"
#include "str.h"
#include "module-context.h"
#include "mail-storage-private.h"
#include "mailbox-list-private.h"

#include <ctype.h>

#define ESCAPE_CHAR '\\'
#define REAL_SEP '.'

/* Use '/' as separator */
#define VIRTUAL_SEP '/'

/* Use '^' as separator, allow '/' in mailbox names:
#define ALT_REAL_SEP '/'
#define VIRTUAL_SEP '^'
*/

#define LIST_ESCAPE_CONTEXT(obj) \
	MODULE_CONTEXT(obj, listescape_storage_module)
#define LIST_ESCAPE_LIST_CONTEXT(obj) \
	MODULE_CONTEXT(obj, listescape_list_module)

struct listescape_mail_storage {
	union mail_storage_module_context module_ctx;
};

struct listescape_mailbox_list {
	union mailbox_list_module_context module_ctx;
	struct mailbox_info info;
	string_t *list_name;
	bool name_escaped;
};

const char *listescape_plugin_version = PACKAGE_VERSION;

static void (*listescape_next_hook_mail_storage_created)
	(struct mail_storage *storage);
static void (*listescape_next_hook_mailbox_list_created)
	(struct mailbox_list *list);
static void (*listescape_next_hook_mail_namespaces_created)
	(struct mail_namespace *namespaces);

static MODULE_CONTEXT_DEFINE_INIT(listescape_storage_module,
				  &mail_storage_module_register);
static MODULE_CONTEXT_DEFINE_INIT(listescape_list_module,
				  &mailbox_list_module_register);

static const char *list_escape(const char *str, bool change_sep)
{
	string_t *esc = t_str_new(64);

	if (*str == '~') {
		str_printfa(esc, "%c%02x", ESCAPE_CHAR, *str);
		str++;
	}
	for (; *str != '\0'; str++) {
		if (*str == REAL_SEP ||
#ifdef ALT_REAL_SEP
		    *str == ALT_REAL_SEP ||
#endif
		    *str == ESCAPE_CHAR)
			str_printfa(esc, "%c%02x", ESCAPE_CHAR, *str);
		else if (*str == VIRTUAL_SEP && change_sep)
			str_append_c(esc, REAL_SEP);
		else
			str_append_c(esc, *str);
	}
	return str_c(esc);
}

static void list_unescape_str(const char *str, string_t *dest)
{
	unsigned int num;

	for (; *str != '\0'; str++) {
		if (*str == ESCAPE_CHAR &&
		    i_isxdigit(str[1]) && i_isxdigit(str[2])) {
			if (str[1] >= '0' && str[1] <= '9')
				num = str[1] - '0';
			else
				num = i_toupper(str[1]) - 'A' + 10;
			num *= 16;
			if (str[2] >= '0' && str[2] <= '9')
				num += str[2] - '0';
			else
				num += i_toupper(str[2]) - 'A' + 10;

			str_append_c(dest, num);
			str += 2;
		} else if (*str == REAL_SEP)
			str_append_c(dest, VIRTUAL_SEP);
		else
			str_append_c(dest, *str);
	}
}

static struct mailbox_list_iterate_context *
listescape_mailbox_list_iter_init(struct mailbox_list *list,
				  const char *const *patterns,
				  enum mailbox_list_iter_flags flags)
{
	struct listescape_mailbox_list *mlist = LIST_ESCAPE_LIST_CONTEXT(list);
	struct mailbox_list_iterate_context *ctx;
	const char **escaped_patterns;
	unsigned int i;

	t_push();
	if ((flags & MAILBOX_LIST_ITER_VIRTUAL_NAMES) != 0) {
		escaped_patterns = t_new(const char *,
					 str_array_length(patterns) + 1);
		for (i = 0; patterns[i] != NULL; i++)
			escaped_patterns[i] = list_escape(patterns[i], FALSE);
		patterns = escaped_patterns;
	}

	/* Listing breaks if ns->real_sep isn't correct, but with everything
	   else we need real_sep == virtual_sep. maybe some day lib-storage
	   API gets changed so that it sees only virtual mailbox names and
	   convers them internally and we don't have this problem. */
	list->ns->real_sep = REAL_SEP;
	ctx = mlist->module_ctx.super.iter_init(list, patterns, flags);
	list->ns->real_sep = VIRTUAL_SEP;
	t_pop();
	return ctx;
}

static const struct mailbox_info *
listescape_mailbox_list_iter_next(struct mailbox_list_iterate_context *ctx)
{
	struct listescape_mailbox_list *mlist =
		LIST_ESCAPE_LIST_CONTEXT(ctx->list);
	const struct mailbox_info *info;

	ctx->list->ns->real_sep = REAL_SEP;
	info = mlist->module_ctx.super.iter_next(ctx);
	ctx->list->ns->real_sep = VIRTUAL_SEP;
	if (info == NULL || (ctx->flags & MAILBOX_LIST_ITER_VIRTUAL_NAMES) == 0)
		return info;

	str_truncate(mlist->list_name, 0);
	list_unescape_str(info->name, mlist->list_name);
	mlist->info = *info;
	mlist->info.name = str_c(mlist->list_name);
	return &mlist->info;
}

static int
listescape_mailbox_list_iter_deinit(struct mailbox_list_iterate_context *ctx)
{
	struct mailbox_list *list = ctx->list;
	struct listescape_mailbox_list *mlist =
		LIST_ESCAPE_LIST_CONTEXT(ctx->list);
	int ret;

	list->ns->real_sep = REAL_SEP;
	ret = mlist->module_ctx.super.iter_deinit(ctx);
	list->ns->real_sep = VIRTUAL_SEP;
	return ret;
}

static struct mailbox *
listescape_mailbox_open(struct mail_storage *storage, const char *name,
			struct istream *input, enum mailbox_open_flags flags)
{
	struct listescape_mail_storage *mstorage =
		LIST_ESCAPE_CONTEXT(storage);
	struct listescape_mailbox_list *mlist =
		LIST_ESCAPE_LIST_CONTEXT(storage->list);

	if (!mlist->name_escaped)
		name = list_escape(name, TRUE);
	return mstorage->module_ctx.super.
		mailbox_open(storage, name, input, flags);
}

static int
listescape_mailbox_create(struct mail_storage *storage, const char *name,
			  bool directory)
{
	struct listescape_mail_storage *mstorage =
		LIST_ESCAPE_CONTEXT(storage);

	name = list_escape(name, TRUE);
	return mstorage->module_ctx.super.
		mailbox_create(storage, name, directory);
}

static int
listescape_delete_mailbox(struct mailbox_list *list, const char *name)
{
	struct listescape_mailbox_list *mlist = LIST_ESCAPE_LIST_CONTEXT(list);
	int ret;

	/* at least quota plugin opens the mailbox when deleting it */
	name = list_escape(name, TRUE);
	mlist->name_escaped = TRUE;
	ret = mlist->module_ctx.super.delete_mailbox(list, name);
	mlist->name_escaped = FALSE;
	return ret;
}

static int
listescape_rename_mailbox(struct mailbox_list *list, const char *oldname,
			  const char *newname)
{
	struct listescape_mailbox_list *mlist = LIST_ESCAPE_LIST_CONTEXT(list);

	oldname = list_escape(oldname, TRUE);
	newname = list_escape(newname, TRUE);
	return mlist->module_ctx.super.rename_mailbox(list, oldname, newname);
}

static int listescape_set_subscribed(struct mailbox_list *list, 
				     const char *name, bool set)
{
	struct listescape_mailbox_list *mlist = LIST_ESCAPE_LIST_CONTEXT(list);

	name = list_escape(name, TRUE);
	return mlist->module_ctx.super.set_subscribed(list, name, set);
}

static int listescape_get_mailbox_name_status(struct mailbox_list *list,
					      const char *name,
					      enum mailbox_name_status *status)
{
	struct listescape_mailbox_list *mlist = LIST_ESCAPE_LIST_CONTEXT(list);

	name = list_escape(name, TRUE);
	return mlist->module_ctx.super.
		get_mailbox_name_status(list, name, status);
}

static bool listescape_is_valid_existing_name(struct mailbox_list *list,
					      const char *name)
{
	struct listescape_mailbox_list *mlist = LIST_ESCAPE_LIST_CONTEXT(list);

	name = list_escape(name, TRUE);
	return mlist->module_ctx.super.is_valid_existing_name(list, name);
}

static bool listescape_is_valid_create_name(struct mailbox_list *list,
					    const char *name)
{
	struct listescape_mailbox_list *mlist = LIST_ESCAPE_LIST_CONTEXT(list);

	name = list_escape(name, TRUE);
	return mlist->module_ctx.super.is_valid_create_name(list, name);
}

static void listescape_mail_storage_created(struct mail_storage *storage)
{
	struct listescape_mail_storage *mstorage;

	if (listescape_next_hook_mail_storage_created != NULL)
		listescape_next_hook_mail_storage_created(storage);

	if (storage->list->hierarchy_sep != REAL_SEP)
		return;

	mstorage = p_new(storage->pool, struct listescape_mail_storage, 1);
	mstorage->module_ctx.super = storage->v;
	storage->v.mailbox_open = listescape_mailbox_open;
	storage->v.mailbox_create = listescape_mailbox_create;

	MODULE_CONTEXT_SET(storage, listescape_storage_module, mstorage);
}

static void listescape_mailbox_list_created(struct mailbox_list *list)
{
	struct listescape_mailbox_list *mlist;

	if (listescape_next_hook_mailbox_list_created != NULL)
		listescape_next_hook_mailbox_list_created(list);

	if (list->hierarchy_sep != REAL_SEP)
		return;

	mlist = p_new(list->pool, struct listescape_mailbox_list, 1);
	mlist->module_ctx.super = list->v;
	mlist->list_name = str_new(list->pool, 256);
	list->v.iter_init = listescape_mailbox_list_iter_init;
	list->v.iter_next = listescape_mailbox_list_iter_next;
	list->v.iter_deinit = listescape_mailbox_list_iter_deinit;
	list->v.delete_mailbox = listescape_delete_mailbox;
	list->v.rename_mailbox = listescape_rename_mailbox;
	list->v.set_subscribed = listescape_set_subscribed;
	list->v.get_mailbox_name_status = listescape_get_mailbox_name_status;
	list->v.is_valid_existing_name = listescape_is_valid_existing_name;
	list->v.is_valid_create_name = listescape_is_valid_create_name;

	MODULE_CONTEXT_SET(list, listescape_list_module, mlist);
}

static void
listescape_mail_namespaces_created(struct mail_namespace *namespaces)
{
	for (; namespaces != NULL; namespaces = namespaces->next) {
		if (namespaces->real_sep == REAL_SEP)
			namespaces->real_sep = VIRTUAL_SEP;
	}

	if (listescape_next_hook_mail_namespaces_created != NULL)
		listescape_next_hook_mail_namespaces_created(namespaces);
}

void listescape_plugin_init(void);
void listescape_plugin_deinit(void);

void listescape_plugin_init(void)
{
	listescape_next_hook_mail_storage_created = hook_mail_storage_created;
	hook_mail_storage_created = listescape_mail_storage_created;

	listescape_next_hook_mailbox_list_created = hook_mailbox_list_created;
	hook_mailbox_list_created = listescape_mailbox_list_created;

	listescape_next_hook_mail_namespaces_created =
		hook_mail_namespaces_created;
	hook_mail_namespaces_created = listescape_mail_namespaces_created;
}

void listescape_plugin_deinit(void)
{
	hook_mail_storage_created = listescape_next_hook_mail_storage_created;
	hook_mailbox_list_created = listescape_next_hook_mailbox_list_created;
	hook_mail_namespaces_created =
		listescape_next_hook_mail_namespaces_created;
}
