/* Copyright (c) 2002-2018 Pigeonhole authors, see the included COPYING file
 */

#include "lib.h"

#include "sieve-common.h"

#include "sieve-ldap-storage.h"

/* FIXME: Imported this from Dovecot auth for now. We're working on a proper
   lib-ldap, but, until then, some code is duplicated here. */

#if defined(SIEVE_BUILTIN_LDAP) || defined(PLUGIN_BUILD)

#include "net.h"
#include "ioloop.h"
#include "array.h"
#include "hash.h"
#include "aqueue.h"
#include "str.h"
#include "time-util.h"
#include "env-util.h"
#include "var-expand.h"
#include "istream.h"

#include <stddef.h>
#include <unistd.h>

struct db_ldap_result {
	int refcount;
	LDAPMessage *msg;
};

struct db_ldap_result_iterate_context {
	pool_t pool;

	struct auth_request *auth_request;
	const ARRAY_TYPE(ldap_field) *attr_map;
	unsigned int attr_idx;

	/* attribute name => value */
	HASH_TABLE(char *, struct db_ldap_value *) ldap_attrs;

	const char *val_1_arr[2];
	string_t *var, *debug;

	bool skip_null_values;
	bool iter_dn_values;
};

struct db_ldap_sasl_bind_context {
	const char *authcid;
	const char *passwd;
	const char *realm;
	const char *authzid;
};

static struct ldap_connection *ldap_connections = NULL;

static int db_ldap_bind(struct ldap_connection *conn);
static void db_ldap_conn_close(struct ldap_connection *conn);

int ldap_deref_from_str(const char *str, int *deref_r)
{
	if (strcasecmp(str, "never") == 0)
		*deref_r = LDAP_DEREF_NEVER;
	else if (strcasecmp(str, "searching") == 0)
		*deref_r = LDAP_DEREF_SEARCHING;
	else if (strcasecmp(str, "finding") == 0)
		*deref_r = LDAP_DEREF_FINDING;
	else if (strcasecmp(str, "always") == 0)
		*deref_r = LDAP_DEREF_ALWAYS;
	else 
		return -1;
	return 0;
}

int ldap_scope_from_str(const char *str, int *scope_r)
{
	if (strcasecmp(str, "base") == 0)
		*scope_r = LDAP_SCOPE_BASE;
	else if (strcasecmp(str, "onelevel") == 0)
		*scope_r = LDAP_SCOPE_ONELEVEL;
	else if (strcasecmp(str, "subtree") == 0)
		*scope_r = LDAP_SCOPE_SUBTREE;
	else
		return -1;
	return 0;
}

#ifdef OPENLDAP_TLS_OPTIONS
int ldap_tls_require_cert_from_str(const char *str, int *opt_x_tls_r)
{
	if (strcasecmp(str, "never") == 0)
		*opt_x_tls_r = LDAP_OPT_X_TLS_NEVER;
	else if (strcasecmp(str, "hard") == 0)
		*opt_x_tls_r = LDAP_OPT_X_TLS_HARD;
	else if (strcasecmp(str, "demand") == 0)
		*opt_x_tls_r = LDAP_OPT_X_TLS_DEMAND;
	else if (strcasecmp(str, "allow") == 0)
		*opt_x_tls_r = LDAP_OPT_X_TLS_ALLOW;
	else if (strcasecmp(str, "try") == 0)
		*opt_x_tls_r = LDAP_OPT_X_TLS_TRY;
	else
		return -1;
	return 0;
}
#endif


static int ldap_get_errno(struct ldap_connection *conn)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	int ret, err;

	ret = ldap_get_option(conn->ld, LDAP_OPT_ERROR_NUMBER, (void *) &err);
	if (ret != LDAP_SUCCESS) {
		e_error(storage->event, "db: "
			"Can't get error number: %s",
			ldap_err2string(ret));
		return LDAP_UNAVAILABLE;
	}

	return err;
}

const char *ldap_get_error(struct ldap_connection *conn)
{
	const char *ret;
	char *str = NULL;

	ret = ldap_err2string(ldap_get_errno(conn));

	ldap_get_option(conn->ld, LDAP_OPT_ERROR_STRING, (void *)&str);
	if (str != NULL) {
		ret = t_strconcat(ret, ", ", str, NULL);
		ldap_memfree(str);
	}
	ldap_set_option(conn->ld, LDAP_OPT_ERROR_STRING, NULL);
	return ret;
}

static void ldap_conn_reconnect(struct ldap_connection *conn)
{
	db_ldap_conn_close(conn);
	if (sieve_ldap_db_connect(conn) < 0)
		db_ldap_conn_close(conn);
}

static int ldap_handle_error(struct ldap_connection *conn)
{
	int err = ldap_get_errno(conn);

	switch (err) {
	case LDAP_SUCCESS:
		i_unreached();
	case LDAP_SIZELIMIT_EXCEEDED:
	case LDAP_TIMELIMIT_EXCEEDED:
	case LDAP_NO_SUCH_ATTRIBUTE:
	case LDAP_UNDEFINED_TYPE:
	case LDAP_INAPPROPRIATE_MATCHING:
	case LDAP_CONSTRAINT_VIOLATION:
	case LDAP_TYPE_OR_VALUE_EXISTS:
	case LDAP_INVALID_SYNTAX:
	case LDAP_NO_SUCH_OBJECT:
	case LDAP_ALIAS_PROBLEM:
	case LDAP_INVALID_DN_SYNTAX:
	case LDAP_IS_LEAF:
	case LDAP_ALIAS_DEREF_PROBLEM:
	case LDAP_FILTER_ERROR:
		/* invalid input */
		return -1;
	case LDAP_SERVER_DOWN:
	case LDAP_TIMEOUT:
	case LDAP_UNAVAILABLE:
	case LDAP_BUSY:
#ifdef LDAP_CONNECT_ERROR
	case LDAP_CONNECT_ERROR:
#endif
	case LDAP_LOCAL_ERROR:
	case LDAP_INVALID_CREDENTIALS:
	case LDAP_OPERATIONS_ERROR:
	default:
		/* connection problems */
		ldap_conn_reconnect(conn);
		return 0;
	}
}

