/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "net.h"
#include "istream.h"
#include "ostream.h"
#include "base64.h"
#include "write-full.h"
#include "str.h"
#include "time-util.h"
#include "dns-lookup.h"
#include "dsasl-client.h"
#include "iostream-rawlog.h"
#include "iostream-ssl.h"
#include "imap-quote.h"
#include "imap-util.h"
#include "imap-parser.h"
#include "imapc-client-private.h"
#include "imapc-connection.h"
#include "settings.h"

#include <unistd.h>
#include <ctype.h>

#define IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE 10000
#define IMAPC_MAX_INLINE_LITERAL_SIZE (1024*32)
/* If LOGOUT reply takes longer than this, disconnect. */
#define IMAPC_LOGOUT_TIMEOUT_MSECS 5000

enum imapc_input_state {
	IMAPC_INPUT_STATE_NONE = 0,
	IMAPC_INPUT_STATE_PLUS,
	IMAPC_INPUT_STATE_UNTAGGED,
	IMAPC_INPUT_STATE_UNTAGGED_NUM,
	IMAPC_INPUT_STATE_TAGGED
};

struct imapc_command_stream {
	unsigned int pos;
	uoff_t size;
	struct istream *input;
};

struct imapc_command {
	pool_t pool;
	buffer_t *data;
	unsigned int send_pos;
	unsigned int tag;

	enum imapc_command_flags flags;
	struct imapc_connection *conn;
	/* If non-NULL, points to the mailbox where this command should be
	   executed */
	struct imapc_client_mailbox *box;

	ARRAY(struct imapc_command_stream) streams;

	imapc_command_callback_t *callback;
	void *context;

	struct timeval start_time;

	/* This is the AUTHENTICATE command */
	bool authenticate:1;
	/* This is the IDLE command */
	bool idle:1;
	/* Waiting for '+' literal reply before we can continue */
	bool wait_for_literal:1;
	/* Command is fully sent to server */
	bool sent:1;
};
ARRAY_DEFINE_TYPE(imapc_command, struct imapc_command *);

struct imapc_connection_literal {
	char *temp_path;
	int fd;
	uoff_t bytes_left;

	const struct imap_arg *parent_arg;
	unsigned int list_idx;
};

struct imapc_connection {
	struct imapc_client *client;
	struct event *event;
	int refcount;

	int fd;
	struct io *io;
	struct istream *input, *raw_input;
	struct ostream *output, *raw_output;
	struct imap_parser *parser;
	struct timeout *to;
	struct timeout *to_output;
	struct dns_lookup *dns_lookup;
	struct dsasl_client *sasl_client;

	struct ssl_iostream *ssl_iostream;

	int (*input_callback)(struct imapc_connection *conn);
	enum imapc_input_state input_state;
	unsigned int cur_tag;
	uint32_t cur_num;
	struct timeval last_connect;
	unsigned int reconnect_count;

	/* If QRESYNC isn't used, this is set immediately after issuing
	   SELECT/EXAMINE. We could differentiate better whether a mailbox is
	   "being selected" vs "fully selected", but that code is already in
	   the imapc-storage side so it would have to be moved or duplicated
	   here. And since nothing actually cares about this distinction (yet),
	   don't bother with it for now. This is set to NULL when the mailbox
	   is closed from imapc-storage point of view, even if the server is
	   still in selected state (see selected_on_server). */
	struct imapc_client_mailbox *selected_box;
	/* If QRESYNC is used, this is set when SELECT/EXAMINE is issued.
	   If the server is already in selected state, the selected_box is most
	   likely already NULL at this point, because imapc-storage has closed
	   it. */
	struct imapc_client_mailbox *qresync_selecting_box;
	enum imapc_connection_state state;
	char *disconnect_reason;

	enum imapc_capability capabilities;
	char **capabilities_list;

	imapc_command_callback_t *login_callback;
	void *login_context;

	/* commands pending in queue to be sent */
	ARRAY_TYPE(imapc_command) cmd_send_queue;
	/* commands that have been sent, waiting for their tagged reply */
	ARRAY_TYPE(imapc_command) cmd_wait_list;
	/* commands that were already sent, but were aborted since (due to
	   unselecting mailbox). */
	ARRAY_TYPE(seq_range) aborted_cmd_tags;
	unsigned int reconnect_command_count;

	unsigned int ips_count, prev_connect_idx;
	struct ip_addr *ips;

	struct imapc_connection_literal literal;
	ARRAY(struct imapc_arg_file) literal_files;

	unsigned int throttle_msecs;
	unsigned int throttle_shrink_msecs;
	unsigned int last_successful_throttle_msecs;
	bool throttle_pending;
	struct timeval throttle_end_timeval;
	struct timeout *to_throttle, *to_throttle_shrink;

	bool reconnecting:1;
	bool reconnect_waiting:1;
	bool reconnect_ok:1;
	bool idling:1;
	bool idle_stopping:1;
	bool idle_plus_waiting:1;
	bool imap4rev2_enabled:1;
	bool select_waiting_reply:1;
	/* The name of the folder if in SELECTED state. select_box may be NULL
	   though, if we already closed the mailbox from client point of
	   view. */
	char *selected_on_server;
};

static void imapc_connection_capability_cb(const struct imapc_command_reply *reply,
					   void *context);
static int imapc_connection_output(struct imapc_connection *conn);
static int imapc_connection_ssl_init(struct imapc_connection *conn);
static void imapc_command_free(struct imapc_command *cmd);
static void imapc_command_send_more(struct imapc_connection *conn);
static void
imapc_login_callback(struct imapc_connection *conn,
		     const struct imapc_command_reply *reply);

static void
imapc_auth_ok(struct imapc_connection *conn)
{
	e_debug(conn->event, "Authenticated successfully");

	if (conn->client->state_change_callback == NULL)
		return;

	conn->client->state_change_callback(conn->client->state_change_context,
					    IMAPC_STATE_CHANGE_AUTH_OK, NULL);
}

static void
imapc_auth_failed(struct imapc_connection *conn, const struct imapc_command_reply *_reply,
		  const char *error)
{
	struct imapc_command_reply reply = *_reply;

	reply.text_without_resp = reply.text_full =
		t_strdup_printf("Authentication failed: %s", error);
	if (reply.state != IMAPC_COMMAND_STATE_DISCONNECTED) {
		reply.state = IMAPC_COMMAND_STATE_AUTH_FAILED;
		e_error(conn->event, "%s", reply.text_full);
	}
	imapc_login_callback(conn, &reply);

	if (conn->client->state_change_callback == NULL)
		return;

	conn->client->state_change_callback(conn->client->state_change_context,
					    IMAPC_STATE_CHANGE_AUTH_FAILED,
					    error);
}

struct imapc_connection *
imapc_connection_init(struct imapc_client *client,
		      imapc_command_callback_t *login_callback,
		      void *login_context)
{
	struct imapc_connection *conn;

	conn = i_new(struct imapc_connection, 1);
	conn->event = event_create(client->event);
	conn->refcount = 1;
	conn->client = client;
	conn->login_callback = login_callback;
	conn->login_context = login_context;
	conn->fd = -1;
	conn->literal.fd = -1;
	conn->reconnect_ok = (client->set->imapc_connection_retry_count>0);
	i_array_init(&conn->cmd_send_queue, 8);
	i_array_init(&conn->cmd_wait_list, 32);
	i_array_init(&conn->literal_files, 4);
	i_array_init(&conn->aborted_cmd_tags, 8);

	e_debug(conn->event, "Created new connection");

	imapc_client_ref(client);
	return conn;
}

static void imapc_connection_ref(struct imapc_connection *conn)
{
	i_assert(conn->refcount > 0);

	conn->refcount++;
}

static void imapc_connection_unref(struct imapc_connection **_conn)
{
	struct imapc_connection *conn = *_conn;

	i_assert(conn->refcount > 0);

	*_conn = NULL;
	if (--conn->refcount > 0)
		return;

	i_assert(conn->disconnect_reason == NULL);

	if (conn->capabilities_list != NULL)
		p_strsplit_free(default_pool, conn->capabilities_list);
	array_free(&conn->cmd_send_queue);
	array_free(&conn->cmd_wait_list);
	array_free(&conn->literal_files);
	array_free(&conn->aborted_cmd_tags);
	imapc_client_unref(&conn->client);
	event_unref(&conn->event);
	i_free(conn->selected_on_server);
	i_free(conn->ips);
	i_free(conn);
}

void imapc_connection_deinit(struct imapc_connection **_conn)
{
	imapc_connection_disconnect(*_conn);
	imapc_connection_unref(_conn);
}

void imapc_connection_ioloop_changed(struct imapc_connection *conn)
{
	if (conn->io != NULL)
		conn->io = io_loop_move_io(&conn->io);
	if (conn->to != NULL)
		conn->to = io_loop_move_timeout(&conn->to);
	if (conn->to_throttle != NULL)
		conn->to_throttle = io_loop_move_timeout(&conn->to_throttle);
	if (conn->to_throttle_shrink != NULL)
		conn->to_throttle_shrink = io_loop_move_timeout(&conn->to_throttle_shrink);
	if (conn->output != NULL)
		o_stream_switch_ioloop(conn->output);
	if (conn->dns_lookup != NULL)
		dns_lookup_switch_ioloop(conn->dns_lookup);

	if (conn->client->ioloop == NULL && conn->to_output != NULL) {
		/* we're only once moving the to_output to the main ioloop,
		   since timeout moves currently also reset the timeout.
		   (the rest of the times this is a no-op) */
		conn->to_output = io_loop_move_timeout(&conn->to_output);
	}
}

static const char *imapc_command_get_readable(struct imapc_command *cmd)
{
	string_t *str = t_str_new(256);
	const unsigned char *data = cmd->data->data;
	unsigned int i;

	for (i = 0; i < cmd->data->used; i++) {
		if (data[i] != '\r' && data[i] != '\n')
			str_append_c(str, data[i]);
	}
	return str_c(str);
}

static void
imapc_connection_abort_commands_array(ARRAY_TYPE(imapc_command) *cmd_array,
				      ARRAY_TYPE(imapc_command) *dest_array,
				      struct imapc_client_mailbox *only_box,
				      bool keep_retriable)
{
	struct imapc_command *cmd;
	unsigned int i;

	for (i = 0; i < array_count(cmd_array); ) {
		cmd = array_idx_elem(cmd_array, i);

		if (cmd->box != only_box && only_box != NULL)
			i++;
		else if (keep_retriable &&
			 (cmd->flags & IMAPC_COMMAND_FLAG_RETRIABLE) != 0) {
			cmd->send_pos = 0;
			cmd->wait_for_literal = 0;
			cmd->flags |= IMAPC_COMMAND_FLAG_RECONNECTED;
			i++;
		} else {
			array_delete(cmd_array, i, 1);
			array_push_back(dest_array, &cmd);
		}
	}
}

