view src/lib-storage/index/imapc/imapc-connection.c @ 12582:3949d0091a44

imapc: Fixed authenticating with AUTHENTICATE command without SASL-IR.
author Timo Sirainen <tss@iki.fi>
date Wed, 19 Jan 2011 01:21:03 +0200
parents 9db6762167fb
children 9e471f267fb4
line wrap: on
line source

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

#include "lib.h"
#include "ioloop.h"
#include "network.h"
#include "istream.h"
#include "ostream.h"
#include "base64.h"
#include "str.h"
#include "dns-lookup.h"
#include "imap-quote.h"
#include "imap-util.h"
#include "imap-parser.h"
#include "imapc-client-private.h"
#include "imapc-seqmap.h"
#include "imapc-connection.h"

#include <ctype.h>

#define IMAPC_DNS_LOOKUP_TIMEOUT_MSECS (1000*30)
#define IMAPC_CONNECT_TIMEOUT_MSECS (1000*30)

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

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

	imapc_command_callback_t *callback;
	void *context;
};

struct imapc_connection {
	struct imapc_client *client;
	char *name;

	int fd;
	struct io *io;
	struct istream *input;
	struct ostream *output;
	struct imap_parser *parser;
	struct timeout *to;

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

	struct imapc_client_mailbox *selecting_box, *selected_box;

	imapc_connection_state_change *state_callback;
	enum imapc_connection_state state;

	enum imapc_capability capabilities;
	char **capabilities_list;

	ARRAY_DEFINE(cmd_send_queue, struct imapc_command *);
	ARRAY_DEFINE(cmd_wait_list, struct imapc_command *);

	unsigned int ips_count, prev_connect_idx;
	struct ip_addr *ips;
};

static void imapc_connection_disconnect(struct imapc_connection *conn);

static void imapc_command_send_more(struct imapc_connection *conn,
				    struct imapc_command *cmd);

struct imapc_connection *
imapc_connection_init(struct imapc_client *client,
		      imapc_connection_state_change *state_callback)
{
	struct imapc_connection *conn;

	conn = i_new(struct imapc_connection, 1);
	conn->client = client;
	conn->state_callback = state_callback;
	conn->fd = -1;
	conn->name = i_strdup_printf("%s:%u", client->set.host,
				     client->set.port);
	i_array_init(&conn->cmd_send_queue, 8);
	i_array_init(&conn->cmd_wait_list, 32);
	return conn;
}

void imapc_connection_deinit(struct imapc_connection **_conn)
{
	struct imapc_connection *conn = *_conn;

	*_conn = NULL;

	imapc_connection_disconnect(conn);
	p_strsplit_free(default_pool, conn->capabilities_list);
	array_free(&conn->cmd_send_queue);
	array_free(&conn->cmd_wait_list);
	i_free(conn->ips);
	i_free(conn->name);
	i_free(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);
}

static void imapc_connection_set_state(struct imapc_connection *conn,
				       enum imapc_connection_state state)
{
	enum imapc_connection_state prev_state = conn->state;

	if (state == IMAPC_CONNECTION_STATE_DISCONNECTED) {
		/* abort all pending commands */
		struct imapc_command_reply reply;
		struct imapc_command *const *cmdp, *cmd;

		memset(&reply, 0, sizeof(reply));
		reply.state = IMAPC_COMMAND_STATE_DISCONNECTED;
		reply.text = "Disconnected from server";

		while (array_count(&conn->cmd_wait_list) > 0) {
			cmdp = array_idx(&conn->cmd_wait_list, 0);
			cmd = *cmdp;
			array_delete(&conn->cmd_wait_list, 0, 1);

			cmd->callback(&reply, cmd->context);
			pool_unref(&cmd->pool);
		}
		while (array_count(&conn->cmd_send_queue) > 0) {
			cmdp = array_idx(&conn->cmd_send_queue, 0);
			cmd = *cmdp;
			array_delete(&conn->cmd_send_queue, 0, 1);

			cmd->callback(&reply, cmd->context);
			pool_unref(&cmd->pool);
		}
	}
	if (state == IMAPC_CONNECTION_STATE_DONE) {
		if (array_count(&conn->cmd_send_queue) > 0) {
			struct imapc_command *const *cmd_p =
				array_idx(&conn->cmd_send_queue, 0);
			imapc_command_send_more(conn, *cmd_p);
		}
	}

