/* Copyright (c) 2009-2016 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "lib-signals.h"
#include "ioloop.h"
#include "istream.h"
#include "istream-dot.h"
#include "istream-seekable.h"
#include "str.h"
#include "unichar.h"
#include "module-dir.h"
#include "wildcard-match.h"
#include "master-service.h"
#include "mail-user.h"
#include "mail-namespace.h"
#include "mail-storage.h"
#include "mail-storage-settings.h"
#include "mail-storage-service.h"
#include "mail-storage-hooks.h"
#include "mail-search-build.h"
#include "mail-search-parser.h"
#include "mailbox-list-iter.h"
#include "client-connection.h"
#include "doveadm.h"
#include "doveadm-settings.h"
#include "doveadm-print.h"
#include "doveadm-dsync.h"
#include "doveadm-mail.h"

#include <stdio.h>

#define DOVEADM_MAIL_CMD_INPUT_TIMEOUT_MSECS (5*60*1000)

ARRAY_TYPE(doveadm_mail_cmd) doveadm_mail_cmds;
void (*hook_doveadm_mail_init)(struct doveadm_mail_cmd_context *ctx);
struct doveadm_mail_cmd_module_register
	doveadm_mail_cmd_module_register = { 0 };
char doveadm_mail_cmd_hide = '\0';

static int killed_signo = 0;

bool doveadm_is_killed(void)
{
	return killed_signo != 0;
}

int doveadm_killed_signo(void)
{
	return killed_signo;
}

void doveadm_mail_failed_error(struct doveadm_mail_cmd_context *ctx,
			       enum mail_error error)
{
	int exit_code = 0;

	switch (error) {
	case MAIL_ERROR_NONE:
		i_unreached();
	case MAIL_ERROR_TEMP:
		exit_code = EX_TEMPFAIL;
		break;
	case MAIL_ERROR_NOTPOSSIBLE:
	case MAIL_ERROR_EXISTS:
	case MAIL_ERROR_CONVERSION:
	case MAIL_ERROR_INVALIDDATA:
		exit_code = DOVEADM_EX_NOTPOSSIBLE;
		break;
	case MAIL_ERROR_PARAMS:
		exit_code = EX_USAGE;
		break;
	case MAIL_ERROR_PERM:
		exit_code = EX_NOPERM;
		break;
	case MAIL_ERROR_NOQUOTA:
		exit_code = EX_CANTCREAT;
		break;
	case MAIL_ERROR_NOTFOUND:
		exit_code = DOVEADM_EX_NOTFOUND;
		break;
	case MAIL_ERROR_EXPUNGED:
	case MAIL_ERROR_INUSE:
		exit_code = EX_TEMPFAIL;
		break;
	}
	/* tempfail overrides all other exit codes, otherwise use whatever
	   error happened first */
	if (ctx->exit_code == 0 || exit_code == EX_TEMPFAIL)
		ctx->exit_code = exit_code;
}

void doveadm_mail_failed_storage(struct doveadm_mail_cmd_context *ctx,
				 struct mail_storage *storage)
{
	enum mail_error error;

	mail_storage_get_last_error(storage, &error);
	doveadm_mail_failed_error(ctx, error);
}

void doveadm_mail_failed_mailbox(struct doveadm_mail_cmd_context *ctx,
				 struct mailbox *box)
{
	doveadm_mail_failed_storage(ctx, mailbox_get_storage(box));
}

void doveadm_mail_failed_list(struct doveadm_mail_cmd_context *ctx,
			      struct mailbox_list *list)
{
	enum mail_error error;

	mailbox_list_get_last_error(list, &error);
	doveadm_mail_failed_error(ctx, error);
}

struct doveadm_mail_cmd_context *
doveadm_mail_cmd_alloc_size(size_t size)
{
	struct doveadm_mail_cmd_context *ctx;
	pool_t pool;

	i_assert(size >= sizeof(struct doveadm_mail_cmd_context));

	pool = pool_alloconly_create("doveadm mail cmd", 1024);
	ctx = p_malloc(pool, size);
	ctx->pool = pool;
	return ctx;
}

static int
cmd_purge_run(struct doveadm_mail_cmd_context *ctx, struct mail_user *user)
{
	struct mail_namespace *ns;
	struct mail_storage *storage;
	int ret = 0;

	for (ns = user->namespaces; ns != NULL; ns = ns->next) {
		if (ns->type != MAIL_NAMESPACE_TYPE_PRIVATE ||
		    ns->alias_for != NULL)
			continue;

		storage = mail_namespace_get_default_storage(ns);
		if (mail_storage_purge(storage) < 0) {
			i_error("Purging namespace '%s' failed: %s", ns->prefix,
				mail_storage_get_last_error(storage, NULL));
			doveadm_mail_failed_storage(ctx, storage);
			ret = -1;
		}
	}
	return ret;
}