void imapc_connection_abort_commands(struct imapc_connection *conn,
				     struct imapc_client_mailbox *only_box,
				     bool keep_retriable)
{
	struct imapc_command *cmd;
	ARRAY_TYPE(imapc_command) tmp_array;
	struct imapc_command_reply reply;

	t_array_init(&tmp_array, 8);
	imapc_connection_abort_commands_array(&conn->cmd_wait_list, &tmp_array,
					      only_box, keep_retriable);
	imapc_connection_abort_commands_array(&conn->cmd_send_queue, &tmp_array,
					      only_box, keep_retriable);

	if (array_count(&conn->cmd_wait_list) > 0 && only_box == NULL) {
		/* need to move all the waiting commands to send queue */
		array_append_array(&conn->cmd_wait_list,
				   &conn->cmd_send_queue);
		array_clear(&conn->cmd_send_queue);
		array_append_array(&conn->cmd_send_queue,
				   &conn->cmd_wait_list);
		array_clear(&conn->cmd_wait_list);
	}

	/* abort the commands. we'll do it here later so that if the
	   callback recurses us back here we don't crash */
	i_zero(&reply);
	reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
	if (only_box != NULL) {
		reply.text_without_resp = reply.text_full =
			"Unselecting mailbox";
	} else {
		reply.text_without_resp = reply.text_full =
			"Disconnected from server";
	}
	array_foreach_elem(&tmp_array, cmd) {
		if (cmd->sent && conn->state == IMAPC_CONNECTION_STATE_DONE) {
			/* We're not disconnected, so the reply will still
			   come. Remember that it needs to be ignored. */
			seq_range_array_add(&conn->aborted_cmd_tags, cmd->tag);
		}
		cmd->callback(&reply, cmd->context);
		imapc_command_free(cmd);
	}
	if (array_count(&conn->cmd_wait_list) == 0)
		timeout_remove(&conn->to);
}

static void
imapc_login_callback(struct imapc_connection *conn,
		     const struct imapc_command_reply *reply)
{
	if (conn->login_callback != NULL)
		conn->login_callback(reply, conn->login_context);
}

static void imapc_connection_set_state(struct imapc_connection *conn,
				       enum imapc_connection_state state)
{
	struct imapc_command_reply reply;

	conn->state = state;

	switch (state) {
	case IMAPC_CONNECTION_STATE_DISCONNECTED:
		i_zero(&reply);
		reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
		reply.text_full = "Disconnected from server";
		if (conn->disconnect_reason != NULL) {
			reply.text_full = t_strdup_printf("%s: %s",
				reply.text_full, conn->disconnect_reason);
			i_free_and_null(conn->disconnect_reason);
		}
		reply.text_without_resp = reply.text_full;
		if (!conn->reconnecting) {
			imapc_login_callback(conn, &reply);
			i_free(conn->ips);
			conn->ips_count = 0;
		}
		array_clear(&conn->aborted_cmd_tags);
		conn->idling = FALSE;
		conn->idle_plus_waiting = FALSE;
		conn->idle_stopping = FALSE;

		conn->select_waiting_reply = FALSE;
		conn->qresync_selecting_box = NULL;
		conn->selected_box = NULL;
		i_free_and_null(conn->selected_on_server);
		/* fall through */
	case IMAPC_CONNECTION_STATE_DONE:
		/* if we came from imapc_client_get_capabilities(), stop so
		   it can finish up and not just hang indefinitely. */
		if (conn->client->stop_on_state_finish && !conn->reconnecting)
			imapc_client_stop(conn->client);
		break;
	default:
		break;
	}
}

static void imapc_connection_lfiles_free(struct imapc_connection *conn)
{
	struct imapc_arg_file *lfile;

	array_foreach_modifiable(&conn->literal_files, lfile) {
		if (close(lfile->fd) < 0)
			e_error(conn->event, "imapc: close(literal file) failed: %m");
	}
	array_clear(&conn->literal_files);
}

static void
imapc_connection_literal_reset(struct imapc_connection_literal *literal)
{
	i_close_fd_path(&literal->fd, literal->temp_path);
	i_free_and_null(literal->temp_path);

	i_zero(literal);
	literal->fd = -1;
}

void imapc_connection_disconnect_full(struct imapc_connection *conn,
				      bool reconnecting)
{
	/* timeout may be set also in disconnected state */
	timeout_remove(&conn->to);
	conn->reconnecting = reconnecting;

	if (conn->state == IMAPC_CONNECTION_STATE_DISCONNECTED) {
		i_assert(array_count(&conn->cmd_wait_list) == 0);
		if (conn->reconnect_command_count == 0)
			imapc_connection_abort_commands(conn, NULL,
						reconnecting);
		return;
	}

	e_debug(conn->event, "Disconnected");

	if (conn->dns_lookup != NULL)
		dns_lookup_abort(&conn->dns_lookup);
	imapc_connection_lfiles_free(conn);
	imapc_connection_literal_reset(&conn->literal);
	timeout_remove(&conn->to_output);
	timeout_remove(&conn->to_throttle);
	timeout_remove(&conn->to_throttle_shrink);
	if (conn->parser != NULL)
		imap_parser_unref(&conn->parser);
	io_remove(&conn->io);
	ssl_iostream_destroy(&conn->ssl_iostream);
	if (conn->fd != -1) {
		i_stream_destroy(&conn->input);
		o_stream_destroy(&conn->output);
		net_disconnect(conn->fd);
		conn->fd = -1;
	}

	/* get capabilities again after reconnection. this is especially
	   important because post-login capabilities often do not contain AUTH=
	   capabilities. */
	conn->capabilities = 0;
	if (conn->capabilities_list != NULL) {
		p_strsplit_free(default_pool, conn->capabilities_list);
		conn->capabilities_list = NULL;
	}

	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DISCONNECTED);
	imapc_connection_abort_commands(conn, NULL, reconnecting);

	if (!reconnecting) {
		imapc_client_try_stop(conn->client);
	}
}

void imapc_connection_set_no_reconnect(struct imapc_connection *conn)
{
	conn->reconnect_ok = FALSE;
}

void imapc_connection_disconnect(struct imapc_connection *conn)
{
	imapc_connection_disconnect_full(conn, FALSE);
}

static void imapc_connection_set_disconnected(struct imapc_connection *conn)
{
	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DISCONNECTED);
	imapc_connection_abort_commands(conn, NULL, FALSE);
}

static bool imapc_connection_can_reconnect(struct imapc_connection *conn)
{
	if (conn->client->logging_out)
		return FALSE;
	if (conn->client->set->imapc_connection_retry_count == 0 ||
	    (conn->client->set->imapc_connection_retry_count < UINT_MAX &&
	     conn->reconnect_count >= conn->client->set->imapc_connection_retry_count))
		return FALSE;

	if (conn->selected_box != NULL)
		return imapc_client_mailbox_can_reconnect(conn->selected_box);
	else {
		return conn->reconnect_command_count == 0 &&
			conn->reconnect_ok;
	}
}

static void imapc_connection_reconnect(struct imapc_connection *conn)
{
	conn->reconnect_ok = FALSE;
	conn->reconnect_waiting = FALSE;

	if (conn->selected_box != NULL) {
		i_assert(!conn->selected_box->reconnecting);
		conn->selected_box->reconnecting = TRUE;
		/* if we fail again, avoid reconnecting immediately. if the
		   server is broken we could just get into an infinitely
		   failing reconnection loop. */
		conn->selected_box->reconnect_ok = FALSE;
	}
	imapc_connection_disconnect_full(conn, TRUE);
	imapc_connection_connect(conn);
}

void imapc_connection_try_reconnect(struct imapc_connection *conn,
				    const char *errstr,
				    unsigned int delay_msecs,
				    bool connect_error)
{
	/* Try the next IP address only for connect() problems. */
	if (conn->prev_connect_idx + 1 < conn->ips_count && connect_error) {
		e_warning(conn->event, "%s - trying the next IP", errstr);
		conn->reconnect_ok = TRUE;
		imapc_connection_disconnect_full(conn, TRUE);
		imapc_connection_connect(conn);
		return;
	}

	if (!imapc_connection_can_reconnect(conn)) {
		e_error(conn->event, "%s - disconnecting", errstr);
		imapc_connection_disconnect(conn);
	} else {
		conn->reconnecting = TRUE;
		conn->reconnect_count++;
		e_warning(conn->event, "%s - reconnecting (delay %u ms)",
			  errstr, delay_msecs);
		if (delay_msecs == 0)
			imapc_connection_reconnect(conn);
		else {
			imapc_connection_disconnect_full(conn, TRUE);
			conn->to = timeout_add(delay_msecs, imapc_connection_reconnect, conn);
			conn->reconnect_waiting = TRUE;
		}
	}
}

static void ATTR_FORMAT(2, 3)
imapc_connection_input_error(struct imapc_connection *conn,
			     const char *fmt, ...)
{
	va_list va;

	va_start(va, fmt);
	e_error(conn->event, "Server sent invalid input: %s",
		t_strdup_vprintf(fmt, va));
	imapc_connection_disconnect(conn);
	va_end(va);
}

static bool last_arg_is_fetch_body(const struct imap_arg *args,
				   const struct imap_arg **parent_arg_r,
				   unsigned int *idx_r)
{
	const struct imap_arg *list;
	const char *name;
	unsigned int count;

	if (args[0].type == IMAP_ARG_ATOM &&
	    imap_arg_atom_equals(&args[1], "FETCH") &&
	    imap_arg_get_list_full(&args[2], &list, &count) && count >= 2 &&
	    list[count].type == IMAP_ARG_LITERAL_SIZE &&
	    imap_arg_get_atom(&list[count-1], &name) &&
	    str_begins_icase_with(name, "BODY[")) {
		*parent_arg_r = &args[2];
		*idx_r = count;
		return TRUE;
	}
	return FALSE;
}

static int
imapc_connection_read_literal_init(struct imapc_connection *conn, uoff_t size,
				   const struct imap_arg *args)
{
	const char *path;
	const struct imap_arg *parent_arg;
	unsigned int idx;

	i_assert(conn->literal.fd == -1);

	if (size <= IMAPC_MAX_INLINE_LITERAL_SIZE ||
	    !last_arg_is_fetch_body(args, &parent_arg, &idx)) {
		/* read the literal directly into parser */
		return 0;
	}

	conn->literal.fd = imapc_client_create_temp_fd(conn->client, &path);
	if (conn->literal.fd == -1)
		return -1;
	conn->literal.temp_path = i_strdup(path);
	conn->literal.bytes_left = size;
	conn->literal.parent_arg = parent_arg;
	conn->literal.list_idx = idx;
	return 1;
}

static int imapc_connection_read_literal(struct imapc_connection *conn)
{
	struct imapc_arg_file *lfile;
	const unsigned char *data;
	size_t size;

	if (conn->literal.bytes_left == 0)
		return 1;

	data = i_stream_get_data(conn->input, &size);
	if (size > conn->literal.bytes_left)
		size = conn->literal.bytes_left;
	if (size > 0) {
		if (write_full(conn->literal.fd, data, size) < 0) {
			e_error(conn->event, "write(%s) failed: %m",
				conn->literal.temp_path);
			imapc_connection_disconnect(conn);
			return -1;
		}
		i_stream_skip(conn->input, size);
		conn->literal.bytes_left -= size;
	}
	if (conn->literal.bytes_left > 0)
		return 0;

	/* finished */
	lfile = array_append_space(&conn->literal_files);
	lfile->fd = conn->literal.fd;
	lfile->parent_arg = conn->literal.parent_arg;
	lfile->list_idx = conn->literal.list_idx;

	conn->literal.fd = -1;
	imapc_connection_literal_reset(&conn->literal);
	return 1;
}