static int db_ldap_request_search(struct ldap_connection *conn,
				  struct ldap_request *request)
{
	struct sieve_storage *storage = &conn->lstorage->storage;

	i_assert(conn->conn_state == LDAP_CONN_STATE_BOUND);
	i_assert(request->msgid == -1);

	request->msgid =
		ldap_search(conn->ld, *request->base == '\0' ? NULL :
			    request->base, request->scope,
			    request->filter, request->attributes, 0);
	if (request->msgid == -1) {
		e_error(storage->event, "db: "
			"ldap_search(%s) parsing failed: %s",
			request->filter, ldap_get_error(conn));
		if (ldap_handle_error(conn) < 0) {
			/* broken request, remove it */
			return 0;
		}
		return -1;
	}
	return 1;
}

static bool db_ldap_request_queue_next(struct ldap_connection *conn)
{
	struct ldap_request *const *requestp, *request;
	int ret = -1;

	/* connecting may call db_ldap_connect_finish(), which gets us back
	   here. so do the connection before checking the request queue. */
	if (sieve_ldap_db_connect(conn) < 0)
		return FALSE;

	if (conn->pending_count == aqueue_count(conn->request_queue)) {
		/* no non-pending requests */
		return FALSE;
	}
	if (conn->pending_count > DB_LDAP_MAX_PENDING_REQUESTS) {
		/* wait until server has replied to some requests */
		return FALSE;
	}

	requestp = array_idx(&conn->request_array,
			     aqueue_idx(conn->request_queue,
					conn->pending_count));
	request = *requestp;

	switch (conn->conn_state) {
	case LDAP_CONN_STATE_DISCONNECTED:
	case LDAP_CONN_STATE_BINDING:
		/* wait until we're in bound state */
		return FALSE;
	case LDAP_CONN_STATE_BOUND:
		/* we can do anything in this state */
		break;
	}

	ret = db_ldap_request_search(conn, request);
	if (ret > 0) {
		/* success */
		i_assert(request->msgid != -1);
		conn->pending_count++;
		return TRUE;
	} else if (ret < 0) {
		/* disconnected */
		return FALSE;
	} else {
		/* broken request, remove from queue */
		aqueue_delete_tail(conn->request_queue);
		request->callback(conn, request, NULL);
		return TRUE;
	}
}

static bool
db_ldap_check_limits(struct ldap_connection *conn)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	struct ldap_request *const *first_requestp;
	unsigned int count;
	time_t secs_diff;

	count = aqueue_count(conn->request_queue);
	if (count == 0)
		return TRUE;

	first_requestp = array_idx(&conn->request_array,
				   aqueue_idx(conn->request_queue, 0));
	secs_diff = ioloop_time - (*first_requestp)->create_time;
	if (secs_diff > DB_LDAP_REQUEST_LOST_TIMEOUT_SECS) {
		e_error(storage->event, "db: "
			"Connection appears to be hanging, reconnecting");
		ldap_conn_reconnect(conn);
		return TRUE;
	}
	return TRUE;
}

void db_ldap_request(struct ldap_connection *conn,
		     struct ldap_request *request)
{
	request->msgid = -1;
	request->create_time = ioloop_time;

	if (!db_ldap_check_limits(conn)) {
		request->callback(conn, request, NULL);
		return;
	}

	aqueue_append(conn->request_queue, &request);
	(void)db_ldap_request_queue_next(conn);
}

static int db_ldap_connect_finish(struct ldap_connection *conn, int ret)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	const struct sieve_ldap_storage_settings *set = &conn->lstorage->set;

	if (ret == LDAP_SERVER_DOWN) {
		e_error(storage->event, "db: "
			"Can't connect to server: %s",
			set->uris != NULL ?
			set->uris : set->hosts);
		return -1;
	}
	if (ret != LDAP_SUCCESS) {
		e_error(storage->event, "db: "
			"binding failed (dn %s): %s",
			set->dn == NULL ? "(none)" : set->dn,
			ldap_get_error(conn));
		return -1;
	}

	timeout_remove(&conn->to);
	conn->conn_state = LDAP_CONN_STATE_BOUND;
	e_debug(storage->event, "db: "
		"Successfully bound (dn %s)",
		set->dn == NULL ? "(none)" : set->dn);
	while (db_ldap_request_queue_next(conn))
		;
	return 0;
}

static void db_ldap_default_bind_finished(struct ldap_connection *conn,
					  struct db_ldap_result *res)
{
	int ret;

	i_assert(conn->pending_count == 0);
	conn->default_bind_msgid = -1;

	ret = ldap_result2error(conn->ld, res->msg, FALSE);
	if (db_ldap_connect_finish(conn, ret) < 0) {
		/* lost connection, close it */
		db_ldap_conn_close(conn);
	}
}