static struct doveadm_mail_cmd_context *cmd_purge_alloc(void)
{
	struct doveadm_mail_cmd_context *ctx;

	ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
	ctx->v.run = cmd_purge_run;
	return ctx;
}

static void doveadm_mail_cmd_input_input(struct doveadm_mail_cmd_context *ctx)
{
	while (i_stream_read(ctx->cmd_input) > 0)
		i_stream_skip(ctx->cmd_input, i_stream_get_data_size(ctx->cmd_input));
	if (!ctx->cmd_input->eof)
		return;

	if (ctx->cmd_input->stream_errno != 0) {
		i_error("read(%s) failed: %s",
			i_stream_get_name(ctx->cmd_input),
			i_stream_get_error(ctx->cmd_input));
	}
	io_loop_stop(current_ioloop);
}

static void doveadm_mail_cmd_input_timeout(struct doveadm_mail_cmd_context *ctx)
{
	struct istream *input;

	input = i_stream_create_error_str(ETIMEDOUT, "Timed out in %u secs",
			DOVEADM_MAIL_CMD_INPUT_TIMEOUT_MSECS/1000);
	i_stream_set_name(input, i_stream_get_name(ctx->cmd_input));
	i_stream_destroy(&ctx->cmd_input);
	ctx->cmd_input = input;
	io_loop_stop(current_ioloop);
}

static void doveadm_mail_cmd_input_read(struct doveadm_mail_cmd_context *ctx)
{
	struct ioloop *ioloop;
	struct io *io;
	struct timeout *to;

	ioloop = io_loop_create();
	io = io_add(ctx->cmd_input_fd, IO_READ,
		    doveadm_mail_cmd_input_input, ctx);
	to = timeout_add(DOVEADM_MAIL_CMD_INPUT_TIMEOUT_MSECS,
			 doveadm_mail_cmd_input_timeout, ctx);
	/* read the pending input from stream. */
	io_loop_set_running(ioloop);
	doveadm_mail_cmd_input_input(ctx);
	if (io_loop_is_running(ioloop))
		io_loop_run(ioloop);
	io_remove(&io);
	timeout_remove(&to);
	io_loop_destroy(&ioloop);

	i_assert(ctx->cmd_input->eof);
	i_stream_seek(ctx->cmd_input, 0);
}

void doveadm_mail_get_input(struct doveadm_mail_cmd_context *ctx)
{
	struct istream *inputs[2];

	if (ctx->cmd_input != NULL)
		return;

	if (ctx->conn != NULL)
		inputs[0] = i_stream_create_dot(ctx->conn->input, FALSE);
	else {
		inputs[0] = i_stream_create_fd(STDIN_FILENO, 1024*1024, FALSE);
		i_stream_set_name(inputs[0], "stdin");
	}
	inputs[1] = NULL;
	ctx->cmd_input_fd = i_stream_get_fd(inputs[0]);
	ctx->cmd_input = i_stream_create_seekable_path(inputs, 1024*256,
						       "/tmp/doveadm.");
	i_stream_set_name(ctx->cmd_input, i_stream_get_name(inputs[0]));
	i_stream_unref(&inputs[0]);

	doveadm_mail_cmd_input_read(ctx);
}

struct mailbox *
doveadm_mailbox_find(struct mail_user *user, const char *mailbox)
{
	struct mail_namespace *ns;

	if (!uni_utf8_str_is_valid(mailbox)) {
		i_fatal_status(EX_DATAERR,
			       "Mailbox name not valid UTF-8: %s", mailbox);
	}

	ns = mail_namespace_find(user->namespaces, mailbox);
	return mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_IGNORE_ACLS);
}

struct mail_search_args *
doveadm_mail_build_search_args(const char *const args[])
{
	struct mail_search_parser *parser;
	struct mail_search_args *sargs;
	const char *error, *charset = "UTF-8";

	parser = mail_search_parser_init_cmdline(args);
	if (mail_search_build(mail_search_register_get_human(),
			      parser, &charset, &sargs, &error) < 0)
		i_fatal("%s", error);
	mail_search_parser_deinit(&parser);
	return sargs;
}