	conn->state = state;
	conn->state_callback(conn, conn->client, prev_state);
}

static void imapc_connection_disconnect(struct imapc_connection *conn)
{
	if (conn->fd == -1)
		return;

	if (conn->to != NULL)
		timeout_remove(&conn->to);
	imap_parser_destroy(&conn->parser);
	io_remove(&conn->io);
	i_stream_destroy(&conn->input);
	o_stream_destroy(&conn->output);
	net_disconnect(conn->fd);
	conn->fd = -1;

	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DISCONNECTED);
}

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

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

static int
imapc_connection_read_line(struct imapc_connection *conn,
			   const struct imap_arg **imap_args_r)
{
	int ret;
	bool fatal;

	ret = imap_parser_read_args(conn->parser, 0, 0, imap_args_r);
	if (ret == -2) {
		/* need more data */
		return 0;
	}
	if (ret < 0) {
		imapc_connection_input_error(conn, "Error parsing input: %s",
			imap_parser_get_error(conn->parser, &fatal));
		return -1;
	}
	return 1;
}

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

	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++) {
		for (i = 0; imapc_capability_names[i].name != NULL; i++) {
			const struct imapc_capability_name *cap =
				&imapc_capability_names[i];

			if (strcasecmp(*tmp, cap->name) == 0) {
				conn->capabilities |= cap->capability;
				break;
			}
		}
	}

	if ((conn->capabilities & IMAPC_CAPABILITY_IMAP4REV1) == 0) {
		imapc_connection_input_error(conn,
			"CAPABILITY list is missing IMAP4REV1");
		return -1;
	}
	return 0;
}

static int
imapc_connection_handle_resp_text_code(struct imapc_connection *conn,
				       const char *text)
{
	const char *key, *value;

	value = strchr(text, ' ');
	if (value != NULL)
		key = t_strdup_until(text, value++);
	else {
		key = text;
		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->selecting_box != NULL) {
			conn->selected_box = conn->selecting_box;
			conn->selecting_box = NULL;
		}
	}
	return 0;
}

static int
imapc_connection_handle_resp_text(struct imapc_connection *conn,
				  const struct imap_arg *args,
				  const char **text_r)
{
	const char *text, *p;

	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;
	}
	p = strchr(text, ']');
	if (p == NULL) {
		imapc_connection_input_error(conn, "Missing ']' in resp-text");
		return -1;
	}
	if (p[1] == '\0' || p[1] != ' ' || p[2] == '\0') {
		imapc_connection_input_error(conn, "Missing text in resp-text");
		return -1;
	}
	*text_r = text = t_strdup_until(text + 1, p);
	return imapc_connection_handle_resp_text_code(conn, text);
}

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;
	imap_parser_reset(conn->parser);
}

static int imapc_connection_skip_line(struct imapc_connection *conn)
{
	const unsigned char *data;
	size_t i, data_size;
	int ret = 0;

	data = i_stream_get_data(conn->input, &data_size);
	for (i = 0; i < data_size; i++) {
		if (data[i] == '\n') {
			imapc_connection_input_reset(conn);
			ret = 1;
			i++;
			break;
		}
	}
	i_stream_skip(conn->input, i);
	return ret;
}

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);
	} else if (conn->capabilities == 0) {
		imapc_connection_input_error(conn,
			"Capabilities not returned by server");
	} else {
		timeout_remove(&conn->to);
		imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DONE);
	}
}

static void imapc_connection_login_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, "Authentication failed: %s",
					     reply->text);
		return;
	}

	if (conn->capabilities == 0) {
		/* server didn't send capabilities automatically.
		   request them manually before we're done. */
		imapc_connection_cmd(conn, "CAPABILITY",
				     imapc_connection_capability_cb, conn);
		return;
	}

	timeout_remove(&conn->to);
	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_DONE);
}

