changeset 18996:64c73e6bd397

Added imap-hibernate process for gathering IDLEing imap processes. imap_hibernate_timeout setting controls how quickly the connection is moved from imap process to imap-hibernate process. Some IMAP extensions like NOTIFY, SEARCH=CONTEXT and COMPRESS aren't supported yet. There's also a new X-STATE command, which can be used to export the current IMAP connection state to a string and later on imported to get back to the original state (a quick resync feature for IMAP clients). However, this command is disabled for now due to the current code being unoptimized for untrusted input.
author Timo Sirainen <tss@iki.fi>
date Mon, 24 Aug 2015 14:13:02 +0300
parents 4da6efa3fd91
children 63ada4bc7508
files .hgignore configure.ac src/Makefile.am src/imap-hibernate/Makefile.am src/imap-hibernate/imap-client.c src/imap-hibernate/imap-client.h src/imap-hibernate/imap-hibernate-client.c src/imap-hibernate/imap-hibernate-client.h src/imap-hibernate/imap-hibernate-settings.c src/imap-hibernate/imap-master-connection.c src/imap-hibernate/imap-master-connection.h src/imap-hibernate/main.c src/imap/Makefile.am src/imap/cmd-idle.c src/imap/cmd-x-state.c src/imap/imap-client-hibernate.c src/imap/imap-client.c src/imap/imap-client.h src/imap/imap-commands.c src/imap/imap-commands.h src/imap/imap-common.h src/imap/imap-master-client.c src/imap/imap-master-client.h src/imap/imap-settings.c src/imap/imap-settings.h src/imap/imap-state.c src/imap/imap-state.h src/imap/main.c src/lib-storage/mail-user.c src/plugins/imap-zlib/imap-zlib-plugin.c
diffstat 30 files changed, 2780 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Mon Aug 24 14:14:59 2015 +0300
+++ b/.hgignore	Mon Aug 24 14:13:02 2015 +0300
@@ -68,6 +68,7 @@
 src/dns/dns-client
 src/doveadm/doveadm
 src/doveadm/doveadm-server
+src/imap-hibernate/imap-hibernate
 src/imap-login/imap-login
 src/imap-urlauth/imap-urlauth
 src/imap-urlauth/imap-urlauth-login
--- a/configure.ac	Mon Aug 24 14:14:59 2015 +0300
+++ b/configure.ac	Mon Aug 24 14:13:02 2015 +0300
@@ -2931,6 +2931,7 @@
 src/indexer/Makefile
 src/ipc/Makefile
 src/imap/Makefile
+src/imap-hibernate/Makefile
 src/imap-login/Makefile
 src/imap-urlauth/Makefile
 src/login-common/Makefile
--- a/src/Makefile.am	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/Makefile.am	Mon Aug 24 14:13:02 2015 +0300
@@ -37,6 +37,7 @@
 	ipc \
 	master \
 	login-common \
+	imap-hibernate \
 	imap-login \
 	imap \
 	imap-urlauth \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/Makefile.am	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,24 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = imap-hibernate
