summaryrefslogtreecommitdiffstats
path: root/src/imap-hibernate/imap-client.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/imap-hibernate/imap-client.c799
1 files changed, 799 insertions, 0 deletions
diff --git a/src/imap-hibernate/imap-client.c b/src/imap-hibernate/imap-client.c
new file mode 100644
index 0000000..8e0f933
--- /dev/null
+++ b/src/imap-hibernate/imap-client.c
@@ -0,0 +1,799 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "fdpass.h"
+#include "hostpid.h"
+#include "connection.h"
+#include "iostream.h"
+#include "istream.h"
+#include "ostream.h"
+#include "llist.h"
+#include "priorityq.h"
+#include "base64.h"
+#include "str.h"
+#include "strescape.h"
+#include "time-util.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\n<tag> IDLE\r\n" */
+#define IMAP_MAX_INBUF 12 + 1 + 128 /* DONE\r\nIDLE\r\n + ' ' + <tag> */
+#define IMAP_MAX_OUTBUF 1024
+
+/* If client has sent input and we can't recreate imap process in this
+ many seconds, disconnect the client. */
+#define IMAP_CLIENT_MOVE_BACK_WITH_INPUT_TIMEOUT_SECS 10
+/* If there's a change notification and we can't recreate imap process in this
+ many seconds, disconnect the client. */
+#define IMAP_CLIENT_MOVE_BACK_WITHOUT_INPUT_TIMEOUT_SECS (60*5)
+
+/* How often to try to unhibernate clients. */
+#define IMAP_UNHIBERNATE_RETRY_MSECS 100
+
+#define IMAP_CLIENT_BUFFER_FULL_ERROR "Client output buffer is full"
+#define IMAP_CLIENT_UNHIBERNATE_ERROR "Failed to unhibernate client"
+
+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 priorityq_item item;
+
+ struct imap_client *prev, *next;
+ pool_t pool;
+ struct event *event;
+ struct imap_client_state state;
+ ARRAY(struct imap_client_notify) notifys;
+
+ time_t move_back_start;
+ struct timeout *to_move_back;
+
+ int fd;
+ struct io *io;
+ struct istream *input;
+ struct ostream *output;
+ struct timeout *to_keepalive;
+ struct imap_master_connection *master_conn;
+ struct ioloop_context *ioloop_ctx;
+ const char *log_prefix;
+ unsigned int next_read_threshold;
+ bool bad_done, idle_done;
+ bool unhibernate_queued;
+ bool input_pending;
+};
+
+static struct imap_client *imap_clients;
+static struct priorityq *unhibernate_queue;
+static struct timeout *to_unhibernate;
+static const char imap_still_here_text[] = "* OK Still here\r\n";
+
+static struct event_category event_category_imap = {
+ .name = "imap",
+};
+static struct event_category event_category_imap_hibernate = {
+ .name = "imap-hibernate",
+ .parent = &event_category_imap,
+};
+
+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_clients_unhibernate(void *context);
+static void imap_client_stop_notify_listening(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, client->output);
+ imap_client_destroy(_client, reason);
+}
+
+static void
+imap_client_unhibernate_failed(struct imap_client **_client, const char *error)
+{
+ struct imap_client *client = *_client;
+ struct timeval created;
+ event_get_create_time(client->event, &created);
+
+ struct event_passthrough *e =
+ event_create_passthrough(client->event)->
+ set_name("imap_client_unhibernated")->
+ add_int("hibernation_usecs",
+ timeval_diff_usecs(&ioloop_timeval, &created))->
+ add_str("error", error);
+ e_error(e->event(), IMAP_CLIENT_UNHIBERNATE_ERROR": %s", error);
+ imap_client_destroy(_client, IMAP_CLIENT_UNHIBERNATE_ERROR);
+}
+
+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 (str_begins(field[i], "auth_user="))
+ *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);
+ struct timeval created;
+ const unsigned char *input_data;
+ size_t input_size;
+ ssize_t ret;
+
+ str_append_tabescaped(str, state->username);
+ event_get_create_time(client->event, &created);
+ str_printfa(str, "\thibernation_started=%"PRIdTIME_T".%06u",
+ created.tv_sec, (unsigned int)created.tv_usec);
+
+ if (state->session_id != NULL) {
+ str_append(str, "\tsession=");
+ str_append_tabescaped(str, state->session_id);
+ }
+ if (state->session_created != 0) {
+ str_printfa(str, "\tsession_created=%s",
+ dec2str(state->session_created));
+ }
+ if (state->tag != NULL)
+ str_printfa(str, "\ttag=%s", client->state.tag);
+ if (state->local_ip.family != 0)
+ str_printfa(str, "\tlip=%s", net_ip2addr(&state->local_ip));
+ if (state->local_port != 0)
+ str_printfa(str, "\tlport=%u", state->local_port);
+ if (state->remote_ip.family != 0)
+ str_printfa(str, "\trip=%s", net_ip2addr(&state->remote_ip));
+ if (state->remote_port != 0)
+ str_printfa(str, "\trport=%u", state->remote_port);
+ if (state->userdb_fields != NULL) {
+ str_append(str, "\tuserdb_fields=");
+ str_append_tabescaped(str, state->userdb_fields);
+ }
+ if (major(state->peer_dev) != 0 || minor(state->peer_dev) != 0) {
+ str_printfa(str, "\tpeer_dev_major=%lu\tpeer_dev_minor=%lu",
+ (unsigned long)major(state->peer_dev),
+ (unsigned long)minor(state->peer_dev));
+ }
+ if (state->peer_ino != 0)
+ str_printfa(str, "\tpeer_ino=%llu", (unsigned long long)state->peer_ino);
+ 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);
+ }
+ i_assert(o_stream_get_buffer_used_size(client->output) == 0);
+ 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) {
+ const char *error = t_strdup_printf(
+ "fd_send(%s) failed: %m", o_stream_get_name(output));
+ imap_client_unhibernate_failed(&client, error);
+ 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_unhibernate_failed(&client, line+1);
+ } else {
+ imap_client_destroy(&client, NULL);
+ }
+}
+
+static bool imap_move_has_reached_timeout(struct imap_client *client)
+{
+ int max_secs = client->input_pending ?
+ IMAP_CLIENT_MOVE_BACK_WITH_INPUT_TIMEOUT_SECS :
+ IMAP_CLIENT_MOVE_BACK_WITHOUT_INPUT_TIMEOUT_SECS;
+ return client->move_back_start != 0 &&
+ ioloop_time - client->move_back_start > max_secs;
+}
+
+static bool imap_client_try_move_back(struct imap_client *client)
+{
+ const struct master_service_settings *master_set;
+ const char *path, *error;
+ int ret;
+
+ if (o_stream_get_buffer_used_size(client->output) > 0) {
+ /* there is data buffered, so we have to disconnect you */
+ imap_client_destroy(&client, IMAP_CLIENT_BUFFER_FULL_ERROR);
+ return TRUE;
+ }
+
+ master_set = master_service_settings_get(master_service);
+ path = t_strconcat(master_set->base_dir,
+ "/"IMAP_MASTER_SOCKET_NAME, NULL);
+ ret = imap_master_connection_init(path,
+ imap_client_move_back_send_callback,
+ imap_client_move_back_read_callback,
+ client, &client->master_conn, &error);
+ if (ret > 0) {
+ /* success */
+ imap_client_stop(client);
+ return TRUE;
+ } else if (ret < 0 || imap_move_has_reached_timeout(client)) {
+ /* failed to connect to the imap-master socket */
+ imap_client_unhibernate_failed(&client, error);
+ return TRUE;
+ }
+
+ e_debug(event_create_passthrough(client->event)->
+ set_name("imap_client_unhibernate_retried")->
+ add_str("error", error)->event(),
+ "Unhibernation failed: %s - retrying", error);
+ /* Stop listening for client's IOs while waiting for the next
+ reconnection attempt. However if we got here because of an external
+ notification keep waiting to see if client sends any IO, since that
+ will cause the unhibernation to be aborted earlier. */
+ if (client->input_pending)
+ io_remove(&client->io);
+ imap_client_stop_notify_listening(client);
+ return FALSE;
+}
+
+static void imap_client_move_back(struct imap_client *client)
+{
+ if (imap_client_try_move_back(client))
+ return;
+
+ /* imap-master socket is busy. retry in a while. */
+ if (client->move_back_start == 0)
+ client->move_back_start = ioloop_time;
+ if (!client->unhibernate_queued) {
+ client->unhibernate_queued = TRUE;
+ priorityq_add(unhibernate_queue, &client->item);
+ }
+ if (to_unhibernate == NULL) {
+ to_unhibernate = timeout_add_short(IMAP_UNHIBERNATE_RETRY_MSECS,
+ imap_clients_unhibernate, NULL);
+ }
+}
+
+static enum imap_client_input_state
+imap_client_input_parse(const unsigned char *data, size_t size, const char **tag_r)
+{
+ const unsigned char *tag_start, *tag_end;
+
+ 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--;
+ if (size == 0)
+ return state;
+
+ tag_start = data;
+
+ /* skip over tag */
+ while(data[0] != ' ' &&
+ data[0] != '\r' &&
+ data[0] != '\t' ) { data++; size--; }
+
+ tag_end = data;
+
+ if (size == 0)
+ return state;
+ if (data[0] != ' ')
+ 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--;
+ }
+ if (size == 1 && data[0] == '\n') {
+ *tag_r = t_strdup_until(tag_start, tag_end);
+ return IMAP_CLIENT_INPUT_STATE_DONEIDLE;
+ }
+ return state;
+}
+
+static void imap_client_input_idle_cmd(struct imap_client *client)
+{
+ char *old_tag;
+ const char *new_tag;
+ const char *output;
+ const unsigned char *data;
+ size_t size;
+ bool done = TRUE;
+ 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_bytes(client->input, &data, &size,
+ client->next_read_threshold + 1);
+ if (size == 0) {
+ if (ret < 0)
+ imap_client_disconnected(&client);
+ return;
+ }
+ client->next_read_threshold = 0;
+ switch (imap_client_input_parse(data, size, &new_tag)) {
+ 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. */
+ old_tag = client->state.tag;
+ client->state.tag = i_strdup(new_tag);
+ output = t_strdup_printf("%s OK Idle completed.\r\n+ idling\r\n", old_tag);
+ i_free(old_tag);
+ ret = o_stream_flush(client->output);
+ if (ret > 0)
+ ret = o_stream_send_str(client->output, output);
+ if (ret < 0) {
+ imap_client_disconnected(&client);
+ return;
+ }
+ if ((size_t)ret != strlen(output)) {
+ /* disconnect */
+ imap_client_destroy(&client, IMAP_CLIENT_BUFFER_FULL_ERROR);
+ return;
+ } else {
+ done = FALSE;
+ i_stream_skip(client->input, size);
+ }
+ break;
+ }
+
+ if (done) {
+ client->idle_done = TRUE;
+ client->input_pending = TRUE;
+ imap_client_move_back(client);
+ } else
+ imap_client_add_idle_keepalive_timeout(client);
+}
+
+static void imap_client_input_nonidle(struct imap_client *client)
+{
+ if (i_stream_read(client->input) < 0)
+ imap_client_disconnected(&client);
+ else {
+ client->input_pending = TRUE;
+ 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)
+{
+ ssize_t ret;
+
+ /* do not send this if there is data buffered */
+ if ((ret = o_stream_flush(client->output)) < 0) {
+ imap_client_disconnected(&client);
+ return;
+ } else if (ret == 0)
+ return;
+
+ ret = o_stream_send_str(client->output, imap_still_here_text);
+ if (ret < 0) {
+ imap_client_disconnected(&client);
+ return;
+ }
+ /* ostream buffer size is definitely large enough for this text */
+ i_assert((size_t)ret == strlen(imap_still_here_text));
+ 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);
+
+ 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)
+{
+ const char *username = t_strcut(client->state.username, '@');
+ const char *domain = i_strchr_to_next(client->state.username, '@');
+ const char *local_ip = client->state.local_ip.family == 0 ? NULL :
+ net_ip2addr(&client->state.local_ip);
+ const char *remote_ip = client->state.remote_ip.family == 0 ? NULL :
+ net_ip2addr(&client->state.remote_ip);
+
+ const char *auth_user, *auth_username, *auth_domain;
+ imap_client_parse_userdb_fields(client, &auth_user);
+ if (auth_user == NULL) {
+ auth_user = client->state.username;
+ auth_username = username;
+ auth_domain = domain;
+ } else {
+ auth_username = t_strcut(auth_user, '@');
+ auth_domain = i_strchr_to_next(auth_user, '@');
+ }
+
+ const struct var_expand_table stack_tab[] = {
+ { 'u', client->state.username, "user" },
+ { 'n', username, "username" },
+ { 'd', domain, "domain" },
+ { 's', "imap-hibernate", "service" },
+ { 'h', NULL /* we shouldn't need this */, "home" },
+ { 'l', local_ip, "lip" },
+ { 'r', remote_ip, "rip" },
+ { 'p', my_pid, "pid" },
+ { 'i', dec2str(client->state.uid), "uid" },
+ { '\0', dec2str(client->state.gid), "gid" },
+ { '\0', client->state.session_id, "session" },
+ { '\0', auth_user, "auth_user" },
+ { '\0', auth_username, "auth_username" },
+ { '\0', auth_domain, "auth_domain" },
+ /* aliases: */
+ { '\0', local_ip, "local_ip" },
+ { '\0', remote_ip, "remote_ip" },
+ /* NOTE: keep this synced with lib-storage's
+ mail_user_var_expand_table() */
+ { '\0', NULL, NULL }
+ };
+ struct var_expand_table *tab;
+
+ tab = t_malloc_no0(sizeof(stack_tab));
+ memcpy(tab, stack_tab, sizeof(stack_tab));
+ return tab;
+}
+
+static int
+imap_client_var_expand_func_userdb(const char *data, void *context,
+ const char **value_r, const char **error_r ATTR_UNUSED)
+{
+ const char *const *fields = context;
+ const char *field_name = t_strdup_printf("%s=",t_strcut(data, ':'));
+ const char *default_value = i_strchr_to_next(data, ':');
+ const char *value = NULL;
+
+ for(;*fields != NULL; fields++) {
+ if (str_begins(*fields, field_name)) {
+ value = *fields+strlen(field_name);
+ break;
+ }
+ }
+
+ *value_r = value != NULL ? value : default_value;
+
+ return 1;
+}
+
+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: ");
+}
+
+static const char *imap_client_get_anvil_userip_ident(struct imap_client_state *state)
+{
+ if (state->remote_ip.family == 0)
+ return NULL;
+ return t_strconcat(net_ip2addr(&state->remote_ip), "/",
+ str_tabescape(state->username), NULL);
+}
+
+struct imap_client *
+imap_client_create(int fd, const struct imap_client_state *state)
+{
+ const struct var_expand_func_table funcs[] = {
+ { "userdb", imap_client_var_expand_func_userdb },
+ { NULL, NULL }
+ };
+ struct imap_client *client;
+ pool_t pool = pool_alloconly_create("imap client", 256);
+ void *statebuf;
+ const char *ident, *error;
+
+ 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);
+ client->output = o_stream_create_fd(fd, IMAP_MAX_OUTBUF);
+ client->state = *state;
+ 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->event = event_create(NULL);
+ event_add_category(client->event, &event_category_imap_hibernate);
+ event_add_str(client->event, "user", state->username);
+ event_add_str(client->event, "session", state->session_id);
+ if (state->mailbox_vname != NULL)
+ event_add_str(client->event, "mailbox", state->mailbox_vname);
+ if (state->local_ip.family != 0)
+ event_add_str(client->event, "local_ip",
+ net_ip2addr(&state->local_ip));
+ if (state->local_port != 0)
+ event_add_int(client->event, "local_port", state->local_port);
+ if (state->remote_ip.family != 0)
+ event_add_str(client->event, "remote_ip",
+ net_ip2addr(&state->remote_ip));
+ if (state->remote_port != 0)
+ event_add_int(client->event, "remote_port", state->remote_port);
+
+ 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;
+ char **fields = p_strsplit_tabescaped(unsafe_data_stack_pool,
+ client->state.userdb_fields);
+ str = t_str_new(256);
+ if (var_expand_with_funcs(str, state->mail_log_prefix,
+ imap_client_get_var_expand_table(client),
+ funcs, fields, &error) <= 0) {
+ e_error(client->event,
+ "Failed to expand mail_log_prefix=%s: %s",
+ state->mail_log_prefix, error);
+ }
+ client->log_prefix = p_strdup(pool, str_c(str));
+ } T_END;
+
+ ident = imap_client_get_anvil_userip_ident(&client->state);
+ if (ident != NULL) {
+ master_service_anvil_send(master_service, t_strconcat(
+ "CONNECT\t", my_pid, "\timap/", ident, "\n", NULL));
+ client->state.anvil_sent = TRUE;
+ }
+
+ p_array_init(&client->notifys, pool, 2);
+ DLLIST_PREPEND(&imap_clients, client);
+ return client;
+}
+
+static void imap_client_stop_notify_listening(struct imap_client *client)
+{
+ struct imap_client_notify *notify;
+
+ array_foreach_modifiable(&client->notifys, notify) {
+ io_remove(&notify->io);
+ i_close_fd(&notify->fd);
+ }
+}
+
+static void imap_client_stop(struct imap_client *client)
+{
+ if (client->unhibernate_queued) {
+ priorityq_remove(unhibernate_queue, &client->item);
+ client->unhibernate_queued = FALSE;
+ }
+ io_remove(&client->io);
+ timeout_remove(&client->to_keepalive);
+ imap_client_stop_notify_listening(client);
+}
+
+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. */
+ e_info(client->event, "Disconnected: %s %s",
+ reason, client->state.stats);
+ }
+
+ if (client->state.anvil_sent) {
+ master_service_anvil_send(master_service, t_strconcat(
+ "DISCONNECT\t", my_pid, "\timap/",
+ imap_client_get_anvil_userip_ident(&client->state),
+ "\n", NULL));
+ }
+
+ if (client->master_conn != NULL)
+ imap_master_connection_free(&client->master_conn);
+ 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);
+ }
+
+ if (client->state.tag != NULL)
+ i_free(client->state.tag);
+
+ DLLIST_REMOVE(&imap_clients, client);
+ imap_client_stop(client);
+ i_stream_destroy(&client->input);
+ o_stream_destroy(&client->output);
+ i_close_fd(&client->fd);
+ event_unref(&client->event);
+ 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);
+ io_loop_context_switch(client->ioloop_ctx);
+
+ 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);
+ }
+}
+
+static int client_unhibernate_cmp(const void *p1, const void *p2)
+{
+ const struct imap_client *c1 = p1, *c2 = p2;
+ time_t t1, t2;
+
+ t1 = c1->move_back_start +
+ (c1->input_pending ?
+ IMAP_CLIENT_MOVE_BACK_WITH_INPUT_TIMEOUT_SECS :
+ IMAP_CLIENT_MOVE_BACK_WITHOUT_INPUT_TIMEOUT_SECS);
+ t2 = c2->move_back_start +
+ (c2->input_pending ?
+ IMAP_CLIENT_MOVE_BACK_WITH_INPUT_TIMEOUT_SECS :
+ IMAP_CLIENT_MOVE_BACK_WITHOUT_INPUT_TIMEOUT_SECS);
+ if (t1 < t2)
+ return -1;
+ if (t1 > t2)
+ return 1;
+ return 0;
+}
+
+static void imap_clients_unhibernate(void *context ATTR_UNUSED)
+{
+ struct priorityq_item *item;
+
+ while ((item = priorityq_peek(unhibernate_queue)) != NULL) {
+ struct imap_client *client = (struct imap_client *)item;
+
+ if (!imap_client_try_move_back(client))
+ return;
+ }
+ timeout_remove(&to_unhibernate);
+}
+
+void imap_clients_init(void)
+{
+ unhibernate_queue = priorityq_init(client_unhibernate_cmp, 64);
+}
+
+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");
+ }
+ timeout_remove(&to_unhibernate);
+ priorityq_deinit(&unhibernate_queue);
+}