static const char *
imapc_connection_get_sasl_plain_request(struct imapc_connection *conn)
{
	const struct imapc_client_settings *set = &conn->client->set;
	string_t *in, *out;

	in = t_str_new(128);
	if (set->master_user != NULL) {
		str_append(in, set->username);
		str_append_c(in, '\0');
		str_append(in, set->master_user);
	} else {
		str_append_c(in, '\0');
		str_append(in, set->username);
	}
	str_append_c(in, '\0');
	str_append(in, set->password);

	out = t_str_new(128);
	base64_encode(in->data, in->used, out);
	return str_c(out);
}

static int imapc_connection_input_banner(struct imapc_connection *conn)
{
	const struct imapc_client_settings *set = &conn->client->set;
	const struct imap_arg *imap_args;
	const char *cmd, *text;
	int ret;

	if ((ret = imapc_connection_read_line(conn, &imap_args)) <= 0)
		return ret;

	if (imapc_connection_handle_resp_text(conn, imap_args, &text) < 0)
		return -1;
	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_AUTHENTICATING);

	if (set->master_user == NULL &&
	    need_literal(set->username) && need_literal(set->password)) {
		/* We can use LOGIN command */
		imapc_connection_cmdf(conn, imapc_connection_login_cb, conn,
				      "LOGIN %s %s",
				      set->username, set->password);
	} else if ((conn->capabilities & IMAPC_CAPABILITY_SASL_IR) != 0) {
		cmd = t_strdup_printf("AUTHENTICATE PLAIN %s",
			imapc_connection_get_sasl_plain_request(conn));
		imapc_connection_cmd(conn, cmd,
				     imapc_connection_login_cb, conn);
	} else {
		cmd = t_strdup_printf("AUTHENTICATE PLAIN\r\n%s",
			imapc_connection_get_sasl_plain_request(conn));
		imapc_connection_cmd(conn, cmd,
				     imapc_connection_login_cb, 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 char *name;
	struct imapc_untagged_reply reply;
	int ret;

	if (conn->state == IMAPC_CONNECTION_STATE_CONNECTING) {
		/* input banner */
		name = imap_parser_read_word(conn->parser);
		if (name == NULL)
			return 0;

		if (strcasecmp(name, "OK") != 0) {
			imapc_connection_input_error(conn,
				"Banner doesn't begin with OK: %s", name);
			return -1;
		}
		conn->input_callback = imapc_connection_input_banner;
		return 1;
	}

	if ((ret = imapc_connection_read_line(conn, &imap_args)) <= 0)
		return ret;
	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++;
	}
	memset(&reply, 0, sizeof(reply));

	if (strcasecmp(name, "OK") == 0) {
		if (imapc_connection_handle_resp_text(conn, imap_args,
						      &reply.resp_text) < 0)
			return -1;
	}

	reply.name = name;
	reply.num = conn->cur_num;
	reply.args = imap_args;
	if (conn->selected_box != NULL) {
		reply.untagged_box_context =
			conn->selected_box->untagged_box_context;
	}
	conn->client->untagged_callback(&reply, conn->client->untagged_context);
	if (imap_arg_atom_equals(imap_args, "EXPUNGE") &&
	    conn->selected_box != NULL) {
		/* keep track of expunge map internally */
		imapc_seqmap_expunge(conn->selected_box->seqmap, conn->cur_num);
	}
	imapc_connection_input_reset(conn);
	return 1;
}