+
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/src/lib \
+	-I$(top_srcdir)/src/lib-settings \
+	-I$(top_srcdir)/src/lib-master \
+	-I$(top_srcdir)/src/lib-imap
+
+imap_hibernate_LDADD = $(LIBDOVECOT)
+imap_hibernate_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+imap_hibernate_SOURCES = \
+	imap-client.c \
+	imap-hibernate-client.c \
+	imap-hibernate-settings.c \
+	imap-master-connection.c \
+	main.c
+
+noinst_HEADERS = \
+	imap-client.h \
+	imap-hibernate-client.h \
+	imap-master-connection.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/imap-client.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,530 @@
+/* Copyright (c) 2014-2015 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "fd-set-nonblock.h"
+#include "fdpass.h"
+#include "hostpid.h"
+#include "connection.h"
+#include "iostream.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "base64.h"
+#include "str.h"
+#include "strescape.h"
+#include "var-expand.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "imap-keepalive.h"
+#include "imap-master-connection.h"
+#include "imap-client.h"
+
+#include <unistd.h>
+
+#define IMAP_MASTER_SOCKET_NAME "imap-master"
+
+/* we only need enough for "DONE\r\nIDLE\r\n" */
+#define IMAP_MAX_INBUF 12
+
+enum imap_client_input_state {
+	IMAP_CLIENT_INPUT_STATE_UNKNOWN,
+	IMAP_CLIENT_INPUT_STATE_BAD,
+	IMAP_CLIENT_INPUT_STATE_DONE_LF,
+	IMAP_CLIENT_INPUT_STATE_DONE_CRLF,
+	IMAP_CLIENT_INPUT_STATE_DONEIDLE
+};
+
+struct imap_client_notify {
+	int fd;
+	struct io *io;
+};
+
+struct imap_client {
+	struct imap_client *prev, *next;
+	pool_t pool;
+	struct imap_client_state state;
+	ARRAY(struct imap_client_notify) notifys;
+
+	int fd;
+	struct io *io;
+	struct istream *input;
+	struct timeout *to_keepalive;
+	struct imap_master_connection *master_conn;
+	struct ioloop_context *ioloop_ctx;
+	const char *log_prefix;
+	unsigned int imap_still_here_text_pos;
+	unsigned int next_read_threshold;
+	bool bad_done, idle_done;
+};
+
+static struct imap_client *imap_clients;
+static const char imap_still_here_text[] = "* OK Still here\r\n";
+
+static void imap_client_stop(struct imap_client *client);
+void imap_client_destroy(struct imap_client **_client, const char *reason);
+static void imap_client_add_idle_keepalive_timeout(struct imap_client *client);
+
+static void imap_client_disconnected(struct imap_client **_client)
+{
+	struct imap_client *client = *_client;
+	const char *reason;
+
+	reason = io_stream_get_disconnect_reason(client->input, NULL);
+	imap_client_destroy(_client, reason);
+}
+
+static void
+imap_client_parse_userdb_fields(struct imap_client *client,
+				const char **auth_user_r)
+{
+	const char *const *field;
+	unsigned int i;
+
+	*auth_user_r = NULL;
+
+	if (client->state.userdb_fields == NULL)
+		return;
+
+	field = t_strsplit_tabescaped(client->state.userdb_fields);
+	for (i = 0; field[i] != NULL; i++) {
+		if (strncmp(field[i], "auth_user=", 10) == 0)
+			*auth_user_r = field[i] + 10;
+	}
+}
+
+static void
+imap_client_move_back_send_callback(void *context, struct ostream *output)
+{
+	struct imap_client *client = context;
+	const struct imap_client_state *state = &client->state;
+	string_t *str = t_str_new(256);
+	const unsigned char *input_data;
+	size_t input_size;
+	ssize_t ret;
+
+	str_append_tabescaped(str, state->username);
+	if (state->session_id != NULL) {
+		str_append(str, "\tsession=");
+		str_append_tabescaped(str, state->session_id);
+	}
+	if (state->local_ip.family != 0)
+		str_printfa(str, "\tlip=%s", net_ip2addr(&state->local_ip));
+	if (state->remote_ip.family != 0)
+		str_printfa(str, "\trip=%s", net_ip2addr(&state->remote_ip));
+	if (state->userdb_fields != NULL) {
+		str_append(str, "\tuserdb_fields=");
+		str_append_tabescaped(str, state->userdb_fields);
+	}
+	if (state->peer_ip.family != 0)
+		str_printfa(str, "\tpeer_ip=%s", net_ip2addr(&state->peer_ip));
+	if (state->peer_port != 0)
+		str_printfa(str, "\tpeer_port=%u", state->peer_port);
+	if (state->state_size > 0) {
+		str_append(str, "\tstate=");
+		base64_encode(state->state, state->state_size, str);
+	}
+	input_data = i_stream_get_data(client->input, &input_size);
+	if (input_size > 0) {
+		str_append(str, "\tclient_input=");
+		base64_encode(input_data, input_size, str);
+	}
+	if (client->imap_still_here_text_pos != 0) {
+		str_append(str, "\tclient_output=");
+		base64_encode(imap_still_here_text + client->imap_still_here_text_pos,
+			      sizeof(imap_still_here_text)-1 - client->imap_still_here_text_pos,
+			      str);
+	}
+	if (client->idle_done) {
+		if (client->bad_done)
+			str_append(str, "\tbad-done");
+	} else if (client->state.idle_cmd) {
+		/* IDLE continues after sending changes */
+		str_append(str, "\tidle-continue");
+	}
+	str_append_c(str, '\n');
+
+	/* send the fd first */
+	ret = fd_send(o_stream_get_fd(output), client->fd, str_data(str), 1);
+	if (ret < 0) {
+		i_error("fd_send(%s) failed: %m",
+			o_stream_get_name(output));
+		imap_client_destroy(&client, "Failed to recreate imap process");
+		return;
+	}
+	i_assert(ret > 0);
+	o_stream_nsend(output, str_data(str) + 1, str_len(str) - 1);
+}
+
+static void
+imap_client_move_back_read_callback(void *context, const char *line)
+{
+	struct imap_client *client = context;
+
+	if (line[0] != '+') {
+		/* failed - FIXME: retry later? */
+		imap_client_destroy(&client, t_strdup_printf(
+			"Failed to recreate imap process: %s", line+1));
+	} else {
+		imap_client_destroy(&client, NULL);
+	}
+}
+
+static void imap_client_move_back(struct imap_client *client)
+{
+	const struct master_service_settings *master_set;
+	const char *path;
+
+	imap_client_stop(client);
+
+	master_set = master_service_settings_get(master_service);
+	path = t_strconcat(master_set->base_dir,
+			   "/"IMAP_MASTER_SOCKET_NAME, NULL);
+	if (imap_master_connection_init(path,
+					imap_client_move_back_send_callback,
+					imap_client_move_back_read_callback,
+					client, &client->master_conn) < 0) {
+		/* failed to connect to the imap-master socket */
+		imap_client_destroy(&client, "Failed to connect to master socket");
+	}
+}
+
+static enum imap_client_input_state
+imap_client_input_parse(const unsigned char *data, size_t size)
+{
+	enum imap_client_input_state state = IMAP_CLIENT_INPUT_STATE_DONE_LF;
+
+	/* skip over DONE[\r]\n */
+	if (i_memcasecmp(data, "DONE", I_MIN(size, 4)) != 0)
+		return IMAP_CLIENT_INPUT_STATE_BAD;
+	if (size <= 4)
+		return IMAP_CLIENT_INPUT_STATE_UNKNOWN;
+	data += 4; size -= 4;
+
+	if (data[0] == '\r') {
+		state = IMAP_CLIENT_INPUT_STATE_DONE_CRLF;
+		data++; size--;
+	}
+	if (size == 0)
+		return IMAP_CLIENT_INPUT_STATE_UNKNOWN;
+	if (data[0] != '\n')
+		return IMAP_CLIENT_INPUT_STATE_BAD;
+	data++; size--;
+
+	/* skip over IDLE[\r]\n - checking this assumes that the DONE and IDLE
+	   are sent in the same IP packet, otherwise we'll unnecessarily
+	   recreate the imap process and immediately resume IDLE there. if this
+	   becomes an issue we could add a small delay to the imap process
+	   creation and wait for the IDLE command during it. */
+	if (size <= 4 || i_memcasecmp(data, "IDLE", 4) != 0)
+		return state;
+	data += 4; size -= 4;
+
+	if (data[0] == '\r') {
+		data++; size--;
+	}
+	return size == 1 && data[0] == '\n' ?
+		IMAP_CLIENT_INPUT_STATE_DONEIDLE : state;
+}
+
+static void imap_client_input_idle_cmd(struct imap_client *client)
+{
+	const unsigned char *data;
+	size_t size;
+	int ret;
+
+	/* we should read either DONE or disconnection. also handle if client
+	   sends DONE\nIDLE simply to recreate the IDLE. */
+	ret = i_stream_read_data(client->input, &data, &size,
+				 client->next_read_threshold);
+	if (size == 0) {
+		if (ret < 0)
+			imap_client_disconnected(&client);
+		return;
+	}
+	client->next_read_threshold = 0;
+	switch (imap_client_input_parse(data, size)) {
+	case IMAP_CLIENT_INPUT_STATE_UNKNOWN:
+		/* we haven't received a full DONE[\r]\n yet - wait */
+		client->next_read_threshold = size;
+		return;
+	case IMAP_CLIENT_INPUT_STATE_BAD:
+		/* invalid input - return this to the imap process */
+		client->bad_done = TRUE;
+		break;
+	case IMAP_CLIENT_INPUT_STATE_DONE_LF:
+		i_stream_skip(client->input, 4+1);
+		break;
+	case IMAP_CLIENT_INPUT_STATE_DONE_CRLF:
+		i_stream_skip(client->input, 4+2);
+		break;
+	case IMAP_CLIENT_INPUT_STATE_DONEIDLE:
+		/* we received DONE+IDLE, so the client simply wanted to notify
+		   us that it's still there. continue hibernation. */
+		i_stream_skip(client->input, size);
+		return;
+	}
+	client->idle_done = TRUE;
+	imap_client_move_back(client);
+}
+
+static void imap_client_input_nonidle(struct imap_client *client)
+{
+	if (i_stream_read(client->input) < 0)
+		imap_client_disconnected(&client);
+	else
+		imap_client_move_back(client);
+}
+
+static void imap_client_input_notify(struct imap_client *client)
+{
+	imap_client_move_back(client);
+}
+
+static void keepalive_timeout(struct imap_client *client)
+{
+	unsigned int text_left = sizeof(imap_still_here_text)-1 -
+		client->imap_still_here_text_pos;
+	ssize_t ret;
+
+	ret = write(client->fd,
+		    imap_still_here_text + client->imap_still_here_text_pos,
+		    text_left);
+	if (ret < 0) {
+		/* disconnect */
+		const char *reason = errno == EPIPE ? "Connection closed" :
+			t_strdup_printf("Connection closed: %m");
+		imap_client_destroy(&client, reason);
+		return;
+	}
+	client->imap_still_here_text_pos += ret;
+	if (client->imap_still_here_text_pos >= sizeof(imap_still_here_text)-1)
+		client->imap_still_here_text_pos = 0;
+	else {
+		/* we didn't write the entire line, but that's ok. we could
+		   io_add(IO_WRITE) here, but that's probably not necessary.
+		   just continue writing the "still here" line after the next
+		   timeout. its main purpose is to see if the connection is
+		   still alive, and we're successfully doing that already.
+		   besides, it's highly unlikely we'll ever even get here.. */
+	}
+	imap_client_add_idle_keepalive_timeout(client);
+}
+
+static void imap_client_add_idle_keepalive_timeout(struct imap_client *client)
+{
+	unsigned int interval = client->state.imap_idle_notify_interval;
+
+	if (interval == 0)
+		return;
+
+	interval = imap_keepalive_interval_msecs(client->state.username,
+						 &client->state.remote_ip,
+						 interval);
+
+	if (client->to_keepalive!= NULL)
+		timeout_remove(&client->to_keepalive);
+	client->to_keepalive = timeout_add(interval, keepalive_timeout, client);
+}
+
+static const struct var_expand_table *
+imap_client_get_var_expand_table(struct imap_client *client)
+{
+	static struct var_expand_table static_tab[] = {
+		{ 'u', NULL, "user" },
+		{ 'n', NULL, "username" },
+		{ 'd', NULL, "domain" },
+		{ 's', NULL, "service" },
+		{ 'h', NULL, "home" },
+		{ 'l', NULL, "lip" },
+		{ 'r', NULL, "rip" },
+		{ 'p', NULL, "pid" },
+		{ 'i', NULL, "uid" },
+		{ '\0', NULL, "gid" },
+		{ '\0', NULL, "session" },
+		{ '\0', NULL, "auth_user" },
+		{ '\0', NULL, "auth_username" },
+		{ '\0', NULL, "auth_domain" },
+		/* NOTE: keep this synced with lib-storage's
+		   mail_user_var_expand_table() */
+		{ '\0', NULL, NULL }
+	};
+	struct var_expand_table *tab;
+	const char *auth_user;
+
+	tab = t_malloc(sizeof(static_tab));
+	memcpy(tab, static_tab, sizeof(static_tab));
+
+	tab[0].value = client->state.username;
+	tab[1].value = t_strcut(client->state.username, '@');
+	tab[2].value = strchr(client->state.username, '@');
+	if (tab[2].value != NULL) tab[2].value++;
+	tab[3].value = "imap-hibernate";
+	tab[4].value = NULL; /* we shouldn't need this */
+	tab[5].value = client->state.local_ip.family == 0 ? NULL :
+		net_ip2addr(&client->state.local_ip);
+	tab[6].value = client->state.remote_ip.family == 0 ? NULL :
+		net_ip2addr(&client->state.remote_ip);
+	tab[7].value = my_pid;
+	tab[8].value = dec2str(client->state.uid);
+	tab[9].value = dec2str(client->state.gid);
+	tab[10].value = client->state.session_id;
+
+	imap_client_parse_userdb_fields(client, &auth_user);
+	if (auth_user == NULL) {
+		tab[11].value = tab[0].value;
+		tab[12].value = tab[1].value;
+		tab[13].value = tab[2].value;
+	} else {
+		tab[11].value = auth_user;
+		tab[12].value = t_strcut(auth_user, '@');
+		tab[13].value = strchr(auth_user, '@');
+	}
+	return tab;
+}
+
+static void imap_client_io_activate_user(struct imap_client *client)
+{
+	i_set_failure_prefix("%s", client->log_prefix);
+}
+
+static void imap_client_io_deactivate_user(struct imap_client *client ATTR_UNUSED)
+{
+	i_set_failure_prefix("imap-hibernate: ");
+}
+
+struct imap_client *
+imap_client_create(int fd, const struct imap_client_state *state)
+{
+	struct imap_client *client;
+	pool_t pool = pool_alloconly_create("imap client", 256);
+	void *statebuf;
+
+	i_assert(state->username != NULL);
+	i_assert(state->mail_log_prefix != NULL);
+
+	fd_set_nonblock(fd, TRUE); /* it should already be, but be sure */
+
+	client = p_new(pool, struct imap_client, 1);
+	client->pool = pool;
+	client->fd = fd;
+	client->input = i_stream_create_fd(fd, IMAP_MAX_INBUF, FALSE);
+
+	client->state.username = p_strdup(pool, state->username);
+	client->state.session_id = p_strdup(pool, state->session_id);
+	client->state.userdb_fields = p_strdup(pool, state->userdb_fields);
+	client->state.stats = p_strdup(pool, state->stats);
+	client->state.local_ip = state->local_ip;
+	client->state.remote_ip = state->remote_ip;
+	client->state.imap_idle_notify_interval =
+		state->imap_idle_notify_interval;
+	client->state.idle_cmd = state->idle_cmd;
+
+	if (state->state_size > 0) {
+		client->state.state = statebuf = p_malloc(pool, state->state_size);
+		memcpy(statebuf, state->state, state->state_size);
+		client->state.state_size = state->state_size;
+	}
+	T_BEGIN {
+		string_t *str;
+
+		str = t_str_new(256);
+		var_expand(str, state->mail_log_prefix,
+			   imap_client_get_var_expand_table(client));
+		client->log_prefix = p_strdup(pool, str_c(str));
+	} T_END;
+
+	p_array_init(&client->notifys, pool, 2);
+	DLLIST_PREPEND(&imap_clients, client);
+	return client;
+}
+
+static void imap_client_stop(struct imap_client *client)
+{
+	struct imap_client_notify *notify;
+
+	if (client->io != NULL)
+		io_remove(&client->io);
+	if (client->to_keepalive != NULL)
+		timeout_remove(&client->to_keepalive);
+
+	array_foreach_modifiable(&client->notifys, notify) {
+		if (notify->io != NULL)
+			io_remove(&notify->io);
+		if (notify->fd != -1)
+			i_close_fd(&notify->fd);
+	}
+}
+
+void imap_client_destroy(struct imap_client **_client, const char *reason)
+{
+	struct imap_client *client = *_client;
+
+	*_client = NULL;
+
+	if (reason != NULL) {
+		/* the client input/output bytes don't count the DONE+IDLE by
+		   imap-hibernate, but that shouldn't matter much. */
+		i_info("%s %s", reason, client->state.stats);
+	}
+
+	if (client->ioloop_ctx != NULL) {
+		io_loop_context_remove_callbacks(client->ioloop_ctx,
+						 imap_client_io_activate_user,
+						 imap_client_io_deactivate_user, client);
+		imap_client_io_deactivate_user(client);
+		io_loop_context_unref(&client->ioloop_ctx);
+	}
+
+	DLLIST_REMOVE(&imap_clients, client);
+	imap_client_stop(client);
+	i_stream_destroy(&client->input);
+	i_close_fd(&client->fd);
+	pool_unref(&client->pool);
+
+	master_service_client_connection_destroyed(master_service);
+}
+
+void imap_client_add_notify_fd(struct imap_client *client, int fd)
+{
+	struct imap_client_notify *notify;
+
+	notify = array_append_space(&client->notifys);
+	notify->fd = fd;
+}
+
+void imap_client_create_finish(struct imap_client *client)
+{
+	struct imap_client_notify *notify;
+
+	client->ioloop_ctx = io_loop_context_new(current_ioloop);
+	io_loop_context_add_callbacks(client->ioloop_ctx,
+				      imap_client_io_activate_user,
+				      imap_client_io_deactivate_user, client);
+	imap_client_io_activate_user(client);
+
+	if (client->state.idle_cmd) {
+		client->io = io_add(client->fd, IO_READ,
+				    imap_client_input_idle_cmd, client);
+	} else {
+		client->io = io_add(client->fd, IO_READ,
+				    imap_client_input_nonidle, client);
+	}
+	imap_client_add_idle_keepalive_timeout(client);
+
+	array_foreach_modifiable(&client->notifys, notify) {
+		notify->io = io_add(notify->fd, IO_READ,
+				    imap_client_input_notify, client);
+	}
+}
+
+void imap_clients_deinit(void)
+{
+	while (imap_clients != NULL) {
+		struct imap_client *client = imap_clients;
+
+		imap_client_io_activate_user(client);
+		imap_client_destroy(&client, "Shutting down");
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/imap-client.h	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,35 @@
+#ifndef IMAP_CLIENT_H
+#define IMAP_CLIENT_H
+
+#include "net.h"
+
+struct imap_client_state {
+	/* required: */
+	const char *username, *mail_log_prefix;
+	/* optional: */
+	const char *session_id, *userdb_fields, *stats;
+	struct ip_addr local_ip, remote_ip;
+
+	uid_t uid;
+	gid_t gid;
+
+	struct ip_addr peer_ip;
+	unsigned int peer_port;
+
+	const unsigned char *state;
+	size_t state_size;
+
+	unsigned int imap_idle_notify_interval;
+	bool idle_cmd;
+	bool have_notify_fd;
+};
+
+struct imap_client *
+imap_client_create(int fd, const struct imap_client_state *state);
+void imap_client_add_notify_fd(struct imap_client *client, int fd);
+void imap_client_create_finish(struct imap_client *client);
+void imap_client_destroy(struct imap_client **_client, const char *reason);
+
+void imap_clients_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/imap-hibernate-client.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,262 @@
+/* Copyright (c) 2014-2015 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "connection.h"
+#include "istream.h"
+#include "istream-unix.h"
+#include "ostream.h"
+#include "buffer.h"
+#include "base64.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "imap-client.h"
+#include "imap-hibernate-client.h"
+
+struct imap_hibernate_client {
+	struct connection conn;
+	struct imap_client *imap_client;
+	bool imap_client_created;
+	bool debug;
+};
+
+struct imap_hibernate_input {
+	/* input we've already read from the IMAP client. */
+	buffer_t *client_input;
+	/* IMAP connection state */
+	buffer_t *state;
+};
+
+static struct connection_list *hibernate_clients = NULL;
+
+static void imap_hibernate_client_destroy(struct connection *conn)
+{
+	struct imap_hibernate_client *client = (struct imap_hibernate_client *)conn;
+
+	if (!client->imap_client_created)
+		master_service_client_connection_destroyed(master_service);
+	connection_deinit(conn);
+	i_free(conn);
+}
+
+static int
+imap_hibernate_client_parse_input(const char *const *args, pool_t pool,
+				  struct imap_client_state *state_r,
+				  const char **error_r)
+{
+	const char *key, *value;
+
+	memset(state_r, 0, sizeof(*state_r));
+	if (args[0] == NULL) {
+		*error_r = "Missing username in input";
+		return -1;
+	}
+	state_r->username = args[0]; args++;
+	if (args[0] == NULL) {
+		*error_r = "Missing mail_log_prefix in input";
+		return -1;
+	}
+	state_r->mail_log_prefix = args[0]; args++;
+
+	for (; *args != NULL; args++) {
+		value = strchr(*args, '=');
+		if (value != NULL)
+			key = t_strdup_until(*args, value++);
+		else {
+			key = *args;
+			value = "";
+		}
+		if (strcmp(key, "lip") == 0) {
+			if (net_addr2ip(value, &state_r->local_ip) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid lip value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "rip") == 0) {
+			if (net_addr2ip(value, &state_r->remote_ip) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid rip value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "peer_ip") == 0) {
+			if (net_addr2ip(value, &state_r->peer_ip) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid peer_ip value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "peer_port") == 0) {
+			if (str_to_uint(value, &state_r->peer_port) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid peer_ip value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "uid") == 0) {
+			if (str_to_uid(value, &state_r->uid) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid uid value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "gid") == 0) {
+			if (str_to_gid(value, &state_r->gid) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid gid value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "stats") == 0) {
+			state_r->stats = value;
+		} else if (strcmp(key, "idle-cmd") == 0) {
+			state_r->idle_cmd = TRUE;
+		} else if (strcmp(key, "session") == 0) {
+			state_r->session_id = value;
+		} else if (strcmp(key, "userdb_fields") == 0) {
+			state_r->userdb_fields = value;
+		} else if (strcmp(key, "notify_fd") == 0) {
+			state_r->have_notify_fd = TRUE;
+		} else if (strcmp(key, "idle_notify_interval") == 0) {
+			if (str_to_uint(value, &state_r->imap_idle_notify_interval) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid idle_notify_interval value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "state") == 0) {
+			buffer_t *state_buf;
+
+			state_buf = buffer_create_dynamic(pool, 1024);
+			if (base64_decode(value, strlen(value), NULL,
+					  state_buf) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid state base64 value: %s", value);
+				return -1;
+			}
+			state_r->state = state_buf->data;
+			state_r->state_size = state_buf->used;
+		}
+	}
+	return 0;
+}
+
+static int
+imap_hibernate_client_input_args(struct connection *conn,
+				 const char *const *args, int fd, pool_t pool)
+{
+	struct imap_hibernate_client *client =
+		(struct imap_hibernate_client *)conn;
+	struct imap_client_state state;
+	const char *error;
+
+	if (imap_hibernate_client_parse_input(args, pool, &state, &error) < 0) {
+		i_error("Failed to parse client input: %s", error);
+		o_stream_send_str(conn->output, t_strdup_printf(
+			"-Failed to parse client input: %s\n", error));
+		return -1;
+	}
+	client->imap_client = imap_client_create(fd, &state);
+	/* the transferred imap client fd is now counted as the client. */
+	client->imap_client_created = TRUE;
+	return state.have_notify_fd ? 0 : 1;
+}
+
+static int
+imap_hibernate_client_input_line(struct connection *conn, const char *line)
+{
+	struct imap_hibernate_client *client =
+		(struct imap_hibernate_client *)conn;
+	int fd = -1, ret;
+
+	if (!conn->version_received) {
+		if (connection_verify_version(conn, t_strsplit_tabescaped(line)) < 0)
+			return -1;
+		conn->version_received = TRUE;
+		return 1;
+	}
+
+	if (client->imap_client == NULL) {
+		char *const *args;
+		pool_t pool;
+
+		fd = i_stream_unix_get_read_fd(conn->input);
+		if (fd == -1) {
+			i_error("IMAP client fd not received");
+			return -1;
+		}
+
+		pool = pool_alloconly_create("client cmd", 1024);
+		args = p_strsplit_tabescaped(pool, line);
+		ret = imap_hibernate_client_input_args(conn, (void *)args, fd, pool);
+		if (ret >= 0 && client->debug)
+			i_debug("Create client with input: %s", line);
+		pool_unref(&pool);
+	} else {
+		fd = i_stream_unix_get_read_fd(conn->input);
+		if (fd == -1) {
+			i_error("IMAP notify fd not received (input: %s)", line);
+			ret = -1;
+		} else if (line[0] != '\0') {
+			i_error("Expected empty notify fd line from client, but got: %s", line);
+			o_stream_send_str(conn->output,
+					  "Expected empty notify fd line");
+			ret = -1;
+		} else {
+			imap_client_add_notify_fd(client->imap_client, fd);
+			ret = 1;
+		}
+	}
+
+	if (ret < 0) {
+		if (client->imap_client != NULL)
+			imap_client_destroy(&client->imap_client, NULL);
+		if (fd != -1)
+			i_close_fd(&fd);
+		return -1;
+	} else if (ret == 0) {
+		/* still need to read another fd */
+		i_stream_unix_set_read_fd(conn->input);
+		o_stream_send_str(conn->output, "+\n");
+		return 1;
+	} else {
+		/* finished - always disconnect the hibernate client
+		   afterwards */
+		o_stream_send_str(conn->output, "+\n");
+		imap_client_create_finish(client->imap_client);
+		return -1;
+	}
+}
+
+void imap_hibernate_client_create(int fd, bool debug)
+{
+	struct imap_hibernate_client *client;
+
+	client = i_new(struct imap_hibernate_client, 1);
+	client->debug = debug;
+	connection_init_server(hibernate_clients, &client->conn,
+			       "imap-hibernate", fd, fd);
+
+	i_assert(client->conn.input == NULL);
+	client->conn.input = i_stream_create_unix(fd, (size_t)-1);
+	i_stream_unix_set_read_fd(client->conn.input);
+}
+
+static struct connection_settings client_set = {
+	.service_name_in = "imap-hibernate",
+	.service_name_out = "imap-hibernate",
+	.major_version = 1,
+	.minor_version = 0,
+
+	.input_max_size = 0, /* don't auto-create istream */
+	.output_max_size = (size_t)-1,
+	.client = FALSE
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+	.destroy = imap_hibernate_client_destroy,
+	.input_line = imap_hibernate_client_input_line
+};
+
+void imap_hibernate_clients_init(void)
+{
+	hibernate_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void imap_hibernate_clients_deinit(void)
+{
+	connection_list_deinit(&hibernate_clients);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/imap-hibernate-client.h	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,9 @@
+#ifndef IMAP_HIBERNATE_CLIENT_H
+#define IMAP_HIBERNATE_CLIENT_H
+
+void imap_hibernate_client_create(int fd, bool debug);
+
+void imap_hibernate_clients_init(void);
+void imap_hibernate_clients_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/imap-hibernate-settings.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,48 @@
+/* Copyright (c) 2014-2015 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/* <settings checks> */
+static struct file_listener_settings imap_hibernate_unix_listeners_array[] = {
+	{ "imap-hibernate", 0600, "", "" }
+};
+static struct file_listener_settings *imap_hibernate_unix_listeners[] = {
+	&imap_hibernate_unix_listeners_array[0]
+};
+static buffer_t imap_hibernate_unix_listeners_buf = {
+	imap_hibernate_unix_listeners, sizeof(imap_hibernate_unix_listeners), { NULL, }
+};
+/* </settings checks> */
+
+struct service_settings imap_hibernate_service_settings = {
+	.name = "imap-hibernate",
+	.protocol = "imap",
+	.type = "",
+	.executable = "imap-hibernate",
+	.user = "$default_internal_user",
+	.group = "",
+	.privileged_group = "",
+	.extra_groups = "",
+	.chroot = "",
+
+	.drop_priv_before_exec = FALSE,
+
+	.process_min_avail = 0,
+	.process_limit = 0,
+	.client_limit = 0,
+	.service_count = 0,
+	.idle_kill = 0,
+	.vsz_limit = (uoff_t)-1,
+
+	.unix_listeners = { { &imap_hibernate_unix_listeners_buf,
+			      sizeof(imap_hibernate_unix_listeners[0]) } },
+	.fifo_listeners = ARRAY_INIT,
+	.inet_listeners = ARRAY_INIT
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/imap-master-connection.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,121 @@
+/* Copyright (c) 2015 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "connection.h"
+#include "imap-master-connection.h"
+
+#define IMAP_MASTER_CONNECTION_TIMEOUT_MSECS 5000
+
+struct imap_master_connection {
+	struct connection conn;
+	struct timeout *to;
+
+	imap_master_connection_send_callback_t *send_callback;
+	imap_master_connection_read_callback_t *read_callback;
+	void *context;
+};
+
+static struct connection_list *master_clients;
+
+static void imap_master_connection_timeout(struct imap_master_connection *conn)
+{
+	i_error("Timeout communicating with %s", conn->conn.name);
+	imap_master_connection_deinit(&conn);
+}
+
+int imap_master_connection_init(const char *path,
+				imap_master_connection_send_callback_t *send_callback,
+				imap_master_connection_read_callback_t *read_callback,
+				void *context,
+				struct imap_master_connection **conn_r)
+{
+	struct imap_master_connection *conn;
+
+	conn = i_new(struct imap_master_connection, 1);
+	conn->send_callback = send_callback;
+	conn->read_callback = read_callback;
+	conn->context = context;
+	connection_init_client_unix(master_clients, &conn->conn, path);
+	if (connection_client_connect(&conn->conn) < 0) {
+		i_error("net_connect_unix(%s) failed: %m", path);
+		connection_deinit(&conn->conn);
+		i_free(conn);
+		return -1;
+	}
+	conn->to = timeout_add(IMAP_MASTER_CONNECTION_TIMEOUT_MSECS,
+			       imap_master_connection_timeout, conn);
+	*conn_r = conn;
+	return 0;
+}
+
+void imap_master_connection_deinit(struct imap_master_connection **_conn)
+{
+	struct imap_master_connection *conn = *_conn;
+
+	*_conn = NULL;
+
+	if (conn->read_callback != NULL)
+		conn->read_callback(conn->context, "-");
+
+	timeout_remove(&conn->to);
+	connection_deinit(&conn->conn);
+	i_free(conn);
+}
+
+static void imap_master_client_destroy(struct connection *_conn)
+{
+	struct imap_master_connection *conn =
+		(struct imap_master_connection *)_conn;
+
+	imap_master_connection_deinit(&conn);
+}
+
+static int
+imap_master_client_input_line(struct connection *_conn, const char *line)
+{
+	struct imap_master_connection *conn =
+		(struct imap_master_connection *)_conn;
+
+	if (!_conn->version_received) {
+		if (connection_input_line_default(_conn, line) < 0)
+			return -1;
+
+		conn->send_callback(conn->context, _conn->output);
+		return 1;
+	} else {
+		imap_master_connection_read_callback_t *read_callback =
+			conn->read_callback;
+
+		conn->read_callback = NULL;
+		read_callback(conn->context, line);
+		/* we're finished now with this connection - disconnect it */
+		return -1;
+	}
+}
+
+static struct connection_settings client_set = {
+	.service_name_in = "imap-master",
+	.service_name_out = "imap-master",
+	.major_version = 1,
+	.minor_version = 0,
+
+	.input_max_size = (size_t)-1,
+	.output_max_size = (size_t)-1,
+	.client = TRUE
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+	.destroy = imap_master_client_destroy,
+	.input_line = imap_master_client_input_line
+};
+
+void imap_master_connections_init(void)
+{
+	master_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void imap_master_connections_deinit(void)
+{
+	connection_list_deinit(&master_clients);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/imap-master-connection.h	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,21 @@
+#ifndef IMAP_MASTER_CONNECTION_H
+#define IMAP_MASTER_CONNECTION_H
+
+struct imap_master_connection;
+
+typedef void
+imap_master_connection_send_callback_t(void *context, struct ostream *output);
+typedef void
+imap_master_connection_read_callback_t(void *context, const char *reply);
+
+int imap_master_connection_init(const char *path,
+				imap_master_connection_send_callback_t *send_callback,
+				imap_master_connection_read_callback_t *read_callback,
+				void *context,
+				struct imap_master_connection **conn_r);
+void imap_master_connection_deinit(struct imap_master_connection **conn);
+
+void imap_master_connections_init(void);
+void imap_master_connections_deinit(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap-hibernate/main.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,56 @@
+/* Copyright (c) 2014-2015 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "restrict-access.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "imap-client.h"
+#include "imap-hibernate-client.h"
+#include "imap-master-connection.h"
+
+static bool debug = FALSE;
+
+static void client_connected(struct master_service_connection *conn)
+{
+	master_service_client_connection_accept(conn);
+	imap_hibernate_client_create(conn->fd, debug);
+}
+
+int main(int argc, char *argv[])
+{
+	enum master_service_flags service_flags =
+		MASTER_SERVICE_FLAG_UPDATE_PROCTITLE;
+	const char *error;
+	int c;
+
+	master_service = master_service_init("imap-hibernate", service_flags,
+					     &argc, &argv, "D");
+	while ((c = master_getopt(master_service)) > 0) {
+		switch (c) {
+		case 'D':
+			debug = TRUE;
+			break;
+		default:
+			return FATAL_DEFAULT;
+		}
+	}
+
+	if (master_service_settings_read_simple(master_service, NULL, &error) < 0)
+		i_fatal("Error reading configuration: %s", error);
+
+	master_service_init_log(master_service, "imap-hibernate: ");
+	restrict_access_by_env(NULL, FALSE);
+	restrict_access_allow_coredumps(TRUE);
+
+	imap_master_connections_init();
+	imap_hibernate_clients_init();
+	master_service_init_finish(master_service);
+
+	master_service_run(master_service, client_connected);
+
+	imap_master_connections_deinit();
+	imap_hibernate_clients_deinit();
+	imap_clients_deinit();
+	master_service_deinit(&master_service);
+        return 0;
+}
--- a/src/imap/Makefile.am	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/Makefile.am	Mon Aug 24 14:13:02 2015 +0300
@@ -61,22 +61,26 @@
 	cmd-unselect.c \
 	cmd-unsubscribe.c \
 	cmd-urlfetch.c \
-	cmd-x-cancel.c
+	cmd-x-cancel.c \
+	cmd-x-state.c
 
 imap_SOURCES = \
 	$(cmds) \
 	imap-client.c \
+	imap-client-hibernate.c \
 	imap-commands.c \
 	imap-commands-util.c \
 	imap-expunge.c \
 	imap-fetch.c \
 	imap-fetch-body.c \
 	imap-list.c \
+	imap-master-client.c \
 	imap-notify.c \
 	imap-search.c \
 	imap-search-args.c \
 	imap-settings.c \
 	imap-status.c \
+	imap-state.c \
 	imap-sync.c \
 	mail-storage-callbacks.c \
 	main.c
@@ -89,11 +93,13 @@
 	imap-expunge.h \
 	imap-fetch.h \
 	imap-list.h \
+	imap-master-client.h \
 	imap-notify.h \
 	imap-search.h \
 	imap-search-args.h \
 	imap-settings.h \
 	imap-status.h \
+	imap-state.h \
 	imap-sync.h
 
 pkginc_libdir=$(pkgincludedir)
--- a/src/imap/cmd-idle.c	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/cmd-idle.c	Mon Aug 24 14:13:02 2015 +0300
@@ -1,8 +1,6 @@
 /* Copyright (c) 2002-2015 Dovecot authors, see the included COPYING file */
 
 #include "imap-common.h"
-#include "net.h"
-#include "ioloop.h"
 #include "istream.h"
 #include "ostream.h"
 #include "crc32.h"
@@ -11,14 +9,12 @@
 #include "imap-keepalive.h"
 #include "imap-sync.h"
 
-#include <stdlib.h>
-
 struct cmd_idle_context {
 	struct client *client;
 	struct client_command_context *cmd;
 
 	struct imap_sync_context *sync_ctx;
-	struct timeout *keepalive_to;
+	struct timeout *keepalive_to, *to_hibernate;
 
 	unsigned int manual_cork:1;
 	unsigned int sync_pending:1;
@@ -34,6 +30,8 @@
 
 	if (ctx->keepalive_to != NULL)
 		timeout_remove(&ctx->keepalive_to);
+	if (ctx->to_hibernate != NULL)
+		timeout_remove(&ctx->to_hibernate);
 
 	if (ctx->sync_ctx != NULL) {
 		/* we're here only in connection failure cases */
@@ -178,6 +176,34 @@
 	ctx->keepalive_to = timeout_add(interval, keepalive_timeout, ctx);
 }
 
+static void idle_hibernate_timeout(struct cmd_idle_context *ctx)
+{
+	struct client *client = ctx->client;
+
+	i_assert(ctx->sync_ctx == NULL);
+	i_assert(!ctx->sync_pending);
+
+	if (imap_client_hibernate(&client)) {
+		/* client may be destroyed now */
+	} else {
+		/* failed - don't bother retrying */
+		timeout_remove(&ctx->to_hibernate);
+	}
+}
+
+static void idle_add_hibernate_timeout(struct cmd_idle_context *ctx)
+{
+	unsigned int secs = ctx->client->set->imap_hibernate_timeout;
+
+	i_assert(ctx->to_hibernate == NULL);
+
+	if (secs == 0)
+		return;
+
+	ctx->to_hibernate =
+		timeout_add(secs * 1000, idle_hibernate_timeout, ctx);
+}
+
 static bool cmd_idle_continue(struct client_command_context *cmd)
 {
 	struct client *client = cmd->client;
@@ -189,6 +215,9 @@
 		return TRUE;
 	}
 
+	if (ctx->to_hibernate != NULL)
+		timeout_reset(ctx->to_hibernate);
+
 	if (ctx->manual_cork)  {
 		/* we're coming from idle_callback instead of a normal
 		   I/O handler, so we'll have to do corking manually */
@@ -225,6 +254,8 @@
 		   so we return here instead of doing everything twice. */
 		return FALSE;
 	}
+	if (ctx->to_hibernate == NULL)
+		idle_add_hibernate_timeout(ctx);
 	cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT;
 
 	if (ctx->manual_cork) {
@@ -254,10 +285,16 @@
 	ctx->cmd = cmd;
 	ctx->client = client;
 	idle_add_keepalive_timeout(ctx);
+	idle_add_hibernate_timeout(ctx);
 
 	if (client->mailbox != NULL)
 		mailbox_notify_changes(client->mailbox, idle_callback, ctx);
-	client_send_line(client, "+ idling");
+	if (!client->state_import_idle_continue)
+		client_send_line(client, "+ idling");
+	else {
+		/* continuing an IDLE after hibernation */
+		client->state_import_idle_continue = FALSE;
+	}
 
 	io_remove(&client->io);
 	client->io = io_add_istream(client->input, idle_client_input, ctx);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/cmd-x-state.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,68 @@
+/* Copyright (c) 2015 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "base64.h"
+#include "str.h"
+#include "imap-commands.h"
+#include "imap-state.h"
+
+bool cmd_x_state(struct client_command_context *cmd)
+{
+	/* FIXME: state importing can cause unnecessarily large memory usage
+	   by specifying an old modseq, because the EXPUNGE/FETCH replies
+	   aren't currently sent asynchronously. so this command is disabled
+	   for now. */
+#if 0
+	const struct imap_arg *args;
+	const char *str, *error;
+	buffer_t *state, *state_encoded;
+	int ret;
+
+	if (!client_read_args(cmd, 0, 0, &args))
+		return FALSE;
+
+	state = buffer_create_dynamic(cmd->pool, 256);
+	if (imap_arg_get_astring(&args[0], &str)) {
+		if (cmd->client->mailbox != NULL) {
+			client_send_tagline(cmd,
+				"BAD Can't be used in SELECTED state");
+			return TRUE;
+		}
+		if (base64_decode(str, strlen(str), NULL, state) < 0)
+			ret = 0;
+		else {
+			ret = imap_state_import_external(cmd->client,
+				state->data, state->used, &error);
+		}
+		if (ret < 0) {
+			client_send_tagline(cmd, t_strdup_printf(
+				"NO Failed to restore state: %s", error));
+		} else if (ret == 0) {
+			client_send_tagline(cmd, t_strdup_printf(
+				"BAD Broken state: %s", error));
+		} else {
+			client_send_tagline(cmd, "OK State imported.");
+		}
+		return TRUE;
+	} else if (args[0].type == IMAP_ARG_EOL) {
+		if (!imap_state_export_external(cmd->client, state, &error)) {
+			client_send_tagline(cmd, t_strdup_printf(
+				"NO Can't save state: %s", error));
+			return TRUE;
+		}
+		state_encoded = buffer_create_dynamic(cmd->pool,
+				MAX_BASE64_ENCODED_SIZE(state->used)+10);
+		str_append(state_encoded, "* STATE ");
+		base64_encode(state->data, state->used, state_encoded);
+		client_send_line(cmd->client, str_c(state_encoded));
+		client_send_tagline(cmd, "OK State exported.");
+		return TRUE;
+	} else {
+		client_send_command_error(cmd, "Invalid arguments.");
+		return TRUE;
+	}
+#else
+	client_send_command_error(cmd, "Command is disabled for now.");
+	return TRUE;
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-client-hibernate.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,225 @@
+/* Copyright (c) 2014-2015 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "fdpass.h"
+#include "net.h"
+#include "ostream.h"
+#include "write-full.h"
+#include "base64.h"
+#include "str.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "mailbox-watch.h"
+#include "imap-state.h"
+#include "imap-client.h"
+
+#define IMAP_HIBERNATE_SOCKET_NAME "imap-hibernate"
+#define IMAP_HIBERNATE_SEND_TIMEOUT_SECS 10
+#define IMAP_HIBERNATE_HANDSHAKE "VERSION\timap-hibernate\t1\t0\n"
+
+static int imap_hibernate_handshake(int fd, const char *path)
+{
+	char buf[1024];
+	ssize_t ret;
+
+	if (write_full(fd, IMAP_HIBERNATE_HANDSHAKE,
+		       strlen(IMAP_HIBERNATE_HANDSHAKE)) < 0) {
+		i_error("write(%s) failed: %m", path);
+		return -1;
+	} else if ((ret = read(fd, buf, sizeof(buf)-1)) < 0) {
+		i_error("read(%s) failed: %m", path);
+		return -1;
+	} else if (ret > 0 && buf[ret-1] == '\n') {
+		buf[ret-1] = '\0';
+		if (version_string_verify(buf, "imap-hibernate", 1))
+			return 0;
+	}
+	i_error("%s sent invalid VERSION handshake: %s", path, buf);
+	return -1;
+}
+
+static void imap_hibernate_write_cmd(struct client *client, string_t *cmd,
+				     const buffer_t *state, int fd_notify)
+{
+	struct ip_addr peer_ip;
+	unsigned int peer_port;
+
+	str_append_tabescaped(cmd, client->user->username);
+	str_append_c(cmd, '\t');
+	str_append_tabescaped(cmd, client->user->set->mail_log_prefix);
+	str_printfa(cmd, "\tidle_notify_interval=%u",
+		    client->set->imap_idle_notify_interval);
+	if (net_getpeername(client->fd_in, &peer_ip, &peer_port) == 0) {
+		str_printfa(cmd, "\tpeer_ip=%s\tpeer_port=%u",
+			    net_ip2addr(&peer_ip), peer_port);
+	}
+
+	if (client->session_id != NULL) {
+		str_append(cmd, "\tsession=");
+		str_append_tabescaped(cmd, client->session_id);
+	}
+	if (client->user->local_ip != NULL)
+		str_printfa(cmd, "\tlip=%s", net_ip2addr(client->user->local_ip));
+	if (client->user->remote_ip != NULL)
+		str_printfa(cmd, "\trip=%s", net_ip2addr(client->user->remote_ip));
+	if (client->userdb_fields != NULL) {
+		string_t *userdb_fields = t_str_new(256);
+		unsigned int i;
+
+		for (i = 0; client->userdb_fields[i] != NULL; i++) {
+			if (i > 0)
+				str_append_c(userdb_fields, '\t');
+			str_append_tabescaped(userdb_fields, client->userdb_fields[i]);
+		}
+		str_append(cmd, "\tuserdb_fields=");
+		str_append_tabescaped(cmd, str_c(userdb_fields));
+	}
+	if (client->user->uid != (uid_t)-1)
+		str_printfa(cmd, "\tuid=%s", dec2str(client->user->uid));
+	if (client->user->gid != (gid_t)-1)
+		str_printfa(cmd, "\tgid=%s", dec2str(client->user->gid));
+	str_append(cmd, "\tstats=");
+	str_append_tabescaped(cmd, client_stats(client));
+	if (client->command_queue != NULL &&
+	    strcasecmp(client->command_queue->name, "IDLE") == 0)
+		str_append(cmd, "\tidle-cmd");
+	if (fd_notify != -1)
+		str_append(cmd, "\tnotify_fd");
+	str_append(cmd, "\tstate=");
+	base64_encode(state->data, state->used, cmd);
+	str_append_c(cmd, '\n');
+}
+
+static int
+imap_hibernate_process_send_cmd(int fd_socket, const char *path,
+				const string_t *cmd, int fd_client)
+{
+	ssize_t ret;
+
+	i_assert(fd_socket != -1);
+	i_assert(str_len(cmd) > 1);
+
+	if (imap_hibernate_handshake(fd_socket, path) < 0)
+		return -1;
+	if ((ret = fd_send(fd_socket, fd_client, str_data(cmd), 1)) < 0) {
+		i_error("fd_send(%s) failed: %m", path);
+		return -1;
+	}
+	if ((ret = write_full(fd_socket, str_data(cmd)+1, str_len(cmd)-1)) < 0) {
+		i_error("write(%s) failed: %m", path);
+		return -1;
+	}
+	return 0;
+}
+
+static int imap_hibernate_process_read(int fd, const char *path)
+{
+	char buf[1024];
+	ssize_t ret;
+
+	if ((ret = read(fd, buf, sizeof(buf)-1)) < 0) {
+		i_error("read(%s) failed: %m", path);
+		return -1;
+	} else if (ret == 0) {
+		i_error("%s disconnected", path);
+		return -1;
+	} else if (buf[0] != '+') {
+		buf[ret] = '\0';
+		i_error("%s returned failure: %s", path,
+			ret > 0 && buf[0] == '-' ? buf+1 : buf);
+		return -1;
+	} else {
+		return 0;
+	}
+}
+
+static int
+imap_hibernate_process_send(struct client *client,
+			    const buffer_t *state, int fd_notify)
+{
+	string_t *cmd = t_str_new(512);
+	const char *path;
+	ssize_t ret = 0;
+	int fd;
+
+	i_assert(state->used > 0);
+
+	path = t_strconcat(client->user->set->base_dir,
+			   "/"IMAP_HIBERNATE_SOCKET_NAME, NULL);
+	fd = net_connect_unix_with_retries(path, 1000);
+	if (fd == -1) {
+		i_error("net_connect_unix(%s) failed: %m", path);
+		return -1;
+	}
+	net_set_nonblock(fd, FALSE);
+
+	imap_hibernate_write_cmd(client, cmd, state, fd_notify);
+
+	alarm(IMAP_HIBERNATE_SEND_TIMEOUT_SECS);
+	if (imap_hibernate_process_send_cmd(fd, path, cmd, client->fd_in) < 0 ||
+	    imap_hibernate_process_read(fd, path) < 0)
+		ret = -1;
+	else if (fd_notify != -1) {
+		if ((ret = fd_send(fd, fd_notify, "\n", 1)) < 0)
+			i_error("fd_send(%s) failed: %m", path);
+		else
+			ret = imap_hibernate_process_read(fd, path);
+	}
+	alarm(0);
+	net_disconnect(fd);
+	return ret < 0 ? -1 : 0;
+}
+
+bool imap_client_hibernate(struct client **_client)
+{
+	struct client *client = *_client;
+	buffer_t *state;
+	const char *error;
+	int ret, fd_notify = -1;
+
+	if (client->fd_in != client->fd_out) {
+		/* we won't try to hibernate stdio clients */
+		return FALSE;
+	}
+	if (o_stream_get_buffer_used_size(client->output) > 0) {
+		/* wait until we've sent the pending output to client */
+		return FALSE;
+	}
+
+	state = buffer_create_dynamic(default_pool, 1024);
+	ret = imap_state_export_internal(client, state, &error);
+	if (ret < 0) {
+		i_error("Couldn't hibernate imap client: "
+			"Couldn't export state: %s", error);
+	} else if (ret == 0 && client->user->mail_debug) {
+		i_debug("Couldn't hibernate imap client: "
+			"Couldn't export state: %s", error);
+	}
+	if (ret > 0 && client->mailbox != NULL) {
+		fd_notify = mailbox_watch_extract_notify_fd(client->mailbox,
+							    &error);
+		if (fd_notify == -1) {
+			if (client->user->mail_debug) {
+				i_debug("Couldn't hibernate imap client: "
+					"Couldn't extract notifications fd: %s",
+					error);
+			}
+			ret = -1;
+		}
+	}
+	if (ret > 0) {
+		if (imap_hibernate_process_send(client, state, fd_notify) < 0)
+			ret = -1;
+	}
+	if (fd_notify != -1)
+		i_close_fd(&fd_notify);
+	if (ret > 0) {
+		/* hide the disconnect log message, because the client didn't
+		   actually log out */
+		client->disconnected = TRUE;
+		client_destroy(client, NULL);
+		*_client = NULL;
+	}
+	buffer_free(&state);
+	return ret > 0;
+}
--- a/src/imap/imap-client.c	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/imap-client.c	Mon Aug 24 14:13:02 2015 +0300
@@ -18,6 +18,7 @@
 #include "mail-error.h"
 #include "mail-namespace.h"
 #include "mail-storage-service.h"
+#include "imap-state.h"
 #include "imap-search.h"
 #include "imap-notify.h"
 #include "imap-commands.h"
@@ -212,7 +213,7 @@
 	}
 }
 
-static const char *client_stats(struct client *client)
+const char *client_stats(struct client *client)
 {
 	static struct var_expand_table static_tab[] = {
 		{ 'i', NULL, "input" },
@@ -655,7 +656,7 @@
 	return cmd;
 }
 
-static void client_add_missing_io(struct client *client)
+void client_add_missing_io(struct client *client)
 {
 	if (client->io == NULL && !client->disconnected)
 		client->io = io_add_istream(client->input, client_input, client);
@@ -1163,5 +1164,7 @@
 }
 
 struct imap_client_vfuncs imap_client_vfuncs = {
+	imap_state_export_base,
+	imap_state_import_base,
 	client_default_destroy
 };
--- a/src/imap/imap-client.h	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/imap-client.h	Mon Aug 24 14:13:02 2015 +0300
@@ -95,6 +95,17 @@
 };
 
 struct imap_client_vfuncs {
+	/* Export client state into buffer. Returns 1 if ok, 0 if some state
+	   couldn't be preserved, -1 if temporary internal error occurred. */
+	int (*state_export)(struct client *client, bool internal,
+			    buffer_t *dest, const char **error_r);
+	/* Import a single block of client state from the given data. Returns
+	   number of bytes successfully imported from the block, or 0 if state
+	   is corrupted or contains unknown data (e.g. some plugin is no longer
+	   loaded), -1 if temporary internal error occurred. */
+	ssize_t (*state_import)(struct client *client, bool internal,
+				const unsigned char *data, size_t size,
+				const char **error_r);
 	void (*destroy)(struct client *client, const char *reason);
 };
 
@@ -103,6 +114,7 @@
 
 	struct imap_client_vfuncs v;
 	const char *session_id;
+	const char *const *userdb_fields; /* for internal session saving/restoring */
 
 	int fd_in, fd_out;
 	struct io *io;
@@ -180,6 +192,8 @@
 	unsigned int notify_flag_changes:1;
 	unsigned int imap_metadata_enabled:1;
 	unsigned int nonpermanent_modseqs:1;
+	unsigned int state_import_bad_idle_done:1;
+	unsigned int state_import_idle_continue:1;
 };
 
 struct imap_module_register {
@@ -241,6 +255,9 @@
 bool client_handle_search_save_ambiguity(struct client_command_context *cmd);
 
 int client_enable(struct client *client, enum mailbox_feature features);
+/* Send client processing to imap-idle process. If successful, returns TRUE
+   and destroys the client. */
+bool imap_client_hibernate(struct client **client);
 
 struct imap_search_update *
 client_search_update_lookup(struct client *client, const char *tag,
@@ -253,6 +270,8 @@
 
 bool client_handle_unfinished_cmd(struct client_command_context *cmd);
 void client_continue_pending_input(struct client *client);
+void client_add_missing_io(struct client *client);
+const char *client_stats(struct client *client);
 
 void client_input(struct client *client);
 bool client_handle_input(struct client *client);
--- a/src/imap/imap-commands.c	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/imap-commands.c	Mon Aug 24 14:13:02 2015 +0300
@@ -68,6 +68,7 @@
 	{ "UID THREAD",		cmd_thread,      COMMAND_FLAG_BREAKS_SEQS },
 	{ "UNSELECT",		cmd_unselect,    COMMAND_FLAG_BREAKS_MAILBOX },
 	{ "X-CANCEL",		cmd_x_cancel,    0 },
+	{ "X-STATE",		cmd_x_state,     COMMAND_FLAG_REQUIRES_SYNC },
 	{ "XLIST",		cmd_list,        0 },
 	/* IMAP URLAUTH (RFC4467): */
 	{ "GENURLAUTH",		cmd_genurlauth,  0 },
--- a/src/imap/imap-commands.h	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/imap-commands.h	Mon Aug 24 14:13:02 2015 +0300
@@ -116,6 +116,7 @@
 bool cmd_move(struct client_command_context *cmd);
 bool cmd_unselect(struct client_command_context *cmd);
 bool cmd_x_cancel(struct client_command_context *cmd);
+bool cmd_x_state(struct client_command_context *cmd);
 
 /* IMAP URLAUTH (RFC4467): */
 bool cmd_genurlauth(struct client_command_context *cmd);
--- a/src/imap/imap-common.h	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/imap-common.h	Mon Aug 24 14:13:02 2015 +0300
@@ -17,9 +17,12 @@
 #include "imap-client.h"
 #include "imap-settings.h"
 
+struct mail_storage_service_input;
+
 typedef void imap_client_created_func_t(struct client **client);
 
 extern imap_client_created_func_t *hook_client_created;
+extern bool imap_debug;
 
 /* Sets the hook_client_created and returns the previous hook,
    which the new_hook should call if it's non-NULL. */
@@ -28,4 +31,8 @@
 
 void imap_refresh_proctitle(void);
 
+int client_create_from_input(const struct mail_storage_service_input *input,
+			     int fd_in, int fd_out, struct client **client_r,
+			     const char **error_r);
+
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-master-client.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,321 @@
+/* Copyright (c) 2014-2015 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "connection.h"
+#include "istream.h"
+#include "istream-unix.h"
+#include "ostream.h"
+#include "base64.h"
+#include "strescape.h"
+#include "master-service.h"
+#include "mail-storage-service.h"
+#include "imap-client.h"
+#include "imap-state.h"
+#include "imap-master-client.h"
+
+struct imap_master_client {
+	struct connection conn;
+	bool imap_client_created;
+};
+
+struct imap_master_input {
+	/* input we've already read from the IMAP client. */
+	buffer_t *client_input;
+	/* output that imap-hibernate was supposed to send to IMAP client,
+	   but couldn't send it yet. */
+	buffer_t *client_output;
+	/* IMAP connection state */
+	buffer_t *state;
+
+	struct ip_addr peer_ip;
+	unsigned int peer_port;
+
+	bool state_import_bad_idle_done;
+	bool state_import_idle_continue;
+};
+
+static struct connection_list *master_clients = NULL;
+
+static void imap_master_client_destroy(struct connection *conn)
+{
+	struct imap_master_client *client = (struct imap_master_client *)conn;
+
+	if (!client->imap_client_created)
+		master_service_client_connection_destroyed(master_service);
+	connection_deinit(conn);
+	i_free(conn);
+}
+
+static int
+imap_master_client_parse_input(const char *const *args, pool_t pool,
+			       struct mail_storage_service_input *input_r,
+			       struct imap_master_input *master_input_r,
+			       const char **error_r)
+{
+	const char *key, *value;
+
+	memset(input_r, 0, sizeof(*input_r));
+	memset(master_input_r, 0, sizeof(*master_input_r));
+	master_input_r->client_input = buffer_create_dynamic(pool, 64);
+	master_input_r->client_output = buffer_create_dynamic(pool, 16);
+	master_input_r->state = buffer_create_dynamic(pool, 512);
+
+	input_r->module = input_r->service = "imap";
+	/* we never want to do userdb lookup again when restoring the client.
+	   we have the userdb_fields cached already. */
+	input_r->flags_override_remove = MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
+
+	if (args[0] == NULL) {
+		*error_r = "Missing username in input";
+		return -1;
+	}
+	input_r->username = args[0];
+
+	for (args++; *args != NULL; args++) {
+		value = strchr(*args, '=');
+		if (value != NULL)
+			key = t_strdup_until(*args, value++);
+		else {
+			key = *args;
+			value = "";
+		}
+		if (strcmp(key, "lip") == 0) {
+			if (net_addr2ip(value, &input_r->local_ip) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid lip value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "rip") == 0) {
+			if (net_addr2ip(value, &input_r->remote_ip) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid rip value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "peer_ip") == 0) {
+			if (net_addr2ip(value, &master_input_r->peer_ip) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid peer_ip value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "peer_port") == 0) {
+			if (str_to_uint32(value, &master_input_r->peer_port) < 0 ||
+			    master_input_r->peer_port == 0) {
+				*error_r = t_strdup_printf(
+					"Invalid peer_port value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "session") == 0) {
+			input_r->session_id = value;
+		} else if (strcmp(key, "userdb_fields") == 0) {
+			input_r->userdb_fields =
+				t_strsplit_tabescaped(value);
+		} else if (strcmp(key, "client_input") == 0) {
+			if (base64_decode(value, strlen(value), NULL,
+					  master_input_r->client_input) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid client_input base64 value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "client_output") == 0) {
+			if (base64_decode(value, strlen(value), NULL,
+					  master_input_r->client_output) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid client_output base64 value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "state") == 0) {
+			if (base64_decode(value, strlen(value), NULL,
+					  master_input_r->state) < 0) {
+				*error_r = t_strdup_printf(
+					"Invalid state base64 value: %s", value);
+				return -1;
+			}
+		} else if (strcmp(key, "bad-done") == 0) {
+			master_input_r->state_import_bad_idle_done = TRUE;
+		} else if (strcmp(key, "idle-continue") == 0) {
+			master_input_r->state_import_idle_continue = TRUE;
+		}
+	}
+	return 0;
+}
+
+static int imap_master_client_verify(const struct imap_master_input *master_input,
+				     int fd_client, const char **error_r)
+{
+	struct ip_addr peer_ip;
+	unsigned int peer_port;
+
+	if (master_input->peer_port == 0)
+		return 0;
+
+	/* make sure we have the right fd */
+	if (net_getpeername(fd_client, &peer_ip, &peer_port) < 0) {
+		*error_r = t_strdup_printf("net_getpeername() failed: %m");
+		return -1;
+	}
+	if (!net_ip_compare(&peer_ip, &master_input->peer_ip) ||
+	    peer_port != master_input->peer_port) {
+		*error_r = t_strdup_printf(
+			"BUG: Expected peer_ip=%s peer_port=%u doesn't match "
+			"client fd's actual ip=%s port=%u",
+			net_ip2addr(&master_input->peer_ip),
+			master_input->peer_port,
+			net_ip2addr(&peer_ip), peer_port);
+		return -1;
+	}
+	return 0;
+}
+
+static int
+imap_master_client_input_args(struct connection *conn, const char *const *args,
+			      int fd_client, pool_t pool)
+{
+	struct imap_master_client *client = (struct imap_master_client *)conn;
+	struct client *imap_client;
+	struct mail_storage_service_input input;
+	struct imap_master_input master_input;
+	const char *error;
+	int ret;
+
+	if (imap_master_client_parse_input(args, pool, &input, &master_input,
+					   &error) < 0) {
+		i_error("imap-master: Failed to parse client input: %s", error);
+		o_stream_send_str(conn->output, t_strdup_printf(
+			"-Failed to parse client input: %s\n", error));
+		i_close_fd(&fd_client);
+		return -1;
+	}
+	if (imap_master_client_verify(&master_input, fd_client, &error) < 0) {
+		i_error("imap-master: Failed to verify client input: %s", error);
+		o_stream_send_str(conn->output, t_strdup_printf(
+			"-Failed to verify client input: %s\n", error));
+		i_close_fd(&fd_client);
+		return -1;
+	}
+
+	/* NOTE: before client_create_from_input() on failures we need to close
+	   fd_client, but afterward it gets closed by client_destroy() */
+	ret = client_create_from_input(&input, fd_client, fd_client,
+				       &imap_client, &error);
+	if (ret < 0) {
+		i_error("imap-master(%s): Failed to create client: %s",
+			input.username, error);
+		o_stream_send_str(conn->output, t_strdup_printf(
+			"-Failed to create client: %s\n", error));
+		master_service_client_connection_destroyed(master_service);
+		i_close_fd(&fd_client);
+		return -1;
+	}
+	/* log prefix is set at this point, so we don't need to add the
+	   username anymore to the log messages */
+	client->imap_client_created = TRUE;
+
+	o_stream_nsend(imap_client->output,
+		       master_input.client_output->data,
+		       master_input.client_output->used);
+	if (master_input.client_input->used > 0 &&
+	    !i_stream_add_data(imap_client->input,
+			       master_input.client_input->data,
+			       master_input.client_input->used)) {
+		i_error("imap-master: Couldn't add %"PRIuSIZE_T
+			" bytes to client's input stream",
+			master_input.client_input->used);
+		o_stream_send_str(conn->output,
+				  "-Couldn't add client input\n");
+		client_destroy(imap_client, "Client initialization failed");
+		return -1;
+	}
+	imap_client->state_import_bad_idle_done =
+		master_input.state_import_bad_idle_done;
+	imap_client->state_import_idle_continue =
+		master_input.state_import_idle_continue;
+	ret = imap_state_import_internal(imap_client, master_input.state->data,
+					 master_input.state->used, &error);
+	if (ret <= 0) {
+		i_error("imap-master: Failed to import client state: %s", error);
+		client_destroy(imap_client, "Client state initialization failed");
+		return -1;
+	}
+
+	/* make sure all pending input gets handled */
+	i_assert(imap_client->to_delayed_input == NULL);
+	if (master_input.client_input->used > 0) {
+		imap_client->to_delayed_input =
+			timeout_add(0, client_input, imap_client);
+	}
+
+	o_stream_send_str(conn->output, "+\n");
+	imap_refresh_proctitle();
+	/* we'll always disconnect the client afterwards */
+	return -1;
+}
+
+static int
+imap_master_client_input_line(struct connection *conn, const char *line)
+{
+	char *const *args;
+	pool_t pool;
+	int fd_client, ret;
+
+	if (!conn->version_received) {
+		if (connection_verify_version(conn, t_strsplit_tabescaped(line)) < 0)
+			return -1;
+		conn->version_received = TRUE;
+		return 1;
+	}
+
+	fd_client = i_stream_unix_get_read_fd(conn->input);
+	if (fd_client == -1) {
+		i_error("imap-master: IMAP client fd not received");
+		return -1;
+	}
+
+	if (imap_debug)
+		i_debug("imap-master: Client input: %s", line);
+
+	pool = pool_alloconly_create("imap master client cmd", 1024);
+	args = p_strsplit_tabescaped(pool, line);
+	ret = imap_master_client_input_args(conn, (void *)args, fd_client, pool);
+	pool_unref(&pool);
+	return ret;
+}
+
+void imap_master_client_create(int fd)
+{
+	struct imap_master_client *client;
+
+	client = i_new(struct imap_master_client, 1);
+	connection_init_server(master_clients, &client->conn,
+			       "imap-master", fd, fd);
+
+	i_assert(client->conn.input == NULL);
+	client->conn.input = i_stream_create_unix(fd, (size_t)-1);
+	/* read the first file descriptor that we can */
+	i_stream_unix_set_read_fd(client->conn.input);
+}
+
+static struct connection_settings client_set = {
+	.service_name_in = "imap-master",
+	.service_name_out = "imap-master",
+	.major_version = 1,
+	.minor_version = 0,
+
+	.input_max_size = 0, /* don't auto-create istream */
+	.output_max_size = (size_t)-1,
+	.client = FALSE
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+	.destroy = imap_master_client_destroy,
+	.input_line = imap_master_client_input_line
+};
+
+void imap_master_clients_init(void)
+{
+	master_clients = connection_list_init(&client_set, &client_vfuncs);
+}
+
+void imap_master_clients_deinit(void)
+{
+	connection_list_deinit(&master_clients);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-master-client.h	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,9 @@
+#ifndef IMAP_MASTER_CLIENT_H
+#define IMAP_MASTER_CLIENT_H
+
+void imap_master_client_create(int fd);
+
+void imap_master_clients_init(void);
+void imap_master_clients_deinit(void);
+
+#endif
--- a/src/imap/imap-settings.c	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/imap-settings.c	Mon Aug 24 14:13:02 2015 +0300
@@ -16,10 +16,12 @@
 
 /* <settings checks> */
 static struct file_listener_settings imap_unix_listeners_array[] = {
-	{ "login/imap", 0666, "", "" }
+	{ "login/imap", 0666, "", "" },
+	{ "imap-master", 0600, "", "" }
 };
 static struct file_listener_settings *imap_unix_listeners[] = {
-	&imap_unix_listeners_array[0]
+	&imap_unix_listeners_array[0],
+	&imap_unix_listeners_array[1]
 };
 static buffer_t imap_unix_listeners_buf = {
 	imap_unix_listeners, sizeof(imap_unix_listeners), { NULL, }
@@ -70,6 +72,7 @@
 	DEF(SET_STR, imap_id_send),
 	DEF(SET_STR, imap_id_log),
 	DEF(SET_BOOL, imap_metadata),
+	DEF(SET_TIME, imap_hibernate_timeout),
 
 	DEF(SET_STR, imap_urlauth_host),
 	DEF(SET_UINT, imap_urlauth_port),
@@ -91,6 +94,7 @@
 	.imap_id_send = "name *",
 	.imap_id_log = "",
 	.imap_metadata = FALSE,
+	.imap_hibernate_timeout = 0,
 
 	.imap_urlauth_host = "",
 	.imap_urlauth_port = 143
--- a/src/imap/imap-settings.h	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/imap-settings.h	Mon Aug 24 14:13:02 2015 +0300
@@ -23,6 +23,7 @@
 	const char *imap_id_send;
 	const char *imap_id_log;
 	bool imap_metadata;
+	unsigned int imap_hibernate_timeout;
 
 	/* imap urlauth: */
 	const char *imap_urlauth_host;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-state.c	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,889 @@
+/* Copyright (c) 2014-2015 Dovecot authors, see the included COPYING file */
+
+#include "imap-common.h"
+#include "crc32.h"
+#include "numpack.h"
+#include "net.h"
+#include "ostream.h"
+#include "str.h"
+#include "str-sanitize.h"
+#include "imap-util.h"
+#include "mail-search-build.h"
+#include "mail-storage.h"
+#include "mailbox-recent-flags.h"
+#include "imap-client.h"
+#include "imap-fetch.h"
+#include "imap-search-args.h"
+#include "imap-state.h"
+
+enum imap_state_type_public {
+	IMAP_STATE_TYPE_MAILBOX			= 'B',
+	IMAP_STATE_TYPE_ENABLED_FEATURES	= 'F',
+	IMAP_STATE_TYPE_SEARCHRES		= '1',
+};
+
+enum imap_state_type_internal {
+	IMAP_STATE_TYPE_ID_LOGGED		= 'I',
+	IMAP_STATE_TYPE_TLS_COMPRESSION		= 'C',
+	IMAP_STATE_TYPE_IDLE_CMD_TAG		= 'T'
+};
+
+enum imap_state_feature {
+	IMAP_STATE_FEATURE_CONDSTORE	= 'C',
+	IMAP_STATE_FEATURE_QRESYNC	= 'Q'
+};
+
+struct mailbox_import_state {
+	const char *vname;
+	guid_128_t mailbox_guid;
+	bool examined;
+	uint32_t keywords_count, keywords_crc32, uids_crc32;
+	uint32_t uidvalidity, uidnext, messages;
+	uint64_t highest_modseq;
+	ARRAY_TYPE(seq_range) recent_uids;
+};
+
+static void
+export_seq_range(buffer_t *dest, const ARRAY_TYPE(seq_range) *range)
+{
+	const struct seq_range *uids;
+	unsigned int i, count;
+	uint32_t next_uid;
+
+	uids = array_get(range, &count);
+	numpack_encode(dest, count);
+	next_uid = 1;
+	for (i = 0; i < count; i++) {
+		i_assert(uids[i].seq1 >= next_uid);
+		if (uids[i].seq1 == uids[i].seq2) {
+			numpack_encode(dest, (uids[i].seq1 - next_uid) << 1);
+		} else {
+			numpack_encode(dest, 1 | ((uids[i].seq1 - next_uid) << 1));
+			numpack_encode(dest, uids[i].seq2 - uids[i].seq1 - 1);
+		}
+		next_uid = uids[i].seq2 + 1;
+	}
+}
+
+static int
+import_seq_range(const unsigned char **data, const unsigned char *end,
+		 ARRAY_TYPE(seq_range) *range)
+{
+	uint32_t i, count, next_uid, num, uid1, uid2;
+
+	if (numpack_decode32(data, end, &count) < 0)
+		return -1;
+	next_uid = 1;
+
+	for (i = 0; i < count; i++) {
+		if (numpack_decode32(data, end, &num) < 0)
+			return -1;
+		uid1 = next_uid + (num >> 1);
+		if ((num & 1) == 0) {
+			uid2 = uid1;
+			seq_range_array_add(range, uid1);
+		} else {
+			if (numpack_decode32(data, end, &num) < 0)
+				return -1;
+			uid2 = uid1 + num + 1;
+			seq_range_array_add_range(range, uid1, uid2);
+		}
+		next_uid = uid2 + 1;
+	}
+	return 0;
+}
+
+int imap_state_export_internal(struct client *client, buffer_t *dest,
+			       const char **error_r)
+{
+	/* the only IMAP command we allow running is IDLE or X-STATE */
+	if (client->command_queue_size > 1) {
+		*error_r = "Multiple commands in progress";
+		return 0;
+	}
+	if (client->command_queue == NULL ||
+	    strcasecmp(client->command_queue->name, "IDLE") != 0) {
+		/* this would require saving the seq <-> uid mapping
+		   and restore it on import. quite a lot of trouble if
+		   messages have been expunged in the mean time. */
+		*error_r = "Non-IDLE connections not supported currently";
+		return 0;
+	}
+	return client->v.state_export(client, TRUE, dest, error_r);
+}
+
+int imap_state_export_external(struct client *client, buffer_t *dest,
+			       const char **error_r)
+{
+	if (client->command_queue_size > 1) {
+		*error_r = "Multiple commands in progress";
+		return 0;
+	}
+
+	i_assert(client->command_queue_size == 1);
+	i_assert(strcmp(client->command_queue->name, "X-STATE") == 0);
+	return client->v.state_export(client, FALSE, dest, error_r);
+}
+
+static int
+imap_state_import(struct client *client, bool internal,
+		  const unsigned char *data, size_t size, const char **error_r)
+{
+	ssize_t ret;
+
+	while (size > 0) {
+		ret = client->v.state_import(client, internal,
+					     data, size, error_r);
+		if (ret <= 0) {
+			i_assert(*error_r != NULL);
+			return ret < 0 ? -1 : 0;
+		}
+		i_assert((size_t)ret <= size);
+		data += ret;
+		size -= ret;
+	}
+	return 1;
+}
+
+int imap_state_import_internal(struct client *client,
+			       const unsigned char *data, size_t size,
+			       const char **error_r)
+{
+	return imap_state_import(client, TRUE, data, size, error_r);
+}
+
+int imap_state_import_external(struct client *client,
+			       const unsigned char *data, size_t size,
+			       const char **error_r)
+{
+	return imap_state_import(client, FALSE, data, size, error_r);
+}
+
+static int
+imap_state_export_mailbox_mails(buffer_t *dest, struct mailbox *box,
+				const char **error_r)
+{
+	struct mailbox_transaction_context *trans;
+	struct mail_search_args *search_args;
+	struct mail_search_context *search_ctx;
+	struct mail *mail;
+	ARRAY_TYPE(seq_range) recent_uids;
+	uint32_t crc = 0;
+	int ret = 1;
+
+	search_args = mail_search_build_init();
+	mail_search_build_add_all(search_args);
+
+	trans = mailbox_transaction_begin(box, 0);
+	search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL);
+	mail_search_args_unref(&search_args);
+
+	t_array_init(&recent_uids, 8);
+	while (mailbox_search_next(search_ctx, &mail)) {
+		crc = crc32_data_more(crc, &mail->uid, sizeof(mail->uid));
+		if ((mail_get_flags(mail) & MAIL_RECENT) != 0)
+			seq_range_array_add(&recent_uids, mail->uid);
+	}
+	if (mailbox_search_deinit(&search_ctx) < 0) {
+		*error_r = mailbox_get_last_error(box, NULL);
+		ret = -1;
+	}
+	(void)mailbox_transaction_commit(&trans);
+
+	numpack_encode(dest, crc);
+	export_seq_range(dest, &recent_uids);
+	return ret;
+}
+
+static uint32_t
+mailbox_status_keywords_crc32(const struct mailbox_status *status)
+{
+	const char *const *strp;
+	uint32_t crc = 0;
+
+	array_foreach(status->keywords, strp)
+		crc = crc32_str(*strp);
+	return crc;
+}
+
+static int
+imap_state_export_mailbox(buffer_t *dest, struct client *client,
+			  struct mailbox *box, const char **error_r)
+{
+        struct mailbox_status status;
+	struct mailbox_metadata metadata;
+	const char *vname = mailbox_get_vname(box);
+
+	mailbox_get_open_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT |
+				STATUS_MESSAGES | STATUS_HIGHESTMODSEQ |
+				STATUS_KEYWORDS,
+				&status);
+	if (status.nonpermanent_modseqs) {
+		*error_r = "Nonpermanent modseqs";
+		return 0;
+	}
+
+	if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
+		*error_r = mailbox_get_last_error(box, NULL);
+		return -1;
+	}
+
+	buffer_append_c(dest, IMAP_STATE_TYPE_MAILBOX);
+	buffer_append(dest, vname, strlen(vname)+1);
+	buffer_append(dest, metadata.guid, sizeof(metadata.guid));
+
+	buffer_append_c(dest, client->mailbox_examined ? 1 : 0);
+	numpack_encode(dest, status.uidvalidity);
+	numpack_encode(dest, status.uidnext);
+	numpack_encode(dest, status.messages);
+	if ((client->enabled_features & MAILBOX_FEATURE_QRESYNC) != 0 &&
+	    !client->nonpermanent_modseqs)
+		numpack_encode(dest, client->sync_last_full_modseq);
+	else
+		numpack_encode(dest, status.highest_modseq);
+
+	/* keywords count + CRC32 should be enough to figure out if it
+	   needs to be resent */
+	numpack_encode(dest, array_count(status.keywords));
+	numpack_encode(dest, mailbox_status_keywords_crc32(&status));
+
+	/* we're now basically done, but just in case there's a bug add a
+	   checksum of the currently existing UIDs and verify it when
+	   importing. this also writes the list of recent UIDs. */
+	return imap_state_export_mailbox_mails(dest, box, error_r);
+}
+
+int imap_state_export_base(struct client *client, bool internal,
+			   buffer_t *dest, const char **error_r)
+{
+	int ret;
+
+	str_append(dest, "base\n");
+	if (array_is_created(&client->search_updates) &&
+	    array_count(&client->search_updates) > 0) {
+		/* these could be tricky */
+		*error_r = "SEARCH=CONTEXT updates not supported currently";
+		return 0;
+	}
+	if (client->notify_ctx != NULL) {
+		/* FIXME: this really should be supported. also IDLE wouldn't
+		   be needed if NOTIFY allows sending EXPUNGEs to selected
+		   mailbox. */
+		*error_r = "NOTIFY not supported currently";
+		return 0;
+	}
+
+	if (client->mailbox != NULL) {
+		ret = imap_state_export_mailbox(dest, client,
+						client->mailbox, error_r);
+		if (ret <= 0)
+			return ret;
+	}
+
+	/* IMAP features */
+	if (client->enabled_features != 0) {
+		i_assert((client->enabled_features & ~(MAILBOX_FEATURE_CONDSTORE |
+						       MAILBOX_FEATURE_QRESYNC)) == 0);
+		buffer_append_c(dest, IMAP_STATE_TYPE_ENABLED_FEATURES);
+		if ((client->enabled_features & MAILBOX_FEATURE_CONDSTORE) != 0)
+			buffer_append_c(dest, IMAP_STATE_FEATURE_CONDSTORE);
+		if ((client->enabled_features & MAILBOX_FEATURE_QRESYNC) != 0)
+			buffer_append_c(dest, IMAP_STATE_FEATURE_QRESYNC);
+		buffer_append_c(dest, '\0');
+	}
+	if (internal) {
+		if (client->id_logged)
+			buffer_append_c(dest, IMAP_STATE_TYPE_ID_LOGGED);
+		if (client->tls_compression)
+			buffer_append_c(dest, IMAP_STATE_TYPE_TLS_COMPRESSION);
+		if (client->command_queue != NULL) {
+			i_assert(strcasecmp(client->command_queue->name, "IDLE") == 0);
+			buffer_append_c(dest, IMAP_STATE_TYPE_IDLE_CMD_TAG);
+			buffer_append(dest, client->command_queue->tag,
+				      strlen(client->command_queue->tag) + 1);
+		}
+	}
+
+	/* IMAP SEARCHRES extension */
+	if (array_is_created(&client->search_saved_uidset) &&
+	    array_count(&client->search_saved_uidset) > 0) {
+		buffer_append_c(dest, IMAP_STATE_TYPE_SEARCHRES);
+		export_seq_range(dest, &client->search_saved_uidset);
+	}
+	return 1;
+}
+
+static int
+import_string(const unsigned char **data, const unsigned char *end,
+	      const char **str_r)
+{
+	const unsigned char *p;
+
+	p = memchr(*data, '\0', end - *data);
+	if (p == NULL)
+		return -1;
+	*str_r = (const void *)*data;
+	*data = p + 1;
+	return 0;
+}
+
+static int
+import_send_expunges(struct client *client,
+		     const struct mailbox_import_state *state,
+		     unsigned int *expunge_count_r,
+		     const char **error_r)
+{
+	struct mailbox_transaction_context *trans;
+	struct mail_search_args *search_args;
+	struct mail_search_context *search_ctx;
+	struct mail *mail;
+	uint32_t crc = 0, seq, expunged_uid;
+	ARRAY_TYPE(seq_range) uids_filter, expunged_uids;
+	ARRAY_TYPE(uint32_t) expunged_seqs;
+	struct seq_range_iter iter;
+	const uint32_t *seqs;
+	unsigned int i, expunge_count, n = 0;
+	string_t *str;
+	int ret = 0;
+
+	*expunge_count_r = 0;
+
+	if (state->messages == 0) {
+		/* the mailbox was empty originally - there couldn't be any
+		   pending expunges. */
+		return 0;
+	}
+	if (state->uidnext <= 1) {
+		*error_r = "Invalid UIDNEXT";
+		return -1;
+	}
+
+	/* get all the message UIDs expunged since the last known modseq */
+	t_array_init(&uids_filter, 1);
+	t_array_init(&expunged_uids, 128);
+	seq_range_array_add_range(&uids_filter, 1, state->uidnext-1);
+	if (!mailbox_get_expunged_uids(client->mailbox, state->highest_modseq,
+				       &uids_filter, &expunged_uids)) {
+		*error_r = t_strdup_printf(
+			"Couldn't get recently expunged UIDs "
+			"(uidnext=%u highest_modseq=%llu)", state->uidnext,
+			(unsigned long long)state->highest_modseq);
+		return -1;
+	}
+	seq_range_array_iter_init(&iter, &expunged_uids);
+
+	search_args = mail_search_build_init();
+	mail_search_build_add_all(search_args);
+
+	trans = mailbox_transaction_begin(client->mailbox, 0);
+	search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL);
+	mail_search_args_unref(&search_args);
+
+	/* find sequence numbers for the expunged UIDs */
+	t_array_init(&expunged_seqs, array_count(&expunged_uids)+1); seq = 0;
+	while (mailbox_search_next(search_ctx, &mail)) {
+		while (seq_range_array_iter_nth(&iter, n, &expunged_uid) &&
+		       expunged_uid < mail->uid) {
+			seq++; n++;
+			array_append(&expunged_seqs, &seq, 1);
+			crc = crc32_data_more(crc, &expunged_uid,
+					      sizeof(expunged_uid));
+		}
+		if (seq == state->messages)
+			break;
+		crc = crc32_data_more(crc, &mail->uid, sizeof(mail->uid));
+		if (++seq == state->messages)
+			break;
+	}
+	while (seq_range_array_iter_nth(&iter, n, &expunged_uid)) {
+		seq++; n++;
+		array_append(&expunged_seqs, &seq, 1);
+		crc = crc32_data_more(crc, &expunged_uid,
+				      sizeof(expunged_uid));
+	}
+
+	if (mailbox_search_deinit(&search_ctx) < 0) {
+		*error_r = mailbox_get_last_error(client->mailbox, NULL);
+		ret = -1;
+	} else if (seq != state->messages) {
+		*error_r = "Message count mismatch after handling expunges";
+		ret = -1;
+	}
+	(void)mailbox_transaction_commit(&trans);
+	if (ret < 0)
+		return -1;
+
+	seqs = array_get(&expunged_seqs, &expunge_count);
+	if (client->messages_count + expunge_count < state->messages) {
+		*error_r = "Message count too low after handling expunges";
+		return -1;
+	}
+	if (crc != state->uids_crc32) {
+		*error_r = "Message UIDs CRC32 mismatch";
+		return -1;
+	}
+
+	if ((client->enabled_features & MAILBOX_FEATURE_QRESYNC) == 0) {
+		str = t_str_new(32);
+		for (i = expunge_count; i > 0; i--) {
+			str_truncate(str, 0);
+			str_printfa(str, "* %u EXPUNGE", seqs[i-1]);
+			client_send_line(client, str_c(str));
+		}
+	} else {
+		str = str_new(default_pool, 128);
+		str_append(str, "* VANISHED ");
+		imap_write_seq_range(str, &expunged_uids);
+		str_append(str, "\r\n");
+		o_stream_nsend(client->output, str_data(str), str_len(str));
+		str_free(&str);
+	}
+	*expunge_count_r = expunge_count;
+	return 0;
+}
+
+static int
+import_send_flag_changes(struct client *client,
+			 const struct mailbox_import_state *state)
+{
+	struct imap_fetch_context *fetch_ctx;
+	struct mail_search_args *search_args;
+	ARRAY_TYPE(seq_range) old_uids;
+	pool_t pool;
+	int ret;
+
+	if (state->messages == 0)
+		return 0;
+
+	t_array_init(&old_uids, 1);
+	seq_range_array_add_range(&old_uids, 1, state->uidnext-1);
+
+	search_args = mail_search_build_init();
+	search_args->args = p_new(search_args->pool, struct mail_search_arg, 1);
+	search_args->args->type = SEARCH_UIDSET;
+	search_args->args->value.seqset = old_uids;
+	imap_search_add_changed_since(search_args, state->highest_modseq);
+
+	pool = pool_alloconly_create("imap state flag changes", 1024);
+	fetch_ctx = imap_fetch_alloc(client, pool);
+	pool_unref(&pool);
+
+	imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_flags_init);
+	if ((client->enabled_features & MAILBOX_FEATURE_QRESYNC) != 0) {
+		imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_uid_init);
+		imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_modseq_init);
+	}
+
+	imap_fetch_begin(fetch_ctx, client->mailbox, search_args);
+	mail_search_args_unref(&search_args);
+
+	/* FIXME: ideally do this asynchronously.. */
+	while (imap_fetch_more_no_lock_update(fetch_ctx) == 0) ;
+
+	ret = imap_fetch_end(fetch_ctx);
+	imap_fetch_free(&fetch_ctx);
+	return ret;
+}
+
+static ssize_t
+import_state_mailbox_struct(const unsigned char *data, size_t size,
+			    struct mailbox_import_state *state_r,
+			    const char **error_r)
+{
+	const unsigned char *p = data, *end = data + size;
+
+	memset(state_r, 0, sizeof(*state_r));
+	t_array_init(&state_r->recent_uids, 8);
+
+	/* vname */
+	if (import_string(&p, end, &state_r->vname) < 0) {
+		*error_r = "Mailbox state truncated at name";
+		return 0;
+	}
+
+	/* GUID */
+	if (end-p < (int)sizeof(state_r->mailbox_guid)) {
+		*error_r = "Mailbox state truncated at GUID";
+		return 0;
+	}
+	memcpy(state_r->mailbox_guid, p, sizeof(state_r->mailbox_guid));
+	p += sizeof(state_r->mailbox_guid);
+
+	if (guid_128_is_empty(state_r->mailbox_guid)) {
+		*error_r = "Empty GUID";
+		return 0;
+	}
+
+	/* EXAMINEd vs SELECTed */
+	if (p == end) {
+		*error_r = "Mailbox state truncated at examined-flag";
+		return 0;
+	}
+	state_r->examined = p[0] != 0;
+	p++;
+
+	/* mailbox state */
+	if (numpack_decode32(&p, end, &state_r->uidvalidity) < 0 ||
+	    numpack_decode32(&p, end, &state_r->uidnext) < 0 ||
+	    numpack_decode32(&p, end, &state_r->messages) < 0 ||
+	    numpack_decode(&p, end, &state_r->highest_modseq) < 0 ||
+	    numpack_decode32(&p, end, &state_r->keywords_count) < 0 ||
+	    numpack_decode32(&p, end, &state_r->keywords_crc32) < 0 ||
+	    numpack_decode32(&p, end, &state_r->uids_crc32) < 0 ||
+	    import_seq_range(&p, end, &state_r->recent_uids) < 0) {
+		*error_r = "Mailbox state truncated";
+		return 0;
+	}
+	if (state_r->uidvalidity == 0) {
+		*error_r = "Empty UIDVALIDITY";
+		return 0;
+	}
+	if (state_r->uidnext == 0) {
+		*error_r = "Empty UIDNEXT";
+		return 0;
+	}
+	return p - data;
+}
+
+static int
+import_state_mailbox_open(struct client *client,
+			  const struct mailbox_import_state *state,
+			  const char **error_r)
+{
+	struct mail_namespace *ns;
+	struct mailbox *box;
+	struct mailbox_metadata metadata;
+        struct mailbox_status status;
+	const struct seq_range *range;
+	enum mailbox_flags flags = 0;
+	unsigned int expunge_count;
+	uint32_t uid;
+	int ret = 0;
+
+	ns = mail_namespace_find(client->user->namespaces, state->vname);
+	if (ns == NULL) {
+		*error_r = "Namespace not found for mailbox";
+		return -1;
+	}
+
+	if (state->examined)
+		flags |= MAILBOX_FLAG_READONLY;
+	else
+		flags |= MAILBOX_FLAG_DROP_RECENT;
+	box = mailbox_alloc(ns->list, state->vname, flags);
+	if (mailbox_open(box) < 0) {
+		*error_r = t_strdup_printf("Couldn't open mailbox: %s",
+			mailbox_get_last_error(box, NULL));
+		mailbox_free(&box);
+		return -1;
+	}
+
+	if (client->enabled_features != 0)
+		ret = mailbox_enable(box, client->enabled_features);
+	if (ret < 0 || mailbox_sync(box, 0) < 0) {
+		*error_r = t_strdup_printf("Couldn't sync mailbox: %s",
+			mailbox_get_last_error(box, NULL));
+		mailbox_free(&box);
+		return -1;
+	}
+	/* verify that this still looks like the same mailbox */
+	if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
+		*error_r = mailbox_get_last_error(box, NULL);
+		return -1;
+	}
+	if (!guid_128_equals(metadata.guid, state->mailbox_guid)) {
+		*error_r = "Mailbox GUID has changed";
+		return -1;
+	}
+	mailbox_get_open_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT |
+				STATUS_HIGHESTMODSEQ | STATUS_RECENT |
+				STATUS_KEYWORDS, &status);
+	if (status.uidvalidity != state->uidvalidity) {
+		*error_r = "Mailbox UIDVALIDITY has changed";
+		return -1;
+	}
+	if (status.uidnext < state->uidnext) {
+		*error_r = "Mailbox UIDNEXT shrank";
+		return -1;
+	}
+	if (status.highest_modseq < state->highest_modseq) {
+		*error_r = "Mailbox HIGHESTMODSEQ shrank";
+		return -1;
+	}
+
+	client->mailbox = box;
+	client->mailbox_examined = state->examined;
+	client->messages_count = status.messages;
+	client->recent_count = status.recent;
+	client->uidvalidity = status.uidvalidity;
+	client->notify_uidnext = status.uidnext;
+
+	if (import_send_expunges(client, state, &expunge_count, error_r) < 0)
+		return -1;
+	i_assert(expunge_count <= state->messages);
+	if (state->messages - expunge_count > client->messages_count) {
+		*error_r = "Mailbox message count shrank";
+		return -1;
+	}
+	if (state->messages - expunge_count < client->messages_count) {
+		/* new messages arrived */
+		client_send_line(client,
+			t_strdup_printf("* %u EXISTS", client->messages_count));
+		client_send_line(client,
+			t_strdup_printf("* %u RECENT", client->recent_count));
+	}
+
+	client_update_mailbox_flags(client, status.keywords);
+	array_foreach(&state->recent_uids, range) {
+		for (uid = range->seq1; uid <= range->seq2; uid++)
+			mailbox_recent_flags_set_uid_forced(box, uid);
+	}
+	if (array_count(status.keywords) == state->keywords_count &&
+	    mailbox_status_keywords_crc32(&status) == state->keywords_crc32) {
+		/* no changes to keywords */
+		client->keywords.announce_count = state->keywords_count;
+	} else {
+		client_send_mailbox_flags(client, TRUE);
+	}
+	if (import_send_flag_changes(client, state) < 0) {
+		*error_r = "Couldn't send flag changes";
+		return -1;
+	}
+	if ((client->enabled_features & MAILBOX_FEATURE_QRESYNC) != 0 &&
+	    !client->nonpermanent_modseqs &&
+	    status.highest_modseq != state->highest_modseq) {
+		client_send_line(client, t_strdup_printf(
+			"* OK [HIGHESTMODSEQ %llu] Highest",
+			(unsigned long long)status.highest_modseq));
+		client->sync_last_full_modseq = status.highest_modseq;
+	}
+	return 0;
+}
+
+static ssize_t
+import_state_mailbox(struct client *client, const unsigned char *data,
+		     size_t size, const char **error_r)
+{
+	struct mailbox_import_state state;
+	ssize_t ret;
+
+	if (client->mailbox != NULL) {
+		*error_r = "Duplicate mailbox state";
+		return 0;
+	}
+
+	ret = import_state_mailbox_struct(data, size, &state, error_r);
+	if (ret <= 0) {
+		i_assert(*error_r != NULL);
+		return ret;
+	}
+	if (import_state_mailbox_open(client, &state, error_r) < 0)
+		return -1;
+	return ret;
+}
+
+static ssize_t
+import_state_enabled_features(struct client *client, const unsigned char *data,
+			      size_t size, const char **error_r)
+{
+	enum imap_state_feature feature;
+	size_t i = 0;
+
+	for (i = 0; i < size; i++) {
+		if (data[i] == '\0')
+			return i+1;
+		feature = data[i];
+		switch (feature) {
+		case IMAP_STATE_FEATURE_CONDSTORE:
+			client->enabled_features |= MAILBOX_FEATURE_CONDSTORE;
+			break;
+		case IMAP_STATE_FEATURE_QRESYNC:
+			client->enabled_features |= MAILBOX_FEATURE_QRESYNC;
+			break;
+		default:
+			*error_r = t_strdup_printf(
+				"Unknown feature '%c'", feature);
+			return 0;
+		}
+
+	}
+	*error_r = "Non-terminated features list";
+	return 0;
+}
+
+static ssize_t
+import_state_searchres(struct client *client, const unsigned char *data,
+		       size_t size, const char **error_r)
+{
+	const unsigned char *p = data;
+
+	i_array_init(&client->search_saved_uidset, 128);
+	if (import_seq_range(&p, data+size, &client->search_saved_uidset) < 0) {
+		*error_r = "Invalid SEARCHRES seq-range";
+		return 0;
+	}
+	return p - data;
+}
+
+static ssize_t
+import_state_id_logged(struct client *client,
+		       const unsigned char *data ATTR_UNUSED,
+		       size_t size ATTR_UNUSED,
+		       const char **error_r ATTR_UNUSED)
+{
+	client->id_logged = TRUE;
+	return 0;
+}
+
+static ssize_t
+import_state_tls_compression(struct client *client,
+			     const unsigned char *data ATTR_UNUSED,
+			     size_t size ATTR_UNUSED,
+			     const char **error_r ATTR_UNUSED)
+{
+	client->tls_compression = TRUE;
+	return 0;
+}
+
+static ssize_t
+import_state_idle_cmd_tag(struct client *client, const unsigned char *data,
+			  size_t size, const char **error_r)
+{
+	const unsigned char *p = data, *end = data + size;
+	const char *tag;
+
+	if (import_string(&p, end, &tag) < 0) {
+		*error_r = "Missing idle tag";
+		return 0;
+	}
+
+	if (client->state_import_idle_continue) {
+		/* IDLE command continues */
+		struct client_command_context *cmd;
+		struct command *command;
+
+		cmd = client_command_alloc(client);
+		cmd->tag = p_strdup(cmd->pool, tag);
+		cmd->name = "IDLE";
+
+		command = command_find("IDLE");
+		i_assert(command != NULL);
+		cmd->func = command->func;
+		cmd->cmd_flags = command->flags;
+		if (command_exec(cmd)) {
+			/* IDLE terminated because of an external change, but
+			   DONE was already buffered */
+			client_command_free(&cmd);
+			client_add_missing_io(client);
+		} else {
+			i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT);
+		}
+	} else {
+		/* we're finishing IDLE command */
+		client_send_line(client, t_strdup_printf(
+			"%s %s Idle completed.", tag,
+			client->state_import_bad_idle_done ? "BAD" : "OK"));
+	}
+	return p - data;
+}
+
+static struct {
+	enum imap_state_type_public type;
+	ssize_t (*import)(struct client *client, const unsigned char *data,
+			  size_t size, const char **error_r);
+} imap_states_public[] = {
+	{ IMAP_STATE_TYPE_MAILBOX, import_state_mailbox },
+	{ IMAP_STATE_TYPE_ENABLED_FEATURES, import_state_enabled_features },
+	{ IMAP_STATE_TYPE_SEARCHRES, import_state_searchres }
+};
+
+static struct {
+	enum imap_state_type_public type;
+	ssize_t (*import)(struct client *client, const unsigned char *data,
+			  size_t size, const char **error_r);
+} imap_states_internal[] = {
+	{ IMAP_STATE_TYPE_ID_LOGGED, import_state_id_logged },
+	{ IMAP_STATE_TYPE_TLS_COMPRESSION, import_state_tls_compression },
+	{ IMAP_STATE_TYPE_IDLE_CMD_TAG, import_state_idle_cmd_tag },
+};
+
+static ssize_t
+imap_state_try_import_public(struct client *client, const unsigned char *data,
+			     size_t size, const char **error_r)
+{
+	unsigned int i;
+	ssize_t ret;
+
+	i_assert(size > 0);
+
+	for (i = 0; i < N_ELEMENTS(imap_states_public); i++) {
+		if (imap_states_public[i].type == data[0]) {
+			ret = imap_states_public[i].
+				import(client, data+1, size-1, error_r);
+			return ret < 0 ? -1 : ret+1;
+		}
+	}
+	return -2;
+}
+
+static ssize_t
+imap_state_try_import_internal(struct client *client, const unsigned char *data,
+			       size_t size, const char **error_r)
+{
+	unsigned int i;
+	ssize_t ret;
+
+	i_assert(size > 0);
+
+	for (i = 0; i < N_ELEMENTS(imap_states_internal); i++) {
+		if (imap_states_internal[i].type == data[0]) {
+			ret = imap_states_internal[i].
+				import(client, data+1, size-1, error_r);
+			return ret < 0 ? -1 : ret+1;
+		}
+	}
+	return -2;
+}
+
+ssize_t imap_state_import_base(struct client *client, bool internal,
+			       const unsigned char *data, size_t size,
+			       const char **error_r)
+{
+	const unsigned char *p;
+	ssize_t ret;
+	size_t pos;
+
+	i_assert(client->mailbox == NULL);
+
+	*error_r = NULL;
+
+	if (size < 5 || memcmp(data, "base\n", 5) != 0) {
+		p = memchr(data, '\n', size);
+		if (p == NULL)
+			p = data + I_MIN(size, 20);
+		*error_r = t_strdup_printf("Unknown state block '%s'",
+					   str_sanitize(t_strdup_until(data, p), 20));
+		return 0;
+	}
+
+	pos = 5;
+	while (pos < size) {
+		ret = imap_state_try_import_public(client, data+pos,
+						   size-pos, error_r);
+		if (ret == -2 && internal) {
+			ret = imap_state_try_import_internal(client, data+pos,
+							     size-pos, error_r);
+		}
+		if (ret < 0 || *error_r != NULL) {
+			if (ret == -2) {
+				*error_r = t_strdup_printf("Unknown type '%c'",
+							   data[pos]);
+			}
+			i_assert(*error_r != NULL);
+			return ret < 0 ? -1 : 0;
+		}
+		i_assert(size - pos >= (size_t)ret);
+		pos += ret;
+	}
+	return pos;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/imap/imap-state.h	Mon Aug 24 14:13:02 2015 +0300
@@ -0,0 +1,30 @@
+#ifndef IMAP_STATE_H
+#define IMAP_STATE_H
+
+/* Export the IMAP client state to the given buffer. Returns 1 if ok,
+   0 if state couldn't be exported, -1 if temporary internal error error. */
+int imap_state_export_internal(struct client *client, buffer_t *dest,
+			       const char **error_r);
+int imap_state_export_external(struct client *client, buffer_t *dest,
+			       const char **error_r);
+
+/* Returns 1 if ok, 0 if state was corrupted, -1 if other error. Internal state
+   comes from another Dovecot component, which can override IP addresses,
+   session IDs, etc. */
+int imap_state_import_internal(struct client *client,
+			       const unsigned char *data, size_t size,
+			       const char **error_r);
+int imap_state_import_external(struct client *client,
+			       const unsigned char *data, size_t size,
+			       const char **error_r);
+
+/* INTERNAL API: Note that the "internal" flag specifies whether we're doing
+   the import/export from/to another Dovecot component or an untrusted
+   IMAP client. */
+int imap_state_export_base(struct client *client, bool internal,
+			   buffer_t *dest, const char **error_r);
+ssize_t imap_state_import_base(struct client *client, bool internal,
+			       const unsigned char *data, size_t size,
+			       const char **error_r);
+
+#endif
--- a/src/imap/main.c	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/imap/main.c	Mon Aug 24 14:13:02 2015 +0300
@@ -17,6 +17,7 @@
 #include "master-login.h"
 #include "mail-user.h"
 #include "mail-storage-service.h"
+#include "imap-master-client.h"
 #include "imap-resp-code.h"
 #include "imap-commands.h"
 #include "imap-fetch.h"
@@ -35,6 +36,7 @@
 static struct master_login *master_login = NULL;
 
 imap_client_created_func_t *hook_client_created = NULL;
+bool imap_debug = FALSE;
 
 imap_client_created_func_t *
 imap_client_created_hook_set(imap_client_created_func_t *new_hook)
@@ -203,10 +205,9 @@
 	o_stream_unref(&output);
 }
 