static int
imapc_connection_read_line_more(struct imapc_connection *conn,
				const struct imap_arg **imap_args_r)
{
	uoff_t literal_size;
	int ret;

	if ((ret = imapc_connection_read_literal(conn)) <= 0)
		return ret;

	ret = imap_parser_read_args(conn->parser, 0,
				    IMAP_PARSE_FLAG_LITERAL_SIZE |
				    IMAP_PARSE_FLAG_ATOM_ALLCHARS |
				    IMAP_PARSE_FLAG_LITERAL8 |
				    IMAP_PARSE_FLAG_SERVER_TEXT, imap_args_r);
	if (ret == -2) {
		/* need more data */
		return 0;
	}
	if (ret < 0) {
		enum imap_parser_error parser_error;
		const char *err_msg = imap_parser_get_error(conn->parser, &parser_error);
		if (parser_error != IMAP_PARSE_ERROR_BAD_SYNTAX)
			imapc_connection_input_error(conn, "Error parsing input: %s", err_msg);
		else
			e_error(conn->event, "Error parsing input: %s", err_msg);
		return -1;
	}

	if (imap_parser_get_literal_size(conn->parser, &literal_size)) {
		if (imapc_connection_read_literal_init(conn, literal_size,
						       *imap_args_r) <= 0) {
			imap_parser_read_last_literal(conn->parser);
			return 2;
		}
		return imapc_connection_read_line_more(conn, imap_args_r);
	}
	return 1;
}

static int
imapc_connection_read_line(struct imapc_connection *conn,
			   const struct imap_arg **imap_args_r)
{
	const unsigned char *data;
	size_t size;
	int ret;

	while ((ret = imapc_connection_read_line_more(conn, imap_args_r)) == 2)
		;

	if (ret > 0) {
		data = i_stream_get_data(conn->input, &size);
		if (size >= 2 && data[0] == '\r' && data[1] == '\n')
			i_stream_skip(conn->input, 2);
		else if (size >= 1 && data[0] == '\n')
			i_stream_skip(conn->input, 1);
		else
			i_panic("imapc: Missing LF from input line");
	} else if (ret < 0) {
		data = i_stream_get_data(conn->input, &size);
		unsigned char *lf = memchr(data, '\n', size);
		if (lf != NULL)
			i_stream_skip(conn->input, (lf - data) + 1);
	}
	return ret;
}

static void
imapc_connection_imap4rev2_enable_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
					   void *context)
{
	struct imapc_connection *conn = context;

	if (reply->state == IMAPC_COMMAND_STATE_OK)
		conn->imap4rev2_enabled = TRUE;
}

static void imapc_connection_send_enable_imap4rev2(struct imapc_connection *conn)
{
	struct imapc_command *cmd;

	if ((conn->capabilities & IMAPC_CAPABILITY_IMAP4REV2) == 0 ||
	     conn->imap4rev2_enabled)
		return;

	cmd = imapc_connection_cmd(conn,
				   imapc_connection_imap4rev2_enable_callback,
				   conn);
	imapc_command_send(cmd, "ENABLE IMAP4REV2");
}

static int
imapc_connection_parse_capability(struct imapc_connection *conn,
				  const char *value)
{
	const char *const *tmp;

	e_debug(conn->event, "Server capabilities: %s", value);

	conn->capabilities = 0;
	if (conn->capabilities_list != NULL)
		p_strsplit_free(default_pool, conn->capabilities_list);
	conn->capabilities_list = p_strsplit(default_pool, value, " ");

	for (tmp = t_strsplit(value, " "); *tmp != NULL; tmp++)
		conn->capabilities |= imapc_capability_lookup(*tmp);

	if ((conn->capabilities & IMAPC_CAPABILITY_IMAP4REV1) == 0) {
		imapc_connection_input_error(conn,
			"CAPABILITY list is missing IMAP4REV1");
		return -1;
	}
	conn->capabilities &= ENUM_NEGATE(conn->client->set->parsed_disabled_capabilities);
#ifndef EXPERIMENTAL_IMAP4REV2
	e_debug(conn->event,
		"Disable IMAP4REV2 capability, as it is not supported with this build");
	conn->capabilities &= ENUM_NEGATE(IMAPC_CAPABILITY_IMAP4REV2);
#endif

	return 0;
}

static int
imapc_connection_handle_resp_text_code(struct imapc_connection *conn,
				       const char *key, const char *value)
{
	if (strcasecmp(key, "CAPABILITY") == 0) {
		if (imapc_connection_parse_capability(conn, value) < 0)
			return -1;
	}
	if (strcasecmp(key, "CLOSED") == 0) {
		/* QRESYNC: SELECTing another mailbox */
		if (conn->qresync_selecting_box != NULL) {
			conn->selected_box = conn->qresync_selecting_box;
			conn->qresync_selecting_box = NULL;
		} else {
			i_free_and_null(conn->selected_on_server);
		}
	}
	return 0;
}

static int
imapc_connection_handle_resp_text(struct imapc_connection *conn,
				  const char *text,
				  const char **key_r, const char **value_r)
{
	const char *p, *value;

	i_assert(text[0] == '[');

	p = strchr(text, ']');
	if (p == NULL) {
		imapc_connection_input_error(conn, "Missing ']' in resp-text");
		return -1;
	}
	text = t_strdup_until(text + 1, p);
	value = strchr(text, ' ');
	if (value != NULL) {
		*key_r = t_strdup_until(text, value);
		*value_r = value + 1;
	} else {
		*key_r = text;
		*value_r = "";
	}
	return imapc_connection_handle_resp_text_code(conn, *key_r, *value_r);
}

static int
imapc_connection_handle_imap_resp_text(struct imapc_connection *conn,
				       const struct imap_arg *args,
				       const char **key_r, const char **value_r)
{
	const char *text;

	if (args->type != IMAP_ARG_ATOM)
		return 0;

	text = imap_args_to_str(args);
	if (*text != '[') {
		if (*text == '\0') {
			imapc_connection_input_error(conn,
				"Missing text in resp-text");
			return -1;
		}
		return 0;
	}
	return imapc_connection_handle_resp_text(conn, text, key_r, value_r);
}

static bool need_literal(const char *str)
{
	unsigned int i;

	for (i = 0; str[i] != '\0'; i++) {
		unsigned char c = str[i];

		if ((c & 0x80) != 0 || c == '\r' || c == '\n')
			return TRUE;
	}
	return FALSE;
}

static void imapc_connection_input_reset(struct imapc_connection *conn)
{
	conn->input_state = IMAPC_INPUT_STATE_NONE;
	conn->cur_tag = 0;
	conn->cur_num = 0;
	if (conn->parser != NULL)
		imap_parser_reset(conn->parser);
	imapc_connection_lfiles_free(conn);
}

static void
imapc_connection_auth_finish(struct imapc_connection *conn,
			     const struct imapc_command_reply *reply)
{
	if (reply->state != IMAPC_COMMAND_STATE_OK) {
		imapc_auth_failed(conn, reply, reply->text_full);
		imapc_connection_disconnect(conn);
		return;
	}

	imapc_auth_ok(conn);

	i_assert(array_count(&conn->cmd_wait_list) == 0);
	timeout_remove(&conn->to);
	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DONE);
	imapc_login_callback(conn, reply);

	/* Enable IMAP4REV2 post login, if available */
	imapc_connection_send_enable_imap4rev2(conn);

	imapc_command_send_more(conn);
}

static void imapc_connection_login_cb(const struct imapc_command_reply *reply,
				      void *context)
{
	struct imapc_connection *conn = context;

	imapc_connection_auth_finish(conn, reply);
}

static void
imapc_connection_proxyauth_login_cb(const struct imapc_command_reply *reply,
				    void *context)
{
	struct imapc_connection *conn = context;
	const struct imapc_settings *set = conn->client->set;
	struct imapc_command *cmd;

	if (reply->state == IMAPC_COMMAND_STATE_OK) {
		cmd = imapc_connection_cmd(conn, imapc_connection_login_cb,
					   conn);
		imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
		imapc_command_sendf(cmd, "PROXYAUTH %s", set->imapc_user);
		imapc_command_send_more(conn);
	} else {
		imapc_connection_auth_finish(conn, reply);
	}
}

static void
imapc_connection_authenticate_cb(const struct imapc_command_reply *reply,
				 void *context)
{
	struct imapc_connection *conn = context;
	const unsigned char *sasl_output;
	size_t input_len, sasl_output_len;
	buffer_t *buf;
	const char *error;

	if ((int)reply->state != IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE) {
		dsasl_client_free(&conn->sasl_client);
		imapc_connection_auth_finish(conn, reply);
		return;
	}

	input_len = strlen(reply->text_full);
	buf = t_buffer_create(MAX_BASE64_DECODED_SIZE(input_len));
	if (base64_decode(reply->text_full, input_len, buf) < 0) {
		imapc_auth_failed(conn, reply,
				  t_strdup_printf("Server sent non-base64 input for AUTHENTICATE: %s",
						  reply->text_full));
	} else if (dsasl_client_input(conn->sasl_client, buf->data, buf->used,
				      &error) != DSASL_CLIENT_RESULT_OK) {
		imapc_auth_failed(conn, reply, error);
	} else if (dsasl_client_output(conn->sasl_client, &sasl_output,
				       &sasl_output_len, &error) != DSASL_CLIENT_RESULT_OK) {
		imapc_auth_failed(conn, reply, error);
	} else if (sasl_output_len == 0) {
		o_stream_nsend_str(conn->output, "\r\n");
		return;
	} else {
		string_t *imap_output =
			t_str_new(MAX_BASE64_ENCODED_SIZE(sasl_output_len)+2);
		base64_encode(sasl_output, sasl_output_len, imap_output);
		str_append(imap_output, "\r\n");
		o_stream_nsend(conn->output, str_data(imap_output),
			       str_len(imap_output));
		return;
	}
	imapc_connection_disconnect(conn);
}

static bool imapc_connection_have_auth(struct imapc_connection *conn,
				       const char *mech_name)
{
	const char *auth;
	char *const *capa;

	for (capa = conn->capabilities_list; *capa != NULL; capa++) {
		if (str_begins_icase(*capa, "AUTH=", &auth) &&
		    strcasecmp(auth, mech_name) == 0)
			return TRUE;
	}
	return FALSE;
}

static int
imapc_connection_get_sasl_mech(struct imapc_connection *conn,
			       const struct dsasl_client_mech **mech_r,
			       const char **error_r)
{
	const struct imapc_settings *set = conn->client->set;
	const char *const *mechanisms =
		settings_boollist_get(&set->imapc_sasl_mechanisms);

	/* find one of the specified SASL mechanisms */
	for (; *mechanisms != NULL; mechanisms++) {
		if (imapc_connection_have_auth(conn, *mechanisms)) {
			*mech_r = dsasl_client_mech_find(*mechanisms);
			if (*mech_r != NULL)
				return 0;

			*error_r = t_strdup_printf(
				"Support for SASL method '%s' is missing", *mechanisms);
			return -1;
		}
	}
	*error_r = t_strdup_printf("IMAP server doesn't support any of the requested SASL mechanisms: %s",
				   t_strarray_join(mechanisms, ", "));
	return -1;
}

