changeset 14899:f9d0ea98157f

imap: Implemented NOTIFY extension. Requires mailbox_list_index=yes to work (and to show up in capabilities). SubscriptionChange event is still unimplemented.
author Timo Sirainen <tss@iki.fi>
date Mon, 13 Aug 2012 15:23:32 +0300
parents a16d77a075bb
children 87e14707d210
files src/imap/Makefile.am src/imap/cmd-cancelupdate.c src/imap/cmd-fetch.c src/imap/cmd-idle.c src/imap/cmd-list.c src/imap/cmd-notify.c src/imap/cmd-search.c src/imap/cmd-select.c src/imap/cmd-sort.c src/imap/imap-client.c src/imap/imap-client.h src/imap/imap-commands.c src/imap/imap-commands.h src/imap/imap-fetch.c src/imap/imap-fetch.h src/imap/imap-list.c src/imap/imap-list.h src/imap/imap-notify.c src/imap/imap-notify.h src/imap/imap-search.c src/imap/imap-search.h src/imap/imap-sync.c src/imap/main.c
diffstat 23 files changed, 1644 insertions(+), 178 deletions(-) [+]
line wrap: on
line diff
--- a/src/imap/Makefile.am	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/Makefile.am	Mon Aug 13 15:23:32 2012 +0300
@@ -43,6 +43,7 @@
 	cmd-lsub.c \
 	cmd-namespace.c \
 	cmd-noop.c \
+	cmd-notify.c \
 	cmd-rename.c \
 	cmd-search.c \
 	cmd-select.c \
@@ -64,6 +65,8 @@
 	imap-expunge.c \
 	imap-fetch.c \
 	imap-fetch-body.c \
+	imap-list.c \
+	imap-notify.c \
 	imap-search.c \
 	imap-search-args.c \
 	imap-settings.c \
@@ -79,6 +82,8 @@
 	imap-common.h \
 	imap-expunge.h \
 	imap-fetch.h \
+	imap-list.h \
+	imap-notify.h \
 	imap-search.h \
 	imap-search-args.h \
 	imap-settings.h \
--- a/src/imap/cmd-cancelupdate.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/cmd-cancelupdate.c	Mon Aug 13 15:23:32 2012 +0300
@@ -1,6 +1,7 @@
 /* Copyright (c) 2008-2012 Dovecot authors, see the included COPYING file */
 
 #include "imap-common.h"
+#include "imap-search.h"
 #include "imap-commands.h"
 
 static bool client_search_update_cancel(struct client *client, const char *tag)
@@ -12,8 +13,7 @@
 	if (update == NULL)
 		return FALSE;
 
-	i_free(update->tag);
-	mailbox_search_result_free(&update->result);
+	imap_search_update_free(update);
 	array_delete(&client->search_updates, idx, 1);
 	return TRUE;
 }
--- a/src/imap/cmd-fetch.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/cmd-fetch.c	Mon Aug 13 15:23:32 2012 +0300
@@ -103,6 +103,7 @@
 static bool
 fetch_parse_modifier(struct imap_fetch_context *ctx,
 		     struct client_command_context *cmd,
+		     struct mail_search_args *search_args,
 		     const char *name, const struct imap_arg **args,
 		     bool *send_vanished)
 {
@@ -117,7 +118,7 @@
 			return FALSE;
 		}
 		*args += 1;
-		imap_fetch_add_changed_since(ctx, modseq);
+		imap_fetch_add_changed_since(ctx, search_args, modseq);
 		return TRUE;
 	}
 	if (strcmp(name, "VANISHED") == 0 && cmd->uid) {
@@ -137,6 +138,7 @@
 static bool
 fetch_parse_modifiers(struct imap_fetch_context *ctx,
 		      struct client_command_context *cmd,
+		      struct mail_search_args *search_args,
 		      const struct imap_arg *args, bool *send_vanished_r)
 {
 	const char *name;
@@ -150,13 +152,14 @@
 			return FALSE;
 		}
 		args++;
-		if (!fetch_parse_modifier(ctx, cmd, t_str_ucase(name),
+		if (!fetch_parse_modifier(ctx, cmd, search_args,
+					  t_str_ucase(name),
 					  &args, send_vanished_r))
 			return FALSE;
 	}
 	if (*send_vanished_r &&
-	    (ctx->search_args->args->next == NULL ||
-	     ctx->search_args->args->next->type != SEARCH_MODSEQ)) {
+	    (search_args->args->next == NULL ||
+	     search_args->args->next->type != SEARCH_MODSEQ)) {
 		client_send_command_error(cmd,
 			"VANISHED used without CHANGEDSINCE");
 		return FALSE;
@@ -250,23 +253,27 @@
 		return ret < 0;
 
 	ctx = imap_fetch_alloc(client, cmd->pool);
-	ctx->search_args = search_args;
 
 	if (!fetch_parse_args(ctx, cmd, &args[1], &next_arg) ||
 	    (imap_arg_get_list(next_arg, &list_arg) &&
-	     !fetch_parse_modifiers(ctx, cmd, list_arg, &send_vanished))) {
+	     !fetch_parse_modifiers(ctx, cmd, search_args, list_arg,
+				    &send_vanished))) {
 		imap_fetch_free(&ctx);
+		mail_search_args_unref(&search_args);
 		return TRUE;
 	}
 
 	if (send_vanished) {
 		memset(&qresync_args, 0, sizeof(qresync_args));
 		if (imap_fetch_send_vanished(client, client->mailbox,
-					     search_args, &qresync_args) < 0)
+					     search_args, &qresync_args) < 0) {
+			mail_search_args_unref(&search_args);
 			return cmd_fetch_finish(ctx, cmd);
+		}
 	}
 
-	imap_fetch_begin_once(ctx, client->mailbox);
+	imap_fetch_begin(ctx, client->mailbox, search_args);
+	mail_search_args_unref(&search_args);
 
 	if (imap_fetch_more(ctx, cmd) == 0) {
 		/* unfinished */
--- a/src/imap/cmd-idle.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/cmd-idle.c	Mon Aug 13 15:23:32 2012 +0300
@@ -115,7 +115,7 @@
 
 static void keepalive_timeout(struct cmd_idle_context *ctx)
 {
-	if (ctx->client->output_lock != NULL) {
+	if (ctx->client->output_locked) {
 		/* it's busy sending output */
 		return;
 	}
--- a/src/imap/cmd-list.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/cmd-list.c	Mon Aug 13 15:23:32 2012 +0300
@@ -9,6 +9,7 @@
 #include "imap-match.h"
 #include "imap-status.h"
 #include "imap-commands.h"
+#include "imap-list.h"
 #include "mail-namespace.h"
 
 struct cmd_list_context {
@@ -49,40 +50,22 @@
 	if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) == 0)
 		flags &= ~(MAILBOX_CHILDREN|MAILBOX_NOCHILDREN);
 
-	if ((flags & MAILBOX_SUBSCRIBED) != 0 &&
-	    (ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) != 0)
-		str_append(str, "\\Subscribed ");
+	if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) == 0)
+		flags &= ~MAILBOX_SUBSCRIBED;
 
 	if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 &&
 	    (flags & MAILBOX_SUBSCRIBED) == 0 && !ctx->used_listext) {
 		/* LSUB uses \Noselect for this */
 		flags |= MAILBOX_NOSELECT;
 	}
-
-	if ((flags & MAILBOX_NOSELECT) != 0)
-		str_append(str, "\\Noselect ");
-	if ((flags & MAILBOX_NONEXISTENT) != 0)
-		str_append(str, "\\NonExistent ");
-
-	if ((flags & MAILBOX_CHILDREN) != 0)
-		str_append(str, "\\HasChildren ");
-	else if ((flags & MAILBOX_NOINFERIORS) != 0)
-		str_append(str, "\\NoInferiors ");
-	else if ((flags & MAILBOX_NOCHILDREN) != 0)
-		str_append(str, "\\HasNoChildren ");
-
-	if ((flags & MAILBOX_MARKED) != 0)
-		str_append(str, "\\Marked ");
-	if ((flags & MAILBOX_UNMARKED) != 0)
-		str_append(str, "\\UnMarked ");
+	imap_mailbox_flags2str(str, flags);
 
 	if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SPECIALUSE) != 0 &&
 	    special_use != NULL) {
+		if (str_len(str) != orig_len)
+			str_append_c(str, ' ');
 		str_append(str, special_use);
-		str_append_c(str, ' ');
 	}
-	if (str_len(str) != orig_len)
-		str_truncate(str, str_len(str)-1);
 }
 
 static void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/cmd-notify.c	Mon Aug 13 15:23:32 2012 +0300
@@ -0,0 +1,542 @@
+/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.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 == NAMESPACE_PRIVATE) {
+			cmd_notify_add_mailbox(ctx, ns, "",
+				IMAP_NOTIFY_TYPE_SUBTREE, events);
+		}
+	}
+}
+
+static void cmd_notify_add_subscribed(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) {
+		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;
+
+	/* add to all matching namespaces */
+	for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) {
+		if (mail_namespace_find(ns, name) == ns)
+			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_quote_append_string(str, mailbox_get_vname(box), FALSE);
+	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;
+
+	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;
+
+	iter = mailbox_list_iter_init(notify_ns->ns->list, "*",
+				      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_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.");
+}
--- a/src/imap/cmd-search.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/cmd-search.c	Mon Aug 13 15:23:32 2012 +0300
@@ -31,6 +31,7 @@
 		if (!imap_arg_get_astring(&args[1], &charset)) {
 			client_send_command_error(cmd,
 				"Invalid charset argument.");
+			imap_search_context_free(ctx);
 			return TRUE;
 		}
 		args += 2;
@@ -39,8 +40,10 @@
 	}
 
 	ret = imap_search_args_build(cmd, args, charset, &sargs);
-	if (ret <= 0)
+	if (ret <= 0) {
+		imap_search_context_free(ctx);
 		return ret < 0;
+	}
 
 	return imap_search_start(ctx, sargs, NULL);
 }