static int imapc_connection_input_plus(struct imapc_connection *conn)
{
	struct imapc_command *const *cmd_p;

	if (array_count(&conn->cmd_send_queue) == 0) {
		imapc_connection_input_error(conn, "Unexpected '+'");
		return -1;
	}
	cmd_p = array_idx(&conn->cmd_send_queue, 0);
	imapc_command_send_more(conn, *cmd_p);

	conn->input_state = IMAPC_INPUT_STATE_SKIPLINE;
	return imapc_connection_skip_line(conn);
}

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;

	memset(&reply, 0, sizeof(reply));

	linep = strchr(line, ' ');
	if (linep == NULL)
		reply.text = "";
	else {
		*linep = '\0';
		reply.text = 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 if (strcasecmp(line, "bad") == 0) {
		imapc_connection_input_error(conn,
			"Invalid state in tagged reply: %u %s",
			conn->cur_tag, line);
		return -1;
	}

	if (reply.text[0] == '[') {
		/* get resp-text */
		p = strchr(reply.text, ']');
		if (p == NULL) {
			imapc_connection_input_error(conn,
				"Missing ']' from resp-text: %u %s",
				conn->cur_tag, line);
			return -1;
		}
		reply.resp_text = t_strndup(reply.text + 1, p - reply.text - 1);
		if (imapc_connection_handle_resp_text_code(conn,
							   reply.resp_text) < 0)
			return -1;
	}

	/* 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_delete(&conn->cmd_send_queue, 0, 1);
	} 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 (cmd == NULL) {
		imapc_connection_input_error(conn,
			"Unknown tag in a reply: %u %s", conn->cur_tag, line);
		return -1;
	}

	imapc_connection_input_reset(conn);
	cmd->callback(&reply, cmd->context);
	pool_unref(&cmd->pool);
	return 0;
}

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) {
			/* FIXME: why do we get here.. */
			conn->input_state = IMAPC_INPUT_STATE_SKIPLINE;
			return imapc_connection_skip_line(conn);
		} else 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) {
			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_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;
	case IMAPC_INPUT_STATE_SKIPLINE:
		ret = imapc_connection_skip_line(conn);
		break;
	}
	return ret;
}

static void imapc_connection_input(struct imapc_connection *conn)
{
	int ret;

	if (i_stream_read(conn->input) == -1) {
		/* disconnected */
		i_error("imapc(%s): Server disconnected unexpectedly",
			conn->name);
		imapc_connection_disconnect(conn);
		return;
	}

	o_stream_cork(conn->output);
	do {
		T_BEGIN {
			ret = imapc_connection_input_one(conn);
		} T_END;
	} while (ret > 0);

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

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

	err = net_geterror(conn->fd);
	if (err != 0) {
		i_error("imapc(%s): connect(%s, %u) failed: %s",
			conn->name, net_ip2addr(ip), conn->client->set.port,
			strerror(err));
		imapc_connection_disconnect(conn);
		return;
	}
	io_remove(&conn->io);
	conn->io = io_add(conn->fd, IO_READ, imapc_connection_input, conn);
}

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

	switch (conn->state) {
	case IMAPC_CONNECTION_STATE_CONNECTING:
		i_error("imapc(%s): connect(%s, %u) timed out after %u seconds",
			conn->name, net_ip2addr(ip), conn->client->set.port,
			IMAPC_CONNECT_TIMEOUT_MSECS/1000);
		break;
	case IMAPC_CONNECTION_STATE_AUTHENTICATING:
		i_error("imapc(%s): Authentication timed out after %u seconds",
			conn->name, IMAPC_CONNECT_TIMEOUT_MSECS/1000);
		break;
	default:
		i_unreached();
	}
	imapc_connection_disconnect(conn);
}

static void imapc_connection_connect_next_ip(struct imapc_connection *conn)
{
	int fd;

	conn->prev_connect_idx = (conn->prev_connect_idx+1) % conn->ips_count;
	fd = net_connect_ip(&conn->ips[conn->prev_connect_idx],
			    conn->client->set.port, NULL);
	if (fd == -1) {
		imapc_connection_set_state(conn,
			IMAPC_CONNECTION_STATE_DISCONNECTED);
		return;
	}
	conn->fd = fd;
	conn->input = i_stream_create_fd(fd, (size_t)-1, FALSE);
	conn->output = o_stream_create_fd(fd, (size_t)-1, FALSE);
	conn->io = io_add(fd, IO_WRITE, imapc_connection_connected, conn);
	conn->parser = imap_parser_create(conn->input, NULL, (size_t)-1);
	conn->to = timeout_add(IMAPC_CONNECT_TIMEOUT_MSECS,
			       imapc_connection_timeout, conn);
}