static int
imapc_connection_channel_bind_callback(const char *type, void *context,
				       const buffer_t **data_r,
				       const char **error_r)
{
	struct imapc_connection *conn = context;

	return ssl_iostream_get_channel_binding(conn->ssl_iostream,
						type, data_r, error_r);
}

static void imapc_connection_authenticate(struct imapc_connection *conn)
{
	const struct imapc_settings *set = conn->client->set;
	struct imapc_command *cmd;
	struct dsasl_client_settings sasl_set;
	const struct dsasl_client_mech *sasl_mech = NULL;
	const char *error;

	if (*set->imapc_master_user == '\0') {
		e_debug(conn->event, "Authenticating as %s", set->imapc_user);
	} else {
		e_debug(conn->event, "Authenticating as %s for user %s",
			set->imapc_master_user, set->imapc_user);
	}

	if (array_not_empty(&set->imapc_sasl_mechanisms)) {
		if (imapc_connection_get_sasl_mech(conn, &sasl_mech, &error) < 0) {
			struct imapc_command_reply reply;
			i_zero(&reply);
			reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
			reply.text_full = "";
			imapc_auth_failed(conn, &reply, error);
			imapc_connection_disconnect(conn);
			return;
		}
	}

	if ((set->parsed_features & IMAPC_FEATURE_PROXYAUTH) != 0 &&
	    *set->imapc_master_user != '\0') {
		/* We can use LOGIN command */
		cmd = imapc_connection_cmd(conn, imapc_connection_proxyauth_login_cb,
					   conn);
		imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
		imapc_command_sendf(cmd, "LOGIN %s %s",
				    set->imapc_master_user, conn->client->password);
		return;
	}
	if (sasl_mech == NULL &&
	    ((*set->imapc_master_user == '\0' &&
	      !need_literal(set->imapc_user) && !need_literal(conn->client->password)) ||
	     (conn->capabilities & IMAPC_CAPABILITY_AUTH_PLAIN) == 0)) {
		/* We can use LOGIN command */
		cmd = imapc_connection_cmd(conn, imapc_connection_login_cb,
					   conn);
		imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
		imapc_command_sendf(cmd, "LOGIN %s %s",
				    set->imapc_user, conn->client->password);
		return;
	}

	i_zero(&sasl_set);
	sasl_set.event_parent = conn->event;
	if (*set->imapc_master_user == '\0')
		sasl_set.authid = set->imapc_user;
	else {
		sasl_set.authid = set->imapc_master_user;
		sasl_set.authzid = set->imapc_user;
	}
	sasl_set.password = conn->client->password;
	sasl_set.protocol = "imap";
	sasl_set.host = conn->client->set->imapc_host;
	sasl_set.port = conn->client->set->imapc_port;

	if (sasl_mech == NULL)
		sasl_mech = &dsasl_client_mech_plain;
	conn->sasl_client = dsasl_client_new(sasl_mech, &sasl_set);
	if (conn->ssl_iostream != NULL) {
		dsasl_client_enable_channel_binding(
			conn->sasl_client,
			ssl_iostream_get_protocol_version(conn->ssl_iostream),
			imapc_connection_channel_bind_callback, conn);
	}

	cmd = imapc_connection_cmd(conn, imapc_connection_authenticate_cb, conn);
	cmd->authenticate = TRUE;
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);

	if ((conn->capabilities & IMAPC_CAPABILITY_SASL_IR) != 0) {
		const unsigned char *sasl_output;
		size_t sasl_output_len;
		string_t *sasl_output_base64;
		const char *error;

		if (dsasl_client_output(conn->sasl_client, &sasl_output,
					&sasl_output_len,
					&error) != DSASL_CLIENT_RESULT_OK) {
			e_error(conn->event,
				"Failed to create initial SASL reply: %s",
				error);
			imapc_connection_disconnect(conn);
			return;
		}
		sasl_output_base64 = t_str_new(MAX_BASE64_ENCODED_SIZE(sasl_output_len));
		base64_encode(sasl_output, sasl_output_len, sasl_output_base64);

		imapc_command_sendf(cmd, "AUTHENTICATE %1s %1s",
				    dsasl_client_mech_get_name(sasl_mech),
				    str_c(sasl_output_base64));
	} else {
		imapc_command_sendf(cmd, "AUTHENTICATE %1s",
				    dsasl_client_mech_get_name(sasl_mech));
	}
}

static void
imapc_connection_starttls_cb(const struct imapc_command_reply *reply,
			     void *context)
{
	struct imapc_connection *conn = context;
	struct imapc_command *cmd;

	if (reply->state != IMAPC_COMMAND_STATE_OK) {
		imapc_connection_input_error(conn, "STARTTLS failed: %s",
					     reply->text_full);
		return;
	}

	if (imapc_connection_ssl_init(conn) < 0)
		imapc_connection_disconnect(conn);
	else {
		/* get updated capabilities */
		cmd = imapc_connection_cmd(conn, imapc_connection_capability_cb,
					   conn);
		imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
		imapc_command_send(cmd, "CAPABILITY");
	}
}

static void
imapc_connection_id_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
			     void *context ATTR_UNUSED)
{
}

static void imapc_connection_send_id(struct imapc_connection *conn)
{
	static unsigned int global_id_counter = 0;
	struct imapc_command *cmd;

	if ((conn->capabilities & IMAPC_CAPABILITY_ID) == 0 ||
	    conn->client->params.session_id_prefix == NULL)
		return;

	cmd = imapc_connection_cmd(conn, imapc_connection_id_callback, conn);
	imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
	imapc_command_send(cmd, t_strdup_printf(
		"ID (\"name\" \"Dovecot\" \"x-session-ext-id\" \"%s-%u\")",
		conn->client->params.session_id_prefix, ++global_id_counter));
}

static void imapc_connection_starttls(struct imapc_connection *conn)
{
	struct imapc_command *cmd;

	if (conn->client->ssl_mode == IMAPC_CLIENT_SSL_MODE_STARTTLS &&
	    conn->ssl_iostream == NULL) {
		if ((conn->capabilities & IMAPC_CAPABILITY_STARTTLS) == 0) {
			e_error(conn->event, "Requested STARTTLS, "
				"but server doesn't support it");
			imapc_connection_disconnect(conn);
			return;
		}
		cmd = imapc_connection_cmd(conn, imapc_connection_starttls_cb,
					   conn);
		imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
		imapc_command_send(cmd, "STARTTLS");
		return;
	}
	imapc_connection_send_id(conn);
	imapc_connection_authenticate(conn);
}

static void
imapc_connection_capability_cb(const struct imapc_command_reply *reply,
			       void *context)
{
	struct imapc_connection *conn = context;

	if (reply->state != IMAPC_COMMAND_STATE_OK) {
		imapc_connection_input_error(conn,
			"Failed to get capabilities: %s", reply->text_full);
	} else if (conn->capabilities == 0) {
		imapc_connection_input_error(conn,
			"Capabilities not returned by server");
	} else {
		imapc_connection_starttls(conn);
	}
}

static int imapc_connection_input_banner(struct imapc_connection *conn)
{
	const struct imap_arg *imap_args;
	const char *key, *value;
	struct imapc_command *cmd;
	int ret;

	if ((ret = imapc_connection_read_line(conn, &imap_args)) <= 0)
		return ret;
	/* we already verified that the banner begins with OK */
	i_assert(imap_arg_atom_equals(imap_args, "OK"));
	imap_args++;

	if (imapc_connection_handle_imap_resp_text(conn, imap_args,
						   &key, &value) < 0)
		return -1;
	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_AUTHENTICATING);

	if (conn->capabilities == 0) {
		/* capabilities weren't sent in the banner. ask for them. */
		cmd = imapc_connection_cmd(conn, imapc_connection_capability_cb,
					   conn);
		imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_PRELOGIN);
		imapc_command_send(cmd, "CAPABILITY");
	} else {
		imapc_connection_starttls(conn);
	}
	conn->input_callback = NULL;
	imapc_connection_input_reset(conn);
	return 1;
}

static int imapc_connection_input_untagged(struct imapc_connection *conn)
{
	const struct imap_arg *imap_args;
	const unsigned char *data;
	size_t size;
	const char *name, *value;
	struct imap_parser *parser;
	struct imapc_untagged_reply reply;
	int ret;

	if (conn->state == IMAPC_CONNECTION_STATE_CONNECTING) {
		/* input banner */
		data = i_stream_get_data(conn->input, &size);
		if (size < 3 && memchr(data, '\n', size) == NULL)
			return 0;
		if (i_memcasecmp(data, "OK ", 3) != 0) {
			imapc_connection_input_error(conn,
				"Banner doesn't begin with OK: %s",
				t_strcut(t_strndup(data, size), '\n'));
			return -1;
		}
		conn->input_callback = imapc_connection_input_banner;
		return 1;
	}

	if ((ret = imapc_connection_read_line(conn, &imap_args)) == 0)
		return 0;
	else if (ret < 0) {
		imapc_connection_input_reset(conn);
		return 1;
	}
	if (!imap_arg_get_atom(&imap_args[0], &name)) {
		imapc_connection_input_error(conn, "Invalid untagged reply");
		return -1;
	}
	imap_args++;

	if (conn->input_state == IMAPC_INPUT_STATE_UNTAGGED &&
	    str_to_uint32(name, &conn->cur_num) == 0) {
		/* <seq> <event> */
		conn->input_state = IMAPC_INPUT_STATE_UNTAGGED_NUM;
		if (!imap_arg_get_atom(&imap_args[0], &name)) {
			imapc_connection_input_error(conn,
						     "Invalid untagged reply");
			return -1;
		}
		imap_args++;
	}
	i_zero(&reply);

	if (strcasecmp(name, "OK") == 0) {
		if (imapc_connection_handle_imap_resp_text(conn, imap_args,
						&reply.resp_text_key,
						&reply.resp_text_value) < 0)
			return -1;
	} else if (strcasecmp(name, "CAPABILITY") == 0) {
		value = imap_args_to_str(imap_args);
		if (imapc_connection_parse_capability(conn, value) < 0)
			return -1;
	} else if (strcasecmp(name, "BYE") == 0) {
		i_free(conn->disconnect_reason);
		conn->disconnect_reason = i_strdup(imap_args_to_str(imap_args));
	}

	reply.name = name;
	reply.num = conn->cur_num;
	reply.args = imap_args;
	reply.file_args = array_get(&conn->literal_files,
				    &reply.file_args_count);

	if (conn->selected_box != NULL) {
		reply.untagged_box_context =
			conn->selected_box->untagged_box_context;
	}

	/* the callback may disconnect and destroy the parser */
	parser = conn->parser;
	imap_parser_ref(parser);
	conn->client->untagged_callback(&reply, conn->client->untagged_context);
	imap_parser_unref(&parser);
	imapc_connection_input_reset(conn);
	return 1;
}