--- a/src/imap/cmd-select.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/cmd-select.c	Mon Aug 13 15:23:32 2012 +0300
@@ -251,14 +251,16 @@
 	}
 
 	fetch_ctx = imap_fetch_alloc(ctx->cmd->client, ctx->cmd->pool);
-	fetch_ctx->search_args = search_args;
 
-	imap_fetch_add_changed_since(fetch_ctx, ctx->qresync_modseq);
+	imap_fetch_add_changed_since(fetch_ctx, search_args,
+				     ctx->qresync_modseq);
 	imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_uid_init);
 	imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_flags_init);
 	imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_modseq_init);
 
-	imap_fetch_begin_once(fetch_ctx, ctx->box);
+	imap_fetch_begin(fetch_ctx, ctx->box, search_args);
+	mail_search_args_unref(&search_args);
+
 	if (imap_fetch_more(fetch_ctx, ctx->cmd) == 0) {
 		/* unfinished */
 		ctx->fetch_ctx = fetch_ctx;
@@ -312,6 +314,7 @@
 	client->messages_count = status.messages;
 	client->recent_count = status.recent;
 	client->uidvalidity = status.uidvalidity;
+	client->notify_uidnext = status.uidnext;
 
 	client_update_mailbox_flags(client, status.keywords);
 	client_send_mailbox_flags(client, TRUE);
--- a/src/imap/cmd-sort.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/cmd-sort.c	Mon Aug 13 15:23:32 2012 +0300
@@ -114,23 +114,29 @@
 	/* sort program */
 	if (!imap_arg_get_list(args, &list_args)) {
 		client_send_command_error(cmd, "Invalid sort argument.");
+		imap_search_context_free(ctx);
 		return TRUE;
 	}
 
-	if (get_sort_program(cmd, list_args, sort_program) < 0)
+	if (get_sort_program(cmd, list_args, sort_program) < 0) {
+		imap_search_context_free(ctx);
 		return TRUE;
+	}
 	args++;
 
 	/* charset */
 	if (!imap_arg_get_astring(args, &charset)) {
 		client_send_command_error(cmd, "Invalid charset argument.");
+		imap_search_context_free(ctx);
 		return TRUE;
 	}
 	args++;
 
 	ret = imap_search_args_build(cmd, args, charset, &sargs);
-	if (ret <= 0)
+	if (ret <= 0) {
+		imap_search_context_free(ctx);
 		return ret < 0;
+	}
 
 	return imap_search_start(ctx, sargs, sort_program);
 }
--- a/src/imap/imap-client.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-client.c	Mon Aug 13 15:23:32 2012 +0300
@@ -14,6 +14,8 @@
 #include "imap-util.h"
 #include "mail-namespace.h"
 #include "mail-storage-service.h"
+#include "imap-search.h"
+#include "imap-notify.h"
 #include "imap-commands.h"
 
 #include <stdlib.h>
@@ -29,7 +31,7 @@
 
 static void client_idle_timeout(struct client *client)
 {
-	if (client->output_lock == NULL)
+	if (!client->output_locked)
 		client_send_line(client, "* BYE Disconnected for inactivity.");
 	client_destroy(client, "Disconnected for inactivity");
 }
@@ -39,6 +41,7 @@
 			     struct mail_storage_service_user *service_user,
 			     const struct imap_settings *set)
 {
+	const struct mail_storage_settings *mail_set;
 	struct client *client;
 	const char *ident;
 	pool_t pool;
@@ -73,6 +76,8 @@
 	client->command_pool =
 		pool_alloconly_create(MEMPOOL_GROWING"client command", 1024*2);
 	client->user = user;
+	client->notify_count_changes = TRUE;
+	client->notify_flag_changes = TRUE;
 
 	mail_namespaces_set_storage_callbacks(user->namespaces,
 					      &mail_storage_callbacks, client);
@@ -96,6 +101,14 @@
 		str_append(client->capability_string, " SEARCH=FUZZY");
 	}
 
