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

#include "imap-common.h"
#include "str.h"
#include "mailbox-list-iter.h"
#include "imap-quote.h"
#include "imap-commands.h"
#include "imap-fetch.h"
#include "imap-list.h"
#include "imap-status.h"
#include "imap-notify.h"

#define IMAP_NOTIFY_MAX_NAMES_PER_NS 100

static const char *imap_notify_event_names[] = {
	"MessageNew", "MessageExpunge", "FlagChange", "AnnotationChange",
	"MailboxName", "SubscriptionChange", "MailboxMetadataChange",
	"ServerMetadataChange"
};

static int
cmd_notify_parse_event(const struct imap_arg *arg,
		       enum imap_notify_event *event_r)
{
	const char *str;
	unsigned int i;

	if (!imap_arg_get_atom(arg, &str))
		return -1;

	for (i = 0; i < N_ELEMENTS(imap_notify_event_names); i++) {
		if (strcasecmp(str, imap_notify_event_names[i]) == 0) {
			*event_r = (enum imap_notify_event)(1 << i);
			return 0;
		}
	}
	return -1;
}

static int
cmd_notify_parse_fetch(struct imap_notify_context *ctx,
		       const struct imap_arg *list)
{
	return imap_fetch_att_list_parse(ctx->client, ctx->pool, list,
					 &ctx->fetch_ctx, &ctx->error);
}

static int
cmd_notify_set_selected(struct imap_notify_context *ctx,
			const struct imap_arg *events)
{
#define EV_NEW_OR_EXPUNGE \
	(IMAP_NOTIFY_EVENT_MESSAGE_NEW | IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE)
	const struct imap_arg *list, *fetch_att_list;
	const char *str;
	enum imap_notify_event event;

	if (imap_arg_get_atom(events, &str) &&
	    strcasecmp(str, "NONE") == 0) {
		/* no events for selected mailbox. this is also the default
		   when NOTIFY command doesn't specify it explicitly */
		return 0;
	}

	if (!imap_arg_get_list(events, &list))
		return -1;

	for (; list->type != IMAP_ARG_EOL; list++) {
		if (cmd_notify_parse_event(list, &event) < 0)
			return -1;
		ctx->selected_events |= event;
		ctx->global_used_events |= event;

		if (event == IMAP_NOTIFY_EVENT_MESSAGE_NEW &&
		    imap_arg_get_list(&list[1], &fetch_att_list)) {
			/* MessageNew: list of fetch-att */
			if (cmd_notify_parse_fetch(ctx, fetch_att_list) < 0)
				return -1;
			list++;
		}
	}

	/* if MessageNew or MessageExpunge is specified, both of them must */
	if ((ctx->selected_events & EV_NEW_OR_EXPUNGE) != 0 &&
	    (ctx->selected_events & EV_NEW_OR_EXPUNGE) != EV_NEW_OR_EXPUNGE) {
		ctx->error = "MessageNew and MessageExpunge must be together";
		return -1;
	}

	/* if FlagChange or AnnotationChange is specified,
	   MessageNew and MessageExpunge must also be specified */
	if ((ctx->selected_events &
	     (IMAP_NOTIFY_EVENT_FLAG_CHANGE |
	      IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE)) != 0 &&
	    (ctx->selected_events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) == 0) {
		ctx->error = "FlagChange requires MessageNew and MessageExpunge";
		return -1;
	}
	return 0;
}

static struct imap_notify_namespace *
imap_notify_namespace_get(struct imap_notify_context *ctx,
			  struct mail_namespace *ns)
{
	struct imap_notify_namespace *notify_ns;

	array_foreach_modifiable(&ctx->namespaces, notify_ns) {
		if (notify_ns->ns == ns)
			return notify_ns;
	}
	notify_ns = array_append_space(&ctx->namespaces);
	notify_ns->ctx = ctx;
	notify_ns->ns = ns;
	p_array_init(&notify_ns->mailboxes, ctx->pool, 4);
	return notify_ns;
}