static int imapc_connection_input_plus(struct imapc_connection *conn)
{
	struct imapc_command *const *cmds;
	unsigned int cmds_count;
	const char *line;

	if ((line = i_stream_next_line(conn->input)) == NULL)
		return 0;

	cmds = array_get(&conn->cmd_send_queue, &cmds_count);
	if (conn->idle_plus_waiting) {
		/* "+ idling" reply for IDLE command */
		conn->idle_plus_waiting = FALSE;
		conn->idling = TRUE;
		/* no timing out while IDLEing */
		if (conn->to != NULL && !conn->idle_stopping)
			timeout_remove(&conn->to);
	} else if (cmds_count > 0 && cmds[0]->wait_for_literal) {
		/* reply for literal */
		cmds[0]->wait_for_literal = FALSE;
		imapc_command_send_more(conn);
	} else {
		cmds = array_get(&conn->cmd_wait_list, &cmds_count);
		if (cmds_count > 0 && cmds[0]->authenticate) {
			/* continue AUTHENTICATE */
			struct imapc_command_reply reply;

			i_zero(&reply);
			reply.state = (enum imapc_command_state)IMAPC_COMMAND_STATE_AUTHENTICATE_CONTINUE;
			reply.text_full = line;
			cmds[0]->callback(&reply, cmds[0]->context);
		} else {
			imapc_connection_input_error(conn, "Unexpected '+': %s", line);
			return -1;
		}
	}

	imapc_connection_input_reset(conn);
	return 1;
}

static void
imapc_connection_throttle_shrink_timeout(struct imapc_connection *conn)
{
	if (conn->throttle_msecs <= 1)
		conn->throttle_msecs = 0;
	else
		conn->throttle_msecs = conn->throttle_msecs*3 / 4;

	if (conn->throttle_shrink_msecs <= conn->client->set->throttle_shrink_min_msecs)
		conn->throttle_shrink_msecs = 0;
	else
		conn->throttle_shrink_msecs = conn->throttle_shrink_msecs*3 / 4;

	timeout_remove(&conn->to_throttle_shrink);
	if (conn->throttle_shrink_msecs > 0) {
		conn->to_throttle_shrink =
			timeout_add(conn->throttle_shrink_msecs,
				    imapc_connection_throttle_shrink_timeout, conn);
	}
}

static void
imapc_connection_throttle(struct imapc_connection *conn,
			  const struct imapc_command_reply *reply)
{
	timeout_remove(&conn->to_throttle);

	/* If GMail returns [THROTTLED], start slowing down commands.
	   Unfortunately this isn't a nice resp-text-code, but just
	   appended at the end of the line (although we kind of support
	   it as resp-text-code also in here if it's uppercased). */
	if (strstr(reply->text_full, "[THROTTLED]") != NULL) {
		if (conn->throttle_msecs == 0)
			conn->throttle_msecs = conn->client->set->throttle_init_msecs;
		else if (conn->throttle_msecs < conn->last_successful_throttle_msecs)
			conn->throttle_msecs = conn->last_successful_throttle_msecs;
		else {
			conn->throttle_msecs *= 2;
			if (conn->throttle_msecs > conn->client->set->throttle_max_msecs)
				conn->throttle_msecs = conn->client->set->throttle_max_msecs;
		}
		if (conn->throttle_shrink_msecs == 0)
			conn->throttle_shrink_msecs = conn->client->set->throttle_shrink_min_msecs;
		else
			conn->throttle_shrink_msecs *= 2;
		if (conn->to_throttle_shrink != NULL)
			timeout_reset(conn->to_throttle_shrink);
	} else {
		if (conn->throttle_shrink_msecs > 0 &&
		    conn->to_throttle_shrink == NULL) {
			conn->to_throttle_shrink =
				timeout_add(conn->throttle_shrink_msecs,
					    imapc_connection_throttle_shrink_timeout, conn);
		}
		conn->last_successful_throttle_msecs = conn->throttle_msecs;
	}

	if (conn->throttle_msecs > 0) {
		conn->throttle_end_timeval = ioloop_timeval;
		timeval_add_msecs(&conn->throttle_end_timeval,
				  conn->throttle_msecs);
		conn->throttle_pending = TRUE;
	}
}

static void
imapc_command_reply_free(struct imapc_command *cmd,
			 const struct imapc_command_reply *reply)
{
	cmd->callback(reply, cmd->context);
	imapc_command_free(cmd);
}

