/* Copyright (c) 2013-2016 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) {
		memset(&reply, 0, sizeof(reply));
		reply.url = urlreq->url;
		reply.flags = urlreq->flags;
		reply.succeeded = FALSE;
		reply.error = error;

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

	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 */
	memset(&reply, 0, sizeof(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;
}
