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

#include "lib.h"
#include "array.h"
#include "str.h"
#include "ioloop.h"
#include "unlink-directory.h"
#include "unichar.h"
#include "imap-match.h"
#include "imap-utf7.h"
#include "mailbox-tree.h"
#include "mailbox-list-delete.h"
#include "mailbox-list-subscriptions.h"
#include "mailbox-list-maildir.h"

#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>

struct maildir_list_iterate_context {
	struct mailbox_list_iterate_context ctx;

	const char *dir;
	char prefix_char;

        struct mailbox_tree_context *tree_ctx;
	struct mailbox_tree_iterate_context *tree_iter;

	struct mailbox_info info;
};

static void node_fix_parents(struct mailbox_node *node)
{
	/* Fix parent nodes' children states. also if we happened to create any
	   of the parents, we need to mark them nonexistent. */
	node = node->parent;
	for (; node != NULL; node = node->parent) {
		if ((node->flags & MAILBOX_MATCHED) == 0)
			node->flags |= MAILBOX_NONEXISTENT;

		node->flags |= MAILBOX_CHILDREN;
		node->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
	}
}

static void
maildir_fill_parents(struct maildir_list_iterate_context *ctx,
		     struct imap_match_glob *glob, bool update_only,
		     const char *vname)
{
	struct mail_namespace *ns = ctx->ctx.list->ns;
	struct mailbox_node *node;
	const char *p;
	size_t vname_len = strlen(vname);
	bool created;
	char ns_sep = mail_namespace_get_sep(ns);

	while ((p = strrchr(vname, ns_sep)) != NULL) {
		vname = t_strdup_until(vname, p);
		if (imap_match(glob, vname) != IMAP_MATCH_YES)
			continue;

		if (ns->prefix_len > 0 && vname_len == ns->prefix_len-1 &&
		    strncmp(vname, ns->prefix, ns->prefix_len - 1) == 0 &&
		    vname[ns->prefix_len-1] == ns_sep) {
			/* don't return matches to namespace prefix itself */
			continue;
		}

		created = FALSE;
		node = update_only ?
			mailbox_tree_lookup(ctx->tree_ctx, vname) :
			mailbox_tree_get(ctx->tree_ctx, vname, &created);
		if (node != NULL) {
			if (created) {
				/* we haven't yet seen this mailbox,
				   but we might see it later */
				node->flags = MAILBOX_NONEXISTENT;
			}
			if (!update_only)
				node->flags |= MAILBOX_MATCHED;
			node->flags |= MAILBOX_CHILDREN;
			node->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
			node_fix_parents(node);
		}
	}
}

static void maildir_set_children(struct maildir_list_iterate_context *ctx,
				 const char *vname)
{
	struct mailbox_node *node;
	const char *p;
	char hierarchy_sep;

	hierarchy_sep = mail_namespace_get_sep(ctx->ctx.list->ns);

	/* mark the first existing parent as containing children */
	while ((p = strrchr(vname, hierarchy_sep)) != NULL) {
		vname = t_strdup_until(vname, p);

		node = mailbox_tree_lookup(ctx->tree_ctx, vname);
		if (node != NULL) {
			node->flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
			node->flags |= MAILBOX_CHILDREN;
			break;
		}
	}
}

static int
maildir_fill_inbox(struct maildir_list_iterate_context *ctx,
		   struct imap_match_glob *glob, const char *inbox_name,
		   bool update_only)
{
	struct mailbox_node *node;
	enum mailbox_info_flags flags;
	enum imap_match_result match;
	bool created;
	int ret;

	if ((ctx->ctx.flags & MAILBOX_LIST_ITER_NO_AUTO_BOXES) == 0) {
		/* always show INBOX */
	} else {
		/* INBOX may be Maildir root or completely elsewhere.
		   show it only if it has already been created */
		ret = mailbox_list_mailbox(ctx->ctx.list, "INBOX", &flags);
		if (ret < 0)
			return -1;
		if ((flags & MAILBOX_NONEXISTENT) != 0)
			update_only = TRUE;
	}

	if (update_only) {
		node = mailbox_tree_lookup(ctx->tree_ctx, inbox_name);
		if (node != NULL)
			node->flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
		return 0;
	}

	/* add the INBOX only if it matches the patterns */
	match = imap_match(glob, inbox_name);
	if (match == IMAP_MATCH_PARENT)
		maildir_fill_parents(ctx, glob, FALSE, inbox_name);
	else if (match == IMAP_MATCH_YES) {
		node = mailbox_tree_get(ctx->tree_ctx, inbox_name, &created);
		if (created)
			node->flags = MAILBOX_NOCHILDREN;
		else
			node->flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
		node->flags |= MAILBOX_MATCHED;
	}
	return 0;
}