+	mail_set = mail_user_set_get_storage_set(user);
+	if (mail_set->mailbox_list_index && !explicit_capability) {
+		/* NOTIFY is enabled only when mailbox list indexes are
+		   enabled, although even that doesn't necessarily guarantee
+		   it always */
+		str_append(client->capability_string, " NOTIFY");
+	}
+
 	ident = mail_user_get_anvil_userip_ident(client->user);
 	if (ident != NULL) {
 		master_service_anvil_send(master_service, t_strconcat(
@@ -204,8 +217,8 @@
 	o_stream_close(client->output);
 
 	/* finish off all the queued commands. */
-	if (client->output_lock != NULL)
-		client_command_cancel(&client->output_lock);
+	if (client->output_cmd_lock != NULL)
+		client_command_cancel(&client->output_cmd_lock);
 	while (client->command_queue != NULL) {
 		cmd = client->command_queue;
 		client_command_cancel(&cmd);
@@ -221,6 +234,9 @@
 		client_search_updates_free(client);
 		mailbox_free(&client->mailbox);
 	}
+	if (client->notify_ctx != NULL)
+		imap_notify_deinit(&client->notify_ctx);
+
 	if (client->anvil_sent) {
 		master_service_anvil_send(master_service, t_strconcat(
 			"DISCONNECT\t", my_pid, "\timap/",
@@ -520,8 +536,7 @@
 	return TRUE;
 }
 
-static struct client_command_context *
-client_command_new(struct client *client)
+struct client_command_context *client_command_alloc(struct client *client)
 {
 	struct client_command_context *cmd;
 
@@ -530,6 +545,17 @@
 	cmd->pool = client->command_pool;
 	p_array_init(&cmd->module_contexts, cmd->pool, 5);
 
+	DLLIST_PREPEND(&client->command_queue, cmd);
+	client->command_queue_size++;
+	return cmd;
+}
+
+static struct client_command_context *
+client_command_new(struct client *client)
+{
+	struct client_command_context *cmd;
+
+	cmd = client_command_alloc(client);
 	if (client->free_parser != NULL) {
 		cmd->parser = client->free_parser;
 		client->free_parser = NULL;
@@ -538,10 +564,6 @@
 			imap_parser_create(client->input, client->output,
 					   client->set->imap_max_line_length);
 	}
-
-	DLLIST_PREPEND(&client->command_queue, cmd);
-	client->command_queue_size++;
-
 	return cmd;
 }
 
@@ -552,6 +574,8 @@
 
 	*_cmd = NULL;
 
+	i_assert(client->output_cmd_lock != cmd);
+
 	/* reset input idle time because command output might have taken a
 	   long time and we don't want to disconnect client immediately then */
 	client->last_input = ioloop_time;
@@ -567,16 +591,14 @@
 
 	if (client->input_lock == cmd)
 		client->input_lock = NULL;
-	if (client->output_lock == cmd)
-		client->output_lock = NULL;
 	if (client->mailbox_change_lock == cmd)
 		client->mailbox_change_lock = NULL;
 
-	if (client->free_parser != NULL)
-		imap_parser_unref(&cmd->parser);
-	else {
+	if (client->free_parser == NULL) {
 		imap_parser_reset(cmd->parser);
 		client->free_parser = cmd->parser;
+	} else if (cmd->parser != NULL) {
+		imap_parser_unref(&cmd->parser);
 	}
 
 	client->command_queue_size--;
@@ -589,6 +611,7 @@
 		if (client->to_idle_output != NULL)
 			timeout_remove(&client->to_idle_output);
 	}
+	imap_client_notify_command_freed(client);
 	imap_refresh_proctitle();
 }
 
@@ -775,7 +798,7 @@
 
 	/* beginning a new command */
 	if (client->command_queue_size >= CLIENT_COMMAND_QUEUE_MAX_SIZE ||
-	    client->output_lock != NULL) {
+	    client->output_locked) {
 		/* wait for some of the commands to finish */
 		*remove_io_r = TRUE;
 		return FALSE;
@@ -869,33 +892,19 @@
 	}
 }
 
-int client_output(struct client *client)
+static void client_output_commands(struct client *client)
 {
 	struct client_command_context *cmd;
-	int ret;
-
-	i_assert(!client->destroyed);
-
-	client->last_output = ioloop_time;
-	timeout_reset(client->to_idle);
-	if (client->to_idle_output != NULL)
-		timeout_reset(client->to_idle_output);
-
-	o_stream_cork(client->output);
-	if ((ret = o_stream_flush(client->output)) < 0) {
-		client_destroy(client, NULL);
-		return 1;
-	}
 
 	/* mark all commands non-executed */
 	for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next)
 		cmd->temp_executed = FALSE;
 
-	if (client->output_lock != NULL) {
-		client->output_lock->temp_executed = TRUE;
-		client_output_cmd(client->output_lock);
+	if (client->output_cmd_lock != NULL) {
+		client->output_cmd_lock->temp_executed = TRUE;
+		client_output_cmd(client->output_cmd_lock);
 	}
-	while (client->output_lock == NULL) {
+	while (!client->output_locked) {
 		/* go through the entire commands list every time in case
 		   multiple commands were freed. temp_executed keeps track of
 		   which messages we've called so far */
@@ -913,8 +922,28 @@
 			break;
 		}
 	}
+}
 
-	cmd_sync_delayed(client);
+int client_output(struct client *client)
+{
+	int ret;
+
+	i_assert(!client->destroyed);
+
+	client->last_output = ioloop_time;
+	timeout_reset(client->to_idle);
+	if (client->to_idle_output != NULL)
+		timeout_reset(client->to_idle_output);
+
+	o_stream_cork(client->output);
+	if ((ret = o_stream_flush(client->output)) < 0) {
+		client_destroy(client, NULL);
+		return 1;
+	}
+
+	client_output_commands(client);
+	(void)cmd_sync_delayed(client);
+
 	o_stream_uncork(client->output);
 	if (client->disconnected)
 		client_destroy(client, NULL);
@@ -1003,10 +1032,8 @@
 	if (!array_is_created(&client->search_updates))
 		return;
 
-	array_foreach_modifiable(&client->search_updates, update) {
-		i_free(update->tag);
-		mailbox_search_result_free(&update->result);
-	}
+	array_foreach_modifiable(&client->search_updates, update)
+		imap_search_update_free(update);
 	array_clear(&client->search_updates);
 }
 
--- a/src/imap/imap-client.h	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-client.h	Mon Aug 13 15:23:32 2012 +0300
@@ -29,6 +29,9 @@
 	char *tag;
 	struct mail_search_result *result;
 	bool return_uids;
+
+	pool_t fetch_pool;
+	struct imap_fetch_context *fetch_ctx;
 };
 
 enum client_command_state {
@@ -124,10 +127,13 @@
 	ARRAY_TYPE(seq_range) search_saved_uidset;
 	/* SEARCH=CONTEXT extension: Searches that get updated */
 	ARRAY_DEFINE(search_updates, struct imap_search_update);
+	/* NOTIFY extension */
+	struct imap_notify_context *notify_ctx;
+	uint32_t notify_uidnext;
 
 	/* client input/output is locked by this command */
 	struct client_command_context *input_lock;
-	struct client_command_context *output_lock;
+	struct client_command_context *output_cmd_lock;
 	/* command changing the mailbox */
 	struct client_command_context *mailbox_change_lock;
 
@@ -146,9 +152,13 @@
 	unsigned int mailbox_examined:1;
 	unsigned int anvil_sent:1;
 	unsigned int tls_compression:1;
+	unsigned int output_locked:1;
 	unsigned int input_skip_line:1; /* skip all the data until we've
 					   found a new line */
 	unsigned int modseqs_sent_since_sync:1;
+	unsigned int notify_immediate_expunges:1;
+	unsigned int notify_count_changes:1;
+	unsigned int notify_flag_changes:1;
 };
 
 struct imap_module_register {
@@ -212,6 +222,7 @@
 			    unsigned int *idx_r);
 void client_search_updates_free(struct client *client);
 
+struct client_command_context *client_command_alloc(struct client *client);
 void client_command_cancel(struct client_command_context **cmd);
 void client_command_free(struct client_command_context **cmd);
 
--- a/src/imap/imap-commands.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-commands.c	Mon Aug 13 15:23:32 2012 +0300
@@ -53,6 +53,7 @@
 	{ "IDLE",		cmd_idle,        COMMAND_FLAG_BREAKS_SEQS |
 						 COMMAND_FLAG_REQUIRES_SYNC },
 	{ "NAMESPACE",		cmd_namespace,   0 },
+	{ "NOTIFY",		cmd_notify,      COMMAND_FLAG_BREAKS_SEQS },
 	{ "SORT",		cmd_sort,        COMMAND_FLAG_USES_SEQS },
 	{ "THREAD",		cmd_thread,      COMMAND_FLAG_USES_SEQS },
 	{ "UID EXPUNGE",	cmd_uid_expunge, COMMAND_FLAG_BREAKS_SEQS },
--- a/src/imap/imap-commands.h	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-commands.h	Mon Aug 13 15:23:32 2012 +0300
@@ -107,6 +107,7 @@
 bool cmd_id(struct client_command_context *cmd);
 bool cmd_idle(struct client_command_context *cmd);
 bool cmd_namespace(struct client_command_context *cmd);
+bool cmd_notify(struct client_command_context *cmd);
 bool cmd_sort(struct client_command_context *cmd);
 bool cmd_thread(struct client_command_context *cmd);
 bool cmd_uid_expunge(struct client_command_context *cmd);
--- a/src/imap/imap-fetch.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-fetch.c	Mon Aug 13 15:23:32 2012 +0300
@@ -80,6 +80,38 @@
 		i_unreached();
 }
 
+int imap_fetch_att_list_parse(struct client *client, pool_t pool,
+			      const struct imap_arg *list,
+			      struct imap_fetch_context **fetch_ctx_r,
+			      const char **error_r)
+{
+	struct imap_fetch_init_context init_ctx;
+	const char *str;
+
+	memset(&init_ctx, 0, sizeof(init_ctx));
+	init_ctx.fetch_ctx = imap_fetch_alloc(client, pool);
+	init_ctx.pool = pool;
+	init_ctx.args = list;
+
+	while (imap_arg_get_atom(init_ctx.args, &str)) {
+		init_ctx.name = t_str_ucase(str);
+		init_ctx.args++;
+		if (!imap_fetch_init_handler(&init_ctx)) {
+			*error_r = t_strconcat("Invalid fetch-att list: ",
+					       init_ctx.error, NULL);
+			imap_fetch_free(&init_ctx.fetch_ctx);
+			return -1;
+		}
+	}
+	if (!IMAP_ARG_IS_EOL(init_ctx.args)) {
+		*error_r = "fetch-att list contains non-atoms.";
+		imap_fetch_free(&init_ctx.fetch_ctx);
+		return -1;
+	}
+	*fetch_ctx_r = init_ctx.fetch_ctx;
+	return 0;
+}
+
 struct imap_fetch_context *
 imap_fetch_alloc(struct client *client, pool_t pool)
 {
@@ -98,18 +130,19 @@
 }
 
 void imap_fetch_add_changed_since(struct imap_fetch_context *ctx,
+				  struct mail_search_args *search_args,
 				  uint64_t modseq)
 {
 	struct mail_search_arg *search_arg;
 
-	search_arg = p_new(ctx->search_args->pool, struct mail_search_arg, 1);
+	search_arg = p_new(search_args->pool, struct mail_search_arg, 1);
 	search_arg->type = SEARCH_MODSEQ;
 	search_arg->value.modseq =
-		p_new(ctx->search_args->pool, struct mail_search_modseq, 1);
+		p_new(search_args->pool, struct mail_search_modseq, 1);
 	search_arg->value.modseq->modseq = modseq + 1;
 
-	search_arg->next = ctx->search_args->args->next;
-	ctx->search_args->args->next = search_arg;
+	search_arg->next = search_args->args->next;
+	search_args->args->next = search_arg;
 
 	imap_fetch_init_nofail_handler(ctx, imap_fetch_modseq_init);
 }
@@ -333,12 +366,10 @@
 		ctx->fetch_data |= MAIL_FETCH_NUL_STATE;
 }
 
-static void
-imap_fetch_begin_full(struct imap_fetch_context *ctx,
-	              struct mailbox *box, bool once)
+void imap_fetch_begin(struct imap_fetch_context *ctx, struct mailbox *box,
+		      struct mail_search_args *search_args)
 {
 	struct mailbox_header_lookup_ctx *wanted_headers = NULL;
-	struct mail_search_args *search_args;
 	const char *const *headers;
 
 	i_assert(!ctx->state.fetching);
@@ -361,14 +392,6 @@
 		MAILBOX_TRANSACTION_FLAG_HIDE |
 		MAILBOX_TRANSACTION_FLAG_REFRESH);
 
-	if (!once) {
-		/* fetch can be executed multiple times.
-		   clone the search args for this fetch */
-		search_args = mail_search_args_dup(ctx->search_args);
-	} else {
-		search_args = ctx->search_args;
-		ctx->search_args = NULL;
-	}
 	mail_search_args_init(search_args, box, TRUE,
 			      &ctx->client->search_saved_uidset);
 	ctx->state.search_ctx =
@@ -380,17 +403,6 @@
 
 	if (wanted_headers != NULL)
 		mailbox_header_lookup_unref(&wanted_headers);
-	mail_search_args_unref(&search_args);
-}
-
-void imap_fetch_begin(struct imap_fetch_context *ctx, struct mailbox *box)
-{
-	imap_fetch_begin_full(ctx, box, FALSE);
-}
-
-void imap_fetch_begin_once(struct imap_fetch_context *ctx, struct mailbox *box)
-{
-	imap_fetch_begin_full(ctx, box, TRUE);
 }
 
 static int imap_fetch_flush_buffer(struct imap_fetch_context *ctx)
@@ -557,15 +569,38 @@
 {
 	int ret;
 
-	i_assert(ctx->client->output_lock == NULL ||
-		 ctx->client->output_lock == cmd);
+	i_assert(!ctx->client->output_locked ||
+		 ctx->client->output_cmd_lock == cmd);
 
 	ret = imap_fetch_more_int(ctx, cmd->cancel);
 	if (ret < 0)
 		ctx->state.failed = TRUE;
 	if (ctx->state.line_partial) {
 		/* nothing can be sent until FETCH is finished */
-		ctx->client->output_lock = cmd;
+		ctx->client->output_cmd_lock = cmd;
+	}
+	if (cmd->cancel && ctx->client->output_cmd_lock != NULL) {
+		/* canceling didn't really work. we must not output
+		   anything anymore. */
+		if (!ctx->client->destroyed)
+			client_disconnect(ctx->client, "Failed to cancel FETCH");
+		ctx->client->output_cmd_lock = NULL;
+	}
+	ctx->client->output_locked = ctx->client->output_cmd_lock != NULL;
+	return ret;
+}
+
+int imap_fetch_more_no_lock_update(struct imap_fetch_context *ctx)
+{
+	int ret;
+
+	ret = imap_fetch_more_int(ctx, FALSE);
+	if (ret < 0) {
+		ctx->state.failed = TRUE;
+		if (!ctx->state.line_finished) {
+			client_disconnect(ctx->client,
+				"NOTIFY failed in the middle of FETCH reply");
+		}
 	}
 	return ret;
 }
@@ -617,8 +652,6 @@
 		if (handler->want_deinit)
 			handler->handler(ctx, NULL, handler->context);
 	}
-	if (ctx->search_args != NULL)
-		mail_search_args_unref(&ctx->search_args);
 	pool_unref(&ctx->ctx_pool);
 }
 
--- a/src/imap/imap-fetch.h	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-fetch.h	Mon Aug 13 15:23:32 2012 +0300
@@ -73,8 +73,6 @@
 	struct client *client;
 	pool_t ctx_pool;
 