static void db_ldap_abort_requests(struct ldap_connection *conn,
				   unsigned int max_count,
				   unsigned int timeout_secs,
				   bool error, const char *reason)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	struct ldap_request *const *requestp, *request;
	time_t diff;

	while (aqueue_count(conn->request_queue) > 0 && max_count > 0) {
		requestp = array_idx(&conn->request_array,
				     aqueue_idx(conn->request_queue, 0));
		request = *requestp;

		diff = ioloop_time - request->create_time;
		if (diff < (time_t)timeout_secs)
			break;

		/* timed out, abort */
		aqueue_delete_tail(conn->request_queue);

		if (request->msgid != -1) {
			i_assert(conn->pending_count > 0);
			conn->pending_count--;
		}
		if (error)
			e_error(storage->event, "db: %s", reason);
		else
			e_debug(storage->event, "db: %s", reason);
		request->callback(conn, request, NULL);
		max_count--;
	}
}

static struct ldap_request *
db_ldap_find_request(struct ldap_connection *conn, int msgid,
		     unsigned int *idx_r)
{
	struct ldap_request *const *requests, *request = NULL;
	unsigned int i, count;

	count = aqueue_count(conn->request_queue);
	if (count == 0)
		return NULL;

	requests = array_idx(&conn->request_array, 0);
	for (i = 0; i < count; i++) {
		request = requests[aqueue_idx(conn->request_queue, i)];
		if (request->msgid == msgid) {
			*idx_r = i;
			return request;
		}
		if (request->msgid == -1)
			break;
	}
	return NULL;
}

static bool
db_ldap_handle_request_result(struct ldap_connection *conn,
			      struct ldap_request *request, unsigned int idx,
			      struct db_ldap_result *res)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	int ret;
	bool final_result;

	i_assert(conn->pending_count > 0);

	switch (ldap_msgtype(res->msg)) {
	case LDAP_RES_SEARCH_ENTRY:
	case LDAP_RES_SEARCH_RESULT:
		break;
	case LDAP_RES_SEARCH_REFERENCE:
		/* we're going to ignore this */
		return FALSE;
	default:
		e_error(storage->event, "db: Reply with unexpected type %d",
			ldap_msgtype(res->msg));
		return TRUE;
	}

	if (ldap_msgtype(res->msg) == LDAP_RES_SEARCH_ENTRY) {
		ret = LDAP_SUCCESS;
		final_result = FALSE;
	} else {
		final_result = TRUE;
		ret = ldap_result2error(conn->ld, res->msg, 0);
	}
	if (ret != LDAP_SUCCESS) {
		/* handle search failures here */
		e_error(storage->event, "db: "
			"ldap_search(base=%s filter=%s) failed: %s",
			request->base, request->filter,
			ldap_err2string(ret));
		res = NULL;
	} else {
		if (!final_result && storage->svinst->debug) {
			e_debug(storage->event,
				"db: ldap_search(base=%s filter=%s) returned entry: %s",
				request->base, request->filter,
				ldap_get_dn(conn->ld, res->msg));
		}
	}
	if (res == NULL && !final_result) {
		/* wait for the final reply */
		request->failed = TRUE;
		return TRUE;
	}
	if (request->failed)
		res = NULL;
	if (final_result) {
		conn->pending_count--;
		aqueue_delete(conn->request_queue, idx);
	}

	T_BEGIN {
		request->callback(conn, request, res == NULL ? NULL : res->msg);
	} T_END;

	if (idx > 0) {
		/* see if there are timed out requests */
		db_ldap_abort_requests(conn, idx,
				       DB_LDAP_REQUEST_LOST_TIMEOUT_SECS,
				       TRUE, "Request lost");
	}
	return TRUE;
}

static void db_ldap_result_unref(struct db_ldap_result **_res)
{
	struct db_ldap_result *res = *_res;

	*_res = NULL;
	i_assert(res->refcount > 0);
	if (--res->refcount == 0) {
		ldap_msgfree(res->msg);
		i_free(res);
	}
}

static void
db_ldap_request_free(struct ldap_request *request)
{
	if (request->result != NULL)
		db_ldap_result_unref(&request->result);
}

static void
db_ldap_handle_result(struct ldap_connection *conn, struct db_ldap_result *res)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	struct ldap_request *request;
	unsigned int idx;
	int msgid;

	msgid = ldap_msgid(res->msg);
	if (msgid == conn->default_bind_msgid) {
		db_ldap_default_bind_finished(conn, res);
		return;
	}

	request = db_ldap_find_request(conn, msgid, &idx);
	if (request == NULL) {
		e_error(storage->event,
			"db: Reply with unknown msgid %d", msgid);
		return;
	}

	if (db_ldap_handle_request_result(conn, request, idx, res))
		db_ldap_request_free(request);
}