static int cmd_force_resync_box(struct doveadm_mail_cmd_context *ctx,
				const struct mailbox_info *info)
{
	struct mailbox *box;
	int ret = 0;

	box = mailbox_alloc(info->ns->list, info->vname,
			    MAILBOX_FLAG_IGNORE_ACLS);
	if (mailbox_open(box) < 0) {
		i_error("Opening mailbox %s failed: %s", info->vname,
			mailbox_get_last_error(box, NULL));
		doveadm_mail_failed_mailbox(ctx, box);
		ret = -1;
	} else if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FORCE_RESYNC |
				MAILBOX_SYNC_FLAG_FIX_INCONSISTENT) < 0) {
		i_error("Forcing a resync on mailbox %s failed: %s",
			info->vname, mailbox_get_last_error(box, NULL));
		doveadm_mail_failed_mailbox(ctx, box);
		ret = -1;
	}
	mailbox_free(&box);
	return ret;
}

static int cmd_force_resync_run(struct doveadm_mail_cmd_context *ctx,
				struct mail_user *user)
{
	const enum mailbox_list_iter_flags iter_flags =
		MAILBOX_LIST_ITER_RETURN_NO_FLAGS |
		MAILBOX_LIST_ITER_STAR_WITHIN_NS;
	const enum mail_namespace_type ns_mask = MAIL_NAMESPACE_TYPE_MASK_ALL;
	struct mailbox_list_iterate_context *iter;
	const struct mailbox_info *info;
	int ret = 0;

	iter = mailbox_list_iter_init_namespaces(user->namespaces, ctx->args,
						 ns_mask, iter_flags);
	while ((info = mailbox_list_iter_next(iter)) != NULL) {
		if ((info->flags & (MAILBOX_NOSELECT |
				    MAILBOX_NONEXISTENT)) == 0) T_BEGIN {
			if (cmd_force_resync_box(ctx, info) < 0)
				ret = -1;
		} T_END;
	}
	if (mailbox_list_iter_deinit(&iter) < 0) {
		i_error("Listing mailboxes failed: %s",
			mailbox_list_get_last_error(user->namespaces->list, NULL));
		doveadm_mail_failed_list(ctx, user->namespaces->list);
		ret = -1;
	}
	return ret;
}

static void
cmd_force_resync_init(struct doveadm_mail_cmd_context *_ctx ATTR_UNUSED,
		      const char *const args[])
{
	if (args[0] == NULL)
		doveadm_mail_help_name("force-resync");
}

static struct doveadm_mail_cmd_context *cmd_force_resync_alloc(void)
{
	struct doveadm_mail_cmd_context *ctx;

	ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context);
	ctx->v.init = cmd_force_resync_init;
	ctx->v.run = cmd_force_resync_run;
	return ctx;
}

static void
doveadm_cctx_to_storage_service_input(const struct doveadm_cmd_context *cctx,
					struct mail_storage_service_input *input_r)
{
	memset(input_r, 0, sizeof(*input_r));
	input_r->service = "doveadm";
	input_r->remote_ip = cctx->remote_ip;
	input_r->remote_port = cctx->remote_port;
	input_r->local_ip = cctx->local_ip;
	input_r->local_port = cctx->local_port;
	input_r->username = cctx->username;
}

static int
doveadm_mail_next_user(struct doveadm_mail_cmd_context *ctx,
		       const struct doveadm_cmd_context *cctx,
		       const char **error_r)
{
	struct mail_storage_service_input input;
	const char *error, *ip;
	int ret;

	ip = net_ip2addr(&cctx->remote_ip);
	if (ip[0] == '\0')
		i_set_failure_prefix("doveadm(%s): ", cctx->username);
	else
		i_set_failure_prefix("doveadm(%s,%s): ", ip, cctx->username);
	doveadm_cctx_to_storage_service_input(cctx, &input);

	/* see if we want to execute this command via (another)
	   doveadm server */
	ret = doveadm_mail_server_user(ctx, &input, error_r);
	if (ret != 0)
		return ret;

	ret = mail_storage_service_lookup(ctx->storage_service, &input,
					  &ctx->cur_service_user, &error);
	if (ret <= 0) {
		if (ret < 0) {
			*error_r = t_strdup_printf("User lookup failed: %s",
						   error);
		}
		return ret;
	}

	if (ctx->v.prerun != NULL) {
		if (ctx->v.prerun(ctx, ctx->cur_service_user, error_r) < 0) {
			mail_storage_service_user_free(&ctx->cur_service_user);
			return -1;
		}
	}

	ret = mail_storage_service_next(ctx->storage_service,
					ctx->cur_service_user,
					&ctx->cur_mail_user);
	if (ret < 0) {
		*error_r = "User init failed";
		mail_storage_service_user_free(&ctx->cur_service_user);
		return ret;
	}

	if (ctx->cmd_input != NULL)
		i_stream_seek(ctx->cmd_input, 0);
	if (ctx->v.run(ctx, ctx->cur_mail_user) < 0) {
		i_assert(ctx->exit_code != 0);
	}
	mail_user_unref(&ctx->cur_mail_user);
	mail_storage_service_user_free(&ctx->cur_service_user);
	return 1;
}