-	struct mail_search_args *search_args;
-
 	enum mail_fetch_field fetch_data;
 	ARRAY_TYPE(const_string) all_headers;
 
@@ -111,6 +109,11 @@
 		(imap_fetch_handler_t *)handler, context)
 #endif
 
+int imap_fetch_att_list_parse(struct client *client, pool_t pool,
+			      const struct imap_arg *list,
+			      struct imap_fetch_context **fetch_ctx_r,
+			      const char **error_r);
+
 struct imap_fetch_context *
 imap_fetch_alloc(struct client *client, pool_t pool);
 void imap_fetch_free(struct imap_fetch_context **ctx);
@@ -119,10 +122,11 @@
 				    bool (*init)(struct imap_fetch_init_context *));
 
 void imap_fetch_add_changed_since(struct imap_fetch_context *ctx,
+				  struct mail_search_args *search_args,
 				  uint64_t modseq);
 
-void imap_fetch_begin(struct imap_fetch_context *ctx, struct mailbox *box);
-void imap_fetch_begin_once(struct imap_fetch_context *ctx, struct mailbox *box);
+void imap_fetch_begin(struct imap_fetch_context *ctx, struct mailbox *box,
+		      struct mail_search_args *search_args);
 int imap_fetch_send_vanished(struct client *client, struct mailbox *box,
 			     const struct mail_search_args *search_args,
 			     const struct imap_fetch_qresync_args *qresync_args);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-list.c	Mon Aug 13 15:23:32 2012 +0300