-static int
-client_create_from_input(const struct mail_storage_service_input *input,
-			 int fd_in, int fd_out,
-			 struct client **client_r, const char **error_r)
+int client_create_from_input(const struct mail_storage_service_input *input,
+			     int fd_in, int fd_out,
+			     struct client **client_r, const char **error_r)
 {
 	struct mail_storage_service_user *user;
 	struct mail_user *mail_user;
@@ -246,6 +247,8 @@
 
 	client = client_create(fd_in, fd_out, input->session_id,
 			       mail_user, user, set);
+	client->userdb_fields = input->userdb_fields == NULL ? NULL :
+		p_strarray_dup(client->pool, input->userdb_fields);
 	*client_r = client;
 	return 0;
 }
@@ -340,7 +343,12 @@
 	i_assert(master_login != NULL);
 
 	master_service_client_connection_accept(conn);
-	master_login_add(master_login, conn->fd);
+	if (strcmp(conn->name, "imap-master") == 0) {
+		/* restoring existing IMAP connection (e.g. from imap-idle) */
+		imap_master_client_create(conn->fd);
+	} else {
+		master_login_add(master_login, conn->fd);
+	}
 }
 
 int main(int argc, char *argv[])
@@ -376,7 +384,7 @@
 	}
 
 	master_service = master_service_init("imap", service_flags,
-					     &argc, &argv, "t:u:");
+					     &argc, &argv, "Dt:u:");
 	while ((c = master_getopt(master_service)) > 0) {
 		switch (c) {
 		case 't':
@@ -389,6 +397,9 @@
 				MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
 			username = optarg;
 			break;
+		case 'D':
+			imap_debug = TRUE;
+			break;
 		default:
 			return FATAL_DEFAULT;
 		}
@@ -399,6 +410,7 @@
 	/* plugins may want to add commands, so this needs to be called early */
 	commands_init();
 	imap_fetch_handlers_init();
+	imap_master_clients_init();
 
 	random_init();
 	storage_service =
@@ -437,6 +449,7 @@
 
 	imap_fetch_handlers_deinit();
 	commands_deinit();
+	imap_master_clients_deinit();
 
 	random_deinit();
 	master_service_deinit(&master_service);
--- a/src/lib-storage/mail-user.c	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/lib-storage/mail-user.c	Mon Aug 24 14:13:02 2015 +0300
@@ -216,6 +216,8 @@
 		{ '\0', NULL, "auth_user" },
 		{ '\0', NULL, "auth_username" },
 		{ '\0', NULL, "auth_domain" },
+		/* NOTE: keep this synced with imap-hibernate's
+		   imap_client_var_expand_table() */
 		{ '\0', NULL, NULL }
 	};
 	struct var_expand_table *tab;