static int imapc_connection_input_tagged(struct imapc_connection *conn)
{
	struct imapc_command *const *cmds, *cmd = NULL;
	unsigned int i, count;
	char *line, *linep;
	const char *p;
	struct imapc_command_reply reply;

	line = i_stream_next_line(conn->input);
	if (line == NULL)
		return 0;
	/* make sure reply texts stays valid if input stream gets freed */
	line = t_strdup_noconst(line);

	i_zero(&reply);

	linep = strchr(line, ' ');
	if (linep == NULL)
		reply.text_full = "";
	else {
		*linep = '\0';
		reply.text_full = linep + 1;
	}

	if (strcasecmp(line, "ok") == 0)
		reply.state = IMAPC_COMMAND_STATE_OK;
	else if (strcasecmp(line, "no") == 0)
		reply.state = IMAPC_COMMAND_STATE_NO;
	else if (strcasecmp(line, "bad") == 0)
		reply.state = IMAPC_COMMAND_STATE_BAD;
	else {
		imapc_connection_input_error(conn,
			"Invalid state in tagged reply: %u %s %s",
			conn->cur_tag, line, reply.text_full);
		return -1;
	}

	if (reply.text_full[0] == '[') {
		/* get resp-text */
		if (imapc_connection_handle_resp_text(conn, reply.text_full,
					&reply.resp_text_key,
					&reply.resp_text_value) < 0)
			return -1;

		p = i_strchr_to_next(reply.text_full, ']');
		i_assert(p != NULL);
		reply.text_without_resp = p;
		if (reply.text_without_resp[0] == ' ')
			reply.text_without_resp++;
	} else {
		reply.text_without_resp = reply.text_full;
	}
	/* if we've pipelined multiple commands, handle [THROTTLED] reply
	   from only one of them */
	if (!conn->throttle_pending)
		imapc_connection_throttle(conn, &reply);

	/* find the command. it's either the first command in send queue
	   (literal failed) or somewhere in wait list. */
	cmds = array_get(&conn->cmd_send_queue, &count);
	if (count > 0 && cmds[0]->tag == conn->cur_tag) {
		cmd = cmds[0];
		array_pop_front(&conn->cmd_send_queue);
	} else {
		cmds = array_get(&conn->cmd_wait_list, &count);
		for (i = 0; i < count; i++) {
			if (cmds[i]->tag == conn->cur_tag) {
				cmd = cmds[i];
				array_delete(&conn->cmd_wait_list, i, 1);
				break;
			}
		}
	}
	if (array_count(&conn->cmd_wait_list) == 0 &&
	    array_count(&conn->cmd_send_queue) == 0 &&
	    conn->state == IMAPC_CONNECTION_STATE_DONE && conn->to != NULL)
		timeout_remove(&conn->to);

	if (cmd == NULL) {
		if (seq_range_exists(&conn->aborted_cmd_tags, conn->cur_tag)) {
			/* sent command was already aborted - ignore it */
			seq_range_array_remove(&conn->aborted_cmd_tags,
					       conn->cur_tag);
			imapc_connection_input_reset(conn);
			return 1;
		}
		imapc_connection_input_error(conn,
			"Unknown tag in a reply: %u %s %s",
			conn->cur_tag, line, reply.text_full);
		return -1;
	}
	if ((cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0)
		conn->select_waiting_reply = FALSE;

	if (reply.state == IMAPC_COMMAND_STATE_BAD) {
		e_error(conn->event, "Command '%s' failed with BAD: %u %s",
			imapc_command_get_readable(cmd),
			conn->cur_tag, reply.text_full);
		imapc_connection_disconnect(conn);
	}

	if (reply.state == IMAPC_COMMAND_STATE_NO &&
	    (cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0 &&
	    conn->selected_box != NULL) {
		/* EXAMINE/SELECT failed: mailbox is no longer selected */
		imapc_connection_mailbox_closed(conn->selected_box, TRUE);
	} else if (reply.state == IMAPC_COMMAND_STATE_OK &&
                   (cmd->flags & IMAPC_COMMAND_FLAG_UNSELECT) != 0) {
               	i_free_and_null(conn->selected_on_server);
        }

	if (conn->reconnect_command_count > 0 &&
	    (cmd->flags & IMAPC_COMMAND_FLAG_RECONNECTED) != 0) {
		i_assert(conn->reconnect_command_count > 0);
		if (--conn->reconnect_command_count == 0) {
			/* we've received replies for all the commands started
			   before reconnection. if we get disconnected now, we
			   can safely reconnect without worrying about infinite
			   reconnect loops. */
			if (conn->selected_box != NULL)
				conn->selected_box->reconnect_ok = TRUE;
		}
	}
	if (conn->reconnect_command_count == 0) {
		/* we've successfully received replies to some commands. */
		conn->reconnect_ok = TRUE;
	}

	if (reply.state == IMAPC_COMMAND_STATE_OK &&
	    (cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0 &&
	    conn->qresync_selecting_box != NULL) {
		/* Put the command back to the waiting list, since we're not
		   actually going to finish handling it here. */
		array_push_back(&conn->cmd_wait_list, &cmd);
		/* Work around the problem by reconnecting - there is no
		   selected folder then. */
		conn->qresync_selecting_box->reconnect_ok = TRUE;
		imapc_connection_try_reconnect(conn,
			"QRESYNC capable server didn't send [CLOSED] in untagged reply after SELECT",
			0, FALSE);
		return -1;
	}
	imapc_connection_input_reset(conn);
	imapc_command_reply_free(cmd, &reply);
	imapc_command_send_more(conn);
	return 1;
}

static int imapc_connection_input_one(struct imapc_connection *conn)
{
	const char *tag;
	int ret = -1;

	if (conn->input_callback != NULL)
		return conn->input_callback(conn);

	switch (conn->input_state) {
	case IMAPC_INPUT_STATE_NONE:
		tag = imap_parser_read_word(conn->parser);
		if (tag == NULL)
			return 0;

		if (strcmp(tag, "*") == 0) {
			conn->input_state = IMAPC_INPUT_STATE_UNTAGGED;
			conn->cur_num = 0;
			ret = imapc_connection_input_untagged(conn);
		} else if (strcmp(tag, "+") == 0) {
			conn->input_state = IMAPC_INPUT_STATE_PLUS;
			ret = imapc_connection_input_plus(conn);
		} else {
			conn->input_state = IMAPC_INPUT_STATE_TAGGED;
			if (str_to_uint(tag, &conn->cur_tag) < 0 ||
			    conn->cur_tag == 0) {
				imapc_connection_input_error(conn,
					"Invalid command tag: %s", tag);
				ret = -1;
			} else {
				ret = imapc_connection_input_tagged(conn);
			}
		}
		break;
	case IMAPC_INPUT_STATE_PLUS:
		ret = imapc_connection_input_plus(conn);
		break;
	case IMAPC_INPUT_STATE_UNTAGGED:
	case IMAPC_INPUT_STATE_UNTAGGED_NUM:
		ret = imapc_connection_input_untagged(conn);
		break;
	case IMAPC_INPUT_STATE_TAGGED:
		ret = imapc_connection_input_tagged(conn);
		break;
	}
	return ret;
}

static void imapc_connection_input(struct imapc_connection *conn)
{
	const char *errstr;
	string_t *str;
	ssize_t ret = 0;

	/* we need to read as much as we can with SSL streams to avoid
	   hanging */
	imapc_connection_ref(conn);
	while (conn->input != NULL && (ret = i_stream_read(conn->input)) > 0)
		imapc_connection_input_pending(conn);

	if (ret < 0 && conn->client->logging_out &&
	    conn->disconnect_reason != NULL) {
		/* expected disconnection */
		imapc_connection_disconnect(conn);
	} else if (ret < 0) {
		/* disconnected or buffer full */
		str = t_str_new(128);
		if (conn->disconnect_reason != NULL) {
			str_printfa(str, "Server disconnected with message: %s",
				    conn->disconnect_reason);
		} else if (ret == -2) {
			str_printfa(str, "Server sent too large input "
				    "(buffer full at %zu)",
				    i_stream_get_data_size(conn->input));
		} else if (conn->ssl_iostream == NULL) {
			errstr = conn->input->stream_errno == 0 ? "EOF" :
				i_stream_get_error(conn->input);
			str_printfa(str, "Server disconnected unexpectedly: %s",
				    errstr);
		} else {
			errstr = ssl_iostream_get_last_error(conn->ssl_iostream);
			if (errstr == NULL) {
				errstr = conn->input->stream_errno == 0 ? "EOF" :
					i_stream_get_error(conn->input);
			}
			str_printfa(str, "Server disconnected unexpectedly: %s",
				    errstr);
		}
		imapc_connection_try_reconnect(conn, str_c(str), 0, FALSE);
	}
	imapc_connection_unref(&conn);
}

static enum ssl_iostream_state
imapc_connection_ssl_handshaked(const char **error_r, void *context)
{
	struct imapc_connection *conn = context;
	const char *error;
	enum ssl_iostream_cert_validity validity =
		ssl_iostream_check_cert_validity(conn->ssl_iostream,
			conn->client->set->imapc_host, &error);

	if (validity == SSL_IOSTREAM_CERT_VALIDITY_OK)
		e_debug(conn->event, "SSL handshake successful");
	else if (ssl_iostream_get_allow_invalid_cert(conn->ssl_iostream)) {
		e_debug(conn->event, "SSL handshake successful, "
			"ignoring invalid certificate: %s", error);
	} else {
		*error_r = error;
		return validity == SSL_IOSTREAM_CERT_VALIDITY_NAME_MISMATCH ?
			SSL_IOSTREAM_STATE_NAME_MISMATCH :
			SSL_IOSTREAM_STATE_INVALID_CERT;
	}
	return SSL_IOSTREAM_STATE_OK;
}

static int imapc_connection_ssl_init(struct imapc_connection *conn)
{
	const char *error;

	e_debug(conn->event, "Starting SSL handshake");

	if (conn->raw_input != conn->input) {
		/* recreate rawlog after STARTTLS */
		i_stream_ref(conn->raw_input);
		o_stream_ref(conn->raw_output);
		i_stream_destroy(&conn->input);
		o_stream_destroy(&conn->output);
		conn->input = conn->raw_input;
		conn->output = conn->raw_output;
	}

	io_remove(&conn->io);
	enum ssl_iostream_flags ssl_flags = 0;
	const struct ssl_iostream_client_autocreate_parameters parameters = {
		.event_parent = conn->event,
		.host = conn->client->set->imapc_host,
		.flags = ssl_flags,
		.application_protocols = (const char *const[]) {
			"imap", NULL
		},
	};
	if (io_stream_autocreate_ssl_client(&parameters,
					    &conn->input, &conn->output,
					    &conn->ssl_iostream, &error) < 0) {
		e_error(conn->event, "Couldn't initialize SSL client: %s",
			error);
		return -1;
	}
	conn->io = io_add_istream(conn->input, imapc_connection_input, conn);
	ssl_iostream_set_handshake_callback(conn->ssl_iostream,
					    imapc_connection_ssl_handshaked,
					    conn);
	if (ssl_iostream_handshake(conn->ssl_iostream) < 0) {
		e_error(conn->event, "SSL handshake failed: %s",
			ssl_iostream_get_last_error(conn->ssl_iostream));
		return -1;
	}

	if (*conn->client->imapc_rawlog_dir != '\0') {
		iostream_rawlog_create(conn->event, "imapc_rawlog_dir",
				       conn->client->imapc_rawlog_dir,
				       &conn->input, &conn->output);
	}

	imap_parser_set_streams(conn->parser, conn->input, NULL);
	return 0;
}

static int imapc_connection_connected(struct imapc_connection *conn)
{
	const struct ip_addr *ip = &conn->ips[conn->prev_connect_idx];
	struct ip_addr local_ip;
	in_port_t local_port;
	int err;

	i_assert(conn->io == NULL);

	err = net_geterror(conn->fd);
	if (err != 0) {
		imapc_connection_try_reconnect(conn, t_strdup_printf(
			"connect(%s, %u) failed: %s",
			net_ip2addr(ip), conn->client->set->imapc_port,
			strerror(err)), conn->client->set->imapc_connection_retry_interval_msecs, TRUE);
		return -1;
	}
	if (net_getsockname(conn->fd, &local_ip, &local_port) < 0)
		local_port = 0;
	e_info(conn->event, "Connected to %s:%u (local %s:%u)",
	       net_ip2addr(ip), conn->client->set->imapc_port,
	       net_ip2addr(&local_ip), local_port);
	conn->io = io_add(conn->fd, IO_READ, imapc_connection_input, conn);
	o_stream_set_flush_callback(conn->output, imapc_connection_output,
				    conn);

	if (conn->client->ssl_mode == IMAPC_CLIENT_SSL_MODE_IMMEDIATE) {
		if (imapc_connection_ssl_init(conn) < 0) {
			imapc_connection_disconnect(conn);
			return -1;
		}
	}
	return imapc_connection_output(conn);
}

static void imapc_connection_timeout(struct imapc_connection *conn)
{
	const struct ip_addr *ip = &conn->ips[conn->prev_connect_idx];
	const char *errstr;
	bool connect_error = FALSE;

	switch (conn->state) {
	case IMAPC_CONNECTION_STATE_CONNECTING:
		errstr = t_strdup_printf("connect(%s, %u) timed out after %u seconds",
			net_ip2addr(ip), conn->client->set->imapc_port,
			conn->client->set->imapc_connection_timeout_interval_msecs/1000);
		connect_error = TRUE;
		break;
	case IMAPC_CONNECTION_STATE_AUTHENTICATING:
		errstr = t_strdup_printf("Authentication timed out after %u seconds",
			conn->client->set->imapc_connection_timeout_interval_msecs/1000);
		break;
	default:
		i_unreached();
	}
	imapc_connection_try_reconnect(conn, errstr, 0, connect_error);
}

static void
imapc_noop_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
		    void *context ATTR_UNUSED)
{
}

static void
imapc_reidle_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
		      void *context)
{
	struct imapc_connection *conn = context;

	imapc_connection_idle(conn);
}

static void imapc_connection_reset_idle(struct imapc_connection *conn)
{
	struct imapc_command *cmd;

	if (conn->idling)
		cmd = imapc_connection_cmd(conn, imapc_reidle_callback, conn);
	else if (array_count(&conn->cmd_wait_list) == 0)
		cmd = imapc_connection_cmd(conn, imapc_noop_callback, NULL);
	else {
		/* IMAP command reply is taking a long time */
		return;
	}
	imapc_command_send(cmd, "NOOP");
}

static void imapc_connection_connect_next_ip(struct imapc_connection *conn)
{
	const struct ip_addr *ip = NULL;
	unsigned int i;
	int fd;

	i_assert(conn->client->set->imapc_max_idle_time_secs > 0);

	for (i = 0; i<conn->ips_count;) {
		conn->prev_connect_idx = (conn->prev_connect_idx+1) % conn->ips_count;
		ip = &conn->ips[conn->prev_connect_idx];
		fd = net_connect_ip(ip, conn->client->set->imapc_port, NULL);
		if (fd != -1)
			break;

		/* failed to connect to one of the IPs immediately
		   (e.g. IPv6 address without connectivity). try all IPs
		   before failing completely. */
		const char *error = t_strdup_printf(
			"net_connect_ip(%s:%u) failed: %m",
			net_ip2addr(ip), conn->client->set->imapc_port);
		if (conn->prev_connect_idx+1 == conn->ips_count) {
			imapc_connection_try_reconnect(conn, error,
				conn->client->set->imapc_connection_retry_interval_msecs, TRUE);
			return;
		}
		e_error(conn->event, "%s", error);
	}

	i_assert(ip != NULL);

	conn->fd = fd;
	conn->input = conn->raw_input =
		i_stream_create_fd(fd, conn->client->set->imapc_max_line_length);
	conn->output = conn->raw_output = o_stream_create_fd(fd, SIZE_MAX);
	o_stream_set_no_error_handling(conn->output, TRUE);

	if (*conn->client->imapc_rawlog_dir != '\0' &&
	    conn->client->ssl_mode != IMAPC_CLIENT_SSL_MODE_IMMEDIATE) {
		iostream_rawlog_create(conn->event, "imapc_rawlog_dir",
				       conn->client->imapc_rawlog_dir,
				       &conn->input, &conn->output);
	}

	o_stream_set_flush_pending(conn->output, TRUE);
	o_stream_set_flush_callback(conn->output, imapc_connection_connected,
				    conn);
	conn->parser = imap_parser_create(conn->input, NULL,
					  conn->client->set->imapc_max_line_length,
					  NULL);
	conn->to = timeout_add(conn->client->set->imapc_connection_timeout_interval_msecs,
			       imapc_connection_timeout, conn);
	conn->to_output = timeout_add(conn->client->set->imapc_max_idle_time_secs*1000,
				      imapc_connection_reset_idle, conn);
	e_debug(conn->event, "Connecting to %s:%u", net_ip2addr(ip),
		conn->client->set->imapc_port);
}

static void
imapc_connection_dns_callback(const struct dns_lookup_result *result,
			      struct imapc_connection *conn)
{
	/* We ended up here because dns_lookup_abort() was used */
	if (result->ret == EAI_CANCELED)
		return;
	conn->dns_lookup = NULL;

	if (result->ret != 0) {
		e_error(conn->event, "dns_lookup(%s) failed: %s",
			conn->client->set->imapc_host, result->error);
		imapc_connection_set_disconnected(conn);
		return;
	}

	i_assert(result->ips_count > 0);
	conn->ips_count = result->ips_count;
	conn->ips = i_new(struct ip_addr, conn->ips_count);
	memcpy(conn->ips, result->ips, sizeof(*conn->ips) * conn->ips_count);
	conn->prev_connect_idx = conn->ips_count - 1;

	imapc_connection_connect_next_ip(conn);
}

void imapc_connection_connect(struct imapc_connection *conn)
{
	struct ip_addr ip;

	if (conn->fd != -1 || conn->dns_lookup != NULL)
		return;
	if (conn->reconnect_waiting) {
		/* wait for the reconnection delay to finish before
		   doing anything. */
		return;
	}

	if (conn->reconnecting) {
		conn->reconnecting = FALSE;
		/* if we get disconnected before we've finished all the pending
		   commands, don't reconnect */
		conn->reconnect_command_count =
			array_count(&conn->cmd_wait_list) +
			array_count(&conn->cmd_send_queue);
	}

	imapc_connection_input_reset(conn);
	conn->last_connect = ioloop_timeval;

	e_debug(conn->event, "Looking up IP address "
		"(reconnect_ok=%s, last_connect=%ld)",
		(conn->reconnect_ok ? "true" : "false"),
		(long)conn->last_connect.tv_sec);

	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_CONNECTING);
	if (conn->ips_count > 0) {
		/* do nothing */
	} else if (HAS_ANY_BITS(conn->client->params.flags, IMAPC_PARAMETER_CLIENT_DISABLED)) {
		e_error(conn->event, "imapc host is empty");
		imapc_connection_set_disconnected(conn);
		return;
	} else if (net_addr2ip(conn->client->set->imapc_host, &ip) == 0) {
		conn->ips_count = 1;
		conn->ips = i_new(struct ip_addr, conn->ips_count);
		conn->ips[0] = ip;
	} else {
		(void)dns_lookup(conn->client->set->imapc_host, NULL,
				 conn->event, imapc_connection_dns_callback,
				 conn, &conn->dns_lookup);
		return;
	}
	imapc_connection_connect_next_ip(conn);
}