static bool
maildir_get_type(const char *dir, const char *fname,
		 enum mailbox_list_file_type *type_r,
		 enum mailbox_info_flags *flags)
{
	const char *path;
	struct stat st;

	path = *fname == '\0' ? dir :
		t_strdup_printf("%s/%s", dir, fname);
	if (stat(path, &st) < 0) {
		if (errno == ENOENT) {
			/* just deleted? */
			*flags |= MAILBOX_NONEXISTENT;
		} else {
			*flags |= MAILBOX_NOSELECT;
		}
		return FALSE;
	}

	if (S_ISDIR(st.st_mode)) {
		*type_r = MAILBOX_LIST_FILE_TYPE_DIR;
		return TRUE;
	} else {
		if (str_begins_with(fname, ".nfs"))
			*flags |= MAILBOX_NONEXISTENT;
		else
			*flags |= MAILBOX_NOSELECT;
		return FALSE;
	}
}

int maildir_list_get_mailbox_flags(struct mailbox_list *list,
				   const char *dir, const char *fname,
				   enum mailbox_list_file_type type,
				   enum mailbox_info_flags *flags_r)
{
	*flags_r = 0;

	switch (type) {
	case MAILBOX_LIST_FILE_TYPE_DIR:
	case MAILBOX_LIST_FILE_TYPE_FILE:
	case MAILBOX_LIST_FILE_TYPE_OTHER:
		break;
	case MAILBOX_LIST_FILE_TYPE_UNKNOWN:
	case MAILBOX_LIST_FILE_TYPE_SYMLINK:
		/* need to check with stat() to be sure */
		if (!list->mail_set->maildir_stat_dirs && *fname != '\0' &&
		    strcmp(list->name, MAILBOX_LIST_NAME_MAILDIRPLUSPLUS) == 0 &&
		    !str_begins_with(fname, ".nfs")) {
			/* just assume it's a valid mailbox */
			return 1;
		}

		if (!maildir_get_type(dir, fname, &type, flags_r))
			return 0;
		break;
	}

	switch (type) {
	case MAILBOX_LIST_FILE_TYPE_DIR:
		if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) != 0) {
			*flags_r |= MAILBOX_NOSELECT;
			return 0;
		}
		break;
	case MAILBOX_LIST_FILE_TYPE_FILE:
		if ((list->flags & MAILBOX_LIST_FLAG_MAILBOX_FILES) == 0) {
			*flags_r |= MAILBOX_NOSELECT;
			return 0;
		}
		break;
	case MAILBOX_LIST_FILE_TYPE_OTHER:
		*flags_r |= MAILBOX_NOSELECT;
		return 0;

	case MAILBOX_LIST_FILE_TYPE_UNKNOWN:
	case MAILBOX_LIST_FILE_TYPE_SYMLINK:
		i_unreached();
	}
	if (*fname != '\0') {
		/* this tells maildir storage code that it doesn't need to
		   see if cur/ exists, because just the existence of .dir/
		   assumes that the mailbox exists. */
		*flags_r |= MAILBOX_SELECT;
	}
	return 1;
}

static bool maildir_delete_trash_dir(struct maildir_list_iterate_context *ctx,
				     const char *fname)
{
	const char *path, *error;
	struct stat st;

	if (fname[1] != ctx->prefix_char || ctx->prefix_char == '\0' ||
	    strcmp(fname+2, MAILBOX_LIST_MAILDIR_TRASH_DIR_NAME) != 0)
		return FALSE;

	/* this directory is in the middle of being deleted, or the process
	   trying to delete it had died. delete it ourself if it's been there
	   longer than one hour. */
	path = t_strdup_printf("%s/%s", ctx->dir, fname);
	if (stat(path, &st) == 0 &&
	    st.st_mtime < ioloop_time - 3600)
		(void)mailbox_list_delete_trash(path, &error);

	return TRUE;
}

