view src/lib-imap-urlauth/imap-urlauth-connection.c @ 22325:e01bc3015b2f

lib-index: Check .log.2 rotation only when syncing Instead of also whenever appending transactions to .log file. This shouldn't change the behavior much, and it's needed for the following change to work correctly.
author Timo Sirainen <timo.sirainen@dovecot.fi>
date Tue, 11 Jul 2017 15:33:56 +0300
parents 2e2563132d5f
children cb108f786fb4
line wrap: on
line source

/* Copyright (c) 2013-2017 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "llist.h"
#include "str.h"
#include "str-sanitize.h"
#include "strescape.h"
#include "ioloop.h"
#include "safe-mkstemp.h"
#include "hostpid.h"
#include "net.h"
#include "istream.h"
#include "ostream.h"
#include "write-full.h"
#include "array.h"
#include "aqueue.h"
#include "mail-user.h"
#include "imap-urlauth-fetch.h"

#include "imap-urlauth-connection.h"

enum imap_urlauth_state {
	IMAP_URLAUTH_STATE_DISCONNECTED = 0,
	IMAP_URLAUTH_STATE_AUTHENTICATING,
	IMAP_URLAUTH_STATE_AUTHENTICATED,
	IMAP_URLAUTH_STATE_SELECTING_TARGET,
	IMAP_URLAUTH_STATE_UNSELECTING_TARGET,
	IMAP_URLAUTH_STATE_READY,
	IMAP_URLAUTH_STATE_REQUEST_PENDING,
	IMAP_URLAUTH_STATE_REQUEST_WAIT,
};

struct imap_urlauth_request {
	struct imap_urlauth_target *target;
	struct imap_urlauth_request *prev, *next;
	
	char *url;
	enum imap_urlauth_fetch_flags flags;

	char *bodypartstruct;

	imap_urlauth_request_callback_t *callback;
	void *context;

	unsigned int binary_has_nuls;
};

struct imap_urlauth_target {
	struct imap_urlauth_target *prev, *next;

	char *userid;

	struct imap_urlauth_request *requests_head, *requests_tail;
};

struct imap_urlauth_connection {
	int refcount;

	char *path, *session_id;
	struct mail_user *user;

	int fd;
	struct istream *input;
	struct ostream *output;
	struct io *io;

	struct timeout *to_reconnect, *to_idle, *to_response;
	time_t last_reconnect;
	unsigned int reconnect_attempts;
	unsigned int idle_timeout_msecs;

	char *literal_temp_path;
	int literal_fd;
	buffer_t *literal_buf;
	uoff_t literal_size, literal_bytes_left;

	enum imap_urlauth_state state;

	/* userid => target struct */
	struct imap_urlauth_target *targets_head, *targets_tail;

	unsigned int reading_literal:1;
};

#define IMAP_URLAUTH_RECONNECT_MIN_SECS 2
#define IMAP_URLAUTH_RECONNECT_MAX_ATTEMPTS 3

#define IMAP_URLAUTH_RESPONSE_TIMEOUT_MSECS 2*60*1000

#define IMAP_URLAUTH_HANDSHAKE "VERSION\timap-urlauth\t1\t0\n"

#define IMAP_URLAUTH_MAX_INLINE_LITERAL_SIZE (1024*32)

static void imap_urlauth_connection_disconnect
	(struct imap_urlauth_connection *conn, const char *reason);
static void imap_urlauth_connection_abort
	(struct imap_urlauth_connection *conn, const char *reason);
static void imap_urlauth_connection_reconnect
	(struct imap_urlauth_connection *conn);
static void imap_urlauth_connection_idle_disconnect
	(struct imap_urlauth_connection *conn);
static void imap_urlauth_connection_timeout_abort
	(struct imap_urlauth_connection *conn);
static void imap_urlauth_connection_fail
	(struct imap_urlauth_connection *conn);

struct imap_urlauth_connection *
imap_urlauth_connection_init(const char *path, struct mail_user *user,
			     const char *session_id,
			     unsigned int idle_timeout_msecs)
{
	struct imap_urlauth_connection *conn;

	conn = i_new(struct imap_urlauth_connection, 1);
	conn->refcount = 1;
	conn->path = i_strdup(path);
	if (session_id != NULL)
		conn->session_id = i_strdup(session_id);
	conn->user = user;
	conn->fd = -1;
	conn->literal_fd = -1;
	conn->idle_timeout_msecs = idle_timeout_msecs;
	return conn;
}