static void ldap_input(struct ldap_connection *conn)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	struct timeval timeout;
	struct db_ldap_result *res;
	LDAPMessage *msg;
	time_t prev_reply_diff;
	int ret;

	do {
		if (conn->ld == NULL)
			return;

		i_zero(&timeout);
		ret = ldap_result(conn->ld, LDAP_RES_ANY, 0, &timeout, &msg);
#ifdef OPENLDAP_ASYNC_WORKAROUND
		if (ret == 0) {
			/* try again, there may be another in buffer */
			ret = ldap_result(conn->ld, LDAP_RES_ANY, 0,
					  &timeout, &msg);
		}
#endif
		if (ret <= 0)
			break;

		res = i_new(struct db_ldap_result, 1);
		res->refcount = 1;
		res->msg = msg;
		db_ldap_handle_result(conn, res);
		db_ldap_result_unref(&res);
	} while (conn->io != NULL);

	prev_reply_diff = ioloop_time - conn->last_reply_stamp;
	conn->last_reply_stamp = ioloop_time;

	if (ret > 0) {
		/* input disabled, continue once it's enabled */
		i_assert(conn->io == NULL);
	} else if (ret == 0) {
		/* send more requests */
		while (db_ldap_request_queue_next(conn))
			;
	} else if (ldap_get_errno(conn) != LDAP_SERVER_DOWN) {
		e_error(storage->event, "db: ldap_result() failed: %s",
			ldap_get_error(conn));
		ldap_conn_reconnect(conn);
	} else if (aqueue_count(conn->request_queue) > 0 ||
		   prev_reply_diff < DB_LDAP_IDLE_RECONNECT_SECS) {
		e_error(storage->event,
			"db: Connection lost to LDAP server, reconnecting");
		ldap_conn_reconnect(conn);
	} else {
		/* server probably disconnected an idle connection. don't
		   reconnect until the next request comes. */
		db_ldap_conn_close(conn);
	}
}

#ifdef HAVE_LDAP_SASL
static int
sasl_interact(LDAP *ld ATTR_UNUSED, unsigned flags ATTR_UNUSED,
	      void *defaults, void *interact)
{
	struct db_ldap_sasl_bind_context *context = defaults;
	sasl_interact_t *in;
	const char *str;

	for (in = interact; in->id != SASL_CB_LIST_END; in++) {
		switch (in->id) {
		case SASL_CB_GETREALM:
			str = context->realm;
			break;
		case SASL_CB_AUTHNAME:
			str = context->authcid;
			break;
		case SASL_CB_USER:
			str = context->authzid;
			break;
		case SASL_CB_PASS:
			str = context->passwd;
			break;
		default:
			str = NULL;
			break;
		}
		if (str != NULL) {
			in->len = strlen(str);
			in->result = str;
		}
		
	}
	return LDAP_SUCCESS;
}
#endif

static void ldap_connection_timeout(struct ldap_connection *conn)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	i_assert(conn->conn_state == LDAP_CONN_STATE_BINDING);

	e_error(storage->event, "db: Initial binding to LDAP server timed out");
	db_ldap_conn_close(conn);
}

static int db_ldap_bind(struct ldap_connection *conn)
{
	const struct sieve_ldap_storage_settings *set = &conn->lstorage->set;
	int msgid;

	i_assert(conn->conn_state != LDAP_CONN_STATE_BINDING);
	i_assert(conn->default_bind_msgid == -1);
	i_assert(conn->pending_count == 0);

	msgid = ldap_bind(conn->ld, set->dn, set->dnpass,
			  LDAP_AUTH_SIMPLE);
	if (msgid == -1) {
		i_assert(ldap_get_errno(conn) != LDAP_SUCCESS);
		if (db_ldap_connect_finish(conn, ldap_get_errno(conn)) < 0) {
			/* lost connection, close it */
			db_ldap_conn_close(conn);
		}
		return -1;
	}

	conn->conn_state = LDAP_CONN_STATE_BINDING;
	conn->default_bind_msgid = msgid;

	timeout_remove(&conn->to);
	conn->to = timeout_add(DB_LDAP_REQUEST_LOST_TIMEOUT_SECS*1000,
			       ldap_connection_timeout, conn);
	return 0;
}

static int db_ldap_get_fd(struct ldap_connection *conn)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	int ret;

	/* get the connection's fd */
	ret = ldap_get_option(conn->ld, LDAP_OPT_DESC, (void *)&conn->fd);
	if (ret != LDAP_SUCCESS) {
		e_error(storage->event, "db: Can't get connection fd: %s",
			ldap_err2string(ret));
		return -1;
	}
	if (conn->fd <= STDERR_FILENO) {
		/* Solaris LDAP library seems to be broken */
		e_error(storage->event,
			"db: Buggy LDAP library returned wrong fd: %d",
			conn->fd);
		return -1;
	}
	i_assert(conn->fd != -1);
	net_set_nonblock(conn->fd, TRUE);
	return 0;
}

static int
db_ldap_set_opt(struct ldap_connection *conn, int opt, const void *value,
		const char *optname, const char *value_str)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	int ret;

	ret = ldap_set_option(conn->ld, opt, value);
	if (ret != LDAP_SUCCESS) {
		e_error(storage->event, "db: Can't set option %s to %s: %s",
			optname, value_str, ldap_err2string(ret));
		return -1;
	}
	return 0;
}

static int
db_ldap_set_opt_str(struct ldap_connection *conn, int opt, const char *value,
		    const char *optname)
{
	if (value != NULL)
		return db_ldap_set_opt(conn, opt, value, optname, value);
	return 0;
}