static void
imapc_connection_dns_callback(const struct dns_lookup_result *result,
			      void *context)
{
	struct imapc_connection *conn = context;

	if (result->ret != 0) {
		i_error("imapc(%s): dns_lookup(%s) failed: %s",
			conn->name, conn->client->set.host, result->error);
		imapc_connection_set_state(conn,
			IMAPC_CONNECTION_STATE_DISCONNECTED);
		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 dns_lookup_settings dns_set;

	if (conn->fd != -1)
		return;

	memset(&dns_set, 0, sizeof(dns_set));
	dns_set.dns_client_socket_path =
		conn->client->set.dns_client_socket_path;
	dns_set.timeout_msecs = IMAPC_DNS_LOOKUP_TIMEOUT_MSECS;

	imapc_connection_set_state(conn, IMAPC_CONNECTION_STATE_CONNECTING);
	if (conn->ips_count == 0) {
		(void)dns_lookup(conn->client->set.host, &dns_set,
				 imapc_connection_dns_callback, conn);
	} else {
		imapc_connection_connect_next_ip(conn);
	}
}

static struct imapc_command *
imapc_command_begin(imapc_command_callback_t *callback, void *context)
{
	static unsigned int cmd_tag_counter = 0;
	struct imapc_command *cmd;
	pool_t pool;

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

	if (++cmd_tag_counter == 0)
		cmd_tag_counter++;
	cmd->tag = cmd_tag_counter;
	return cmd;
}

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_more(struct imapc_connection *conn,
				    struct imapc_command *cmd)
{
	const unsigned char *p;
	unsigned int seek_pos, start_pos, end_pos, size;

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

	seek_pos = cmd->send_pos;
	if (seek_pos != 0) {
		/* 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;

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

	if (cmd->send_pos == cmd->data->used) {
		/* everything sent. move command to wait list. */
		i_assert(*array_idx(&conn->cmd_send_queue, 0) == cmd);
		array_delete(&conn->cmd_send_queue, 0, 1);
		array_append(&conn->cmd_wait_list, &cmd, 1);

		if (array_count(&conn->cmd_send_queue) > 0 &&
		    conn->state == IMAPC_CONNECTION_STATE_DONE) {
			/* send the next command in queue */
			struct imapc_command *const *cmd2_p =
				array_idx(&conn->cmd_send_queue, 0);
			imapc_command_send_more(conn, *cmd2_p);
		}
	}
}

static void imapc_command_send(struct imapc_connection *conn,
			       struct imapc_command *cmd)
{
	switch (conn->state) {
	case IMAPC_CONNECTION_STATE_AUTHENTICATING:
		array_insert(&conn->cmd_send_queue, 0, &cmd, 1);
		imapc_command_send_more(conn, cmd);
		break;
	case IMAPC_CONNECTION_STATE_DONE:
		array_append(&conn->cmd_send_queue, &cmd, 1);
		if (array_count(&conn->cmd_send_queue) == 1)
			imapc_command_send_more(conn, cmd);
		break;
	default:
		array_append(&conn->cmd_send_queue, &cmd, 1);
		break;
	}
}

void imapc_connection_cmd(struct imapc_connection *conn, const char *cmdline,
			  imapc_command_callback_t *callback, void *context)
{
	struct imapc_command *cmd;
	unsigned int len = strlen(cmdline);

	cmd = imapc_command_begin(callback, context);
	cmd->data = str_new(cmd->pool, len + 2);
	str_printfa(cmd->data, "%u %s\r\n", cmd->tag, cmdline);
	imapc_command_send(conn, cmd);
}

void imapc_connection_cmdf(struct imapc_connection *conn,
			   imapc_command_callback_t *callback, void *context,
			   const char *cmd_fmt, ...)
{
	va_list args;

	va_start(args, cmd_fmt);
	imapc_connection_cmdvf(conn, callback, context, cmd_fmt, args);
	va_end(args);
}

void imapc_connection_cmdvf(struct imapc_connection *conn,
			   imapc_command_callback_t *callback, void *context,
			   const char *cmd_fmt, va_list args)
{
	struct imapc_command *cmd;
	unsigned int i;

	cmd = imapc_command_begin(callback, context);
	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 's': {
			const char *arg = va_arg(args, const char *);

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

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

	imapc_command_send(conn, cmd);
}

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

void imapc_connection_select(struct imapc_client_mailbox *box, const char *name,
			     imapc_command_callback_t *callback, void *context)
{
	struct imapc_connection *conn = box->conn;

	i_assert(conn->selecting_box == NULL);

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

	imapc_connection_cmdf(conn, callback, context, "SELECT %s", name);
}