void imap_urlauth_connection_deinit(struct imap_urlauth_connection **_conn)
{
	struct imap_urlauth_connection *conn = *_conn;

	*_conn = NULL;

	imap_urlauth_connection_abort(conn, NULL);

	i_free(conn->path);
	if (conn->session_id != NULL)
		i_free(conn->session_id);

	i_assert(conn->to_idle == NULL);
	i_assert(conn->to_reconnect == NULL);
	i_assert(conn->to_response == NULL);

	i_free(conn);
}

static void
imap_urlauth_stop_response_timeout(struct imap_urlauth_connection *conn)
{
	if (conn->to_response != NULL)
		timeout_remove(&conn->to_response);
}

static void
imap_urlauth_start_response_timeout(struct imap_urlauth_connection *conn)
{
	imap_urlauth_stop_response_timeout(conn);
	conn->to_response = timeout_add(IMAP_URLAUTH_RESPONSE_TIMEOUT_MSECS,
		imap_urlauth_connection_timeout_abort, conn);
}

static struct imap_urlauth_target *
imap_urlauth_connection_get_target(struct imap_urlauth_connection *conn,
				   const char *target_user)
{
	struct imap_urlauth_target *target = conn->targets_head;

	while (target != NULL) {
		if (strcmp(target->userid, target_user) == 0)
			return target;
		target = target->next;
	}

	target = i_new(struct imap_urlauth_target, 1);
	target->userid = i_strdup(target_user);
	DLLIST2_APPEND(&conn->targets_head, &conn->targets_tail, target);
	return target;
}

static void
imap_urlauth_target_free(struct imap_urlauth_connection *conn,
			 struct imap_urlauth_target *target)
{
	DLLIST2_REMOVE(&conn->targets_head, &conn->targets_tail, target);
	i_free(target->userid);
	i_free(target);
}

static void
imap_urlauth_connection_select_target(struct imap_urlauth_connection *conn)
{
	struct imap_urlauth_target *target = conn->targets_head;
	const char *cmd;

	if (target == NULL || conn->state != IMAP_URLAUTH_STATE_AUTHENTICATED)
		return;

	if (conn->user->mail_debug)
		i_debug("imap-urlauth: Selecting target user `%s'", target->userid);

	conn->state = IMAP_URLAUTH_STATE_SELECTING_TARGET;
	cmd = t_strdup_printf("USER\t%s\n", str_tabescape(target->userid));
	if (o_stream_send_str(conn->output, cmd) < 0) {
		i_warning("Error sending USER request to imap-urlauth server: %m");
		imap_urlauth_connection_fail(conn);
	}

	imap_urlauth_start_response_timeout(conn);
}