static int db_ldap_set_tls_options(struct ldap_connection *conn)
{
	const struct sieve_ldap_storage_settings *set = &conn->lstorage->set;

	if (!set->tls)
		return 0;

#ifdef OPENLDAP_TLS_OPTIONS
	if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_CACERTFILE,
			    set->tls_ca_cert_file, "tls_ca_cert_file") < 0)
		return -1;
	if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_CACERTDIR,
			    set->tls_ca_cert_dir, "tls_ca_cert_dir") < 0)
		return -1;
	if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_CERTFILE,
			    set->tls_cert_file, "tls_cert_file") < 0)
		return -1;
	if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_KEYFILE,
			    set->tls_key_file, "tls_key_file") < 0)
		return -1;
	if (db_ldap_set_opt_str(conn, LDAP_OPT_X_TLS_CIPHER_SUITE,
			    set->tls_cipher_suite, "tls_cipher_suite") < 0)
		return -1;
	if (set->tls_require_cert != NULL) {
		if (db_ldap_set_opt(conn, LDAP_OPT_X_TLS_REQUIRE_CERT,
				&set->ldap_tls_require_cert,
				"tls_require_cert", set->tls_require_cert) < 0)
			return -1;
	}
#else
	if (set->tls_ca_cert_file != NULL ||
	    set->tls_ca_cert_dir != NULL ||
	    set->tls_cert_file != NULL ||
	    set->tls_key_file != NULL ||
	    set->tls_cipher_suite != NULL) {
		e_warning(&conn->lstorage->storage, "db: "
			  "tls_* settings ignored, "
			  "your LDAP library doesn't seem to support them");
	}
#endif
	return 0;
}

static int db_ldap_set_options(struct ldap_connection *conn)
{
	const struct sieve_ldap_storage_settings *set = &conn->lstorage->set;
	struct sieve_storage *storage = &conn->lstorage->storage;
	unsigned int ldap_version;
	int value;

	if (db_ldap_set_opt(conn, LDAP_OPT_DEREF, &set->ldap_deref,
			"deref", set->deref) < 0)
		return -1;
#ifdef LDAP_OPT_DEBUG_LEVEL
	if (str_to_int(set->debug_level, &value) >= 0 && value != 0) {
		if (db_ldap_set_opt(conn, LDAP_OPT_DEBUG_LEVEL, &value,
				"debug_level", set->debug_level) < 0)
			return -1;
	}
#endif

	if (set->ldap_version < 3) {
		if (set->sasl_bind) {
			e_error(storage->event,
				"db: sasl_bind=yes requires ldap_version=3");
			return -1;
		}
		if (set->tls) {
			e_error(storage->event,
				"db: tls=yes requires ldap_version=3");
			return -1;
		}
	}

	ldap_version = set->ldap_version;
	if (db_ldap_set_opt(conn, LDAP_OPT_PROTOCOL_VERSION, &ldap_version,
			"protocol_version", dec2str(ldap_version)) < 0)
		return -1;
	if (db_ldap_set_tls_options(conn) < 0)
		return -1;
	return 0;
}

int sieve_ldap_db_connect(struct ldap_connection *conn)
{
	const struct sieve_ldap_storage_settings *set = &conn->lstorage->set;
	struct sieve_storage *storage = &conn->lstorage->storage;
	struct timeval start, end;
	int debug_level;
	bool debug;
#if defined(HAVE_LDAP_SASL) || defined(LDAP_HAVE_START_TLS_S)
	int ret;
#endif

	if (conn->conn_state != LDAP_CONN_STATE_DISCONNECTED)
		return 0;

	debug = FALSE;
	if (str_to_int(set->debug_level, &debug_level) >= 0)
		debug = debug_level > 0;

	if (debug)
		i_gettimeofday(&start);
	i_assert(conn->pending_count == 0);
	if (conn->ld == NULL) {
		if (set->uris != NULL) {
#ifdef LDAP_HAVE_INITIALIZE
			if (ldap_initialize(&conn->ld, set->uris) != LDAP_SUCCESS)
				conn->ld = NULL;
#else
			e_error(storage->event, "db: "
				"Your LDAP library doesn't support "
				"'uris' setting, use 'hosts' instead.");
			return -1;
#endif
		} else
			conn->ld = ldap_init(set->hosts, LDAP_PORT);

		if (conn->ld == NULL) {
			e_error(storage->event, "db: "
				"ldap_init() failed with hosts: %s",	set->hosts);
			return -1;
		}

		if (db_ldap_set_options(conn) < 0)
			return -1;
	}

	if (set->tls) {
#ifdef LDAP_HAVE_START_TLS_S
		ret = ldap_start_tls_s(conn->ld, NULL, NULL);
		if (ret != LDAP_SUCCESS) {
			if (ret == LDAP_OPERATIONS_ERROR &&
			    set->uris != NULL &&
			    str_begins(set->uris, "ldaps:")) {
				e_error(storage->event, "db: "
					"Don't use both tls=yes and ldaps URI");
			}
			e_error(storage->event, "db: "
				"ldap_start_tls_s() failed: %s",
				ldap_err2string(ret));
			return -1;
		}
#else
		e_error(storage->event, "db: "
			"Your LDAP library doesn't support TLS");
		return -1;
#endif
	}

	if (set->sasl_bind) {
#ifdef HAVE_LDAP_SASL
		struct db_ldap_sasl_bind_context context;

		i_zero(&context);
		context.authcid = set->dn;
		context.passwd = set->dnpass;
		context.realm = set->sasl_realm;
		context.authzid = set->sasl_authz_id;

		/* There doesn't seem to be a way to do SASL binding
		   asynchronously.. */
		ret = ldap_sasl_interactive_bind_s(conn->ld, NULL,
						   set->sasl_mech,
						   NULL, NULL, LDAP_SASL_QUIET,
						   sasl_interact, &context);
		if (db_ldap_connect_finish(conn, ret) < 0)
			return -1;
#else
		e_error(storage->event, "db: "
			"sasl_bind=yes but no SASL support compiled in");
		return -1;
#endif
		conn->conn_state = LDAP_CONN_STATE_BOUND;
	} else {
		if (db_ldap_bind(conn) < 0)
			return -1;
	}
	if (debug) {
		i_gettimeofday(&end);
		int msecs = timeval_diff_msecs(&end, &start);
		e_debug(storage->event, "db: "
			"Initialization took %d msecs", msecs);
	}

	if (db_ldap_get_fd(conn) < 0)
		return -1;
	conn->io = io_add(conn->fd, IO_READ, ldap_input, conn);
	return 0;
}