void imapc_connection_input_pending(struct imapc_connection *conn)
{
	int ret = 1;

	if (conn->input == NULL)
		return;

	if (conn->to != NULL && !conn->idle_stopping)
		timeout_reset(conn->to);

	o_stream_cork(conn->output);
	while (ret > 0 && conn->input != NULL) {
		T_BEGIN {
			ret = imapc_connection_input_one(conn);
		} T_END;
	}

	if (conn->output != NULL)
		o_stream_uncork(conn->output);
}

static struct imapc_command *
imapc_command_begin(imapc_command_callback_t *callback, void *context)
{
	struct imapc_command *cmd;
	pool_t pool;

	i_assert(callback != NULL);

	pool = pool_alloconly_create("imapc command", 2048);
	cmd = p_new(pool, struct imapc_command, 1);
	cmd->pool = pool;
	cmd->callback = callback;
	cmd->context = context;

	/* use a globally unique tag counter so looking at rawlogs is
	   somewhat easier */
	if (++imapc_client_cmd_tag_counter == 0)
		imapc_client_cmd_tag_counter++;
	cmd->tag = imapc_client_cmd_tag_counter;
	return cmd;
}

static void imapc_command_free(struct imapc_command *cmd)
{
	struct imapc_command_stream *stream;

	if (array_is_created(&cmd->streams)) {
		array_foreach_modifiable(&cmd->streams, stream)
			i_stream_unref(&stream->input);
	}
	pool_unref(&cmd->pool);
}

const char *imapc_command_get_tag(struct imapc_command *cmd)
{
	return t_strdup_printf("%u", cmd->tag);
}

static bool imapc_cmd_remove(ARRAY_TYPE(imapc_command) *entries,
			     struct imapc_command *cmd)
{
	if (array_is_empty(entries))
		return FALSE;

	unsigned int count;
	struct imapc_command *const *items = array_get(entries, &count);
	for (unsigned int ndx = 0; ndx < count; ndx++) {
		if (items[ndx] == cmd) {
			array_delete(entries, ndx, 1);
			return TRUE;
		}
	}
	return FALSE;
}

void imapc_command_abort(struct imapc_command **_cmd)
{
	struct imapc_command *cmd = *_cmd;

	if (cmd == NULL)
		return;
	*_cmd = NULL;

	if (!imapc_cmd_remove(&cmd->conn->cmd_send_queue, cmd))
		(void)imapc_cmd_remove(&cmd->conn->cmd_wait_list, cmd);

	imapc_command_free(cmd);
}

static void imapc_command_timeout(struct imapc_connection *conn)
{
	struct imapc_command *const *cmds;
	unsigned int count;

	cmds = array_get(&conn->cmd_wait_list, &count);
	i_assert(count > 0);

	imapc_connection_try_reconnect(conn, t_strdup_printf(
		"Command '%s' timed out", imapc_command_get_readable(cmds[0])), 0, FALSE);
}

static bool
parse_sync_literal(const unsigned char *data, unsigned int pos,
		   unsigned int *value_r)
{
	unsigned int value = 0, mul = 1;

	/* data should contain "{size}\r\n" and pos points after \n */
	if (pos <= 4 || data[pos-1] != '\n' || data[pos-2] != '\r' ||
	    data[pos-3] != '}' || !i_isdigit(data[pos-4]))
		return FALSE;
	pos -= 4;

	do {
		value += (data[pos] - '0') * mul;
		mul = mul*10;
		pos--;
	} while (pos > 0 && i_isdigit(data[pos]));

	if (pos == 0 || data[pos] != '{')
		return FALSE;

	*value_r = value;
	return TRUE;
}

static void imapc_command_send_finished(struct imapc_connection *conn,
					struct imapc_command *cmd)
{
	struct imapc_command *const *cmdp;

	i_assert(conn->to != NULL);

	if (cmd->idle)
		conn->idle_plus_waiting = TRUE;
	cmd->sent = TRUE;

	/* everything sent. move command to wait list. */
	cmdp = array_front(&conn->cmd_send_queue);
	i_assert(*cmdp == cmd);
	array_pop_front(&conn->cmd_send_queue);
	array_push_back(&conn->cmd_wait_list, &cmd);

	/* send the next command in queue */
	imapc_command_send_more(conn);
}

static struct imapc_command_stream *
imapc_command_get_sending_stream(struct imapc_command *cmd)
{
	struct imapc_command_stream *stream;

	if (!array_is_created(&cmd->streams) || array_count(&cmd->streams) == 0)
		return NULL;

	stream = array_front_modifiable(&cmd->streams);
	if (stream->pos != cmd->send_pos)
		return NULL;
	return stream;
}

static int imapc_command_try_send_stream(struct imapc_connection *conn,
					 struct imapc_command *cmd)
{
	struct imapc_command_stream *stream;
	enum ostream_send_istream_result res;

	stream = imapc_command_get_sending_stream(cmd);
	if (stream == NULL)
		return -2;

	/* we're sending the stream now */
	o_stream_set_max_buffer_size(conn->output, 0);
	res = o_stream_send_istream(conn->output, stream->input);
	o_stream_set_max_buffer_size(conn->output, SIZE_MAX);

	switch (res) {
	case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
		break;
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
		i_unreached();
	case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
		i_assert(stream->input->v_offset < stream->size);
		return 0;
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
		e_error(conn->event, "read(%s) failed: %s",
			i_stream_get_name(stream->input),
			i_stream_get_error(stream->input));
		return -1;
	case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
		/* disconnected */
		return -1;
	}
	i_assert(stream->input->v_offset == stream->size);

	/* finished with the stream */
	i_stream_unref(&stream->input);
	array_pop_front(&cmd->streams);

	i_assert(cmd->send_pos != cmd->data->used);
	return 1;
}

static void imapc_connection_set_selecting(struct imapc_client_mailbox *box)
{
	struct imapc_connection *conn = box->conn;

	i_assert(conn->qresync_selecting_box == NULL);

	if (conn->selected_on_server != NULL &&
	    (conn->capabilities & IMAPC_CAPABILITY_QRESYNC) != 0) {
		/* server will send a [CLOSED] once selected mailbox is
		   closed */
		conn->qresync_selecting_box = box;
	} else {
		/* we'll have to assume that all the future untagged messages
		   are for the mailbox we're selecting */
		i_free_and_null(conn->selected_on_server);
		conn->selected_on_server = i_strdup(box->name);
		conn->selected_box = box;
	}
	conn->select_waiting_reply = TRUE;
}

static bool imapc_connection_is_throttled(struct imapc_connection *conn)
{
	timeout_remove(&conn->to_throttle);

	if (conn->throttle_msecs == 0) {
		/* we haven't received [THROTTLED] recently */
		return FALSE;
	}
	if (array_count(&conn->cmd_wait_list) > 0) {
		/* wait until we have received the existing commands' tagged
		   replies to see if we're still throttled */
		return TRUE;
	}
	if (timeval_cmp(&ioloop_timeval, &conn->throttle_end_timeval) >= 0) {
		/* we reached the throttle timeout - send the next command */
		conn->throttle_pending = FALSE;
		return FALSE;
	}

	/* we're still being throttled - wait for it to end */
	conn->to_throttle = timeout_add_absolute(&conn->throttle_end_timeval,
						 imapc_command_send_more, conn);
	return TRUE;
}

static void imapc_command_send_more(struct imapc_connection *conn)
{
	struct imapc_command *const *cmds, *cmd;
	struct imapc_command_reply reply;
	const unsigned char *p, *data;
	unsigned int count, size;
	size_t seek_pos, start_pos, end_pos;
	int ret;

	if (imapc_connection_is_throttled(conn))
		return;

	cmds = array_get(&conn->cmd_send_queue, &count);
	if (count == 0)
		return;
	cmd = cmds[0];

	if ((cmd->flags & IMAPC_COMMAND_FLAG_PRELOGIN) == 0 &&
	    conn->state != IMAPC_CONNECTION_STATE_DONE) {
		/* wait until we're fully connected */
		return;
	}
	if ((cmd->flags & IMAPC_COMMAND_FLAG_LOGOUT) != 0 &&
	    array_count(&conn->cmd_wait_list) > 0) {
		/* wait until existing commands have finished */
		return;
	}
	if (conn->select_waiting_reply) {
		/* wait for SELECT to finish */
		return;
	}
	if (cmd->wait_for_literal) {
		/* wait until we received '+' */
		return;
	}

	i_assert(cmd->send_pos < cmd->data->used);

	if (cmd->box == NULL) {
		/* non-mailbox command */
	} else if (cmd->send_pos == 0 &&
		   (cmd->flags & IMAPC_COMMAND_FLAG_SELECT) != 0) {
		/* SELECT/EXAMINE command */
		imapc_connection_set_selecting(cmd->box);
	} else if (!imapc_client_mailbox_is_opened(cmd->box)) {
		if (cmd->box->reconnecting) {
			/* wait for SELECT/EXAMINE */
			return;
		}
		/* shouldn't normally happen */
		i_zero(&reply);
		reply.text_without_resp = reply.text_full = "Mailbox not open";
		reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;

		array_pop_front(&conn->cmd_send_queue);
		imapc_command_reply_free(cmd, &reply);
		imapc_command_send_more(conn);
		return;
	}

	/* add timeout for commands if there's not one yet
	   (pre-login has its own timeout) */
	if ((cmd->flags & IMAPC_COMMAND_FLAG_LOGOUT) != 0) {
		/* LOGOUT has a shorter timeout */
		timeout_remove(&conn->to);
		conn->to = timeout_add(IMAPC_LOGOUT_TIMEOUT_MSECS,
				       imapc_command_timeout, conn);
	} else if (conn->to == NULL) {
		conn->to = timeout_add(conn->client->set->imapc_cmd_timeout_secs * 1000,
				       imapc_command_timeout, conn);
	}

	timeout_reset(conn->to_output);
	if ((ret = imapc_command_try_send_stream(conn, cmd)) == 0)
		return;
	if (ret == -1) {
		i_zero(&reply);
		reply.text_without_resp = reply.text_full = "Mailbox not open";
		reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;

		array_pop_front(&conn->cmd_send_queue);
		imapc_command_reply_free(cmd, &reply);
		imapc_command_send_more(conn);
		return;
	}

	seek_pos = cmd->send_pos;
	if (seek_pos != 0 && ret == -2) {
		/* skip over the literal. we can also get here from
		   AUTHENTICATE command, which doesn't use a literal */
		if (parse_sync_literal(cmd->data->data, seek_pos, &size)) {
			seek_pos += size;
			i_assert(seek_pos <= cmd->data->used);
		}
	}

	do {
		start_pos = seek_pos;
		p = memchr(CONST_PTR_OFFSET(cmd->data->data, seek_pos), '\n',
			   cmd->data->used - seek_pos);
		i_assert(p != NULL);

		seek_pos = p - (const unsigned char *)cmd->data->data + 1;
		/* keep going for LITERAL+ command */
	} while (start_pos + 3 < seek_pos &&
		 p[-1] == '\r' && p[-2] == '}' && p[-3] == '+');
	end_pos = seek_pos;

	data = CONST_PTR_OFFSET(cmd->data->data, cmd->send_pos);
	size = end_pos - cmd->send_pos;
	o_stream_nsend(conn->output, data, size);
	cmd->send_pos = end_pos;

	if (cmd->send_pos == cmd->data->used) {
		i_assert(!array_is_created(&cmd->streams) ||
			 array_count(&cmd->streams) == 0);
		imapc_command_send_finished(conn, cmd);
	} else {
		cmd->wait_for_literal = TRUE;
	}
}