static int
maildir_fill_readdir_entry(struct maildir_list_iterate_context *ctx,
			   struct imap_match_glob *glob, const struct dirent *d,
			   bool update_only)
{
	struct mailbox_list *list = ctx->ctx.list;
	const char *fname, *storage_name, *vname;
	enum mailbox_info_flags flags;
	enum imap_match_result match;
	struct mailbox_node *node;
	bool created;
	int ret;

	fname = d->d_name;
	if (fname[0] == ctx->prefix_char)
		storage_name = fname + 1;
	else {
		if (ctx->prefix_char != '\0' || fname[0] == '.')
			return 0;
		storage_name = fname;
	}

	/* skip . and .. */
	if (fname[0] == '.' &&
	    (fname[1] == '\0' || (fname[1] == '.' && fname[2] == '\0')))
		return 0;

	vname = mailbox_list_get_vname(list, storage_name);
	if (!uni_utf8_str_is_valid(vname)) {
		/* the storage_name is completely invalid, rename it to
		   something more sensible. we could do this for all names that
		   aren't valid mUTF-7, but that might lead to accidents in
		   future when UTF-8 storage names are used */
		const char *src = t_strdup_printf("%s/%s", ctx->dir, fname);
		string_t *destvname = t_str_new(128);
		string_t *dest = t_str_new(128);

		if (uni_utf8_get_valid_data((const void *)fname,
					    strlen(fname), destvname))
			i_unreached(); /* already checked that it was invalid */

		str_append(dest, ctx->dir);
		str_append_c(dest, '/');
		(void)imap_utf8_to_utf7(str_c(destvname), dest);

		if (rename(src, str_c(dest)) < 0 && errno != ENOENT)
			e_error(ctx->ctx.list->event,
				"rename(%s, %s) failed: %m", src, str_c(dest));
		/* just skip this in this iteration, we'll see it on the
		   next list */
		return 0;
	}

	/* make sure the pattern matches */
	match = imap_match(glob, vname);
	if ((match & (IMAP_MATCH_YES | IMAP_MATCH_PARENT)) == 0)
		return 0;

	/* check if this is an actual mailbox */
	if (maildir_delete_trash_dir(ctx, fname))
		return 0;

	if ((ctx->ctx.flags & MAILBOX_LIST_ITER_SKIP_ALIASES) != 0) {
		ret = mailbox_list_dirent_is_alias_symlink(list, ctx->dir, d);
		if (ret != 0)
			return ret < 0 ? -1 : 0;
	}
	T_BEGIN {
		ret = list->v.get_mailbox_flags(list, ctx->dir, fname,
				mailbox_list_get_file_type(d), &flags);
	} T_END;
	if (ret <= 0)
		return ret;

	/* If the on-disk fname's storage_name does not re-escape back from
	   vname under the current rules, the entry was written with legacy
	   escape rules (e.g. an older version that escaped leading '~' on
	   every hierarchy part). Rename it so subsequent lookups find it.
	   Deferred until after get_mailbox_flags() and the alias-symlink
	   lstat() so those operations see the original on-disk path. */
	if (list->mail_set->mailbox_list_storage_escape_char[0] != '\0') T_BEGIN {
		const char *expected_storage_name =
			mailbox_list_get_storage_name(list, vname);
		const char *expected_fname = ctx->prefix_char != '\0' ?
			t_strdup_printf("%c%s", ctx->prefix_char,
					expected_storage_name) :
			expected_storage_name;
		ret = mailbox_list_try_migrate_legacy_escape(
			list, ctx->dir, fname, expected_fname);
		if (ret > 0)
			fname = expected_fname;
	} T_END_PASS_STR_IF(ret > 0, &fname);
	if (ret < 0)
		return -1;

	/* we know the children flags ourself, so ignore if any of
	   them were set. */
	flags &= ENUM_NEGATE(MAILBOX_NOINFERIORS | MAILBOX_CHILDREN | MAILBOX_NOCHILDREN);

	if ((match & IMAP_MATCH_PARENT) != 0)
		maildir_fill_parents(ctx, glob, update_only, vname);
	else {
		created = FALSE;
		node = update_only ?
			mailbox_tree_lookup(ctx->tree_ctx, vname) :
			mailbox_tree_get(ctx->tree_ctx, vname, &created);

		if (node != NULL) {
			if (created)
				node->flags = MAILBOX_NOCHILDREN;
			else
				node->flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT);
			if (!update_only)
				node->flags |= MAILBOX_MATCHED;
			node->flags |= flags;
			node_fix_parents(node);
		} else {
			i_assert(update_only);
			maildir_set_children(ctx, vname);
		}
	}
	return 0;
}

static int
maildir_fill_readdir(struct maildir_list_iterate_context *ctx,
		     struct imap_match_glob *glob, bool update_only)
{
	struct mailbox_list *list = ctx->ctx.list;
	struct mail_namespace *ns = list->ns;
	DIR *dirp;
	struct dirent *d;
	const char *vname;
	int ret = 0;