static void sig_die(const siginfo_t *si, void *context ATTR_UNUSED)
{
	killed_signo = si->si_signo;
}

int doveadm_mail_single_user(struct doveadm_mail_cmd_context *ctx,
			     const struct doveadm_cmd_context *cctx,
			     const char **error_r)
{
	i_assert(cctx->username != NULL);

	doveadm_cctx_to_storage_service_input(cctx, &ctx->storage_service_input);
	ctx->cur_client_ip = cctx->remote_ip;
	ctx->cur_username = cctx->username;
	ctx->storage_service = mail_storage_service_init(master_service, NULL,
							 ctx->service_flags);
	ctx->v.init(ctx, ctx->args);
	if (hook_doveadm_mail_init != NULL)
		hook_doveadm_mail_init(ctx);

	lib_signals_set_handler(SIGINT, 0, sig_die, NULL);
	lib_signals_set_handler(SIGTERM, 0, sig_die, NULL);

	return doveadm_mail_next_user(ctx, cctx, error_r);
}

static void
doveadm_mail_all_users(struct doveadm_mail_cmd_context *ctx,
		       const char *wildcard_user)
{
	struct doveadm_cmd_context cctx;
	unsigned int user_idx;
	const char *ip, *user, *error;
	int ret;

	ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;

	memset(&cctx, 0, sizeof(cctx));

	doveadm_cctx_to_storage_service_input(&cctx, &ctx->storage_service_input);
	ctx->storage_service = mail_storage_service_init(master_service, NULL,
							 ctx->service_flags);
        lib_signals_set_handler(SIGINT, 0, sig_die, NULL);
	lib_signals_set_handler(SIGTERM, 0, sig_die, NULL);

	ctx->v.init(ctx, ctx->args);

	mail_storage_service_all_init(ctx->storage_service);

	if (hook_doveadm_mail_init != NULL)
		hook_doveadm_mail_init(ctx);

	user_idx = 0;
	while ((ret = ctx->v.get_next_user(ctx, &user)) > 0) {
		if (wildcard_user != NULL) {
			if (!wildcard_match_icase(user, wildcard_user))
				continue;
		}
		cctx.username = user;
		ctx->cur_username = user;
		doveadm_print_sticky("username", user);
		T_BEGIN {
			ret = doveadm_mail_next_user(ctx, &cctx, &error);
			if (ret < 0)
				i_error("%s", error);
			else if (ret == 0)
				i_info("User no longer exists, skipping");
		} T_END;
		if (ret == -1)
			break;
		if (doveadm_verbose) {
			if (++user_idx % 100 == 0) {
				printf("\r%d", user_idx);
				fflush(stdout);
			}
		}
		if (killed_signo != 0) {
			i_warning("Killed with signal %d", killed_signo);
			ret = -1;
			break;
		}
	}
	if (doveadm_verbose)
		printf("\n");
	ip = net_ip2addr(&ctx->cur_client_ip);
	if (ip[0] == '\0')
		i_set_failure_prefix("doveadm: ");
	else
		i_set_failure_prefix("doveadm(%s): ", ip);
	if (ret < 0) {
		i_error("Failed to iterate through some users");
		ctx->exit_code = EX_TEMPFAIL;
	}
}

static void
doveadm_mail_cmd_init_noop(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED,
			   const char *const args[] ATTR_UNUSED)
{
}

static int
doveadm_mail_cmd_get_next_user(struct doveadm_mail_cmd_context *ctx,
			       const char **username_r)
{
	if (ctx->users_list_input == NULL)
		return mail_storage_service_all_next(ctx->storage_service, username_r);

	*username_r = i_stream_read_next_line(ctx->users_list_input);
	if (ctx->users_list_input->stream_errno != 0) {
		i_error("read(%s) failed: %s",
			i_stream_get_name(ctx->users_list_input),
			i_stream_get_error(ctx->users_list_input));
		return -1;
	}
	return *username_r != NULL ? 1 : 0;
}