@@ -0,0 +1,35 @@
+/* Copyright (c) 2002-2012 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "imap-list.h"
+
+bool imap_mailbox_flags2str(string_t *str, enum mailbox_info_flags flags)
+{
+	unsigned int orig_len = str_len(str);
+
+	if ((flags & MAILBOX_SUBSCRIBED) != 0)
+		str_append(str, "\\Subscribed ");
+
+	if ((flags & MAILBOX_NOSELECT) != 0)
+		str_append(str, "\\Noselect ");
+	if ((flags & MAILBOX_NONEXISTENT) != 0)
+		str_append(str, "\\NonExistent ");
+
+	if ((flags & MAILBOX_CHILDREN) != 0)
+		str_append(str, "\\HasChildren ");
+	else if ((flags & MAILBOX_NOINFERIORS) != 0)
+		str_append(str, "\\NoInferiors ");
+	else if ((flags & MAILBOX_NOCHILDREN) != 0)
+		str_append(str, "\\HasNoChildren ");
+
+	if ((flags & MAILBOX_MARKED) != 0)
+		str_append(str, "\\Marked ");
+	if ((flags & MAILBOX_UNMARKED) != 0)
+		str_append(str, "\\UnMarked ");
+
+	if (str_len(str) == orig_len)
+		return FALSE;
+	str_truncate(str, str_len(str)-1);
+	return TRUE;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-list.h	Mon Aug 13 15:23:32 2012 +0300
@@ -0,0 +1,7 @@
+#ifndef IMAP_LIST_H
+#define IMAP_LIST_H
+
+/* Returns TRUE if anything was added to the string. */
+bool imap_mailbox_flags2str(string_t *str, enum mailbox_info_flags flags);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-notify.c	Mon Aug 13 15:23:32 2012 +0300
@@ -0,0 +1,518 @@
+/* Copyright (c) 2012 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "str.h"
+#include "ostream.h"
+#include "imap-quote.h"
+#include "mailbox-list-notify.h"
+#include "mail-search.h"
+#include "mail-search-build.h"
+#include "imap-commands.h"
+#include "imap-fetch.h"
+#include "imap-list.h"
+#include "imap-status.h"
+#include "imap-notify.h"
+
+#define IMAP_NOTIFY_WATCH_ADD_DELAY_MSECS 1000
+
+static bool notify_hook_registered;
+
+static int imap_notify_list(struct imap_notify_namespace *notify_ns,
+			    const struct mailbox_list_notify_rec *rec,
+			    enum mailbox_info_flags flags)
+{
+	string_t *str = t_str_new(128);
+	char ns_sep = mail_namespace_get_sep(notify_ns->ns);
+
+	str_append(str, "* LIST (");
+	imap_mailbox_flags2str(str, flags);
+	str_append(str, ") \"");
+	if (ns_sep == '\\')
+		str_append_c(str, '\\');
+	str_append_c(str, ns_sep);
+	str_append(str, "\" ");
+
+	imap_quote_append_string(str, rec->vname, FALSE);
+	if (rec->old_vname != NULL) {
+		str_append(str, " (\"OLDNAME\" (");
+		imap_quote_append_string(str, rec->old_vname, FALSE);
+		str_append(str, "))");
+	}
+	return client_send_line_next(notify_ns->ctx->client, str_c(str));
+}
+
+static int imap_notify_status(struct imap_notify_namespace *notify_ns,
+			      const struct mailbox_list_notify_rec *rec)
+{
+	struct client *client = notify_ns->ctx->client;
+	struct mailbox *box;
+	struct imap_status_items items;
+	struct imap_status_result result;
+	enum mail_error error;
+	int ret = 1;
+
+	memset(&items, 0, sizeof(items));
+	if ((client->enabled_features & MAILBOX_FEATURE_CONDSTORE) != 0)
+		items.status |= STATUS_HIGHESTMODSEQ;
+
+	box = mailbox_alloc(notify_ns->ns->list, rec->vname, 0);
+	switch (rec->event) {
+	case MAILBOX_LIST_NOTIFY_UIDVALIDITY:
+		items.status |= STATUS_UIDVALIDITY | STATUS_UIDNEXT |
+			STATUS_MESSAGES | STATUS_UNSEEN;
+		break;
+	case MAILBOX_LIST_NOTIFY_APPENDS:
+	case MAILBOX_LIST_NOTIFY_EXPUNGES:
+		items.status |= STATUS_UIDNEXT | STATUS_MESSAGES | STATUS_UNSEEN;
+		break;
+	case MAILBOX_LIST_NOTIFY_SEEN_CHANGES:
+		items.status |= STATUS_UNSEEN;
+		break;
+	case MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES:
+		/* if HIGHESTMODSEQ isn't being sent, don't send anything */
+		break;
+	case MAILBOX_LIST_NOTIFY_CREATE:
+	case MAILBOX_LIST_NOTIFY_DELETE:
+	case MAILBOX_LIST_NOTIFY_RENAME:
+	case MAILBOX_LIST_NOTIFY_SUBSCRIPTION_CHANGE:
+		i_unreached();
+	}
+	if (items.status == 0) {
+		/* don't send anything */
+	} else if (mailbox_get_status(box, items.status, &result.status) < 0) {
+		/* hide permission errors from client. we don't want to leak
+		   information about existence of mailboxes where user doesn't
+		   have access to */
+		(void)mailbox_get_last_error(box, &error);
+		if (error != MAIL_ERROR_PERM)
+			ret = -1;
+	} else {
+		ret = imap_status_send(client, rec->vname, &items, &result);
+	}
+	mailbox_free(&box);
+	return ret;
+}
+
+static int
+imap_notify_next(struct imap_notify_namespace *notify_ns,
+		 const struct mailbox_list_notify_rec *rec)
+{
+	enum mailbox_info_flags mailbox_flags;
+	int ret = 1;
+
+	switch (rec->event) {
+	case MAILBOX_LIST_NOTIFY_CREATE:
+		if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name,
+					 &mailbox_flags) < 0)
+			mailbox_flags = 0;
+		ret = imap_notify_list(notify_ns, rec, mailbox_flags);
+		break;
+	case MAILBOX_LIST_NOTIFY_DELETE:
+		ret = imap_notify_list(notify_ns, rec, MAILBOX_NONEXISTENT);
+		break;
+	case MAILBOX_LIST_NOTIFY_RENAME:
+		if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name,
+					 &mailbox_flags) < 0)
+			mailbox_flags = 0;
+		ret = imap_notify_list(notify_ns, rec, mailbox_flags);
+		break;
+	case MAILBOX_LIST_NOTIFY_SUBSCRIPTION_CHANGE:
+		/* FIXME: set \subscribed when needed */
+		if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name,
+					 &mailbox_flags) < 0)
+			mailbox_flags = 0;
+		ret = imap_notify_list(notify_ns, rec, mailbox_flags);
+		break;
+	case MAILBOX_LIST_NOTIFY_UIDVALIDITY:
+	case MAILBOX_LIST_NOTIFY_APPENDS:
+	case MAILBOX_LIST_NOTIFY_EXPUNGES:
+	case MAILBOX_LIST_NOTIFY_SEEN_CHANGES:
+	case MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES:
+		ret = imap_notify_status(notify_ns, rec);
+		break;
+	}
+	return ret;
+}
+
+static bool
+imap_notify_match_event(struct imap_notify_namespace *notify_ns,
+			const struct imap_notify_mailboxes *notify_boxes,
+			const struct mailbox_list_notify_rec *rec)
+{
+	enum imap_notify_event wanted_events = notify_boxes->events;
+	struct mailbox *box;
+	bool mailbox_event = FALSE;
+
+	switch (rec->event) {
+	case MAILBOX_LIST_NOTIFY_CREATE:
+	case MAILBOX_LIST_NOTIFY_DELETE:
+	case MAILBOX_LIST_NOTIFY_RENAME:
+		if ((wanted_events & IMAP_NOTIFY_EVENT_MAILBOX_NAME) == 0)
+			return FALSE;
+		break;
+	case MAILBOX_LIST_NOTIFY_SUBSCRIPTION_CHANGE:
+		if ((wanted_events & IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE) == 0)
+			return FALSE;
+		break;
+	case MAILBOX_LIST_NOTIFY_UIDVALIDITY:
+		if ((wanted_events & (IMAP_NOTIFY_EVENT_MESSAGE_NEW |
+				      IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE |
+				      IMAP_NOTIFY_EVENT_FLAG_CHANGE)) == 0)
+			return FALSE;
+		mailbox_event = TRUE;
+		break;
+	case MAILBOX_LIST_NOTIFY_APPENDS:
+		if ((wanted_events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) == 0)
+			return FALSE;
+		mailbox_event = TRUE;
+		break;
+	case MAILBOX_LIST_NOTIFY_EXPUNGES:
+		if ((wanted_events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) == 0)
+			return FALSE;
+		mailbox_event = TRUE;
+		break;
+	case MAILBOX_LIST_NOTIFY_SEEN_CHANGES:
+	case MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES:
+		if ((wanted_events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) == 0)
+			return FALSE;
+		mailbox_event = TRUE;
+		break;
+	}
+
+	if (mailbox_event) {
+		/* if this is an even for selected mailbox, ignore it */
+		box = notify_ns->ctx->client->mailbox;
+		if (box != NULL &&
+		    mailbox_equals(box, notify_ns->ns, rec->vname))
+			return FALSE;
+	}
+	return TRUE;
+}
+
+bool imap_notify_match_mailbox(struct imap_notify_namespace *notify_ns,
+			       const struct imap_notify_mailboxes *notify_boxes,
+			       const char *vname)
+{
+	const char *const *namep;
+	unsigned int name_len;
+	char ns_sep;
+
+	switch (notify_boxes->type) {
+	case IMAP_NOTIFY_TYPE_SUBSCRIBED:
+		// FIXME
+		return TRUE;
+	case IMAP_NOTIFY_TYPE_SUBTREE:
+		ns_sep = mail_namespace_get_sep(notify_ns->ns);
+		array_foreach(&notify_boxes->names, namep) {
+			name_len = strlen(*namep);
+			if (name_len == 0) {
+				/* everything under root. NOTIFY spec itself
+				   doesn't define this, but we use it for
+				   implementing "personal" */
+				return TRUE;
+			}
+			if (strncmp(*namep, vname, name_len) == 0 &&
+			    (vname[name_len] == '\0' ||
+			     vname[name_len] == ns_sep))
+				return TRUE;
+		}
+		break;
+	case IMAP_NOTIFY_TYPE_MAILBOX:
+		array_foreach(&notify_boxes->names, namep) {
+			if (strcmp(*namep, vname) == 0)
+				return TRUE;
+		}
+		break;
+	}
+	return FALSE;
+}
+
+static bool
+imap_notify_match(struct imap_notify_namespace *notify_ns,
+		  const struct mailbox_list_notify_rec *rec)
+{
+	const struct imap_notify_mailboxes *notify_boxes;
+
+	array_foreach(&notify_ns->mailboxes, notify_boxes) {
+		if (imap_notify_match_event(notify_ns, notify_boxes, rec) &&
+		    imap_notify_match_mailbox(notify_ns, notify_boxes, rec->vname))
+			return TRUE;
+	}
+	return FALSE;
+}
+
+static int imap_client_notify_ns(struct imap_notify_namespace *notify_ns)
+{
+	const struct mailbox_list_notify_rec *rec;
+	int ret, ret2 = 1;
+
+	while ((ret = mailbox_list_notify_next(notify_ns->notify, &rec)) > 0) {
+		if (imap_notify_match(notify_ns, rec)) T_BEGIN {
+			ret2 = imap_notify_next(notify_ns, rec);
+		} T_END;
+		if (ret2 <= 0)
+			break;
+	}
+	if (ret < 0) {
+		/* failed to get some notifications */
+		return -1;
+	}
+	return ret2;
+}
+
+static int
+imap_client_notify_selected(struct client *client)
+{
+	struct imap_fetch_context *fetch_ctx = client->notify_ctx->fetch_ctx;
+	int ret;
+
+	if (!fetch_ctx->state.fetching)
+		return 1;
+
+	if ((ret = imap_fetch_more_no_lock_update(fetch_ctx)) <= 0)
+		return ret;
+	/* finished the FETCH */
+	if (imap_fetch_end(fetch_ctx) < 0)
+		return -1;
+	return 1;
+}
+
+static int imap_client_notify_more(struct client *client)
+{
+	struct imap_notify_namespace *notify_ns;
+	int ret = 1;
+
+	/* send notifications for selected mailbox first. note that it may
+	   leave the client's output stream in the middle of a FETCH reply. */
+	if (client->notify_ctx->fetch_ctx != NULL) {
+		if ((ret = imap_client_notify_selected(client)) < 0) {
+			client->notify_ctx->fetch_ctx->state.failed = FALSE;
+			ret = -1;
+		}
+	}
+
+	/* send notifications for non-selected mailboxes */
+	array_foreach_modifiable(&client->notify_ctx->namespaces, notify_ns) {
+		if (ret == 0)
+			break;
+		if (imap_client_notify_ns(notify_ns) < 0)
+			ret = -1;
+	}
+
+	if (ret < 0) {
+		client_send_line(notify_ns->ctx->client,
+			"* NO NOTIFY error, some events may have got lost");
+	}
+	return ret;
+}
+
+int imap_client_notify_newmails(struct client *client)
+{
+	struct imap_fetch_context *fetch_ctx = client->notify_ctx->fetch_ctx;
+	struct mailbox_status status;
+	struct mail_search_args *search_args;
+	struct mail_search_arg *arg;
+
+	i_assert(client->mailbox != NULL);
+
+	if (fetch_ctx == NULL) {
+		/* FETCH notifications not enabled in this session */
+		return 1;
+	}
+	if (client->notify_ctx->notifying)
+		return imap_client_notify_more(client);
+	client->notify_ctx->notifying = TRUE;
+
+	i_assert(!fetch_ctx->state.fetching);
+
+	mailbox_get_open_status(client->mailbox, STATUS_UIDNEXT, &status);
+
+	search_args = mail_search_build_init();
+	arg = mail_search_build_add(search_args, SEARCH_UIDSET);
+	p_array_init(&arg->value.seqset, search_args->pool, 1);
+	seq_range_array_add_range(&arg->value.seqset,
+				  client->notify_uidnext, status.uidnext-1);
+	client->notify_uidnext = status.uidnext;
+
+	imap_fetch_begin(fetch_ctx, client->mailbox, search_args);
+	mail_search_args_unref(&search_args);
+
+	return imap_client_notify_more(client);
+}
+
+void imap_client_notify_finished(struct client *client)
+{
+	if (client->notify_ctx != NULL)
+		client->notify_ctx->notifying = FALSE;
+}
+
+static void notify_callback(void *context)
+{
+	struct imap_notify_namespace *notify_ns = context;
+
+	o_stream_cork(notify_ns->ctx->client->output);
+	imap_client_notify_ns(notify_ns);
+	o_stream_uncork(notify_ns->ctx->client->output);
+}
+
+static enum mailbox_list_notify_event
+imap_events_to_notify(enum imap_notify_event events)
+{
+	enum mailbox_list_notify_event ret = 0;
+
+	if ((events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) != 0) {
+		ret |= MAILBOX_LIST_NOTIFY_APPENDS |
+			MAILBOX_LIST_NOTIFY_UIDVALIDITY;
+	}
+	if ((events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) != 0) {
+		ret |= MAILBOX_LIST_NOTIFY_EXPUNGES |
+			MAILBOX_LIST_NOTIFY_UIDVALIDITY;
+	}
+	if ((events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) != 0) {
+		ret |= MAILBOX_LIST_NOTIFY_SEEN_CHANGES |
+			MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES |
+			MAILBOX_LIST_NOTIFY_UIDVALIDITY;
+	}
+	if ((events & IMAP_NOTIFY_EVENT_MAILBOX_NAME) != 0) {
+		ret |= MAILBOX_LIST_NOTIFY_CREATE |
+			MAILBOX_LIST_NOTIFY_DELETE |
+			MAILBOX_LIST_NOTIFY_RENAME;
+	}
+	if ((events & IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE) != 0)
+		ret |= MAILBOX_LIST_NOTIFY_SUBSCRIPTION_CHANGE;
+	return ret;
+}
+
+static void imap_notify_callback(struct mailbox *box, struct client *client)
+{
+	struct client_command_context *cmd;
+	enum mailbox_sync_flags sync_flags = 0;
+
+	i_assert(client->command_queue_size == 0);
+	i_assert(box == client->mailbox);
+
+	/* create a fake command to handle this */
+	cmd = client_command_alloc(client);
+	cmd->tag = "*";
+	cmd->name = "NOTIFY-CALLBACK";
+
+	if (!client->notify_ctx->selected_immediate_expunges)
+		sync_flags |= MAILBOX_SYNC_FLAG_NO_EXPUNGES;
+	if (cmd_sync(cmd, sync_flags, 0, NULL))
+		i_unreached();
+	(void)cmd_sync_delayed(client);
+}
+
+static void imap_notify_watch_selected_mailbox(struct client *client)
+{
+	if (client->command_queue_size > 0) {
+		/* don't add it until all commands are finished */
+		return;
+	}
+	if (client->mailbox == NULL) {
+		/* mailbox not selected */
+		return;
+	}
+	if (client->notify_ctx == NULL || !client->notify_ctx->selected_set) {
+		/* client doesn't want selected mailbox notifications */
+		return;
+
+	}
+	mailbox_notify_changes(client->mailbox, imap_notify_callback, client);
+	client->notify_ctx->watching_mailbox = TRUE;
+}
+
+static void imap_notify_watch_timeout(struct client *client)
+{
+	timeout_remove(&client->notify_ctx->to_watch);
+	imap_notify_watch_selected_mailbox(client);
+}
+
+void imap_client_notify_command_freed(struct client *client)
+{
+	imap_notify_watch_selected_mailbox(client);
+}
+
+static void imap_notify_cmd_hook_pre(struct client_command_context *cmd)
+{
+	struct imap_notify_context *ctx = cmd->client->notify_ctx;
+
+	if (ctx == NULL)
+		return;
+
+	/* remove mailbox watcher before starting any commands */
+	if (ctx->watching_mailbox) {
+		mailbox_notify_changes_stop(cmd->client->mailbox);
+		ctx->watching_mailbox = FALSE;
+	}
+	if (ctx->to_watch != NULL)
+		timeout_remove(&ctx->to_watch);
+}
+
+static void imap_notify_cmd_hook_post(struct client_command_context *cmd)
+{
+	struct imap_notify_context *ctx = cmd->client->notify_ctx;
+
+	if (ctx == NULL)
+		return;
+
+	/* add mailbox watched back after a small delay */
+	if (ctx->to_watch != NULL)
+		timeout_reset(ctx->to_watch);
+	else {
+		ctx->to_watch = timeout_add(IMAP_NOTIFY_WATCH_ADD_DELAY_MSECS,
+					    imap_notify_watch_timeout,
+					    cmd->client);
+	}
+}
+
+int imap_notify_begin(struct imap_notify_context *ctx)
+{
+	struct imap_notify_namespace *notify_ns;
+	const struct imap_notify_mailboxes *notify_boxes;
+	enum mailbox_list_notify_event notify_events;
+	int ret = -1;
+
+	if (!notify_hook_registered) {
+		notify_hook_registered = TRUE;
+		command_hook_register(imap_notify_cmd_hook_pre,
+				      imap_notify_cmd_hook_post);
+	}
+
+	array_foreach_modifiable(&ctx->namespaces, notify_ns) {
+		notify_events = 0;
+		array_foreach(&notify_ns->mailboxes, notify_boxes) {
+			notify_events |=
+				imap_events_to_notify(notify_boxes->events);
+		}
+		if (mailbox_list_notify_init(notify_ns->ns->list, notify_events,
+					     &notify_ns->notify) < 0) {
+			/* notifications not supported */
+		} else {
+			ret = 0;
+			mailbox_list_notify_wait(notify_ns->notify,
+						 notify_callback, notify_ns);
+		}
+	}
+	/* enable NOTIFY as long as even one namespace supports it,
+	   ignore the rest */
+	return ret;
+}
+
+void imap_notify_deinit(struct imap_notify_context **_ctx)
+{
+	struct imap_notify_context *ctx = *_ctx;
+	struct imap_notify_namespace *notify_ns;
+
+	*_ctx = NULL;
+
+	array_foreach_modifiable(&ctx->namespaces, notify_ns) {
+		if (notify_ns->notify != NULL)
+			mailbox_list_notify_deinit(&notify_ns->notify);
+	}
+	if (ctx->to_watch != NULL)
+		timeout_remove(&ctx->to_watch);
+	if (ctx->fetch_ctx != NULL)
+		imap_fetch_free(&ctx->fetch_ctx);
+	pool_unref(&ctx->pool);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-notify.h	Mon Aug 13 15:23:32 2012 +0300
@@ -0,0 +1,71 @@
+#ifndef IMAP_NOTIFY_H
+#define IMAP_NOTIFY_H
+
+enum imap_notify_type {
+	IMAP_NOTIFY_TYPE_SUBSCRIBED,
+	IMAP_NOTIFY_TYPE_SUBTREE,
+	IMAP_NOTIFY_TYPE_MAILBOX
+};
+
+enum imap_notify_event {
+	IMAP_NOTIFY_EVENT_MESSAGE_NEW		= 0x01,
+	IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE	= 0x02,
+	IMAP_NOTIFY_EVENT_FLAG_CHANGE		= 0x04,
+	IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE	= 0x08,
+	IMAP_NOTIFY_EVENT_MAILBOX_NAME		= 0x10,
+	IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE	= 0x20,
+	IMAP_NOTIFY_EVENT_MAILBOX_METADATA_CHANGE = 0x40,
+	IMAP_NOTIFY_EVENT_SERVER_METADATA_CHANGE = 0x80
+};
+#define UNSUPPORTED_EVENTS \
+	(IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE | \
+	 IMAP_NOTIFY_EVENT_MAILBOX_METADATA_CHANGE | \
+	 IMAP_NOTIFY_EVENT_SERVER_METADATA_CHANGE)
+
+struct imap_notify_mailboxes {
+	enum imap_notify_event events;
+	enum imap_notify_type type;
+	ARRAY_TYPE(const_string) names;
+};
+
+struct imap_notify_namespace {
+	struct imap_notify_context *ctx;
+	struct mail_namespace *ns;
+
+	struct mailbox_list_notify *notify;
+	ARRAY_DEFINE(mailboxes, struct imap_notify_mailboxes);
+};
+
+struct imap_notify_context {
+	pool_t pool;
+	struct client *client;
+	const char *error;
+
+	ARRAY_DEFINE(namespaces, struct imap_notify_namespace);
+	enum imap_notify_event selected_events;
+	enum imap_notify_event global_used_events;
+	unsigned int global_max_mailbox_names;
+
+	struct imap_fetch_context *fetch_ctx;
+	struct timeout *to_watch;
+
+	unsigned int selected_set:1;
+	unsigned int selected_immediate_expunges:1;
+	unsigned int send_immediate_status:1;
+	unsigned int watching_mailbox:1;
+	unsigned int notifying:1;
+};
+
+bool imap_notify_match_mailbox(struct imap_notify_namespace *notify_ns,
+			       const struct imap_notify_mailboxes *notify_boxes,
+			       const char *vname);
+
+int imap_client_notify_newmails(struct client *client);
+void imap_client_notify_finished(struct client *client);
+
+void imap_client_notify_command_freed(struct client *client);
+
+int imap_notify_begin(struct imap_notify_context *ctx);
+void imap_notify_deinit(struct imap_notify_context **ctx);
+
+#endif
--- a/src/imap/imap-search.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-search.c	Mon Aug 13 15:23:32 2012 +0300
@@ -10,6 +10,7 @@
 #include "imap-seqset.h"
 #include "imap-util.h"
 #include "mail-search-build.h"
+#include "imap-fetch.h"
 #include "imap-commands.h"
 #include "imap-search-args.h"
 #include "imap-search.h"
@@ -42,10 +43,28 @@
 }
 
 static bool