static struct imap_notify_mailboxes *
imap_notify_mailboxes_get(struct imap_notify_namespace *notify_ns,
			  enum imap_notify_type type,
			  enum imap_notify_event events)
{
	struct imap_notify_mailboxes *notify_boxes;

	array_foreach_modifiable(&notify_ns->mailboxes, notify_boxes) {
		if (notify_boxes->type == type &&
		    notify_boxes->events == events)
			return notify_boxes;
	}
	notify_boxes = array_append_space(&notify_ns->mailboxes);
	notify_boxes->type = type;
	notify_boxes->events = events;
	p_array_init(&notify_boxes->names, notify_ns->ctx->pool, 4);
	return notify_boxes;
}

static void
cmd_notify_add_mailbox(struct imap_notify_context *ctx,
		       struct mail_namespace *ns, const char *name,
		       enum imap_notify_type type,
		       enum imap_notify_event events)
{
	struct imap_notify_namespace *notify_ns;
	struct imap_notify_mailboxes *notify_boxes;
	const char *const *names;
	unsigned int i, count, cur_len, name_len = strlen(name);
	char ns_sep = mail_namespace_get_sep(ns);

	if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
	    strncmp(name, "INBOX", 5) != 0 &&
	    strncasecmp(name, "INBOX", 5) == 0 &&
	    (name[5] == '\0' || name[5] == ns_sep)) {
		/* we'll do only case-sensitive comparisons later,
		   so sanitize INBOX to be uppercase */
		name = t_strconcat("INBOX", name + 5, NULL);
	}

	notify_ns = imap_notify_namespace_get(ctx, ns);
	notify_boxes = imap_notify_mailboxes_get(notify_ns, type, events);

	names = array_get(&notify_boxes->names, &count);
	for (i = 0; i < count; ) {
		if (strcmp(names[i], name) == 0) {
			/* exact duplicate, already added */
			return;
		}
		if (type != IMAP_NOTIFY_TYPE_SUBTREE)
			i++;
		else {
			/* see if one is a subtree of the other */
			cur_len = strlen(names[i]);
			if (strncmp(names[i], name, cur_len) == 0 &&
			    names[i][cur_len] == ns_sep) {
				/* already matched in this subtree */
				return;
			}
			if (strncmp(names[i], name, name_len) == 0 &&
			    names[i][name_len] == ns_sep) {
				/* we're adding a parent, remove the child */
				array_delete(&notify_boxes->names, i, 1);
				names = array_get(&notify_boxes->names, &count);
			} else {
				i++;
			}
		}
	}
	name = p_strdup(ctx->pool, name);
	array_append(&notify_boxes->names, &name, 1);

	ctx->global_max_mailbox_names =
		I_MAX(ctx->global_max_mailbox_names,
		      array_count(&notify_boxes->names));
}

static void cmd_notify_add_personal(struct imap_notify_context *ctx,
				    enum imap_notify_event events)
{
	struct mail_namespace *ns;

	for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) {
		if (ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) {
			cmd_notify_add_mailbox(ctx, ns, "",
				IMAP_NOTIFY_TYPE_SUBTREE, events);
		}
	}
}

static int
imap_notify_refresh_subscriptions(struct client_command_context *cmd,
				  struct imap_notify_context *ctx)
{
	struct mailbox_list_iterate_context *iter;
	struct mail_namespace *ns;

	if (!ctx->have_subscriptions)
		return 0;

	/* make sure subscriptions are refreshed at least once */
	for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) {
		iter = mailbox_list_iter_init(ns->list, "*", MAILBOX_LIST_ITER_SELECT_SUBSCRIBED);
		(void)mailbox_list_iter_next(iter);
		if (mailbox_list_iter_deinit(&iter) < 0) {
			client_send_list_error(cmd, ns->list);
			return -1;
		}
	}
	return 0;
}

static void cmd_notify_add_subscribed(struct imap_notify_context *ctx,
				      enum imap_notify_event events)
{
	struct mail_namespace *ns;

	ctx->have_subscriptions = TRUE;
	for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) {
		cmd_notify_add_mailbox(ctx, ns, "",
				       IMAP_NOTIFY_TYPE_SUBSCRIBED, events);
	}
}