static void imapc_connection_send_idle_done(struct imapc_connection *conn)
{
	if ((conn->idling || conn->idle_plus_waiting) && !conn->idle_stopping) {
		conn->idle_stopping = TRUE;
		o_stream_nsend_str(conn->output, "DONE\r\n");
		if (conn->to == NULL) {
			conn->to = timeout_add(
				conn->client->set->imapc_cmd_timeout_secs * 1000,
				imapc_command_timeout, conn);
		}
	}
}

static void imapc_connection_cmd_send(struct imapc_command *cmd)
{
	struct imapc_connection *conn = cmd->conn;
	struct imapc_command *const *cmds;
	unsigned int i, count;

	imapc_connection_send_idle_done(conn);
	cmd->start_time = ioloop_timeval;

	i_assert((cmd->flags & IMAPC_COMMAND_FLAG_RECONNECTED) == 0);

	if ((cmd->flags & IMAPC_COMMAND_FLAG_PRELOGIN) != 0 &&
	    conn->state == IMAPC_CONNECTION_STATE_AUTHENTICATING) {
		/* pre-login commands get inserted before everything else */
		array_push_front(&conn->cmd_send_queue, &cmd);
		imapc_command_send_more(conn);
		return;
	}

	/* add the command just before retried commands */
	cmds = array_get(&conn->cmd_send_queue, &count);
	for (i = count; i > 0; i--) {
		if ((cmds[i-1]->flags & IMAPC_COMMAND_FLAG_RECONNECTED) == 0)
			break;
	}
	array_insert(&conn->cmd_send_queue, i, &cmd, 1);
	imapc_command_send_more(conn);
}

static int imapc_connection_output(struct imapc_connection *conn)
{
	struct imapc_command *const *cmds;
	unsigned int count;
	int ret;

	if (conn->to != NULL)
		timeout_reset(conn->to);

	if ((ret = o_stream_flush(conn->output)) < 0)
		return 1;

	imapc_connection_ref(conn);
	cmds = array_get(&conn->cmd_send_queue, &count);
	if (count > 0) {
		if (imapc_command_get_sending_stream(cmds[0]) != NULL &&
		    !cmds[0]->wait_for_literal) {
			/* we're sending a stream. send more. */
			imapc_command_send_more(conn);
		}
	}
	imapc_connection_unref(&conn);
	return ret;
}

struct imapc_command *
imapc_connection_cmd(struct imapc_connection *conn,
		     imapc_command_callback_t *callback, void *context)
{
	struct imapc_command *cmd;

	cmd = imapc_command_begin(callback, context);
	cmd->conn = conn;
	return cmd;
}

void imapc_command_set_flags(struct imapc_command *cmd,
			     enum imapc_command_flags flags)
{
	cmd->flags = flags;
}

void imapc_command_set_mailbox(struct imapc_command *cmd,
			       struct imapc_client_mailbox *box)
{
	cmd->box = box;
}

bool imapc_command_connection_is_selected(struct imapc_command *cmd)
{
	return cmd->conn->selected_box != NULL ||
		cmd->conn->qresync_selecting_box != NULL;
}

void imapc_command_send(struct imapc_command *cmd, const char *cmd_str)
{
	size_t len = strlen(cmd_str);

	cmd->data = str_new(cmd->pool, 6 + len + 2);
	str_printfa(cmd->data, "%u %s\r\n", cmd->tag, cmd_str);
	imapc_connection_cmd_send(cmd);
}

void imapc_command_sendf(struct imapc_command *cmd, const char *cmd_fmt, ...)
{
	va_list args;

	va_start(args, cmd_fmt);
	imapc_command_sendvf(cmd, cmd_fmt, args);
	va_end(args);
}

void imapc_command_sendvf(struct imapc_command *cmd,
			  const char *cmd_fmt, va_list args)
{
	unsigned int i;

	cmd->data = str_new(cmd->pool, 128);
	str_printfa(cmd->data, "%u ", cmd->tag);

	for (i = 0; cmd_fmt[i] != '\0'; i++) {
		if (cmd_fmt[i] != '%') {
			str_append_c(cmd->data, cmd_fmt[i]);
			continue;
		}

		switch (cmd_fmt[++i]) {
		case '\0':
			i_unreached();
		case 'u': {
			unsigned int arg = va_arg(args, unsigned int);

			str_printfa(cmd->data, "%u", arg);
			break;
		}
		case 'p': {
			struct istream *input = va_arg(args, struct istream *);
			struct imapc_command_stream *s;
			uoff_t size;

			if (!array_is_created(&cmd->streams))
				p_array_init(&cmd->streams, cmd->pool, 2);
			if (i_stream_get_size(input, TRUE, &size) < 0)
				size = 0;
			str_printfa(cmd->data, "{%"PRIuUOFF_T"}\r\n", size);
			s = array_append_space(&cmd->streams);
			s->pos = str_len(cmd->data);
			s->size = size;
			s->input = input;
			i_stream_ref(input);
			break;
		}
		case 's': {
			const char *arg = va_arg(args, const char *);

			if (!need_literal(arg))
				imap_append_quoted(cmd->data, arg, 0);
			else if ((cmd->conn->capabilities &
				  IMAPC_CAPABILITY_LITERALPLUS) != 0) {
				str_printfa(cmd->data, "{%zu+}\r\n%s",
					    strlen(arg), arg);
			} else {
				str_printfa(cmd->data, "{%zu}\r\n%s",
					    strlen(arg), arg);
			}
			break;
		}
		case '1': {
			/* %1s - no quoting */
			const char *arg = va_arg(args, const char *);

			i++;
			i_assert(cmd_fmt[i] == 's');
			str_append(cmd->data, arg);
			break;
		}
		}
	}
	str_append(cmd->data, "\r\n");

	imapc_connection_cmd_send(cmd);
}

enum imapc_connection_state
imapc_connection_get_state(struct imapc_connection *conn)
{
	return conn->state;
}

enum imapc_capability
imapc_connection_get_capabilities(struct imapc_connection *conn)
{
	return conn->capabilities;
}

bool imapc_cmd_has_imap4rev2(struct imapc_command *cmd)
{
	return cmd->conn->imap4rev2_enabled;
}

void imapc_connection_mailbox_closed(struct imapc_client_mailbox *box,
				     bool via_tagged_reply)
{
	struct imapc_connection *conn = box->conn;

	if (conn->select_waiting_reply) {
		/* Mailbox closing was requested before SELECT/EXAMINE
		   replied. The connection state is now unknown and
		   shouldn't be used anymore. */
		imapc_connection_disconnect(conn);
	} else if (conn->qresync_selecting_box == NULL &&
		   conn->selected_box == NULL) {
		/* There is no mailbox selected currently. */
		i_assert(!via_tagged_reply);
	} else {
		/* Mailbox was closed in a known state. Either due to
		   SELECT/EXAMINE failing (via_tagged_reply) or by
		   imapc-storage after the mailbox was already fully
		   selected. */
		i_assert(conn->qresync_selecting_box == box ||
			 conn->selected_box == box);
		conn->qresync_selecting_box = NULL;
		conn->selected_box = NULL;
		if (via_tagged_reply)
			i_free_and_null(conn->selected_on_server);
		else {
			/* We didn't actually send UNSELECT command, so don't
			   touch selected_on_server state. */
		}
	}

	imapc_connection_send_idle_done(conn);
	imapc_connection_abort_commands(conn, box, FALSE);
}

struct imapc_client_mailbox *
imapc_connection_get_mailbox(struct imapc_connection *conn)
{
	if (conn->qresync_selecting_box != NULL)
		return conn->qresync_selecting_box;
	return conn->selected_box;
}

static void
imapc_connection_idle_callback(const struct imapc_command_reply *reply ATTR_UNUSED,
			       void *context)
{
	struct imapc_connection *conn = context;

	conn->idling = FALSE;
	conn->idle_plus_waiting = FALSE;
	conn->idle_stopping = FALSE;
}

void imapc_connection_idle(struct imapc_connection *conn)
{
	struct imapc_command *cmd;

	if (array_count(&conn->cmd_send_queue) != 0 ||
	    array_count(&conn->cmd_wait_list) != 0 ||
	    conn->idling || conn->idle_plus_waiting ||
	    (conn->capabilities & IMAPC_CAPABILITY_IDLE) == 0)
		return;

	cmd = imapc_connection_cmd(conn, imapc_connection_idle_callback, conn);
	cmd->idle = TRUE;
	imapc_command_send(cmd, "IDLE");
}

struct event *imapc_connection_get_event(struct imapc_connection *conn)
{
	return conn->event;
}

struct imapc_command *
imapc_client_find_command_by_tag(struct imapc_client *client, const char *tag_str)
{
	unsigned int tag;
	if (str_to_uint(tag_str, &tag) != 0) {
		/* All the tags we use are numerical. If it can't be parsed
		   into a number, then we won't find it anyway */
		return NULL;
	}

	struct imapc_client_connection *conn;
	array_foreach_elem(&client->conns, conn) {
		struct imapc_command *cmd;
		array_foreach_elem(&conn->conn->cmd_wait_list, cmd) {
			if (cmd->tag == tag)
				return cmd;
		}
		array_foreach_elem(&conn->conn->cmd_send_queue, cmd) {
			if (cmd->tag == tag)
				return cmd;
		}
	}
	return NULL;
}

struct timeval imapc_command_get_start_time(struct imapc_command *cmd)
{
	return cmd->start_time;
}

const char *
imapc_connection_get_selected_mailbox_name(struct imapc_connection *conn)
{
	return conn->selected_on_server;
}