+search_parse_fetch_att(struct imap_search_context *ctx,
+		       const struct imap_arg *update_args)
+{
+	const char *error;
+
+	ctx->fetch_pool = pool_alloconly_create("search update fetch", 512);
+	if (imap_fetch_att_list_parse(ctx->cmd->client, ctx->fetch_pool,
+				      update_args, &ctx->fetch_ctx, &error) < 0) {
+		client_send_command_error(ctx->cmd, t_strconcat(
+			"SEARCH UPDATE fetch-att: ", error, NULL));
+		pool_unref(&ctx->fetch_pool);
+		return FALSE;
+	}
+	return TRUE;
+}
+
+static bool
 search_parse_return_options(struct imap_search_context *ctx,
 			    const struct imap_arg *args)
 {
 	struct client_command_context *cmd = ctx->cmd;
+	const struct imap_arg *update_args;
 	const char *name, *str;
 	unsigned int idx;
 
@@ -69,9 +88,19 @@
 			ctx->return_options |= SEARCH_RETURN_SAVE;
 		else if (strcmp(name, "CONTEXT") == 0) {
 			/* no-op */
-		} else if (strcmp(name, "UPDATE") == 0)
+		} else if (strcmp(name, "UPDATE") == 0) {
+			if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0) {
+				client_send_command_error(cmd,
+					"SEARCH return options have duplicate UPDATE.");
+				return FALSE;
+			}
 			ctx->return_options |= SEARCH_RETURN_UPDATE;
-		else if (strcmp(name, "RELEVANCY") == 0)
+			if (imap_arg_get_list(args, &update_args)) {
+				if (!search_parse_fetch_att(ctx, update_args))
+					return FALSE;
+				args++;
+			}
+		} else if (strcmp(name, "RELEVANCY") == 0)
 			ctx->return_options |= SEARCH_RETURN_RELEVANCY;
 		else if (strcmp(name, "PARTIAL") == 0) {
 			if (ctx->partial1 != 0) {
@@ -153,6 +182,7 @@
 		str_append_c(str, ']');
 		client_send_line(client, str_c(str));
 		ctx->return_options &= ~SEARCH_RETURN_UPDATE;
+		imap_search_context_free(ctx);
 		return;
 	}
 	result = mailbox_search_result_save(ctx->search_ctx,
@@ -163,6 +193,10 @@
 	update->tag = i_strdup(ctx->cmd->tag);
 	update->result = result;
 	update->return_uids = ctx->cmd->uid;
+	update->fetch_pool = ctx->fetch_pool;
+	update->fetch_ctx = ctx->fetch_ctx;
+	ctx->fetch_pool = NULL;
+	ctx->fetch_ctx = NULL;
 }
 
 static void imap_search_send_result_standard(struct imap_search_context *ctx)
@@ -510,20 +544,24 @@
 		return 1;
 	}
 
-	if (!search_parse_return_options(ctx, list_args))
+	if (!search_parse_return_options(ctx, list_args)) {
+		imap_search_context_free(ctx);
 		return -1;
+	}
 
 	if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0) {
 		/* wait if there is another SEARCH SAVE command running. */
-		cmd->search_save_result = TRUE;
-		if (client_handle_search_save_ambiguity(cmd))
+		if (client_handle_search_save_ambiguity(cmd)) {
+			imap_search_context_free(ctx);
 			return 0;
+		}
 
 		/* make sure the search result gets cleared if SEARCH fails */
 		if (array_is_created(&cmd->client->search_saved_uidset))
 			array_clear(&cmd->client->search_saved_uidset);
 		else
 			i_array_init(&cmd->client->search_saved_uidset, 128);
+		cmd->search_save_result = TRUE;
 	}
 
 	*_args = args + 2;
@@ -553,6 +591,9 @@
 	i_array_init(&ctx->result, 128);
 	if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0)
 		imap_search_result_save(ctx);