void db_ldap_enable_input(struct ldap_connection *conn, bool enable)
{
	if (!enable) {
		io_remove(&conn->io);
	} else {
		if (conn->io == NULL && conn->fd != -1) {
			conn->io = io_add(conn->fd, IO_READ, ldap_input, conn);
			ldap_input(conn);
		}
	}
}

static void db_ldap_disconnect_timeout(struct ldap_connection *conn)
{
	db_ldap_abort_requests(conn, UINT_MAX,
		DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS, FALSE,
		"Aborting (timeout), we're not connected to LDAP server");

	if (aqueue_count(conn->request_queue) == 0) {
		/* no requests left, remove this timeout handler */
		timeout_remove(&conn->to);
	}
}

static void db_ldap_conn_close(struct ldap_connection *conn)
{
	struct ldap_request *const *requests, *request;
	unsigned int i;

	conn->conn_state = LDAP_CONN_STATE_DISCONNECTED;
	conn->default_bind_msgid = -1;

	timeout_remove(&conn->to);

	if (conn->pending_count != 0) {
		requests = array_idx(&conn->request_array, 0);
		for (i = 0; i < conn->pending_count; i++) {
			request = requests[aqueue_idx(conn->request_queue, i)];

			i_assert(request->msgid != -1);
			request->msgid = -1;
		}
		conn->pending_count = 0;
	}

	if (conn->ld != NULL) {
		ldap_unbind(conn->ld);
		conn->ld = NULL;
	}
	conn->fd = -1;

	/* the fd may have already been closed before ldap_unbind(),
	   so we'll have to use io_remove_closed(). */
	io_remove_closed(&conn->io);

	if (aqueue_count(conn->request_queue) > 0) {
		conn->to = timeout_add(DB_LDAP_REQUEST_DISCONNECT_TIMEOUT_SECS *
				       1000/2, db_ldap_disconnect_timeout, conn);
	}
}

struct ldap_field_find_context {
	ARRAY_TYPE(string) attr_names;
	pool_t pool;
};

#define IS_LDAP_ESCAPED_CHAR(c) \
	((c) == '*' || (c) == '(' || (c) == ')' || (c) == '\\')

const char *ldap_escape(const char *str)
{
	const char *p;
	string_t *ret;

	for (p = str; *p != '\0'; p++) {
		if (IS_LDAP_ESCAPED_CHAR(*p))
			break;
	}

	if (*p == '\0')
		return str;

	ret = t_str_new((size_t) (p - str) + 64);
	str_append_data(ret, str, (size_t) (p - str));

	for (; *p != '\0'; p++) {
		if (IS_LDAP_ESCAPED_CHAR(*p))
			str_append_c(ret, '\\');
		str_append_c(ret, *p);
	}
	return str_c(ret);
}

struct ldap_connection *
sieve_ldap_db_init(struct sieve_ldap_storage *lstorage)
{
	struct ldap_connection *conn;
	pool_t pool;

	pool = pool_alloconly_create("ldap_connection", 1024);
	conn = p_new(pool, struct ldap_connection, 1);
	conn->pool = pool;
	conn->refcount = 1;
	conn->lstorage = lstorage;

	conn->conn_state = LDAP_CONN_STATE_DISCONNECTED;
	conn->default_bind_msgid = -1;
	conn->fd = -1;

	i_array_init(&conn->request_array, 512);
	conn->request_queue = aqueue_init(&conn->request_array.arr);

	conn->next = ldap_connections;
	ldap_connections = conn;
	return conn;
}

void sieve_ldap_db_unref(struct ldap_connection **_conn)
{
	struct ldap_connection *conn = *_conn;
	struct ldap_connection **p;

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

	for (p = &ldap_connections; *p != NULL; p = &(*p)->next) {
		if (*p == conn) {
			*p = conn->next;
			break;
		}
	}

	db_ldap_abort_requests(conn, UINT_MAX, 0, FALSE, "Shutting down");
	i_assert(conn->pending_count == 0);
	db_ldap_conn_close(conn);
	i_assert(conn->to == NULL);

	array_free(&conn->request_array);
	aqueue_deinit(&conn->request_queue);

	pool_unref(&conn->pool);
}

static void db_ldap_switch_ioloop(struct ldap_connection *conn)
{
	if (conn->to != NULL)
		conn->to = io_loop_move_timeout(&conn->to);
	if (conn->io != NULL)
		conn->io = io_loop_move_io(&conn->io);
}