static void
cmd_notify_add_mailbox_namespaces(struct imap_notify_context *ctx,
				  const char *name,
				  enum imap_notify_type type,
				  enum imap_notify_event events)
{
	struct mail_namespace *ns;

	ns = mail_namespace_find(ctx->client->user->namespaces, name);
	cmd_notify_add_mailbox(ctx, ns, name, type, events);
}

static int
cmd_notify_add_mailboxes(struct imap_notify_context *ctx,
			 const struct imap_arg *arg,
			 enum imap_notify_type type,
			 enum imap_notify_event events)
{
	const struct imap_arg *list;
	const char *name;

	if (imap_arg_get_astring(arg, &name)) {
		cmd_notify_add_mailbox_namespaces(ctx, name, type, events);
		return 0;
	}
	if (!imap_arg_get_list(arg, &list))
		return -1;

	for (; list->type != IMAP_ARG_EOL; list++) {
		if (!imap_arg_get_astring(list, &name))
			return -1;

		cmd_notify_add_mailbox_namespaces(ctx, name, type, events);
	}
	return 0;
}

static int
cmd_notify_set(struct imap_notify_context *ctx, const struct imap_arg *args)
{
	const struct imap_arg *event_group, *mailboxes, *list;
	const char *str, *filter_mailboxes;
	enum imap_notify_event event, event_mask;

	if (imap_arg_get_atom(args, &str) &&
	    strcasecmp(str, "STATUS") == 0) {
		/* send STATUS replies for all matched mailboxes before
		   NOTIFY's OK reply */
		ctx->send_immediate_status = TRUE;
		args++;
	}
	for (; args->type != IMAP_ARG_EOL; args++) {
		if (!imap_arg_get_list(args, &event_group))
			return -1;

		/* filter-mailboxes */
		if (!imap_arg_get_atom(event_group, &filter_mailboxes))
			return -1;
		event_group++;

		if (strcasecmp(filter_mailboxes, "selected") == 0 ||
		    strcasecmp(filter_mailboxes, "selected-delayed") == 0) {
			/* setting events for selected mailbox.
			   handle specially. */
			if (ctx->selected_set) {
				ctx->error = "Duplicate selected filter";
				return -1;
			}
			ctx->selected_set = TRUE;
			if (strcasecmp(filter_mailboxes, "selected") == 0)
				ctx->selected_immediate_expunges = TRUE;
			if (cmd_notify_set_selected(ctx, event_group) < 0)
				return -1;
			continue;
		}

		if (strcasecmp(filter_mailboxes, "subtree") == 0 ||
		    strcasecmp(filter_mailboxes, "mailboxes") == 0) {
			mailboxes = event_group++;
		} else {
			mailboxes = NULL;
		}

		/* parse events */
		if (imap_arg_get_atom(event_group, &str) &&
		    strcasecmp(str, "NONE") == 0) {
			/* NONE is the default, ignore this */
			continue;
		}
		if (!imap_arg_get_list(event_group, &list) ||
		    list[0].type == IMAP_ARG_EOL)
			return -1;

		event_mask = 0;
		for (; list->type != IMAP_ARG_EOL; list++) {
			if (cmd_notify_parse_event(list, &event) < 0)
				return -1;
			event_mask |= event;
			ctx->global_used_events |= event;
		}

		/* we can't currently know inboxes, so treat it the
		   same as personal */
		if (strcasecmp(filter_mailboxes, "inboxes") == 0 ||
		    strcasecmp(filter_mailboxes, "personal") == 0)
			cmd_notify_add_personal(ctx, event_mask);
		else if (strcasecmp(filter_mailboxes, "subscribed") == 0)
			cmd_notify_add_subscribed(ctx, event_mask);
		else if (strcasecmp(filter_mailboxes, "subtree") == 0) {
			if (cmd_notify_add_mailboxes(ctx, mailboxes,
						     IMAP_NOTIFY_TYPE_SUBTREE,
						     event_mask) < 0)
				return -1;
		} else if (strcasecmp(filter_mailboxes, "mailboxes") == 0) {
			if (cmd_notify_add_mailboxes(ctx, mailboxes,
						     IMAP_NOTIFY_TYPE_MAILBOX,
						     event_mask) < 0)
				return -1;
		} else {
			return -1;
		}
	}
	return 0;
}