static void
doveadm_mail_cmd_deinit_noop(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED)
{
}

struct doveadm_mail_cmd_context *
doveadm_mail_cmd_init(const struct doveadm_mail_cmd *cmd,
		      const struct doveadm_settings *set)
{
	struct doveadm_mail_cmd_context *ctx;

	ctx = cmd->alloc();
	ctx->set = set;
	ctx->cmd = cmd;
	if (ctx->v.init == NULL)
		ctx->v.init = doveadm_mail_cmd_init_noop;
	if (ctx->v.get_next_user == NULL)
		ctx->v.get_next_user = doveadm_mail_cmd_get_next_user;
	if (ctx->v.deinit == NULL)
		ctx->v.deinit = doveadm_mail_cmd_deinit_noop;

	p_array_init(&ctx->module_contexts, ctx->pool, 5);
	return ctx;
}

static struct doveadm_mail_cmd_context *
doveadm_mail_cmdline_init(const struct doveadm_mail_cmd *cmd)
{
	struct doveadm_mail_cmd_context *ctx;

	ctx = doveadm_mail_cmd_init(cmd, doveadm_settings);
	ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT;
	if (doveadm_debug)
		ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_DEBUG;
	ctx->cur_username = getenv("USER");
	return ctx;
}

static void
doveadm_mail_cmd_exec(struct doveadm_mail_cmd_context *ctx,
		      const char *wildcard_user)
{
	int ret;
	const char *error;

	if (ctx->v.preinit != NULL)
		ctx->v.preinit(ctx);

	ctx->iterate_single_user =
		!ctx->iterate_all_users && wildcard_user == NULL;
	if (doveadm_print_is_initialized() && !ctx->iterate_single_user) {
		doveadm_print_header("username", "Username",
				     DOVEADM_PRINT_HEADER_FLAG_STICKY |
				     DOVEADM_PRINT_HEADER_FLAG_HIDE_TITLE);
	}

	if (ctx->iterate_single_user) {
		struct doveadm_cmd_context cctx;

		if (ctx->cur_username == NULL)
			i_fatal_status(EX_USAGE, "USER environment is missing and -u option not used");
		if (!ctx->cli) {
			/* we may access multiple users */
			ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP;
		}

		memset(&cctx, 0, sizeof(cctx));
		cctx.username = ctx->cur_username;
		ret = doveadm_mail_single_user(ctx, &cctx, &error);
		if (ret < 0) {
			/* user lookup/init failed somehow */
			doveadm_exit_code = EX_TEMPFAIL;
			i_error("%s", error);
		} else if (ret == 0) {
			doveadm_exit_code = EX_NOUSER;
			i_error("User doesn't exist");
		}
	} else {
		ctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP;
		doveadm_mail_all_users(ctx, wildcard_user);
	}
	if (ctx->search_args != NULL)
		mail_search_args_unref(&ctx->search_args);
	doveadm_mail_server_flush();
	ctx->v.deinit(ctx);
	doveadm_print_flush();

	/* service deinit unloads mail plugins, so do it late */
	mail_storage_service_deinit(&ctx->storage_service);

	if (ctx->exit_code != 0)
		doveadm_exit_code = ctx->exit_code;
}

static void doveadm_mail_cmd_free(struct doveadm_mail_cmd_context *ctx)
{
	if (ctx->users_list_input != NULL)
		i_stream_unref(&ctx->users_list_input);
	if (ctx->cmd_input != NULL)
		i_stream_unref(&ctx->cmd_input);
	pool_unref(&ctx->pool);
}