static void db_ldap_wait(struct ldap_connection *conn)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	struct ioloop *prev_ioloop = current_ioloop;

	i_assert(conn->ioloop == NULL);

	if (aqueue_count(conn->request_queue) == 0)
		return;

	conn->ioloop = io_loop_create();
	db_ldap_switch_ioloop(conn);
	/* either we're waiting for network I/O or we're getting out of a
	   callback using timeout_add_short(0) */
	i_assert(io_loop_have_ios(conn->ioloop) ||
		 io_loop_have_immediate_timeouts(conn->ioloop));

	do {
		e_debug(storage->event, "db: "
			"Waiting for %d requests to finish",
			aqueue_count(conn->request_queue) );
		io_loop_run(conn->ioloop);
	} while (aqueue_count(conn->request_queue) > 0);

	e_debug(storage->event, "db: All requests finished");

	current_ioloop = prev_ioloop;
	db_ldap_switch_ioloop(conn);
	current_ioloop = conn->ioloop;
	io_loop_destroy(&conn->ioloop);
}

static void sieve_ldap_db_script_free(unsigned char *script)
{
	i_free(script);
}

static int
sieve_ldap_db_get_script_modattr(struct ldap_connection *conn,
	LDAPMessage *entry, pool_t pool, const char **modattr_r)
{
	const struct sieve_ldap_storage_settings *set = &conn->lstorage->set;
	struct sieve_storage *storage = &conn->lstorage->storage;
	char *attr, **vals;
	BerElement *ber;

	*modattr_r = NULL;

	attr = ldap_first_attribute(conn->ld, entry, &ber);
	while (attr != NULL) {
		if (strcmp(attr, set->sieve_ldap_mod_attr) == 0) {
			vals = ldap_get_values(conn->ld, entry, attr);
			if (vals == NULL || vals[0] == NULL)
				return 0;

			if (vals[1] != NULL) {
				e_warning(storage->event, "db: "
					  "Search returned more than one Sieve modified attribute `%s'; "
					  "using only the first one.", set->sieve_ldap_mod_attr);
			} 

			*modattr_r = p_strdup(pool, vals[0]);
		
			ldap_value_free(vals);
			ldap_memfree(attr);
			return 1;
		}
		ldap_memfree(attr);
		attr = ldap_next_attribute(conn->ld, entry, ber);
	}
	ber_free(ber, 0);

	return 0;
}

static int
sieve_ldap_db_get_script(struct ldap_connection *conn,
	LDAPMessage *entry, struct istream **script_r)
{
	const struct sieve_ldap_storage_settings *set = &conn->lstorage->set;
	struct sieve_storage *storage = &conn->lstorage->storage;
	char *attr;
	unsigned char *data;
	size_t size;
	struct berval **vals;
	BerElement *ber;

	attr = ldap_first_attribute(conn->ld, entry, &ber);
	while (attr != NULL) {
		if (strcmp(attr, set->sieve_ldap_script_attr) == 0) {
			vals = ldap_get_values_len(conn->ld, entry, attr);
			if (vals == NULL || vals[0] == NULL)
				return 0;

			if (vals[1] != NULL) {
				e_warning(storage->event, "db: "
					  "Search returned more than one Sieve script attribute `%s'; "
					  "using only the first one.", set->sieve_ldap_script_attr);
			} 

			size = vals[0]->bv_len;
			data = i_malloc(size);

			e_debug(storage->event, "db: "
				"Found script with length %zu", size);

			memcpy(data, vals[0]->bv_val, size);
		
			ldap_value_free_len(vals);
			ldap_memfree(attr);

			*script_r = i_stream_create_from_data(data, size);
			i_stream_add_destroy_callback
				(*script_r,	sieve_ldap_db_script_free, data);
			return 1;
		}
		ldap_memfree(attr);
		attr = ldap_next_attribute(conn->ld, entry, ber);
	}
	ber_free(ber, 0);

	return 0;
}

const struct var_expand_table
auth_request_var_expand_static_tab[] = {
	{ 'u', NULL, "user" },
	{ 'n', NULL, "username" },
	{ 'd', NULL, "domain" },
	{ 'h', NULL, "home" },
	{ '\0', NULL, "name" },
	{ '\0', NULL, NULL }
};

static const struct var_expand_table *
db_ldap_get_var_expand_table(struct ldap_connection *conn,
	const char *name)
{
	struct sieve_ldap_storage *lstorage = conn->lstorage;
	struct sieve_instance *svinst = lstorage->storage.svinst;
	const unsigned int auth_count =
		N_ELEMENTS(auth_request_var_expand_static_tab);
	struct var_expand_table *tab;

	/* keep the extra fields at the beginning. the last static_tab field
	   contains the ending NULL-fields. */
	tab = t_malloc_no0((auth_count) * sizeof(*tab));

	memcpy(tab, auth_request_var_expand_static_tab,
	       auth_count * sizeof(*tab));

	tab[0].value = ldap_escape(lstorage->username);
	tab[1].value = ldap_escape(t_strcut(lstorage->username, '@'));
	tab[2].value = strchr(lstorage->username, '@');
	if (tab[2].value != NULL)
		tab[2].value = ldap_escape(tab[2].value+1);
	tab[3].value = ldap_escape(svinst->home_dir);
	tab[4].value = ldap_escape(name);
	return tab;
}

struct sieve_ldap_script_lookup_request {
	struct ldap_request request;

	unsigned int entries;
	const char *result_dn;
	const char *result_modattr;
};