static void
imap_urlauth_connection_send_request(struct imap_urlauth_connection *conn)
{
	struct imap_urlauth_request *urlreq;
	string_t *cmd;

	if (conn->targets_head == NULL ||
	    (conn->targets_head->requests_head == NULL &&
	     conn->targets_head->next == NULL &&
	     conn->state == IMAP_URLAUTH_STATE_READY)) {
		if (conn->user->mail_debug)
			i_debug("imap-urlauth: No more requests pending; scheduling disconnect");
		if (conn->to_idle != NULL)
			timeout_remove(&conn->to_idle);
		if (conn->idle_timeout_msecs > 0) {
			conn->to_idle =	timeout_add(conn->idle_timeout_msecs,
				imap_urlauth_connection_idle_disconnect, conn);
		}
		return;
	}

	if (conn->state == IMAP_URLAUTH_STATE_AUTHENTICATED) {
		imap_urlauth_connection_select_target(conn);
		return;
	}

	if (conn->state != IMAP_URLAUTH_STATE_READY)
		return;

	urlreq = conn->targets_head->requests_head;
	if (urlreq == NULL) {
		if (conn->targets_head->next == NULL)
			return;
		
		conn->state = IMAP_URLAUTH_STATE_UNSELECTING_TARGET;
		imap_urlauth_target_free(conn, conn->targets_head);

		if (o_stream_send_str(conn->output, "END\n") < 0) {
			i_warning("Error sending END request to imap-urlauth server: %m");
			imap_urlauth_connection_fail(conn);
		}
		imap_urlauth_start_response_timeout(conn);
		return;
	}	

	if (conn->user->mail_debug)
		i_debug("imap-urlauth: Fetching URL `%s'", urlreq->url);

	cmd = t_str_new(128);
	str_append(cmd, "URL\t");
	str_append_tabescaped(cmd, urlreq->url);
	if ((urlreq->flags & IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE) != 0)
		str_append(cmd, "\tbpstruct");
	if ((urlreq->flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0)
		str_append(cmd, "\tbinary");
	else if ((urlreq->flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0)
		str_append(cmd, "\tbody");
	str_append_c(cmd, '\n');

	conn->state = IMAP_URLAUTH_STATE_REQUEST_PENDING;
	if (o_stream_send(conn->output, str_data(cmd), str_len(cmd)) < 0) {
		i_warning("Error sending URL request to imap-urlauth server: %m");
		imap_urlauth_connection_fail(conn);
	}

	imap_urlauth_start_response_timeout(conn);
}

struct imap_urlauth_request *
imap_urlauth_request_new(struct imap_urlauth_connection *conn,
			 const char *target_user, const char *url,
			 enum imap_urlauth_fetch_flags flags,
			 imap_urlauth_request_callback_t *callback,
			 void *context)
{
	struct imap_urlauth_request *urlreq;
	struct imap_urlauth_target *target;

	target = imap_urlauth_connection_get_target(conn, target_user);
	
	urlreq = i_new(struct imap_urlauth_request, 1);
	urlreq->url = i_strdup(url);
	urlreq->flags = flags;
	urlreq->target = target;
	urlreq->callback = callback;
	urlreq->context = context;

	DLLIST2_APPEND(&target->requests_head, &target->requests_tail, urlreq);

	if (conn->to_idle != NULL)
		timeout_remove(&conn->to_idle);
	
	if (conn->user->mail_debug) {
		i_debug("imap-urlauth: Added request for URL `%s' from user `%s'",
			url, target_user);
	}

	imap_urlauth_connection_send_request(conn);
	return urlreq;
}

static void imap_urlauth_request_free(struct imap_urlauth_request *urlreq)
{
	struct imap_urlauth_target *target = urlreq->target;

	DLLIST2_REMOVE(&target->requests_head, &target->requests_tail, urlreq);
	i_free(urlreq->url);
	i_free(urlreq->bodypartstruct);
	i_free(urlreq);
}

static void imap_urlauth_request_drop(struct imap_urlauth_connection *conn,
				struct imap_urlauth_request *urlreq)
{
	if ((conn->state == IMAP_URLAUTH_STATE_REQUEST_PENDING ||
			conn->state == IMAP_URLAUTH_STATE_REQUEST_WAIT) &&
	    conn->targets_head != NULL &&
	    conn->targets_head->requests_head == urlreq) {
		/* cannot just drop pending request without breaking
		   protocol state */
		return;
	}
	imap_urlauth_request_free(urlreq);

}

void imap_urlauth_request_abort(struct imap_urlauth_connection *conn,
				struct imap_urlauth_request *urlreq)
{
	imap_urlauth_request_callback_t *callback;

	callback = urlreq->callback;
	urlreq->callback = NULL;
	if (callback != NULL) {
		T_BEGIN {
			callback(NULL, urlreq->context);
		} T_END;
	}

	imap_urlauth_request_drop(conn, urlreq);
}

static void
imap_urlauth_request_fail(struct imap_urlauth_connection *conn,
			  struct imap_urlauth_request *urlreq,
			  const char *error)
{
	struct imap_urlauth_fetch_reply reply;
	imap_urlauth_request_callback_t *callback;
	int ret = 1;

	callback = urlreq->callback;
	urlreq->callback = NULL;
	if (callback != NULL) {
		i_zero(&reply);
		reply.url = urlreq->url;
		reply.flags = urlreq->flags;
		reply.succeeded = FALSE;
		reply.error = error;

		T_BEGIN {
			ret = callback(&reply, urlreq->context);
		} T_END;
	}

	void *urlreq_context = urlreq->context;
	imap_urlauth_request_drop(conn, urlreq);

	if (ret < 0) {
		/* Drop any related requests upon error */
		imap_urlauth_request_abort_by_context(conn, urlreq_context);
	}

	if (ret != 0)
		imap_urlauth_connection_continue(conn);
}

static void
imap_urlauth_target_abort(struct imap_urlauth_connection *conn,
			  struct imap_urlauth_target *target)
{
	struct imap_urlauth_request *urlreq, *next;

	urlreq = target->requests_head;
	while (urlreq != NULL) {
		next = urlreq->next;
		imap_urlauth_request_abort(conn, urlreq);
		urlreq = next;
	}

	imap_urlauth_target_free(conn, target);
}

static void
imap_urlauth_target_fail(struct imap_urlauth_connection *conn,
			 struct imap_urlauth_target *target, const char *error)
{
	struct imap_urlauth_request *urlreq, *next;

	urlreq = target->requests_head;
	while (urlreq != NULL) {
		next = urlreq->next;
		imap_urlauth_request_fail(conn, urlreq, error);
		urlreq = next;
	}

	imap_urlauth_target_free(conn, target);
}

static void
imap_urlauth_target_abort_by_context(struct imap_urlauth_connection *conn,
				     struct imap_urlauth_target *target,
				     void *context)
{
	struct imap_urlauth_request *urlreq, *next;

	/* abort all matching requests */
	urlreq = target->requests_head;
	while (urlreq != NULL) {
		next = urlreq->next;
		if (urlreq->context == context)
			imap_urlauth_request_abort(conn, urlreq);
		urlreq = next;
	}

	if (target->requests_head == NULL)
		imap_urlauth_target_free(conn, target);
}

static void
imap_urlauth_connection_abort(struct imap_urlauth_connection *conn,
			      const char *reason)
{
	struct imap_urlauth_target *target, *next;
	
	if (reason == NULL)
		reason = "Aborting due to error";
	imap_urlauth_connection_disconnect(conn, reason);

	/* abort all requests */
	target = conn->targets_head;
	while (target != NULL) {
		next = target->next;
		imap_urlauth_target_abort(conn, target);
		target = next;
	}
}

void imap_urlauth_request_abort_by_context(struct imap_urlauth_connection *conn,
					   void *context)
{
	struct imap_urlauth_target *target, *next;

	/* abort all matching requests */
	target = conn->targets_head;
	while (target != NULL) {
		next = target->next;
		imap_urlauth_target_abort_by_context(conn, target, context);
		target = next;
	}
}

static void imap_urlauth_connection_fail(struct imap_urlauth_connection *conn)
{
	if (conn->reconnect_attempts > IMAP_URLAUTH_RECONNECT_MAX_ATTEMPTS) {
		imap_urlauth_connection_abort(conn,
			"Connection failed and connection attempts exhausted");
	} else {
		imap_urlauth_connection_reconnect(conn);
	}
}

static int
imap_urlauth_connection_create_temp_fd(struct imap_urlauth_connection *conn,
				       const char **path_r)
{
	string_t *path;
	int fd;

	path = t_str_new(128);
	mail_user_set_get_temp_prefix(path, conn->user->set);
	fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
	if (fd == -1) {
		i_error("safe_mkstemp(%s) failed: %m", str_c(path));
		return -1;
	}

	/* we just want the fd, unlink it */
	if (i_unlink(str_c(path)) < 0) {
		/* shouldn't happen.. */
		i_close_fd(&fd);
		return -1;
	}

	*path_r = str_c(path);
	return fd;
}

static int
imap_urlauth_connection_read_literal_init(struct imap_urlauth_connection *conn,
					  uoff_t size)
{
	const char *path;

	i_assert(conn->literal_fd == -1 && conn->literal_buf == NULL);

	if (size <= IMAP_URLAUTH_MAX_INLINE_LITERAL_SIZE) {
		/* read the literal directly */
		if (size > 0) {
			conn->literal_buf =
				buffer_create_dynamic(default_pool, size);
		}
	} else {
		/* read it into a file */
		conn->literal_fd =
			imap_urlauth_connection_create_temp_fd(conn, &path);
		if (conn->literal_fd == -1)
			return -1;
		conn->literal_temp_path = i_strdup(path);
	}

	conn->literal_size = size;
	conn->literal_bytes_left = size;
	conn->reading_literal = TRUE;
	return 1;
}

void imap_urlauth_connection_continue(struct imap_urlauth_connection *conn)
{
	i_assert(conn->targets_head != NULL);
	i_assert(conn->targets_head->requests_head != NULL);

	if (conn->state != IMAP_URLAUTH_STATE_REQUEST_WAIT)
		return;

	conn->state = IMAP_URLAUTH_STATE_READY;
	imap_urlauth_request_free(conn->targets_head->requests_head);

	imap_urlauth_connection_send_request(conn);
}

static int
imap_urlauth_connection_read_literal_data(struct imap_urlauth_connection *conn)
{
	const unsigned char *data;
	size_t size;

	/* read data */
	data = i_stream_get_data(conn->input, &size);
	if (size > conn->literal_bytes_left)
		size = conn->literal_bytes_left;

	/* write to buffer or file */
	if (size > 0) {
		if (conn->literal_fd >= 0) {
			if (write_full(conn->literal_fd, data, size) < 0) {
				i_error("imap-urlauth: write(%s) failed: %m",
					conn->literal_temp_path);
				return -1;
			}
		} else {
			i_assert(conn->literal_buf != NULL);
			buffer_append(conn->literal_buf, data, size);
		}
		i_stream_skip(conn->input, size);
		conn->literal_bytes_left -= size;
	}

	/* exit if not finished */
	if (conn->literal_bytes_left > 0)
		return 0;

	/* read LF guard */
	data = i_stream_get_data(conn->input, &size);
	if (size < 1)
		return 0;

	/* check LF guard */
	if (data[0] != '\n') {
		i_error("imap-urlauth: no LF at end of literal (found 0x%x)",
			data[0]);
		return -1;
	}
	i_stream_skip(conn->input, 1);
	return 1;
}

static void literal_stream_destroy(buffer_t *buffer)
{
	buffer_free(&buffer);
}

static int
imap_urlauth_fetch_reply_set_literal_stream(struct imap_urlauth_connection *conn,
					    struct imap_urlauth_fetch_reply *reply)
{
	const unsigned char *data;
	size_t size;
	uoff_t fd_size;

	if (conn->literal_fd != -1) {
		reply->input = i_stream_create_fd_autoclose(&conn->literal_fd,
							    (size_t)-1);
		if (i_stream_get_size(reply->input, TRUE, &fd_size) < 1 ||
		    fd_size != conn->literal_size) {
			i_stream_unref(&reply->input);
			i_error("imap-urlauth: Failed to obtain proper size from literal stream");
			imap_urlauth_connection_abort(conn,
				"Failed during literal transfer");
			return -1;
		}
	} else {
		data = buffer_get_data(conn->literal_buf, &size);
		i_assert(size == conn->literal_size);
		reply->input = i_stream_create_from_data(data, size);
		i_stream_add_destroy_callback(reply->input,
					      literal_stream_destroy,
					      conn->literal_buf);
	}
	reply->size = conn->literal_size;
	return 0;
}

static int
imap_urlauth_connection_read_literal(struct imap_urlauth_connection *conn)
{
	struct imap_urlauth_request *urlreq = conn->targets_head->requests_head;
	struct imap_urlauth_fetch_reply reply;
	imap_urlauth_request_callback_t *callback;
	int ret;

	i_assert(conn->reading_literal);
	i_assert(urlreq != NULL);

	if (conn->literal_size > 0) {
		ret = imap_urlauth_connection_read_literal_data(conn);
		if (ret <= 0)
			return ret;
	}
	i_assert(conn->literal_bytes_left == 0);

	/* reply */
	i_zero(&reply);
	reply.url = urlreq->url;
	reply.flags = urlreq->flags;
	reply.bodypartstruct = urlreq->bodypartstruct;
	reply.binary_has_nuls = urlreq->binary_has_nuls;

	if (conn->literal_size > 0) {
		if (imap_urlauth_fetch_reply_set_literal_stream(conn, &reply) < 0)
			return -1;
	}
	reply.succeeded = TRUE;

	ret = 1;
	callback = urlreq->callback;
	urlreq->callback = NULL;
	if (callback != NULL) T_BEGIN {
		ret = callback(&reply, urlreq->context);
	} T_END;

	if (reply.input != NULL)
		i_stream_unref(&reply.input);

	if (ret < 0) {
		/* Drop any related requests upon error */
		imap_urlauth_request_abort_by_context(conn, urlreq->context);
	}

	conn->state = IMAP_URLAUTH_STATE_REQUEST_WAIT;
	if (ret != 0)
		imap_urlauth_connection_continue(conn);

	/* finished */
	i_free_and_null(conn->literal_temp_path);
	conn->literal_fd = -1;
	conn->literal_buf = NULL;
	conn->reading_literal = FALSE;
	return 1;
}

static int imap_urlauth_input_pending(struct imap_urlauth_connection *conn)
{
	struct imap_urlauth_request *urlreq;
	const char *response, *const *args, *bpstruct = NULL;
	uoff_t literal_size;

	i_assert(conn->targets_head != NULL);
	i_assert(conn->targets_head->requests_head != NULL);
	urlreq = conn->targets_head->requests_head;

	if (conn->reading_literal) {
		/* Read pending literal; may callback */
		return imap_urlauth_connection_read_literal(conn);
	}

	/* "OK"[<metadata-items>]"\t"<literal-size>"\n" or
	   "NO"["\terror="<error>]"\n" */
	if ((response = i_stream_next_line(conn->input)) == NULL)
		return 0;
	imap_urlauth_stop_response_timeout(conn);

	args = t_strsplit_tabescaped(response);
	if (args[0] == NULL) {
		i_error("imap-urlauth: Empty URL response: %s",
			str_sanitize(response, 80));
		return -1;
	}

	if (strcmp(args[0], "OK") != 0 || args[1] == NULL) {
		if (strcmp(args[0], "NO") == 0) {
			const char *param = args[1], *error = NULL;

			if (param != NULL &&
			    strncasecmp(param, "error=", 6) == 0 &&
			    param[6] != '\0') {
				error = param+6;
			}
			conn->state = IMAP_URLAUTH_STATE_REQUEST_WAIT;
			imap_urlauth_request_fail(conn,
				conn->targets_head->requests_head, error);
			return 1;
		}
		i_error("imap-urlauth: Unexpected URL response: %s",
			str_sanitize(response, 80));
		return -1;
	}

	/* read metadata */
	args++;
	for (; args[1] != NULL; args++) {
		const char *param = args[0];

		if (strcasecmp(param, "hasnuls") == 0) {
			urlreq->binary_has_nuls = TRUE;
		} else if (strncasecmp(param, "bpstruct=", 9) == 0 &&
			   param[9] != '\0') {
			bpstruct = param+9;
		}
	}

	/* read literal size */
	if (str_to_uoff(args[0], &literal_size) < 0) {
		i_error("imap-urlauth: "
			"Overflowing unsigned integer value for literal size: %s",
			args[1]);
		return -1;
	}

	/* read literal */
	if (imap_urlauth_connection_read_literal_init(conn, literal_size) < 0)
		return -1;

	urlreq->bodypartstruct = i_strdup(bpstruct);
	return imap_urlauth_connection_read_literal(conn);
}

static int imap_urlauth_input_next(struct imap_urlauth_connection *conn)
{
	const char *response;
	int ret;

	switch (conn->state) {
	case IMAP_URLAUTH_STATE_AUTHENTICATING:
	case IMAP_URLAUTH_STATE_UNSELECTING_TARGET:
		if ((response = i_stream_next_line(conn->input)) == NULL)
			return 0;
		imap_urlauth_stop_response_timeout(conn);

		if (strcasecmp(response, "OK") != 0) {
			if (conn->state == IMAP_URLAUTH_STATE_AUTHENTICATING)
				i_error("imap-urlauth: Failed to authenticate to service: "
					"Got unexpected response: %s", str_sanitize(response, 80));
			else
				i_error("imap-urlauth: Failed to unselect target user: "
					"Got unexpected response: %s", str_sanitize(response, 80));
			imap_urlauth_connection_abort(conn, NULL);
			return -1;
		}

		if (conn->user->mail_debug) {
			if (conn->state == IMAP_URLAUTH_STATE_AUTHENTICATING)
				i_debug("imap-urlauth: Successfully authenticated to service");
			else
				i_debug("imap-urlauth: Successfully unselected target user");
		}

		conn->state = IMAP_URLAUTH_STATE_AUTHENTICATED;
		imap_urlauth_connection_select_target(conn);
		return 0;
	case IMAP_URLAUTH_STATE_SELECTING_TARGET:
		if ((response = i_stream_next_line(conn->input)) == NULL)
			return 0;
		imap_urlauth_stop_response_timeout(conn);

		i_assert(conn->targets_head != NULL);

		if (strcasecmp(response, "NO") == 0) {
			if (conn->user->mail_debug) {
				i_debug("imap-urlauth: Failed to select target user %s",
					conn->targets_head->userid);
			}
			imap_urlauth_target_fail(conn, conn->targets_head, NULL);

			conn->state = IMAP_URLAUTH_STATE_AUTHENTICATED;
			imap_urlauth_connection_select_target(conn);
			return 0;
		}
		if (strcasecmp(response, "OK") != 0) {
			i_error("imap-urlauth: Failed to select target user %s: "
				"Got unexpected response: %s", conn->targets_head->userid,
				str_sanitize(response, 80));
			imap_urlauth_connection_abort(conn, NULL);
			return -1;
		}

		if (conn->user->mail_debug) {
			i_debug("imap-urlauth: Successfully selected target user %s",
				conn->targets_head->userid);
		}
		conn->state = IMAP_URLAUTH_STATE_READY;
		imap_urlauth_connection_send_request(conn);
		return 0;
	case IMAP_URLAUTH_STATE_AUTHENTICATED:
	case IMAP_URLAUTH_STATE_READY:
	case IMAP_URLAUTH_STATE_REQUEST_WAIT:
		if ((response = i_stream_next_line(conn->input)) == NULL)
			return 0;

		i_error("imap-urlauth: Received input while no requests were pending: %s",
			str_sanitize(response, 80));
		imap_urlauth_connection_abort(conn, NULL);
		return -1;
	case IMAP_URLAUTH_STATE_REQUEST_PENDING:
		if ((ret = imap_urlauth_input_pending(conn)) < 0)
			imap_urlauth_connection_fail(conn);
		return ret;
	case IMAP_URLAUTH_STATE_DISCONNECTED:
		break;
	}
	i_unreached();
}

static void imap_urlauth_input(struct imap_urlauth_connection *conn)
{
	int ret;

	i_assert(conn->state != IMAP_URLAUTH_STATE_DISCONNECTED);

	if (conn->input->closed) {
		/* disconnected */
		i_error("imap-urlauth: Service disconnected unexpectedly");
		imap_urlauth_connection_fail(conn);
		return;
	}

	switch (i_stream_read(conn->input)) {
	case -1:
		/* disconnected */
		i_error("imap-urlauth: Service disconnected unexpectedly");
		imap_urlauth_connection_fail(conn);
		return;
	case -2:
		/* input buffer full */
		i_error("imap-urlauth: Service sent too large input");
		imap_urlauth_connection_abort(conn, NULL);
		return;
	}

	while (!conn->input->closed) {
		if ((ret = imap_urlauth_input_next(conn)) <= 0)
			break;
	}
}

static int
imap_urlauth_connection_do_connect(struct imap_urlauth_connection *conn)
{
	string_t *str;
	int fd;

	if (conn->state != IMAP_URLAUTH_STATE_DISCONNECTED) {
		imap_urlauth_connection_send_request(conn);
		return 1;
	}

	if (conn->user->auth_token == NULL) {
		i_error("imap-urlauth: cannot authenticate because no auth token "
			"is available for this session (standalone IMAP?).");
		imap_urlauth_connection_abort(conn, NULL);
		return -1;
	}

	if (conn->user->mail_debug)
		i_debug("imap-urlauth: Connecting to service at %s", conn->path);

	i_assert(conn->fd == -1);
	fd = net_connect_unix(conn->path);
	if (fd == -1) {
		i_error("imap-urlauth: net_connect_unix(%s) failed: %m",
			conn->path);
		imap_urlauth_connection_abort(conn, NULL);
		return -1;
	}

	if (conn->to_reconnect != NULL)
		timeout_remove(&conn->to_reconnect);

	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_READ, imap_urlauth_input, conn);
	conn->state = IMAP_URLAUTH_STATE_AUTHENTICATING;

	str = t_str_new(128);
	str_printfa(str, IMAP_URLAUTH_HANDSHAKE"AUTH\t%s\t", my_pid);
	str_append_tabescaped(str, conn->user->username);
	str_append_c(str, '\t');
	if (conn->session_id != NULL)
		str_append_tabescaped(str, conn->session_id);
	str_append_c(str, '\t');
	str_append_tabescaped(str, conn->user->auth_token);
	str_append_c(str, '\n');
	if (o_stream_send(conn->output, str_data(str), str_len(str)) < 0) {
		i_warning("Error sending handshake to imap-urlauth server: %m");
		imap_urlauth_connection_abort(conn, NULL);
		return -1;
	}

	imap_urlauth_start_response_timeout(conn);
	return 0;
}

int imap_urlauth_connection_connect(struct imap_urlauth_connection *conn)
{
	conn->reconnect_attempts = 0;

	if (conn->to_reconnect == NULL)
		return imap_urlauth_connection_do_connect(conn);
	return 0;
}

static void imap_urlauth_connection_disconnect
(struct imap_urlauth_connection *conn, const char *reason)
{
	conn->state = IMAP_URLAUTH_STATE_DISCONNECTED;

	if (conn->fd != -1) {
		if (conn->user->mail_debug) {
			if (reason == NULL)
				i_debug("imap-urlauth: Disconnecting from service");
			else
				i_debug("imap-urlauth: Disconnected: %s", reason);
		}

		io_remove(&conn->io);
		i_stream_destroy(&conn->input);
		o_stream_destroy(&conn->output);
		net_disconnect(conn->fd);
		conn->fd = -1;
	}
	conn->reading_literal = FALSE;

	if (conn->literal_fd != -1) {
		if (close(conn->literal_fd) < 0)
			i_error("imap-urlauth: close(%s) failed: %m", conn->literal_temp_path);

		i_free_and_null(conn->literal_temp_path);
		conn->literal_fd = -1;
	}

	if (conn->literal_buf != NULL)
		buffer_free(&conn->literal_buf);
	if (conn->to_reconnect != NULL)
		timeout_remove(&conn->to_reconnect);
	if (conn->to_idle != NULL)
		timeout_remove(&conn->to_idle);
	imap_urlauth_stop_response_timeout(conn);
}

static void
imap_urlauth_connection_do_reconnect(struct imap_urlauth_connection *conn)
{
	if (conn->reconnect_attempts >= IMAP_URLAUTH_RECONNECT_MAX_ATTEMPTS) {
		imap_urlauth_connection_abort(conn,
			"Connection failed and connection attempts exhausted");
		return;
	}

	if (ioloop_time - conn->last_reconnect < IMAP_URLAUTH_RECONNECT_MIN_SECS) {
		if (conn->user->mail_debug)
			i_debug("imap-urlauth: Scheduling reconnect");
		if (conn->to_reconnect != NULL)
			timeout_remove(&conn->to_reconnect);
		conn->to_reconnect =
			timeout_add(IMAP_URLAUTH_RECONNECT_MIN_SECS*1000,
				imap_urlauth_connection_do_reconnect, conn);
	} else {
		conn->reconnect_attempts++;
		conn->last_reconnect = ioloop_time;
		(void)imap_urlauth_connection_do_connect(conn);
	}
}

static void
imap_urlauth_connection_reconnect(struct imap_urlauth_connection *conn)
{
	imap_urlauth_connection_disconnect(conn, NULL);

	/* don't reconnect if there are no requests */
	if (conn->targets_head == NULL)
		return;

	imap_urlauth_connection_do_reconnect(conn);
}

static void
imap_urlauth_connection_idle_disconnect(struct imap_urlauth_connection *conn)
{
	imap_urlauth_connection_disconnect(conn, "Idle timeout");
}

static void
imap_urlauth_connection_timeout_abort(struct imap_urlauth_connection *conn)
{
	imap_urlauth_connection_abort(conn, "Service is not responding");
}

bool imap_urlauth_connection_is_connected(struct imap_urlauth_connection *conn)
{
	return conn->fd != -1 && conn->state != IMAP_URLAUTH_STATE_DISCONNECTED;
}