static void
doveadm_mail_cmd(const struct doveadm_mail_cmd *cmd, int argc, char *argv[])
{
	struct doveadm_mail_cmd_context *ctx;
	const char *getopt_args, *wildcard_user;
	int c;

	ctx = doveadm_mail_cmdline_init(cmd);
	ctx->full_args = (const void *)(argv + 1);
	ctx->cli = TRUE;

	getopt_args = "AF:S:u:";
	/* keep context's getopt_args first in case it contains '+' */
	if (ctx->getopt_args != NULL)
		getopt_args = t_strconcat(ctx->getopt_args, getopt_args, NULL);
	i_assert(master_getopt_str_is_valid(getopt_args));

	wildcard_user = NULL;
	while ((c = getopt(argc, argv, getopt_args)) > 0) {
		switch (c) {
		case 'A':
			ctx->iterate_all_users = TRUE;
			break;
		case 'S':
			doveadm_settings->doveadm_socket_path = optarg;
			if (doveadm_settings->doveadm_worker_count == 0)
				doveadm_settings->doveadm_worker_count = 1;
			break;
		case 'u':
			ctx->service_flags |=
				MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
			ctx->cur_username = optarg;
			if (strchr(ctx->cur_username, '*') != NULL ||
			    strchr(ctx->cur_username, '?') != NULL) {
				wildcard_user = ctx->cur_username;
				ctx->cur_username = NULL;
			}
			break;
		case 'F':
			ctx->service_flags |=
				MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
			wildcard_user = "*";
			ctx->users_list_input =
				i_stream_create_file(optarg, 1024);
			break;
		default:
			if (ctx->v.parse_arg == NULL ||
			    !ctx->v.parse_arg(ctx, c))
				doveadm_mail_help(cmd);
		}
	}
	argv += optind;
	if (argv[0] != NULL && cmd->usage_args == NULL) {
		i_fatal_status(EX_USAGE, "doveadm %s: Unknown parameter: %s",
			       cmd->name, argv[0]);
	}
	ctx->args = (const void *)argv;
	doveadm_mail_cmd_exec(ctx, wildcard_user);
	doveadm_mail_cmd_free(ctx);
}

static bool
doveadm_mail_cmd_try_find_multi_word(const struct doveadm_mail_cmd *cmd,
				     const char *cmdname, int *argc,
				     const char *const **argv)
{
	unsigned int len;

	if (*argc < 2)
		return FALSE;
	*argc -= 1;
	*argv += 1;

	len = strlen((*argv)[0]);
	if (strncmp(cmdname, (*argv)[0], len) != 0)
		return FALSE;

	if (cmdname[len] == ' ') {
		/* more args */
		return doveadm_mail_cmd_try_find_multi_word(cmd, cmdname + len + 1,
							    argc, argv);
	}
	if (cmdname[len] != '\0')
		return FALSE;

	/* match */
	return TRUE;
}

const struct doveadm_mail_cmd *
doveadm_mail_cmd_find_from_argv(const char *cmd_name, int *argc,
				const char *const **argv)
{
	const struct doveadm_mail_cmd *cmd;
	unsigned int cmd_name_len;
	const char *const *orig_argv;
	int orig_argc;

	i_assert(*argc > 0);

	cmd_name_len = strlen(cmd_name);
	array_foreach(&doveadm_mail_cmds, cmd) {
		if (strcmp(cmd->name, cmd_name) == 0)
			return cmd;

		/* see if it matches a multi-word command */
		if (strncmp(cmd->name, cmd_name, cmd_name_len) == 0 &&
		    cmd->name[cmd_name_len] == ' ') {
			const char *subcmd = cmd->name + cmd_name_len + 1;

			orig_argc = *argc;
			orig_argv = *argv;
			if (doveadm_mail_cmd_try_find_multi_word(cmd, subcmd,
								 argc, argv))
				return cmd;
			*argc = orig_argc;
			*argv = orig_argv;
		}
	}

	return NULL;
}

bool doveadm_mail_try_run(const char *cmd_name, int argc, char *argv[])
{
	const struct doveadm_mail_cmd *cmd;

	cmd = doveadm_mail_cmd_find_from_argv(cmd_name, &argc, (void *)&argv);
	if (cmd == NULL)
		return FALSE;
	doveadm_mail_cmd(cmd, argc, argv);
	return TRUE;
}

void doveadm_mail_register_cmd(const struct doveadm_mail_cmd *cmd)
{
	/* for now we'll just assume that cmd will be permanently in memory */
	array_append(&doveadm_mail_cmds, cmd, 1);
}

const struct doveadm_mail_cmd *doveadm_mail_cmd_find(const char *cmd_name)
{
	const struct doveadm_mail_cmd *cmd;

	array_foreach(&doveadm_mail_cmds, cmd) {
		if (strcmp(cmd->name, cmd_name) == 0)
			return cmd;
	}
	return NULL;
}

void doveadm_mail_usage(string_t *out)
{
	const struct doveadm_mail_cmd *cmd;

	array_foreach(&doveadm_mail_cmds, cmd) {
		if (cmd->usage_args == &doveadm_mail_cmd_hide)
			continue;
		str_printfa(out, "%s\t"DOVEADM_CMD_MAIL_USAGE_PREFIX, cmd->name);
		if (cmd->usage_args != NULL)
			str_append(out, cmd->usage_args);
		str_append_c(out, '\n');
	}
}