static void
sieve_ldap_lookup_script_callback(struct ldap_connection *conn,
			  struct ldap_request *request, LDAPMessage *res)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	struct sieve_ldap_script_lookup_request *srequest =
		(struct sieve_ldap_script_lookup_request *)request;

	if (res == NULL) {
		io_loop_stop(conn->ioloop);
		return;
	}

	if (ldap_msgtype(res) != LDAP_RES_SEARCH_RESULT) {
		if (srequest->result_dn == NULL) {
			srequest->result_dn = p_strdup
				(request->pool, ldap_get_dn(conn->ld, res));
			(void)sieve_ldap_db_get_script_modattr
				(conn, res, request->pool, &srequest->result_modattr);
		} else if (srequest->entries++ == 0) {
			e_warning(storage->event, "db: "
				  "Search returned more than one entry for Sieve script; "
				  "using only the first one.");
		}
	} else {
		io_loop_stop(conn->ioloop);
		return;
	}
}

int sieve_ldap_db_lookup_script(struct ldap_connection *conn,
	const char *name, const char **dn_r, const char **modattr_r)
{
	struct sieve_ldap_storage *lstorage = conn->lstorage;
	struct sieve_storage *storage = &lstorage->storage;
	const struct sieve_ldap_storage_settings *set = &lstorage->set;
	struct sieve_ldap_script_lookup_request *request;
	const struct var_expand_table *tab;
	char **attr_names;
	const char *error;
	string_t *str;

	pool_t pool = pool_alloconly_create
		("sieve_ldap_script_lookup_request", 512);
	request = p_new(pool, struct sieve_ldap_script_lookup_request, 1);
	request->request.pool = pool;

	tab = db_ldap_get_var_expand_table(conn, name);

	str = t_str_new(512);
	if (var_expand(str, set->base, tab, &error) <= 0) {
		e_error(storage->event, "db: "
			"Failed to expand base=%s: %s",
			set->base, error);
		return -1;
	}
	request->request.base = p_strdup(pool, str_c(str));

	attr_names = p_new(pool, char *, 3);
	attr_names[0] = p_strdup(pool, set->sieve_ldap_mod_attr);

	str_truncate(str, 0);
	if (var_expand(str, set->sieve_ldap_filter, tab, &error) <= 0) {
		e_error(storage->event, "db: "
			"Failed to expand sieve_ldap_filter=%s: %s",
			set->sieve_ldap_filter, error);
		return -1;
	}

	request->request.scope = lstorage->set.ldap_scope;
	request->request.filter = p_strdup(pool, str_c(str));
	request->request.attributes = attr_names;

	e_debug(storage->event, "base=%s scope=%s filter=%s fields=%s",
		request->request.base, lstorage->set.scope,
		request->request.filter,
		t_strarray_join((const char **)attr_names, ","));

	request->request.callback = sieve_ldap_lookup_script_callback;
	db_ldap_request(conn, &request->request);
	db_ldap_wait(conn);

	*dn_r = t_strdup(request->result_dn);
	*modattr_r = t_strdup(request->result_modattr);
	pool_unref(&request->request.pool);
	return (*dn_r == NULL ? 0 : 1);
}

struct sieve_ldap_script_read_request {
	struct ldap_request request;

	unsigned int entries;
	struct istream *result;
};

static void
sieve_ldap_read_script_callback(struct ldap_connection *conn,
			  struct ldap_request *request, LDAPMessage *res)
{
	struct sieve_storage *storage = &conn->lstorage->storage;
	struct sieve_ldap_script_read_request *srequest =
		(struct sieve_ldap_script_read_request *)request;

	if (res == NULL) {
		io_loop_stop(conn->ioloop);
		return;
	}

	if (ldap_msgtype(res) != LDAP_RES_SEARCH_RESULT) {

		if (srequest->result == NULL) {
			(void)sieve_ldap_db_get_script(conn, res, &srequest->result);
		} else {
			e_error(storage->event, "db: "
				"Search returned more than one entry for Sieve script DN");
			i_stream_unref(&srequest->result);
		}

	} else {
		io_loop_stop(conn->ioloop);
		return;
	}
}

int sieve_ldap_db_read_script(struct ldap_connection *conn,
	const char *dn, struct istream **script_r)
{
	struct sieve_ldap_storage *lstorage = conn->lstorage;
	struct sieve_storage *storage = &lstorage->storage;
	const struct sieve_ldap_storage_settings *set = &lstorage->set;
	struct sieve_ldap_script_read_request *request;
	char **attr_names;

	pool_t pool = pool_alloconly_create
		("sieve_ldap_script_read_request", 512);
	request = p_new(pool, struct sieve_ldap_script_read_request, 1);
	request->request.pool = pool;
	request->request.base = p_strdup(pool, dn);

	attr_names = p_new(pool, char *, 3);
	attr_names[0] = p_strdup(pool, set->sieve_ldap_script_attr);

	request->request.scope = LDAP_SCOPE_BASE;
	request->request.filter = "(objectClass=*)";
	request->request.attributes = attr_names;

	e_debug(storage->event, "base=%s scope=base filter=%s fields=%s",
		request->request.base, request->request.filter,
		t_strarray_join((const char **)attr_names, ","));

	request->request.callback = sieve_ldap_read_script_callback;
	db_ldap_request(conn, &request->request);
	db_ldap_wait(conn);

	*script_r = request->result;
	pool_unref(&request->request.pool);
	return (*script_r == NULL ? 0 : 1);
}


#endif