static void
imap_notify_box_list_noperm(struct client *client, struct mailbox *box)
{
	string_t *str = t_str_new(128);
	char ns_sep = mail_namespace_get_sep(mailbox_get_namespace(box));
	enum mailbox_info_flags mailbox_flags;

	if (mailbox_list_mailbox(mailbox_get_namespace(box)->list,
				 mailbox_get_name(box), &mailbox_flags) < 0)
		mailbox_flags = 0;

	str_append(str, "* LIST (");
	if (imap_mailbox_flags2str(str, mailbox_flags))
		str_append_c(str, ' ');
	str_append(str, "\\NoAccess) \"");
	if (ns_sep == '\\')
		str_append_c(str, '\\');
	str_append_c(str, ns_sep);
	str_append(str, "\" ");

	imap_append_astring(str, mailbox_get_vname(box));
	client_send_line(client, str_c(str));
}

static void
imap_notify_box_send_status(struct client_command_context *cmd,
			    struct imap_notify_context *ctx,
			    const struct mailbox_info *info)
{
	struct mailbox *box;
	struct imap_status_items items;
	struct imap_status_result result;

	if ((info->flags & (MAILBOX_NONEXISTENT | MAILBOX_NOSELECT)) != 0)
		return;

	/* don't send STATUS to selected mailbox */
	if (cmd->client->mailbox != NULL &&
	    mailbox_equals(cmd->client->mailbox, info->ns, info->vname))
		return;

	memset(&items, 0, sizeof(items));
	memset(&result, 0, sizeof(result));

	items.status = STATUS_UIDVALIDITY | STATUS_UIDNEXT |
		STATUS_MESSAGES | STATUS_UNSEEN;
	if ((ctx->global_used_events & (IMAP_NOTIFY_EVENT_FLAG_CHANGE |
					IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE)) != 0)
		items.status |= STATUS_HIGHESTMODSEQ;

	box = mailbox_alloc(info->ns->list, info->vname, MAILBOX_FLAG_READONLY);
	if (ctx->client->enabled_features != 0)
		(void)mailbox_enable(box, ctx->client->enabled_features);

	if (imap_status_get(cmd, info->ns, info->vname, &items, &result) < 0) {
		if (result.error == MAIL_ERROR_PERM)
			imap_notify_box_list_noperm(ctx->client, box);
		else if (result.error != MAIL_ERROR_NOTFOUND) {
			client_send_line(ctx->client,
				t_strconcat("* ", result.errstr, NULL));
		}
	} else {
		imap_status_send(ctx->client, info->vname, &items, &result);
	}
	mailbox_free(&box);
}

static bool imap_notify_ns_want_status(struct imap_notify_namespace *notify_ns)
{
	const struct imap_notify_mailboxes *notify_boxes;

	array_foreach(&notify_ns->mailboxes, notify_boxes) {
		if ((notify_boxes->events &
		     (IMAP_NOTIFY_EVENT_MESSAGE_NEW |
		      IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE |
		      IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE |
		      IMAP_NOTIFY_EVENT_FLAG_CHANGE)) != 0)
			return TRUE;
	}
	return FALSE;
}

static void
imap_notify_ns_send_status(struct client_command_context *cmd,
			   struct imap_notify_context *ctx,
			   struct imap_notify_namespace *notify_ns)
{
	struct mailbox_list_iterate_context *iter;
	const struct imap_notify_mailboxes *notify_boxes;
	const struct mailbox_info *info;

	if (!imap_notify_ns_want_status(notify_ns))
		return;