+	else {
+		i_assert(ctx->fetch_ctx == NULL);
+	}
 	if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0)
 		i_array_init(&ctx->relevancy_scores, 128);
 
@@ -592,7 +633,26 @@
 	array_free(&ctx->result);
 	mail_search_args_deinit(ctx->sargs);
 	mail_search_args_unref(&ctx->sargs);
+	imap_search_context_free(ctx);
 
 	ctx->cmd->context = NULL;
 	return ret;
 }
+
+void imap_search_context_free(struct imap_search_context *ctx)
+{
+	if (ctx->fetch_ctx != NULL) {
+		imap_fetch_free(&ctx->fetch_ctx);
+		pool_unref(&ctx->fetch_pool);
+	}
+}
+
+void imap_search_update_free(struct imap_search_update *update)
+{
+	if (update->fetch_ctx != NULL) {
+		imap_fetch_free(&update->fetch_ctx);
+		pool_unref(&update->fetch_pool);
+	}
+	mailbox_search_result_free(&update->result);
+	i_free(update->tag);
+}
--- a/src/imap/imap-search.h	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-search.h	Mon Aug 13 15:23:32 2012 +0300
@@ -26,6 +26,9 @@
 	struct mailbox_transaction_context *trans;
         struct mail_search_context *search_ctx;
 
+	pool_t fetch_pool;
+	struct imap_fetch_context *fetch_ctx;
+
 	struct mail_search_args *sargs;
 	enum search_return_options return_options;
 	uint32_t partial1, partial2;
@@ -47,9 +50,11 @@
 
 int cmd_search_parse_return_if_found(struct imap_search_context *ctx,
 				     const struct imap_arg **args);
+void imap_search_context_free(struct imap_search_context *ctx);
 
 bool imap_search_start(struct imap_search_context *ctx,
 		       struct mail_search_args *sargs,
 		       const enum mail_sort_type *sort_program) ATTR_NULL(3);
+void imap_search_update_free(struct imap_search_update *update);
 
 #endif
--- a/src/imap/imap-sync.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/imap-sync.c	Mon Aug 13 15:23:32 2012 +0300
@@ -3,12 +3,15 @@
 #include "imap-common.h"
 #include "str.h"
 #include "ostream.h"
+#include "mail-user.h"
 #include "mail-storage.h"
-#include "mail-user.h"
+#include "mail-search-build.h"
 #include "imap-quote.h"
 #include "imap-util.h"
+#include "imap-fetch.h"
+#include "imap-notify.h"
+#include "imap-commands.h"
 #include "imap-sync.h"
-#include "imap-commands.h"
 
 struct client_sync_context {
 	/* if multiple commands are in progress, we may need to wait for them
@@ -29,17 +32,24 @@
 	struct mailbox_sync_context *sync_ctx;
 	struct mail *mail;
 
+	struct mailbox_status status;
+	struct mailbox_sync_status sync_status;
+
 	struct mailbox_sync_rec sync_rec;
 	ARRAY_TYPE(keywords) tmp_keywords;
 	ARRAY_TYPE(seq_range) expunges;
 	uint32_t seq;
 
 	ARRAY_TYPE(seq_range) search_adds, search_removes;
+	unsigned int search_update_idx;
 
 	unsigned int messages_count;
 
 	unsigned int failed:1;
+	unsigned int finished:1;
 	unsigned int no_newmail:1;
+	unsigned int have_new_mails:1;
+	unsigned int search_update_notifying:1;
 };
 
 static void uids_to_seqs(struct mailbox *box, ARRAY_TYPE(seq_range) *uids)
@@ -67,17 +77,78 @@
 	} T_END;
 }
 
-static void
+static int search_update_fetch_more(const struct imap_search_update *update)
+{
+	int ret;
+
+	if ((ret = imap_fetch_more_no_lock_update(update->fetch_ctx)) <= 0)
+		return ret;
+	/* finished the FETCH */
+	if (imap_fetch_end(update->fetch_ctx) < 0)
+		return -1;
+	return 1;
+}
+
+static int
+imap_sync_send_fetch_to_search_update(struct imap_sync_context *ctx,
+				      const struct imap_search_update *update)
+{
+	struct mail_search_args *search_args;
+	struct mail_search_arg *arg;
+	ARRAY_TYPE(seq_range) seqs;
+
+	if (ctx->search_update_notifying)
+		return search_update_fetch_more(update);
+
+	i_assert(!update->fetch_ctx->state.fetching);
+
+	if (array_count(&ctx->search_adds) == 0 || !ctx->have_new_mails)
+		return 1;
+
+	search_args = mail_search_build_init();
+	arg = mail_search_build_add(search_args, SEARCH_UIDSET);
+	p_array_init(&arg->value.seqset, search_args->pool, 1);
+
+	/* find the newly appended messages: ctx->messages_count is the message
+	   count before new messages found by sync, client->messages_count is
+	   the number of messages after. */
+	t_array_init(&seqs, 1);
+	seq_range_array_add_range(&seqs, ctx->messages_count+1,
+				  ctx->client->messages_count);
+	mailbox_get_uid_range(ctx->client->mailbox, &seqs, &arg->value.seqset);
+	/* remove messages not in the search_adds list */
+	seq_range_array_intersect(&arg->value.seqset, &ctx->search_adds);
+
+	imap_fetch_begin(update->fetch_ctx, ctx->client->mailbox, search_args);
+	mail_search_args_unref(&search_args);
+	return search_update_fetch_more(update);
+}
+
+static int
 imap_sync_send_search_update(struct imap_sync_context *ctx,
-			     const struct imap_search_update *update)
+			     const struct imap_search_update *update,
+			     bool removes_only)
 {
 	string_t *cmd;
+	int ret = 1;
 
-	mailbox_search_result_sync(update->result, &ctx->search_removes,
-				   &ctx->search_adds);
+	if (!ctx->search_update_notifying) {
+		mailbox_search_result_sync(update->result, &ctx->search_removes,
+					   &ctx->search_adds);
+	}
 	if (array_count(&ctx->search_adds) == 0 &&
 	    array_count(&ctx->search_removes) == 0)
-		return;
+		return 1;
+
+	i_assert(array_count(&ctx->search_adds) == 0 || !removes_only);
+	if (update->fetch_ctx != NULL) {
+		ret = imap_sync_send_fetch_to_search_update(ctx, update);
+		if (ret == 0) {
+			ctx->search_update_notifying = TRUE;
+			return 0;
+		}
+	}
+	ctx->search_update_notifying = FALSE;
 
 	cmd = t_str_new(256);
 	str_append(cmd, "* ESEARCH (TAG ");
@@ -103,23 +174,35 @@
 	}
 	str_append(cmd, "\r\n");
 	o_stream_nsend(ctx->client->output, str_data(cmd), str_len(cmd));
+	return ret;
 }
 
-static void imap_sync_send_search_updates(struct imap_sync_context *ctx)
+static int
+imap_sync_send_search_updates(struct imap_sync_context *ctx, bool removes_only)
 {
-	const struct imap_search_update *update;
+	const struct imap_search_update *updates;
+	unsigned int i, count;
+	int ret = 1;
 
 	if (!array_is_created(&ctx->client->search_updates))
-		return;
+		return 1;
 
 	if (!array_is_created(&ctx->search_removes)) {
 		i_array_init(&ctx->search_removes, 64);
 		i_array_init(&ctx->search_adds, 128);
 	}
 
-	array_foreach(&ctx->client->search_updates, update) T_BEGIN {
-		imap_sync_send_search_update(ctx, update);
-	} T_END;
+	updates = array_get(&ctx->client->search_updates, &count);
+	for (i = ctx->search_update_idx; i < count; i++) {
+		T_BEGIN {
+			ret = imap_sync_send_search_update(ctx, &updates[i],
+							   removes_only);
+		} T_END;
+		if (ret <= 0)
+			break;
+	}
+	ctx->search_update_idx = i;
+	return ret;
 }
 
 struct imap_sync_context *
@@ -130,6 +213,11 @@
 
 	i_assert(client->mailbox == box);
 
+	if (client->notify_immediate_expunges) {
+		/* NOTIFY enabled without SELECTED-DELAYED */
+		flags &= ~MAILBOX_SYNC_FLAG_NO_EXPUNGES;
+	}
+
 	ctx = i_new(struct imap_sync_context, 1);
 	ctx->client = client;
 	ctx->box = box;
@@ -155,37 +243,38 @@
 	/* send search updates the first time after sync is initialized.
 	   it now contains expunged messages that must be sent before
 	   EXPUNGE replies. */
-	imap_sync_send_search_updates(ctx);
+	if (imap_sync_send_search_updates(ctx, TRUE) == 0)
+		i_unreached();
+	ctx->search_update_idx = 0;
 	return ctx;
 }
 
 static void
 imap_sync_send_highestmodseq(struct imap_sync_context *ctx,
-			     const struct mailbox_status *status,
-			     const struct mailbox_sync_status *sync_status,
 			     struct client_command_context *sync_cmd)
 {
 	struct client *client = ctx->client;
 	uint64_t send_modseq = 0;
 
-	if (sync_status->sync_delayed_expunges &&
+	if (ctx->sync_status.sync_delayed_expunges &&
 	    client->highest_fetch_modseq > client->sync_last_full_modseq) {
 		/* if client updates highest-modseq using returned MODSEQs
 		   it loses expunges. try to avoid this by sending it a lower
 		   pre-expunge HIGHESTMODSEQ reply. */
 		send_modseq = client->sync_last_full_modseq;
-	} else if (!sync_status->sync_delayed_expunges &&
-		   status->highest_modseq > client->sync_last_full_modseq &&
-		   status->highest_modseq > client->highest_fetch_modseq) {
+	} else if (!ctx->sync_status.sync_delayed_expunges &&
+		   ctx->status.highest_modseq > client->sync_last_full_modseq &&
+		   ctx->status.highest_modseq > client->highest_fetch_modseq) {
 		/* we've probably sent some VANISHED or EXISTS replies which
 		   increased the highest-modseq. notify the client about
 		   this. */
-		send_modseq = status->highest_modseq;
+		send_modseq = ctx->status.highest_modseq;
 	}
 
 	if (send_modseq == 0) {
 		/* no sending */
 	} else if (sync_cmd->sync != NULL && /* IDLE doesn't have ->sync */
+		   sync_cmd->sync->tagline != NULL && /* NOTIFY doesn't have tagline */
 		   strncmp(sync_cmd->sync->tagline, "OK ", 3) == 0 &&
 		   sync_cmd->sync->tagline[3] != '[') {
 		/* modify the tagged reply directly */
@@ -200,66 +289,95 @@
 			(unsigned long long)send_modseq));
 	}
 