--- a/src/plugins/imap-zlib/imap-zlib-plugin.c	Mon Aug 24 14:14:59 2015 +0300
+++ b/src/plugins/imap-zlib/imap-zlib-plugin.c	Mon Aug 24 14:13:02 2015 +0300
@@ -19,6 +19,8 @@
 struct zlib_client {
 	union imap_module_context module_ctx;
 
+	int (*next_state_export)(struct client *client, bool internal,
+				 buffer_t *dest, const char **error_r);
 	const struct compression_handler *handler;
 };
 
@@ -118,6 +120,19 @@
 	return TRUE;
 }
 
+static int
+imap_zlib_state_export(struct client *client, bool internal,
+		       buffer_t *dest, const char **error_r)
+{
+	struct zlib_client *zclient = IMAP_ZLIB_IMAP_CONTEXT(client);
+
+	if (zclient->handler != NULL && internal) {
+		*error_r = "COMPRESS enabled";
+		return 0;
+	}
+	return zclient->next_state_export(client, internal, dest, error_r);
+}
+
 static void imap_zlib_client_created(struct client **clientp)
 {
 	struct client *client = *clientp;
@@ -128,6 +143,9 @@
 		zclient = p_new(client->pool, struct zlib_client, 1);
 		MODULE_CONTEXT_SET(client, imap_zlib_imap_module, zclient);
 
+		zclient->next_state_export = (*clientp)->v.state_export;
+		(*clientp)->v.state_export = imap_zlib_state_export;
+
 		str_append(client->capability_string, " COMPRESS=DEFLATE");
 	}