	dirp = opendir(ctx->dir);
	if (dirp == NULL) {
		if (ENOACCESS(errno)) {
			mailbox_list_set_critical(list, "%s",
				mail_error_eacces_msg("opendir", ctx->dir));
		} else if (errno != ENOENT) {
			mailbox_list_set_critical(list,
				"opendir(%s) failed: %m", ctx->dir);
			return -1;
		}
		return 0;
	}

	while ((d = readdir(dirp)) != NULL) {
		T_BEGIN {
			ret = maildir_fill_readdir_entry(ctx, glob, d,
							 update_only);
		} T_END;
		if (ret < 0)
			break;
	}

	if (closedir(dirp) < 0) {
		mailbox_list_set_critical(list, "readdir(%s) failed: %m",
					  ctx->dir);
		return -1;
	}
	if (ret < 0)
		return -1;

	if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
		/* make sure INBOX is listed */
		return maildir_fill_inbox(ctx, glob, "INBOX", update_only);
	} else if ((ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0) {
		/* show shared INBOX. */
		vname = mailbox_list_get_vname(ns->list, "INBOX");
		return maildir_fill_inbox(ctx, glob, vname, update_only);
	} else {
		return 0;
	}
}

struct mailbox_list_iterate_context *
maildir_list_iter_init(struct mailbox_list *_list, const char *const *patterns,
		       enum mailbox_list_iter_flags flags)
{
	struct maildir_mailbox_list *list =
		(struct maildir_mailbox_list *)_list;
	struct maildir_list_iterate_context *ctx;
	pool_t pool;
	char ns_sep = mail_namespace_get_sep(_list->ns);
	int ret;

	pool = pool_alloconly_create("mailbox list maildir iter", 1024);
	ctx = p_new(pool, struct maildir_list_iterate_context, 1);
	ctx->ctx.pool = pool;
	ctx->ctx.list = _list;
	ctx->ctx.flags = flags;
	ctx->ctx.glob = imap_match_init_multiple(pool, patterns, TRUE, ns_sep);
	array_create(&ctx->ctx.module_contexts, pool, sizeof(void *), 5);

	ctx->tree_ctx = mailbox_tree_init(ns_sep);
	ctx->info.ns = _list->ns;
	ctx->prefix_char = strcmp(_list->name, MAILBOX_LIST_NAME_IMAPDIR) == 0 ?
		'\0' : list->sep;

	if (_list->mail_set->mailbox_list_iter_from_index_dir)
		ctx->dir = _list->mail_set->mail_index_path;
	else
		ctx->dir = _list->mail_set->mail_path;

	T_BEGIN {
		/* Add/update mailbox list with flags */
		ret = maildir_fill_readdir(ctx, ctx->ctx.glob, FALSE);
	} T_END;

	if (ret < 0) {
		ctx->ctx.failed = TRUE;
		return &ctx->ctx;
	}

	ctx->tree_iter = mailbox_tree_iterate_init(ctx->tree_ctx, NULL,
						   MAILBOX_MATCHED);
	return &ctx->ctx;
}

int maildir_list_iter_deinit(struct mailbox_list_iterate_context *_ctx)
{
	struct maildir_list_iterate_context *ctx =
		(struct maildir_list_iterate_context *)_ctx;
	int ret = _ctx->failed ? -1 : 0;

	if (ctx->tree_iter != NULL)
		mailbox_tree_iterate_deinit(&ctx->tree_iter);
	mailbox_tree_deinit(&ctx->tree_ctx);
	pool_unref(&ctx->ctx.pool);
	return ret;
}

const struct mailbox_info *
maildir_list_iter_next(struct mailbox_list_iterate_context *_ctx)
{
	struct maildir_list_iterate_context *ctx =
		(struct maildir_list_iterate_context *)_ctx;
	struct mailbox_node *node;

	if (_ctx->failed)
		return NULL;

	node = mailbox_tree_iterate_next(ctx->tree_iter, &ctx->info.vname);
	if (node == NULL)
		return mailbox_list_iter_default_next(_ctx);

	ctx->info.flags = node->flags;
	if (strcmp(ctx->info.vname, "INBOX") == 0 &&
	    mail_namespace_is_inbox_noinferiors(ctx->info.ns)) {
		i_assert((ctx->info.flags & MAILBOX_NOCHILDREN) != 0);
		ctx->info.flags &= ENUM_NEGATE(MAILBOX_NOCHILDREN);
		ctx->info.flags |= MAILBOX_NOINFERIORS;
	}
	return &ctx->info;
}