-	if (!sync_status->sync_delayed_expunges) {
+	if (!ctx->sync_status.sync_delayed_expunges) {
 		/* no delayed expunges, remember this for future */
-		client->sync_last_full_modseq = status->highest_modseq;
+		client->sync_last_full_modseq = ctx->status.highest_modseq;
 	}
 	client->highest_fetch_modseq = 0;
 }
 
-int imap_sync_deinit(struct imap_sync_context *ctx,
-		     struct client_command_context *sync_cmd)
+static int imap_sync_finish(struct imap_sync_context *ctx)
 {
 	struct client *client = ctx->client;
-	struct mailbox_status status;
-	struct mailbox_sync_status sync_status;
-	int ret;
+	int ret = ctx->failed ? -1 : 0;
+
+	if (ctx->finished)
+		return ret;
+	ctx->finished = TRUE;
 
 	mail_free(&ctx->mail);
 	if (array_is_created(&ctx->expunges))
 		array_free(&ctx->expunges);
 
-	if (mailbox_sync_deinit(&ctx->sync_ctx, &sync_status) < 0 ||
+	if (mailbox_sync_deinit(&ctx->sync_ctx, &ctx->sync_status) < 0 ||
 	    ctx->failed) {
 		mailbox_transaction_rollback(&ctx->t);
-		array_free(&ctx->tmp_keywords);
-		i_free(ctx);
+		ctx->failed = TRUE;
 		return -1;
 	}
 	mailbox_get_open_status(ctx->box, STATUS_UIDVALIDITY |
 				STATUS_MESSAGES | STATUS_RECENT |
-				STATUS_HIGHESTMODSEQ, &status);
+				STATUS_HIGHESTMODSEQ, &ctx->status);
 
-	ret = mailbox_transaction_commit(&ctx->t);
+	if (mailbox_transaction_commit(&ctx->t) < 0) {
+		ctx->failed = TRUE;
+		ret = -1;
+	}
 
-	if (status.uidvalidity != client->uidvalidity) {
+	if (ctx->status.uidvalidity != client->uidvalidity) {
 		/* most clients would get confused by this. disconnect them. */
 		client_disconnect_with_error(client,
 					     "Mailbox UIDVALIDITY changed");
 	}
 	if (!ctx->no_newmail) {
-		if (status.messages < ctx->messages_count)
+		if (ctx->status.messages < ctx->messages_count)
 			i_panic("Message count decreased");
-		client->messages_count = status.messages;
-		if (status.messages != ctx->messages_count) {
+		if (ctx->status.messages != ctx->messages_count &&
+		    client->notify_count_changes) {
 			client_send_line(client,
-				t_strdup_printf("* %u EXISTS", status.messages));
+				t_strdup_printf("* %u EXISTS", ctx->status.messages));
+			ctx->have_new_mails = TRUE;
+		}
+		if (ctx->status.recent != client->recent_count &&
+		    client->notify_count_changes) {
+			client_send_line(client,
+				t_strdup_printf("* %u RECENT", ctx->status.recent));
 		}
-		if (status.recent != client->recent_count &&
-		    !ctx->no_newmail) {
-			client->recent_count = status.recent;
-			client_send_line(client,
-				t_strdup_printf("* %u RECENT", status.recent));
-		}
+		client->messages_count = ctx->status.messages;
+		client->recent_count = ctx->status.recent;
 	}
+	return ret;
+}
+
+static int imap_sync_notify_more(struct imap_sync_context *ctx)
+{
+	int ret = 1;
+
+	if (ctx->have_new_mails && ctx->client->notify_ctx != NULL) {
+		/* send FETCH replies for the new mails */
+		if ((ret = imap_client_notify_newmails(ctx->client)) == 0)
+			return 0;
+		if (ret < 0)
+			ctx->failed = TRUE;
+	}
+
 	/* send search updates the second time after syncing in done.
 	   now it contains added/removed messages. */
-	imap_sync_send_search_updates(ctx);
+	if ((ret = imap_sync_send_search_updates(ctx, FALSE)) < 0)
+		ctx->failed = TRUE;
+	return ret;
+}
 
-	if ((client->enabled_features & MAILBOX_FEATURE_QRESYNC) != 0) {
-		imap_sync_send_highestmodseq(ctx, &status, &sync_status,
-					     sync_cmd);
-	}
+int imap_sync_deinit(struct imap_sync_context *ctx,
+		     struct client_command_context *sync_cmd)
+{
+	int ret;
+
+	ret = imap_sync_finish(ctx);
+	imap_client_notify_finished(ctx->client);
+
+	if ((ctx->client->enabled_features & MAILBOX_FEATURE_QRESYNC) != 0)
+		imap_sync_send_highestmodseq(ctx, sync_cmd);
 
 	if (array_is_created(&ctx->search_removes)) {
 		array_free(&ctx->search_removes);
@@ -373,6 +491,11 @@
 {
 	int ret = 1;
 
+	if (!ctx->client->notify_count_changes) {
+		/* NOTIFY: MessageEvent not specified for selected mailbox */
+		return 1;
+	}
+
 	if (array_is_created(&ctx->expunges)) {
 		/* Use a single VANISHED line */
 		seq_range_array_add_range(&ctx->expunges,
@@ -400,6 +523,9 @@
 	string_t *str;
 	int ret = 1;
 
+	if (ctx->finished)
+		return imap_sync_notify_more(ctx);
+
 	/* finish syncing even when client has disconnected. otherwise our
 	   internal state (ctx->messages_count) can get messed up and unless
 	   we immediately stop handling all commands and syncs we could end up
@@ -431,6 +557,11 @@
 			 ctx->sync_rec.type == MAILBOX_SYNC_TYPE_EXPUNGE);
 		switch (ctx->sync_rec.type) {
 		case MAILBOX_SYNC_TYPE_FLAGS:
+			if (!ctx->client->notify_flag_changes) {
+				/* NOTIFY: FlagChange not specified for
+				   selected mailbox */
+				break;
+			}
 			if (ctx->seq == 0)
 				ctx->seq = ctx->sync_rec.seq1;
 
@@ -458,6 +589,13 @@
 			if ((ctx->client->enabled_features &
 			     MAILBOX_FEATURE_CONDSTORE) == 0)
 				break;
+			if (!ctx->client->notify_flag_changes) {
+				/* NOTIFY: FlagChange not specified for
+				   selected mailbox. The RFC doesn't explicitly
+				   specify MODSEQ changes, but they're close
+				   enough to flag changes. */
+				break;
+			}
 
 			if (ctx->seq == 0)
 				ctx->seq = ctx->sync_rec.seq1;
@@ -480,6 +618,11 @@
 	}
 	if (array_is_created(&ctx->expunges))
 		imap_sync_vanished(ctx);
+	if (ret > 0) {
+		if (imap_sync_finish(ctx) < 0)
+			return -1;
+		return imap_sync_more(ctx);
+	}
 	return ret;
 }
 
@@ -499,10 +642,10 @@
 {
 	if (cmd->sync->callback != NULL)
 		return cmd->sync->callback(cmd);
-	else {
+
+	if (cmd->sync->tagline != NULL)
 		client_send_tagline(cmd, cmd->sync->tagline);
-		return TRUE;
-	}
+	return TRUE;
 }
 
 static bool cmd_sync_continue(struct client_command_context *sync_cmd)
@@ -586,6 +729,7 @@
 	client->sync_counter++;
 
 	no_newmail = (client->set->parsed_workarounds & WORKAROUND_DELAY_NEWMAIL) != 0 &&
+		client->notify_ctx == NULL && /* always disabled with NOTIFY */
 		(imap_flags & IMAP_SYNC_FLAG_SAFE) == 0;
 	if (no_newmail) {
 		/* expunges might break the client just as badly as new mail
@@ -620,7 +764,7 @@
 {
 	struct client *client = cmd->client;
 
-	i_assert(client->output_lock == cmd || client->output_lock == NULL);
+	i_assert(!client->output_locked);
 
 	if (cmd->cancel)
 		return TRUE;
@@ -628,7 +772,8 @@
 	if (client->mailbox == NULL) {
 		/* no mailbox selected, no point in delaying the sync */
 		i_assert(callback == NULL);
-		client_send_tagline(cmd, tagline);
+		if (tagline != NULL)
+			client_send_tagline(cmd, tagline);
 		return TRUE;
 	}
 
@@ -643,7 +788,6 @@
 	cmd->func = NULL;
 	cmd->context = NULL;
 
-	client->output_lock = NULL;
 	if (client->input_lock == cmd)
 		client->input_lock = NULL;
 	return FALSE;
@@ -693,7 +837,7 @@
 {
 	struct client_command_context *cmd, *first_expunge, *first_nonexpunge;
 
-	if (client->output_lock != NULL) {
+	if (client->output_locked) {
 		/* wait until we can send output to client */
 		return FALSE;
 	}
--- a/src/imap/main.c	Mon Aug 13 15:20:33 2012 +0300
+++ b/src/imap/main.c	Mon Aug 13 15:23:32 2012 +0300
@@ -85,7 +85,7 @@
 
 static void client_kill_idle(struct client *client)
 {
-	if (client->output_lock != NULL)
+	if (client->output_locked)
 		return;
 
 	client_send_line(client, "* BYE Server shutting down.");