void doveadm_mail_help(const struct doveadm_mail_cmd *cmd)
{
	fprintf(stderr, "doveadm %s "DOVEADM_CMD_MAIL_USAGE_PREFIX" %s\n",
		cmd->name, cmd->usage_args == NULL ? "" : cmd->usage_args);
	exit(EX_USAGE);
}

void doveadm_mail_try_help_name(const char *cmd_name)
{
	const struct doveadm_cmd_ver2 *cmd2;
	const struct doveadm_mail_cmd *cmd;

	cmd2 = doveadm_cmd_find_ver2(cmd_name);
	if (cmd2 != NULL)
		help_ver2(cmd2);

	cmd = doveadm_mail_cmd_find(cmd_name);
	if (cmd != NULL)
		doveadm_mail_help(cmd);
}

bool doveadm_mail_has_subcommands(const char *cmd_name)
{
	const struct doveadm_mail_cmd *cmd;
	unsigned int len = strlen(cmd_name);

	array_foreach(&doveadm_mail_cmds, cmd) {
		if (strncmp(cmd->name, cmd_name, len) == 0 &&
		    cmd->name[len] == ' ')
			return TRUE;
	}
	return FALSE;
}

void doveadm_mail_help_name(const char *cmd_name)
{
	doveadm_mail_try_help_name(cmd_name);
	i_fatal("Missing help for command %s", cmd_name);
}