	/* set _RETURN_SUBSCRIBED flag just in case IMAP_NOTIFY_TYPE_SUBSCRIBED
	   is used, which requires refreshing subscriptions */
	iter = mailbox_list_iter_init(notify_ns->ns->list, "*",
				      MAILBOX_LIST_ITER_RETURN_SUBSCRIBED |
				      MAILBOX_LIST_ITER_RETURN_NO_FLAGS);
	while ((info = mailbox_list_iter_next(iter)) != NULL) {
		array_foreach(&notify_ns->mailboxes, notify_boxes) {
			if (imap_notify_match_mailbox(notify_ns, notify_boxes,
						      info->vname)) {
				imap_notify_box_send_status(cmd, ctx, info);
				break;
			}
		}
	}
	if (mailbox_list_iter_deinit(&iter) < 0) {
		client_send_line(notify_ns->ctx->client,
				 "* NO Mailbox listing failed");
	}
}

static void cmd_notify_send_status(struct client_command_context *cmd,
				   struct imap_notify_context *ctx)
{
	struct imap_notify_namespace *notify_ns;

	array_foreach_modifiable(&ctx->namespaces, notify_ns)
		imap_notify_ns_send_status(cmd, ctx, notify_ns);
}

bool cmd_notify(struct client_command_context *cmd)
{
	struct imap_notify_context *ctx;
	const struct imap_arg *args;
	const char *str;
	int ret = 0;
	pool_t pool;

	if (!client_read_args(cmd, 0, 0, &args))
		return FALSE;

	pool = pool_alloconly_create("imap notify context", 1024);
	ctx = p_new(pool, struct imap_notify_context, 1);
	ctx->pool = pool;
	ctx->client = cmd->client;
	p_array_init(&ctx->namespaces, pool, 4);

	if (!imap_arg_get_atom(&args[0], &str))
		ret = -1;
	else if (strcasecmp(str, "NONE") == 0)
		;
	else if (strcasecmp(str, "SET") == 0)
		ret = cmd_notify_set(ctx, args+1);
	else
		ret = -1;

	if (ret < 0) {
		client_send_command_error(cmd, ctx->error != NULL ? ctx->error :
					  "Invalid arguments.");
		pool_unref(&pool);
		return TRUE;
	}

	if ((ctx->global_used_events & UNSUPPORTED_EVENTS) != 0) {
		string_t *str = t_str_new(128);
		unsigned int i;

		str_append(str, "NO [BADEVENT");
		for (i = 0; i < N_ELEMENTS(imap_notify_event_names); i++) {
			if ((ctx->global_used_events & (1 << i)) != 0 &&
			    ((1 << i) & UNSUPPORTED_EVENTS) != 0) {
				str_append_c(str, ' ');
				str_append(str, imap_notify_event_names[i]);
			}
		}
		str_append(str, "] Unsupported NOTIFY events.");
		client_send_tagline(cmd, str_c(str));
		pool_unref(&pool);
		return TRUE;
	}

	if (array_count(&ctx->namespaces) == 0) {
		/* selected mailbox only */
	} else if (ctx->global_max_mailbox_names > IMAP_NOTIFY_MAX_NAMES_PER_NS) {
		client_send_tagline(cmd,
			"NO [NOTIFICATIONOVERFLOW] Too many mailbox names");
		pool_unref(&pool);
		return TRUE;
	} else if (imap_notify_refresh_subscriptions(cmd, ctx) < 0) {
		/* tagline already sent */
		pool_unref(&pool);
		return TRUE;
	} else if (imap_notify_begin(ctx) < 0) {
		client_send_tagline(cmd,
			"NO [NOTIFICATIONOVERFLOW] NOTIFY not supported for these mailboxes.");
		pool_unref(&pool);
		return TRUE;
	}
	if (cmd->client->notify_ctx != NULL)
		imap_notify_deinit(&cmd->client->notify_ctx);

	if (ctx->send_immediate_status)
		cmd_notify_send_status(cmd, ctx);
	cmd->client->notify_immediate_expunges =
		ctx->selected_immediate_expunges;
	cmd->client->notify_count_changes =
		(ctx->selected_events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) != 0;
	cmd->client->notify_flag_changes =
		(ctx->selected_events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) != 0;

	cmd->client->notify_ctx = ctx;
	return cmd_sync(cmd, 0, IMAP_SYNC_FLAG_SAFE, "OK NOTIFY completed.");
}