static struct doveadm_cmd_ver2 doveadm_cmd_force_resync_ver2 = {
	.name = "force-resync",
	.mail_cmd = cmd_force_resync_alloc,
	.usage = DOVEADM_CMD_MAIL_USAGE_PREFIX "<mailbox mask>",
DOVEADM_CMD_PARAMS_START
DOVEADM_CMD_MAIL_COMMON
DOVEADM_CMD_PARAM('\0', "mailbox-mask", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
DOVEADM_CMD_PARAMS_END
};

static struct doveadm_cmd_ver2 doveadm_cmd_purge_ver2 = {
	.name = "purge",
	.mail_cmd = cmd_purge_alloc,
	.usage = DOVEADM_CMD_MAIL_USAGE_PREFIX,
DOVEADM_CMD_PARAMS_START
DOVEADM_CMD_MAIL_COMMON
DOVEADM_CMD_PARAMS_END
};


static struct doveadm_mail_cmd *mail_commands[] = {
	&cmd_batch,
	&cmd_dsync_backup,
	&cmd_dsync_mirror,
	&cmd_dsync_server
};

static struct doveadm_cmd_ver2 *mail_commands_ver2[] = {
	&doveadm_cmd_mailbox_metadata_set_ver2,
	&doveadm_cmd_mailbox_metadata_unset_ver2,
	&doveadm_cmd_mailbox_metadata_get_ver2,
	&doveadm_cmd_mailbox_metadata_list_ver2,
	&doveadm_cmd_mailbox_status_ver2,
	&doveadm_cmd_mailbox_list_ver2,
	&doveadm_cmd_mailbox_create_ver2,
	&doveadm_cmd_mailbox_delete_ver2,
	&doveadm_cmd_mailbox_rename_ver2,
	&doveadm_cmd_mailbox_subscribe_ver2,
	&doveadm_cmd_mailbox_unsubscribe_ver2,
	&doveadm_cmd_fetch_ver2,
	&doveadm_cmd_save_ver2,
	&doveadm_cmd_index_ver2,
	&doveadm_cmd_altmove_ver2,
	&doveadm_cmd_deduplicate_ver2,
	&doveadm_cmd_expunge_ver2,
	&doveadm_cmd_flags_add_ver2,
	&doveadm_cmd_flags_remove_ver2,
	&doveadm_cmd_flags_replace_ver2,
	&doveadm_cmd_import_ver2,
	&doveadm_cmd_force_resync_ver2,
	&doveadm_cmd_purge_ver2,
	&doveadm_cmd_search_ver2,
	&doveadm_cmd_copy_ver2,
	&doveadm_cmd_move_ver2
};

void doveadm_mail_init(void)
{
	struct module_dir_load_settings mod_set;
	unsigned int i;

	i_array_init(&doveadm_mail_cmds, 32);
	for (i = 0; i < N_ELEMENTS(mail_commands); i++)
		doveadm_mail_register_cmd(mail_commands[i]);

	for (i = 0; i < N_ELEMENTS(mail_commands_ver2); i++)
		doveadm_cmd_register_ver2(mail_commands_ver2[i]);

	memset(&mod_set, 0, sizeof(mod_set));
	mod_set.abi_version = DOVECOT_ABI_VERSION;
	mod_set.require_init_funcs = TRUE;
	mod_set.debug = doveadm_debug;
	mod_set.binary_name = "doveadm";

	/* load all configured mail plugins */
	mail_storage_service_modules =
		module_dir_load_missing(mail_storage_service_modules,
					doveadm_settings->mail_plugin_dir,
					doveadm_settings->mail_plugins,
					&mod_set);
}

void doveadm_mail_deinit(void)
{
	mail_storage_hooks_deinit();
	array_free(&doveadm_mail_cmds);
}

void
doveadm_cmd_ver2_to_mail_cmd_wrapper(struct doveadm_cmd_context *cctx)
{
	struct doveadm_mail_cmd_context *mctx;
	const char *wildcard_user;
	const char *fieldstr;

	ARRAY_TYPE(const_string) pargv;
	int i;
	struct doveadm_mail_cmd mail_cmd = {
		cctx->cmd->mail_cmd, cctx->cmd->name, cctx->cmd->usage
	};

	mctx = doveadm_mail_cmdline_init(&mail_cmd);

	mctx->iterate_all_users = FALSE;
	wildcard_user = NULL;
	p_array_init(&pargv, mctx->pool, 8);

	for(i=0;i<cctx->argc;i++) {
		const struct doveadm_cmd_param *arg = &cctx->argv[i];

		if (!arg->value_set)
			continue;

		if (strcmp(arg->name, "all-users") == 0) {
			mctx->iterate_all_users = arg->value.v_bool;
		} else if (strcmp(arg->name, "socket-path") == 0) {
			doveadm_settings->doveadm_socket_path = arg->value.v_string;
			if (doveadm_settings->doveadm_worker_count == 0)
				doveadm_settings->doveadm_worker_count = 1;
		} else if (strcmp(arg->name, "user") == 0) {
			mctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
			mctx->cur_username = arg->value.v_string;
			if (strchr(mctx->cur_username, '*') != NULL ||
			    strchr(mctx->cur_username, '?') != NULL) {
				wildcard_user = mctx->cur_username;
				mctx->cur_username = NULL;
			}
		} else if (strcmp(arg->name, "user-file") == 0) {
			mctx->service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
			wildcard_user = "*";
			mctx->users_list_input = arg->value.v_istream;
			i_stream_ref(mctx->users_list_input);
		} else if (strcmp(arg->name, "field") == 0 ||
			   strcmp(arg->name, "flag") == 0) {
			/* mailbox status, fetch, flags: convert an array into a
			   single space-separated parameter (alternative to
			   fieldstr) */
			fieldstr = p_array_const_string_join(mctx->pool,
					&arg->value.v_array, " ");
			array_append(&pargv, &fieldstr, 1);
		} else if (strcmp(arg->name, "file") == 0) {
			/* input for doveadm_mail_get_input(),
			   used by e.g. save */
			if (mctx->cmd_input != NULL) {
				i_error("Only one file input allowed: %s", arg->name);
				doveadm_mail_cmd_free(mctx);
				doveadm_exit_code = EX_USAGE;
				return;
			}
			mctx->cmd_input = arg->value.v_istream;
			i_stream_ref(mctx->cmd_input);

		/* Keep all named special parameters above this line */

		} else if (mctx->v.parse_arg != NULL && arg->short_opt != '\0') {
			optarg = (char*)arg->value.v_string;
			mctx->v.parse_arg(mctx, arg->short_opt);
		} else if ((arg->flags & CMD_PARAM_FLAG_POSITIONAL) != 0) {
			/* feed this into pargv */
			if (arg->type == CMD_PARAM_ARRAY)
				array_append_array(&pargv, &arg->value.v_array);
			else if (arg->type == CMD_PARAM_STR)
				array_append(&pargv, &arg->value.v_string, 1);
		} else {
			doveadm_exit_code = EX_USAGE;
			i_error("invalid parameter: %s", arg->name);
			doveadm_mail_cmd_free(mctx);
			return;
		}
	}

	array_append_zero(&pargv);
	mctx->args = array_idx(&pargv, 0);
	mctx->full_args = mctx->args;
	mctx->cli = cctx->cli;

	doveadm_mail_cmd_exec(mctx, wildcard_user);
	doveadm_mail_cmd_free(mctx);
}
