/*
 * e-client.c
 *
 * Copyright (C) 2011 Red Hat, Inc. (www.redhat.com)
 *
 * This library is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 */

/* TODO The next time we have a good excuse to break libedataserver's API,
 *      I'd like to purge all the deprecated cruft here and convert EClient
 *      from a GObjectClass to a GTypeInterface, implemented by EBookClient
 *      and ECalClient.  Then we could just bind the "online", "readonly"
 *      and "capabilities" properties to equivalent GDBusProxy properties
 *      and kill e-client-private.h.  Would simplify things.  --mbarnes
 */

/**
 * SECTION: e-client
 * @include: libedataserver/libedataserver.h
 * @short_description: Base class for client handles
 *
 * This class provides some base functionality for clients
 * such as #EBookClient and #ECalClient.
 **/

#include "evolution-data-server-config.h"

#include <glib/gi18n-lib.h>
#include <gio/gio.h>

#include <libedataserver/e-data-server-util.h>

#include "e-flag.h"

#include "e-client.h"
#include "e-client-private.h"

typedef struct _AsyncContext AsyncContext;

struct _EClientPrivate {
	GRecMutex prop_mutex;

	ESource *source;
	gboolean online;
	gboolean readonly;
	GSList *capabilities;
	GMainContext *main_context;
	gchar *bus_name;
};

struct _AsyncContext {
	gchar *capabilities;
	gchar *prop_name;
	gchar *prop_value;
};

enum {
	PROP_0,
	PROP_CAPABILITIES,
	PROP_MAIN_CONTEXT,
	PROP_ONLINE,
	PROP_OPENED,
	PROP_READONLY,
	PROP_SOURCE,
	N_PROPS
};

enum {
	OPENED,
	BACKEND_ERROR,
	BACKEND_DIED,
	BACKEND_PROPERTY_CHANGED,
	LAST_SIGNAL
};

static GParamSpec *properties[N_PROPS] = { NULL, };

static guint signals[LAST_SIGNAL];

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (EClient, e_client, G_TYPE_OBJECT)

static void
async_context_free (AsyncContext *async_context)
{
	g_free (async_context->capabilities);
	g_free (async_context->prop_name);
	g_free (async_context->prop_value);

	g_slice_free (AsyncContext, async_context);
}

/*
 * Well-known client backend properties, which are common for each #EClient:
 * @CLIENT_BACKEND_PROPERTY_OPENED: Is set to "TRUE" or "FALSE" depending
 *   whether the backend is fully opened.
 * @CLIENT_BACKEND_PROPERTY_OPENING: Is set to "TRUE" or "FALSE" depending
 *   whether the backend is processing its opening phase.
 * @CLIENT_BACKEND_PROPERTY_ONLINE: Is set to "TRUE" or "FALSE" depending
 *   on the backend's loaded state. See also e_client_is_online().
 * @CLIENT_BACKEND_PROPERTY_READONLY: Is set to "TRUE" or "FALSE" depending
 *   on the backend's readonly state. See also e_client_is_readonly().
 * @CLIENT_BACKEND_PROPERTY_CACHE_DIR: Local folder with cached data used
 *   by the backend.
 * @CLIENT_BACKEND_PROPERTY_CAPABILITIES: Retrieves comma-separated list
 *   of	capabilities supported by the backend. Preferred method of retreiving
 *   and working with capabilities is e_client_get_capabilities() and
 *   e_client_check_capability().
 */

G_DEFINE_QUARK (e-client-error-quark, e_client_error)

/**
 * e_client_error_to_string:
 * @code: an #EClientError error code
 *
 * Get localized human readable description of the given error code.
 *
 * Returns: Localized human readable description of the given error code
 *
 * Since: 3.2
 **/
const gchar *
e_client_error_to_string (EClientError code)
{
	switch (code) {
	case E_CLIENT_ERROR_INVALID_ARG:
		return _("Invalid argument");
	case E_CLIENT_ERROR_BUSY:
		return _("Backend is busy");
	case E_CLIENT_ERROR_SOURCE_NOT_LOADED:
		return _("Source not loaded");
	case E_CLIENT_ERROR_SOURCE_ALREADY_LOADED:
		return _("Source already loaded");
	case E_CLIENT_ERROR_AUTHENTICATION_FAILED:
		return _("Authentication failed");
	case E_CLIENT_ERROR_AUTHENTICATION_REQUIRED:
		return _("Authentication required");
	case E_CLIENT_ERROR_REPOSITORY_OFFLINE:
		return _("Repository offline");
	case E_CLIENT_ERROR_OFFLINE_UNAVAILABLE:
		/* Translators: This means that the EClient does not
		 * support offline mode, or it's not set to by a user,
		 * thus it is unavailable while user is not connected. */
		return _("Offline unavailable");
	case E_CLIENT_ERROR_PERMISSION_DENIED:
		return _("Permission denied");
	case E_CLIENT_ERROR_CANCELLED:
		return _("Cancelled");
	case E_CLIENT_ERROR_COULD_NOT_CANCEL:
		return _("Could not cancel");
	case E_CLIENT_ERROR_NOT_SUPPORTED:
		return _("Not supported");
	case E_CLIENT_ERROR_UNSUPPORTED_AUTHENTICATION_METHOD:
		return _("Unsupported authentication method");
	case E_CLIENT_ERROR_TLS_NOT_AVAILABLE:
		return _("TLS not available");
	case E_CLIENT_ERROR_SEARCH_SIZE_LIMIT_EXCEEDED:
		return _("Search size limit exceeded");
	case E_CLIENT_ERROR_SEARCH_TIME_LIMIT_EXCEEDED:
		return _("Search time limit exceeded");
	case E_CLIENT_ERROR_INVALID_QUERY:
		return _("Invalid query");
	case E_CLIENT_ERROR_QUERY_REFUSED:
		return _("Query refused");
	case E_CLIENT_ERROR_DBUS_ERROR:
		return _("D-Bus error");
	case E_CLIENT_ERROR_OTHER_ERROR:
		return _("Other error");
	case E_CLIENT_ERROR_NOT_OPENED:
		return _("Backend is not opened yet");
	case E_CLIENT_ERROR_OUT_OF_SYNC:
		return _("Object is out of sync");
	}

	return _("Unknown error");
}

/**
 * e_client_error_create:
 * @code: an #EClientError code to create
 * @custom_msg: (nullable): custom message to use for the error; can be %NULL
 *
 * Returns: (transfer full): a new #GError containing an #E_CLIENT_ERROR of the given
 *    @code. If the @custom_msg is NULL, then the error message is the one returned
 *    from e_client_error_to_string() for the @code, otherwise the given message is used.
 *    Returned pointer should be freed with g_error_free().
 *
 * Since: 3.2
 **/
GError *
e_client_error_create (EClientError code,
                       const gchar *custom_msg)
{
	if (!custom_msg)
		custom_msg = e_client_error_to_string (code);

	return g_error_new_literal (E_CLIENT_ERROR, code, custom_msg);
}

/**
 * e_client_error_create_fmt:
 * @code: an #EClientError
 * @format: (nullable): message format, or %NULL to use the default message for the @code
 * @...: arguments for the format
 *
 * Similar as e_client_error_create(), only here, instead of custom_msg,
 * is used a printf() format to create a custom message for the error.
 *
 * Returns: (transfer full): a newly allocated #GError, which should be
 *   freed with g_error_free(), when no longer needed.
 *   The #GError has set the custom message, or the default message for
 *   @code, when @format is %NULL.
 *
 * Since: 3.34
 **/
GError *
e_client_error_create_fmt (EClientError code,
			   const gchar *format,
			   ...)
{
	GError *error;
	gchar *custom_msg;
	va_list ap;

	if (!format)
		return e_client_error_create (code, NULL);

	va_start (ap, format);
	custom_msg = g_strdup_vprintf (format, ap);
	va_end (ap);

	error = e_client_error_create (code, custom_msg);

	g_free (custom_msg);

	return error;
}

static void
client_set_source (EClient *client,
                   ESource *source)
{
	g_return_if_fail (E_IS_SOURCE (source));
	g_return_if_fail (client->priv->source == NULL);

	client->priv->source = g_object_ref (source);
}

static void
client_set_property (GObject *object,
                     guint property_id,
                     const GValue *value,
                     GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_ONLINE:
			e_client_set_online (
				E_CLIENT (object),
				g_value_get_boolean (value));
			return;

		case PROP_SOURCE:
			client_set_source (
				E_CLIENT (object),
				g_value_get_object (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
client_get_property (GObject *object,
                     guint property_id,
                     GValue *value,
                     GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_CAPABILITIES:
			g_value_set_pointer (
				value,
				(gpointer) e_client_get_capabilities (
				E_CLIENT (object)));
			return;

		case PROP_MAIN_CONTEXT:
			g_value_take_boxed (
				value,
				e_client_ref_main_context (
				E_CLIENT (object)));
			return;

		case PROP_ONLINE:
			g_value_set_boolean (
				value,
				e_client_is_online (
				E_CLIENT (object)));
			return;

		case PROP_OPENED:
			g_value_set_boolean (
				value,
				e_client_is_opened (
				E_CLIENT (object)));
			return;

		case PROP_READONLY:
			g_value_set_boolean (
				value,
				e_client_is_readonly (
				E_CLIENT (object)));
			return;

		case PROP_SOURCE:
			g_value_set_object (
				value,
				e_client_get_source (
				E_CLIENT (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
client_dispose (GObject *object)
{
	EClientPrivate *priv;

	priv = E_CLIENT (object)->priv;
	g_clear_pointer (&priv->main_context, g_main_context_unref);
	g_clear_object (&priv->source);

	g_free (priv->bus_name);
	priv->bus_name = NULL;

	/* Chain up to parent's dispose() method. */
	G_OBJECT_CLASS (e_client_parent_class)->dispose (object);
}

static void
client_finalize (GObject *object)
{
	EClientPrivate *priv;

	priv = E_CLIENT (object)->priv;

	g_slist_free_full (priv->capabilities, (GDestroyNotify) g_free);

	g_rec_mutex_clear (&priv->prop_mutex);

	/* Chain up to parent's finalize() method. */
	G_OBJECT_CLASS (e_client_parent_class)->finalize (object);
}

static void
client_unwrap_dbus_error (EClient *client,
                          GError *dbus_error,
                          GError **out_error)
{
	/* This method is deprecated.  Make it a no-op. */

	if (out_error != NULL)
		*out_error = dbus_error;
}

/* Helper for client_retrieve_capabilities() */
static void
client_retrieve_capabilities_thread (GTask *task,
                                     gpointer source_object,
                                     gpointer task_data,
                                     GCancellable *cancellable)
{
	GError *error = NULL;
	gchar *capabilities;

	if (e_client_retrieve_capabilities_sync (
		E_CLIENT (source_object),
		&capabilities,
		cancellable, &error)) {
		g_task_return_pointer (task, g_steal_pointer (&capabilities), g_free);
	} else {
		g_task_return_error (task, g_steal_pointer (&error));
	}
}

static void
client_retrieve_capabilities (EClient *client,
                              GCancellable *cancellable,
                              GAsyncReadyCallback callback,
                              gpointer user_data)
{
	GTask *task;

	task = g_task_new (client, cancellable, callback, user_data);
	g_task_set_source_tag (task, client_retrieve_capabilities);
	g_task_set_check_cancellable (task, TRUE);

	g_task_run_in_thread (task, client_retrieve_capabilities_thread);

	g_object_unref (task);
}

static gboolean
client_retrieve_capabilities_finish (EClient *client,
                                     GAsyncResult *result,
                                     gchar **capabilities,
                                     GError **error)
{
	GError *local_error = NULL;
	gchar *caps;

	g_return_val_if_fail (g_task_is_valid (result, client), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (result, client_retrieve_capabilities), FALSE);

	caps = g_task_propagate_pointer (G_TASK (result), &local_error);
	if (local_error) {
		g_propagate_error (error, g_steal_pointer (&local_error));
		return FALSE;
	}

	if (capabilities != NULL)
		*capabilities = g_steal_pointer (&caps);
	else
		g_clear_pointer (&caps, g_free);

	return TRUE;
}

static gboolean
client_retrieve_capabilities_sync (EClient *client,
                                   gchar **capabilities,
                                   GCancellable *cancellable,
                                   GError **error)
{
	return e_client_get_backend_property_sync (
		client, CLIENT_BACKEND_PROPERTY_CAPABILITIES,
		capabilities, cancellable, error);
}

/* Helper for client_get_backend_property() */
static void
client_get_backend_property_thread (GTask *task,
                                    gpointer source_object,
                                    gpointer task_data,
                                    GCancellable *cancellable)
{
	const gchar *prop_name = task_data;
	gchar *prop_value = NULL;
	GError *error = NULL;

	if (e_client_get_backend_property_sync (
		E_CLIENT (source_object),
		prop_name,
		&prop_value,
		cancellable, &error)) {
		g_task_return_pointer (task, g_steal_pointer (&prop_value), g_free);
	} else {
		g_task_return_error (task, g_steal_pointer (&error));
	}
}

static void
client_get_backend_property (EClient *client,
                             const gchar *prop_name,
                             GCancellable *cancellable,
                             GAsyncReadyCallback callback,
                             gpointer user_data)
{
	GTask *task;

	task = g_task_new (client, cancellable, callback, user_data);
	g_task_set_source_tag (task, client_get_backend_property);
	g_task_set_check_cancellable (task, TRUE);
	g_task_set_task_data (task, g_strdup (prop_name), g_free);

	g_task_run_in_thread (task, client_get_backend_property_thread);

	g_object_unref (task);
}

static gboolean
client_get_backend_property_finish (EClient *client,
                                    GAsyncResult *result,
                                    gchar **prop_value,
                                    GError **error)
{
	gchar *ret_prop_value;
	gboolean res;

	g_return_val_if_fail (g_task_is_valid (result, client), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (result, client_get_backend_property), FALSE);

	ret_prop_value = g_task_propagate_pointer (G_TASK (result), error);
	res = ret_prop_value != NULL;

	if (prop_value != NULL)
		*prop_value = g_steal_pointer (&ret_prop_value);
	else
		g_clear_pointer (&ret_prop_value, g_free);

	return res;
}

/* Helper for client_set_backend_property() */
static void
client_set_backend_property_thread (GTask *task,
                                    gpointer source_object,
                                    gpointer task_data,
                                    GCancellable *cancellable)
{
	AsyncContext *async_context = task_data;
	GError *error = NULL;

	if (e_client_set_backend_property_sync (
		E_CLIENT (source_object),
		async_context->prop_name,
		async_context->prop_value,
		cancellable, &error)) {
		g_task_return_boolean (task, TRUE);
	} else {
		g_task_return_error (task, g_steal_pointer (&error));
	}
}

static void
client_set_backend_property (EClient *client,
                             const gchar *prop_name,
                             const gchar *prop_value,
                             GCancellable *cancellable,
                             GAsyncReadyCallback callback,
                             gpointer user_data)
{
	GTask *task;
	AsyncContext *async_context;

	async_context = g_slice_new0 (AsyncContext);
	async_context->prop_name = g_strdup (prop_name);
	async_context->prop_value = g_strdup (prop_value);

	task = g_task_new (client, cancellable, callback, user_data);
	g_task_set_source_tag (task, client_set_backend_property);
	g_task_set_check_cancellable (task, TRUE);
	g_task_set_task_data (task, g_steal_pointer (&async_context), (GDestroyNotify) async_context_free);

	g_task_run_in_thread (task, client_set_backend_property_thread);

	g_object_unref (task);
}

static gboolean
client_set_backend_property_finish (EClient *client,
                                    GAsyncResult *result,
                                    GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, client), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (result, client_set_backend_property), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

/* Helper for client_open() */
static void
client_open_thread (GTask *task,
                    gpointer source_object,
                    gpointer task_data,
                    GCancellable *cancellable)
{
	GError *error = NULL;

	if (e_client_open_sync (E_CLIENT (source_object), FALSE, cancellable, &error))
		g_task_return_boolean (task, TRUE);
	else
		g_task_return_error (task, g_steal_pointer (&error));
}

static void
client_open (EClient *client,
             gboolean only_if_exists,
             GCancellable *cancellable,
             GAsyncReadyCallback callback,
             gpointer user_data)
{
	GTask *task;

	task = g_task_new (client, cancellable, callback, user_data);
	g_task_set_source_tag (task, client_open);
	g_task_set_check_cancellable (task, TRUE);

	g_task_run_in_thread (task, client_open_thread);

	g_object_unref (task);
}

static gboolean
client_open_finish (EClient *client,
                    GAsyncResult *result,
                    GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, client), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (result, client_open), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

/* Helper for client_remove() */
static void
client_remove_thread (GTask *task,
                      gpointer source_object,
                      gpointer task_data,
                      GCancellable *cancellable)
{
	GError *error = NULL;

	if (e_client_remove_sync (E_CLIENT (source_object), cancellable, &error))
		g_task_return_boolean (task, TRUE);
	else
		g_task_return_error (task, g_steal_pointer (&error));
}

static void
client_remove (EClient *client,
               GCancellable *cancellable,
               GAsyncReadyCallback callback,
               gpointer user_data)
{
	GTask *task;

	task = g_task_new (client, cancellable, callback, user_data);
	g_task_set_source_tag (task, client_remove);
	g_task_set_check_cancellable (task, TRUE);

	g_task_run_in_thread (task, client_remove_thread);

	g_object_unref (task);
}

static gboolean
client_remove_finish (EClient *client,
                      GAsyncResult *result,
                      GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, client), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (result, client_remove), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

static gboolean
client_remove_sync (EClient *client,
                    GCancellable *cancellable,
                    GError **error)
{
	ESource *source;

	source = e_client_get_source (client);

	return e_source_remove_sync (source, cancellable, error);
}

/* Helper for client_refresh() */
static void
client_refresh_thread (GTask *task,
                      gpointer source_object,
                      gpointer task_data,
                      GCancellable *cancellable)
{
	GError *error = NULL;

	if (e_client_refresh_sync (E_CLIENT (source_object), cancellable, &error))
		g_task_return_boolean (task, TRUE);
	else
		g_task_return_error (task, g_steal_pointer (&error));
}

static void
client_refresh (EClient *client,
                GCancellable *cancellable,
                GAsyncReadyCallback callback,
                gpointer user_data)
{
	GTask *task;

	task = g_task_new (client, cancellable, callback, user_data);
	g_task_set_source_tag (task, client_refresh);
	g_task_set_check_cancellable (task, TRUE);

	g_task_run_in_thread (task, client_refresh_thread);

	g_object_unref (task);
}

static gboolean
client_refresh_finish (EClient *client,
                       GAsyncResult *result,
                       GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, client), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (result, client_refresh), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

static void
e_client_class_init (EClientClass *class)
{
	GObjectClass *object_class;

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = client_set_property;
	object_class->get_property = client_get_property;
	object_class->dispose = client_dispose;
	object_class->finalize = client_finalize;

	class->unwrap_dbus_error = client_unwrap_dbus_error;
	class->retrieve_capabilities = client_retrieve_capabilities;
	class->retrieve_capabilities_finish = client_retrieve_capabilities_finish;
	class->retrieve_capabilities_sync = client_retrieve_capabilities_sync;
	class->get_backend_property = client_get_backend_property;
	class->get_backend_property_finish = client_get_backend_property_finish;
	class->set_backend_property = client_set_backend_property;
	class->set_backend_property_finish = client_set_backend_property_finish;
	class->open = client_open;
	class->open_finish = client_open_finish;
	class->remove = client_remove;
	class->remove_finish = client_remove_finish;
	class->remove_sync = client_remove_sync;
	class->refresh = client_refresh;
	class->refresh_finish = client_refresh_finish;

	/**
	 * EClient:capabilities:
	 *
	 * The capabilities of this client
	 */
	properties[PROP_CAPABILITIES] =
		g_param_spec_pointer (
			"capabilities",
			NULL, NULL,
			G_PARAM_READABLE |
			G_PARAM_EXPLICIT_NOTIFY |
			G_PARAM_STATIC_STRINGS);

	/**
	 * EClient:main-context:
	 *
	 * The main loop context in which notifications for
	 * this client will be delivered.
	 */
	properties[PROP_MAIN_CONTEXT] =
		g_param_spec_boxed (
			"main-context",
			NULL, NULL,
			G_TYPE_MAIN_CONTEXT,
			G_PARAM_READABLE |
			G_PARAM_STATIC_STRINGS);

	/**
	 * EClient:online:
	 *
	 * Whether this client's backing data is online.
	 */
	properties[PROP_ONLINE] =
		g_param_spec_boolean (
			"online",
			NULL, NULL,
			FALSE,
			G_PARAM_READWRITE |
			G_PARAM_EXPLICIT_NOTIFY |
			G_PARAM_STATIC_STRINGS);

	/**
	 * EClient:opened:
	 *
	 * Whether this client is open and ready to use.
	 *
	 * Deprecated: 3.8: This property is no longer relevant and
	 * will always be %TRUE after successfully creating any concrete
	 * type of #EClient.
	 */
	properties[PROP_OPENED] =
		g_param_spec_boolean (
			"opened",
			NULL, NULL,
			FALSE,
			G_PARAM_READABLE |
			G_PARAM_STATIC_STRINGS);

	/**
	 * EClient:readonly:
	 *
	 * Whether this client's backing data is readonly.
	 */
	properties[PROP_READONLY] =
		g_param_spec_boolean (
			"readonly",
			NULL, NULL,
			FALSE,
			G_PARAM_READABLE |
			G_PARAM_STATIC_STRINGS);

	/**
	 * EClient:source:
	 *
	 * The #ESource for which this client was created.
	 */
	properties[PROP_SOURCE] =
		g_param_spec_object (
			"source",
			NULL, NULL,
			E_TYPE_SOURCE,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT_ONLY |
			G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, N_PROPS, properties);

	/**
	 * EClient::opened-signal: (skip)
	 *
	 * Deprecated: 3.8: This signal is no longer emitted.
	 **/
	signals[OPENED] = g_signal_new (
		"opened",
		G_OBJECT_CLASS_TYPE (class),
		G_SIGNAL_RUN_LAST |
		G_SIGNAL_DEPRECATED,
		G_STRUCT_OFFSET (EClientClass, opened),
		NULL, NULL, NULL,
		G_TYPE_NONE, 1,
		G_TYPE_ERROR);

	signals[BACKEND_ERROR] = g_signal_new (
		"backend-error",
		G_OBJECT_CLASS_TYPE (class),
		G_SIGNAL_RUN_FIRST,
		G_STRUCT_OFFSET (EClientClass, backend_error),
		NULL, NULL, NULL,
		G_TYPE_NONE, 1,
		G_TYPE_STRING);

	signals[BACKEND_DIED] = g_signal_new (
		"backend-died",
		G_OBJECT_CLASS_TYPE (class),
		G_SIGNAL_RUN_LAST,
		G_STRUCT_OFFSET (EClientClass, backend_died),
		NULL, NULL, NULL,
		G_TYPE_NONE, 0);

	signals[BACKEND_PROPERTY_CHANGED] = g_signal_new (
		"backend-property-changed",
		G_OBJECT_CLASS_TYPE (class),
		G_SIGNAL_RUN_LAST,
		G_STRUCT_OFFSET (EClientClass, backend_property_changed),
		NULL, NULL, NULL,
		G_TYPE_NONE, 2,
		G_TYPE_STRING,
		G_TYPE_STRING);
}

static void
e_client_init (EClient *client)
{
	client->priv = e_client_get_instance_private (client);

	client->priv->readonly = FALSE;
	client->priv->main_context = g_main_context_ref_thread_default ();

	g_rec_mutex_init (&client->priv->prop_mutex);
}

/**
 * e_client_get_source:
 * @client: an #EClient
 *
 * Get the #ESource that this client has assigned.
 *
 * Returns: (transfer none): The source.
 *
 * Since: 3.2
 **/
ESource *
e_client_get_source (EClient *client)
{
	g_return_val_if_fail (E_IS_CLIENT (client), NULL);

	return client->priv->source;
}

static void
client_ensure_capabilities (EClient *client)
{
	gchar *capabilities = NULL;

	g_return_if_fail (E_IS_CLIENT (client));

	if (client->priv->capabilities != NULL)
		return;

	/* Despite appearances this function does not actually block. */
	e_client_get_backend_property_sync (
		client, CLIENT_BACKEND_PROPERTY_CAPABILITIES,
		&capabilities, NULL, NULL);
	e_client_set_capabilities (client, capabilities);
	g_free (capabilities);
}

/**
 * e_client_get_capabilities:
 * @client: an #EClient
 *
 * Get list of strings with capabilities advertised by a backend.
 * This list, together with inner strings, is owned by the @client.
 * To check for individual capabilities use e_client_check_capability().
 *
 * Returns: (element-type utf8) (transfer none): #GSList of const strings
 *          of capabilities
 *
 * Since: 3.2
 **/
const GSList *
e_client_get_capabilities (EClient *client)
{
	g_return_val_if_fail (E_IS_CLIENT (client), NULL);

	client_ensure_capabilities (client);

	return client->priv->capabilities;
}

/**
 * e_client_ref_main_context:
 * @client: an #EClient
 *
 * Returns the #GMainContext on which event sources for @client are to
 * be attached.
 *
 * The returned #GMainContext is referenced for thread-safety and must be
 * unreferenced with g_main_context_unref() when finished with it.
 *
 * Returns: (transfer full): a #GMainContext
 *
 * Since: 3.8
 **/
GMainContext *
e_client_ref_main_context (EClient *client)
{
	g_return_val_if_fail (E_IS_CLIENT (client), NULL);

	return g_main_context_ref (client->priv->main_context);
}

/**
 * e_client_check_capability:
 * @client: an #EClient
 * @capability: a capability
 *
 * Check if backend supports particular capability.
 * To get all capabilities use e_client_get_capabilities().
 *
 * Returns: #GSList of const strings of capabilities
 *
 * Since: 3.2
 **/
gboolean
e_client_check_capability (EClient *client,
                           const gchar *capability)
{
	GSList *iter;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);
	g_return_val_if_fail (capability, FALSE);

	g_rec_mutex_lock (&client->priv->prop_mutex);

	client_ensure_capabilities (client);

	for (iter = client->priv->capabilities; iter; iter = g_slist_next (iter)) {
		const gchar *cap = iter->data;

		if (cap && g_ascii_strcasecmp (cap, capability) == 0) {
			g_rec_mutex_unlock (&client->priv->prop_mutex);
			return TRUE;
		}
	}

	g_rec_mutex_unlock (&client->priv->prop_mutex);

	return FALSE;
}

/**
 * e_client_check_refresh_supported:
 * @client: A client.
 *
 * Checks whether a client supports explicit refreshing
 * (see e_client_refresh()).
 *
 * Returns: TRUE if the client supports refreshing, FALSE otherwise.
 *
 * Since: 3.2
 **/
gboolean
e_client_check_refresh_supported (EClient *client)
{
	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	return e_client_check_capability (client, "refresh-supported");
}

/* capabilities - comma-separated list of capabilities; can be NULL to unset */
void
e_client_set_capabilities (EClient *client,
                           const gchar *capabilities)
{
	g_return_if_fail (E_IS_CLIENT (client));

	g_rec_mutex_lock (&client->priv->prop_mutex);

	g_slist_foreach (client->priv->capabilities, (GFunc) g_free, NULL);
	g_slist_free (client->priv->capabilities);
	client->priv->capabilities = e_client_util_parse_comma_strings (capabilities);

	g_rec_mutex_unlock (&client->priv->prop_mutex);

	g_object_notify_by_pspec (G_OBJECT (client), properties[PROP_CAPABILITIES]);
}

/**
 * e_client_is_readonly:
 * @client: an #EClient
 *
 * Check if this @client is read-only.
 *
 * Returns: %TRUE if this @client is read-only, otherwise %FALSE.
 *
 * Since: 3.2
 **/
gboolean
e_client_is_readonly (EClient *client)
{
	g_return_val_if_fail (E_IS_CLIENT (client), TRUE);

	return client->priv->readonly;
}

void
e_client_set_readonly (EClient *client,
                       gboolean readonly)
{
	g_return_if_fail (E_IS_CLIENT (client));

	g_rec_mutex_lock (&client->priv->prop_mutex);
	if (client->priv->readonly == readonly) {
		g_rec_mutex_unlock (&client->priv->prop_mutex);
		return;
	}

	client->priv->readonly = readonly;

	g_rec_mutex_unlock (&client->priv->prop_mutex);

	g_object_notify_by_pspec (G_OBJECT (client), properties[PROP_READONLY]);
}

/**
 * e_client_is_online:
 * @client: an #EClient
 *
 * Check if this @client is connected.
 *
 * Returns: %TRUE if this @client is connected, otherwise %FALSE.
 *
 * Since: 3.2
 **/
gboolean
e_client_is_online (EClient *client)
{
	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	return client->priv->online;
}

void
e_client_set_online (EClient *client,
                     gboolean is_online)
{
	g_return_if_fail (E_IS_CLIENT (client));

	/* newly connected/disconnected => make sure capabilities will be correct */
	e_client_set_capabilities (client, NULL);

	g_rec_mutex_lock (&client->priv->prop_mutex);
	if (client->priv->online == is_online) {
		g_rec_mutex_unlock (&client->priv->prop_mutex);
		return;
	}

	client->priv->online = is_online;

	g_rec_mutex_unlock (&client->priv->prop_mutex);

	g_object_notify_by_pspec (G_OBJECT (client), properties[PROP_ONLINE]);
}

/**
 * e_client_is_opened:
 * @client: an #EClient
 *
 * Check if this @client is fully opened. This includes
 * everything from e_client_open() call up to the authentication,
 * if required by a backend. Client cannot do any other operation
 * during the opening phase except of authenticate or cancel it.
 * Every other operation results in an %E_CLIENT_ERROR_BUSY error.
 *
 * Returns: always %TRUE
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Clients don't need to care if they're fully opened
 *                  anymore.  This function always returns %TRUE.
 **/
gboolean
e_client_is_opened (EClient *client)
{
	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	return TRUE;
}

/**
 * e_client_cancel_all:
 * @client: an #EClient
 *
 * Cancels all pending operations started on @client.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: The function no longer does anything.
 **/
void
e_client_cancel_all (EClient *client)
{
	/* Do nothing. */
}

/**
 * e_client_retrieve_capabilities:
 * @client: an #EClient
 * @cancellable: a #GCancellable; can be %NULL
 * @callback: callback to call when a result is ready
 * @user_data: user data for the @callback
 *
 * Initiates retrieval of capabilities on the @client. This is usually
 * required only once, after the @client is opened. The returned value
 * is cached and any subsequent call of e_client_get_capabilities() and
 * e_client_check_capability() is using the cached value.
 * The call is finished by e_client_retrieve_capabilities_finish()
 * from the @callback.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_client_get_capabilities() instead.
 **/
void
e_client_retrieve_capabilities (EClient *client,
                                GCancellable *cancellable,
                                GAsyncReadyCallback callback,
                                gpointer user_data)
{
	EClientClass *class;

	g_return_if_fail (E_IS_CLIENT (client));
	g_return_if_fail (callback != NULL);

	class = E_CLIENT_GET_CLASS (client);
	g_return_if_fail (class != NULL);
	g_return_if_fail (class->retrieve_capabilities != NULL);

	class->retrieve_capabilities (client, cancellable, callback, user_data);
}

/**
 * e_client_retrieve_capabilities_finish:
 * @client: an #EClient
 * @result: a #GAsyncResult
 * @capabilities: (out): Comma-separated list of capabilities of the @client
 * @error: a #GError to set an error, if any
 *
 * Finishes previous call of e_client_retrieve_capabilities().
 * Returned value of @capabilities should be freed with g_free(),
 * when no longer needed.
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_client_get_capabilities() instead.
 **/
gboolean
e_client_retrieve_capabilities_finish (EClient *client,
                                       GAsyncResult *result,
                                       gchar **capabilities,
                                       GError **error)
{
	EClientClass *class;
	gboolean res;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);
	g_return_val_if_fail (capabilities != NULL, FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->retrieve_capabilities_finish != NULL, FALSE);

	*capabilities = NULL;
	res = class->retrieve_capabilities_finish (
		client, result, capabilities, error);

	e_client_set_capabilities (client, res ? *capabilities : NULL);

	return res;
}

/**
 * e_client_retrieve_capabilities_sync:
 * @client: an #EClient
 * @capabilities: (out): Comma-separated list of capabilities of the @client
 * @cancellable: a #GCancellable; can be %NULL
 * @error: a #GError to set an error, if any
 *
 * Initiates retrieval of capabilities on the @client. This is usually
 * required only once, after the @client is opened. The returned value
 * is cached and any subsequent call of e_client_get_capabilities() and
 * e_client_check_capability() is using the cached value. Returned value
 * of @capabilities should be freed with g_free(), when no longer needed.
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_client_get_capabilities() instead.
 **/
gboolean
e_client_retrieve_capabilities_sync (EClient *client,
                                     gchar **capabilities,
                                     GCancellable *cancellable,
                                     GError **error)
{
	EClientClass *class;
	gboolean res = FALSE;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);
	g_return_val_if_fail (capabilities != NULL, FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->retrieve_capabilities_sync != NULL, FALSE);

	*capabilities = NULL;
	res = class->retrieve_capabilities_sync (
		client, capabilities, cancellable, error);

	e_client_set_capabilities (client, res ? *capabilities : NULL);

	return res;
}

/**
 * e_client_get_backend_property:
 * @client: an #EClient
 * @prop_name: property name, whose value to retrieve; cannot be %NULL
 * @cancellable: a #GCancellable; can be %NULL
 * @callback: callback to call when a result is ready
 * @user_data: user data for the @callback
 *
 * Queries @client's backend for a property of name @prop_name.
 * The call is finished by e_client_get_backend_property_finish()
 * from the @callback.
 *
 * Since: 3.2
 **/
void
e_client_get_backend_property (EClient *client,
                               const gchar *prop_name,
                               GCancellable *cancellable,
                               GAsyncReadyCallback callback,
                               gpointer user_data)
{
	EClientClass *class;

	g_return_if_fail (callback != NULL);
	g_return_if_fail (E_IS_CLIENT (client));
	g_return_if_fail (prop_name != NULL);

	class = E_CLIENT_GET_CLASS (client);
	g_return_if_fail (class != NULL);
	g_return_if_fail (class->get_backend_property != NULL);

	class->get_backend_property (
		client, prop_name, cancellable, callback, user_data);
}

/**
 * e_client_get_backend_property_finish:
 * @client: an #EClient
 * @result: a #GAsyncResult
 * @prop_value: (out): Retrieved backend property value; cannot be %NULL
 * @error: a #GError to set an error, if any
 *
 * Finishes previous call of e_client_get_backend_property().
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 **/
gboolean
e_client_get_backend_property_finish (EClient *client,
                                      GAsyncResult *result,
                                      gchar **prop_value,
                                      GError **error)
{
	EClientClass *class;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);
	g_return_val_if_fail (prop_value != NULL, FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->get_backend_property_finish != NULL, FALSE);

	return class->get_backend_property_finish (
		client, result, prop_value, error);
}

/**
 * e_client_get_backend_property_sync:
 * @client: an #EClient
 * @prop_name: property name, whose value to retrieve; cannot be %NULL
 * @prop_value: (out): Retrieved backend property value; cannot be %NULL
 * @cancellable: a #GCancellable; can be %NULL
 * @error: a #GError to set an error, if any
 *
 * Queries @client's backend for a property of name @prop_name.
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 **/
gboolean
e_client_get_backend_property_sync (EClient *client,
                                    const gchar *prop_name,
                                    gchar **prop_value,
                                    GCancellable *cancellable,
                                    GError **error)
{
	EClientClass *class;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);
	g_return_val_if_fail (prop_name != NULL, FALSE);
	g_return_val_if_fail (prop_value != NULL, FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->get_backend_property_sync != NULL, FALSE);

	return class->get_backend_property_sync (
		client, prop_name, prop_value, cancellable, error);
}

/**
 * e_client_set_backend_property:
 * @client: an #EClient
 * @prop_name: property name, whose value to change; cannot be %NULL
 * @prop_value: property value, to set; cannot be %NULL
 * @cancellable: a #GCancellable; can be %NULL
 * @callback: callback to call when a result is ready
 * @user_data: user data for the @callback
 *
 * Sets @client's backend property of name @prop_name
 * to value @prop_value. The call is finished
 * by e_client_set_backend_property_finish() from the @callback.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Clients cannot set backend properties.  Any attempt
 *                  will fail with an %E_CLIENT_ERROR_NOT_SUPPORTED error.
 **/
void
e_client_set_backend_property (EClient *client,
                               const gchar *prop_name,
                               const gchar *prop_value,
                               GCancellable *cancellable,
                               GAsyncReadyCallback callback,
                               gpointer user_data)
{
	EClientClass *class;

	g_return_if_fail (callback != NULL);
	g_return_if_fail (E_IS_CLIENT (client));
	g_return_if_fail (prop_name != NULL);
	g_return_if_fail (prop_value != NULL);

	class = E_CLIENT_GET_CLASS (client);
	g_return_if_fail (class != NULL);
	g_return_if_fail (class->set_backend_property != NULL);

	class->set_backend_property (
		client, prop_name, prop_value,
		cancellable, callback, user_data);
}

/**
 * e_client_set_backend_property_finish:
 * @client: an #EClient
 * @result: a #GAsyncResult
 * @error: a #GError to set an error, if any
 *
 * Finishes previous call of e_client_set_backend_property().
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Clients cannot set backend properties.  Any attempt
 *                  will fail with an %E_CLIENT_ERROR_NOT_SUPPORTED error.
 **/
gboolean
e_client_set_backend_property_finish (EClient *client,
                                      GAsyncResult *result,
                                      GError **error)
{
	EClientClass *class;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->set_backend_property_finish != NULL, FALSE);

	return class->set_backend_property_finish (client, result, error);
}

/**
 * e_client_set_backend_property_sync:
 * @client: an #EClient
 * @prop_name: property name, whose value to change; cannot be %NULL
 * @prop_value: property value, to set; cannot be %NULL
 * @cancellable: a #GCancellable; can be %NULL
 * @error: a #GError to set an error, if any
 *
 * Sets @client's backend property of name @prop_name
 * to value @prop_value.
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Clients cannot set backend properties.  Any attempt
 *                  will fail with an %E_CLIENT_ERROR_NOT_SUPPORTED error.
 **/
gboolean
e_client_set_backend_property_sync (EClient *client,
                                    const gchar *prop_name,
                                    const gchar *prop_value,
                                    GCancellable *cancellable,
                                    GError **error)
{
	EClientClass *class;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);
	g_return_val_if_fail (prop_name != NULL, FALSE);
	g_return_val_if_fail (prop_value != NULL, FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->set_backend_property_sync != NULL, FALSE);

	return class->set_backend_property_sync (
		client, prop_name, prop_value, cancellable, error);
}

/**
 * e_client_open:
 * @client: an #EClient
 * @only_if_exists: this parameter is not used anymore
 * @cancellable: a #GCancellable; can be %NULL
 * @callback: callback to call when a result is ready
 * @user_data: user data for the @callback
 *
 * Opens the @client, making it ready for queries and other operations.
 * The call is finished by e_client_open_finish() from the @callback.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_book_client_connect() and
 *                  e_book_client_connect_finish() or
 *                  e_cal_client_connect() and
 *                  e_cal_client_connect_finish() instead.
 **/
void
e_client_open (EClient *client,
               gboolean only_if_exists,
               GCancellable *cancellable,
               GAsyncReadyCallback callback,
               gpointer user_data)
{
	EClientClass *class;

	g_return_if_fail (callback != NULL);
	g_return_if_fail (E_IS_CLIENT (client));

	class = E_CLIENT_GET_CLASS (client);
	g_return_if_fail (class != NULL);
	g_return_if_fail (class->open != NULL);

	class->open (client, only_if_exists, cancellable, callback, user_data);
}

/**
 * e_client_open_finish:
 * @client: an #EClient
 * @result: a #GAsyncResult
 * @error: a #GError to set an error, if any
 *
 * Finishes previous call of e_client_open().
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_book_client_connect() and
 *                  e_book_client_connect_finish() or
 *                  e_cal_client_connect() and
 *                  e_cal_client_connect_finish() instead.
 **/
gboolean
e_client_open_finish (EClient *client,
                      GAsyncResult *result,
                      GError **error)
{
	EClientClass *class;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->open_finish != NULL, FALSE);

	return class->open_finish (client, result, error);
}

/**
 * e_client_open_sync:
 * @client: an #EClient
 * @only_if_exists: this parameter is not used anymore
 * @cancellable: a #GCancellable; can be %NULL
 * @error: a #GError to set an error, if any
 *
 * Opens the @client, making it ready for queries and other operations.
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_book_client_connect_sync() or
 *                  e_cal_client_connect_sync() instead.
 **/
gboolean
e_client_open_sync (EClient *client,
                    gboolean only_if_exists,
                    GCancellable *cancellable,
                    GError **error)
{
	EClientClass *class;

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->open_sync != NULL, FALSE);

	return class->open_sync (client, only_if_exists, cancellable, error);
}

/**
 * e_client_remove:
 * @client: an #EClient
 * @cancellable: a #GCancellable; can be %NULL
 * @callback: callback to call when a result is ready
 * @user_data: user data for the @callback
 *
 * Removes the backing data for this #EClient. For example, with the file
 * backend this deletes the database file. You cannot get it back!
 * The call is finished by e_client_remove_finish() from the @callback.
 *
 * Since: 3.2
 *
 * Deprecated: 3.6: Use e_source_remove() instead.
 **/
void
e_client_remove (EClient *client,
                 GCancellable *cancellable,
                 GAsyncReadyCallback callback,
                 gpointer user_data)
{
	EClientClass *class;

	g_return_if_fail (E_IS_CLIENT (client));
	g_return_if_fail (callback != NULL);

	class = E_CLIENT_GET_CLASS (client);
	g_return_if_fail (class != NULL);
	g_return_if_fail (class->remove != NULL);

	class->remove (client, cancellable, callback, user_data);
}

/**
 * e_client_remove_finish:
 * @client: an #EClient
 * @result: a #GAsyncResult
 * @error: a #GError to set an error, if any
 *
 * Finishes previous call of e_client_remove().
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 *
 * Deprecated: 3.6: Use e_source_remove_finish() instead.
 **/
gboolean
e_client_remove_finish (EClient *client,
                        GAsyncResult *result,
                        GError **error)
{
	EClientClass *class;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->remove_finish != NULL, FALSE);

	return class->remove_finish (client, result, error);
}

/**
 * e_client_remove_sync:
 * @client: an #EClient
 * @cancellable: a #GCancellable; can be %NULL
 * @error: a #GError to set an error, if any
 *
 * Removes the backing data for this #EClient. For example, with the file
 * backend this deletes the database file. You cannot get it back!
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 *
 * Deprecated: 3.6: Use e_source_remove_sync() instead.
 **/
gboolean
e_client_remove_sync (EClient *client,
                      GCancellable *cancellable,
                      GError **error)
{
	EClientClass *class;

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->remove_sync != NULL, FALSE);

	return class->remove_sync (client, cancellable, error);
}

/**
 * e_client_refresh:
 * @client: an #EClient
 * @cancellable: a #GCancellable; can be %NULL
 * @callback: callback to call when a result is ready
 * @user_data: user data for the @callback
 *
 * Initiates refresh on the @client. Finishing the method doesn't mean
 * that the refresh is done, backend only notifies whether it started
 * refreshing or not. Use e_client_check_refresh_supported() to check
 * whether the backend supports this method.
 * The call is finished by e_client_refresh_finish() from the @callback.
 *
 * Since: 3.2
 **/
void
e_client_refresh (EClient *client,
                  GCancellable *cancellable,
                  GAsyncReadyCallback callback,
                  gpointer user_data)
{
	EClientClass *class;

	g_return_if_fail (E_IS_CLIENT (client));
	g_return_if_fail (callback != NULL);

	class = E_CLIENT_GET_CLASS (client);
	g_return_if_fail (class != NULL);
	g_return_if_fail (class->refresh != NULL);

	class->refresh (client, cancellable, callback, user_data);
}

/**
 * e_client_refresh_finish:
 * @client: an #EClient
 * @result: a #GAsyncResult
 * @error: a #GError to set an error, if any
 *
 * Finishes previous call of e_client_refresh().
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 **/
gboolean
e_client_refresh_finish (EClient *client,
                         GAsyncResult *result,
                         GError **error)
{
	EClientClass *class;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->refresh_finish != NULL, FALSE);

	return class->refresh_finish (client, result, error);
}

/**
 * e_client_refresh_sync:
 * @client: an #EClient
 * @cancellable: a #GCancellable; can be %NULL
 * @error: a #GError to set an error, if any
 *
 * Initiates refresh on the @client. Finishing the method doesn't mean
 * that the refresh is done, backend only notifies whether it started
 * refreshing or not. Use e_client_check_refresh_supported() to check
 * whether the backend supports this method.
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.2
 **/
gboolean
e_client_refresh_sync (EClient *client,
                       GCancellable *cancellable,
                       GError **error)
{
	EClientClass *class;

	class = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (class != NULL, FALSE);
	g_return_val_if_fail (class->refresh_sync != NULL, FALSE);

	return class->refresh_sync (client, cancellable, error);
}

static void
client_wait_for_connected_thread (GTask *task,
				  gpointer source_object,
				  gpointer task_data,
				  GCancellable *cancellable)
{
	guint32 timeout_seconds;
	gboolean success;
	GError *local_error = NULL;

	timeout_seconds = GPOINTER_TO_UINT (task_data);
	success = e_client_wait_for_connected_sync (E_CLIENT (source_object), timeout_seconds, cancellable, &local_error);

	if (local_error != NULL) {
		g_task_return_error (task, local_error);
	} else {
		g_task_return_boolean (task, success);
	}
}

/**
 * e_client_wait_for_connected:
 * @client: an #EClient
 * @timeout_seconds: a timeout for the wait, in seconds
 * @cancellable: a #GCancellable; or %NULL
 * @callback: callback to call when a result is ready
 * @user_data: user data for the @callback
 *
 * Asynchronously waits until the @client is connected (according
 * to @ESource::connection-status property), but not longer than @timeout_seconds.
 *
 * The call is finished by e_client_wait_for_connected_finish() from
 * the @callback.
 *
 * Since: 3.16
 **/
void
e_client_wait_for_connected (EClient *client,
			     guint32 timeout_seconds,
			     GCancellable *cancellable,
			     GAsyncReadyCallback callback,
			     gpointer user_data)
{
	GTask *task;

	g_return_if_fail (E_IS_CLIENT (client));

	task = g_task_new (client, cancellable, callback, user_data);
	g_task_set_source_tag (task, e_client_wait_for_connected);
	g_task_set_task_data (task, GUINT_TO_POINTER (timeout_seconds), NULL);

	g_task_run_in_thread (task, client_wait_for_connected_thread);

	g_object_unref (task);
}

/**
 * e_client_wait_for_connected_finish:
 * @client: an #EClient
 * @result: a #GAsyncResult
 * @error: a #GError to set an error, or %NULL
 *
 * Finishes previous call of e_client_wait_for_connected().
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.16
 **/
gboolean
e_client_wait_for_connected_finish (EClient *client,
				    GAsyncResult *result,
				    GError **error)
{
	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);
	g_return_val_if_fail (g_task_is_valid (result, client), FALSE);

	g_return_val_if_fail (
		g_async_result_is_tagged (
		result, e_client_wait_for_connected), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

static void
client_wait_for_connected_cancelled_cb (GCancellable *cancellable,
					EFlag *flag)
{
	g_return_if_fail (flag != NULL);

	e_flag_set (flag);
}

static void
client_wait_for_connected_notify_cb (ESource *source,
				     GParamSpec *param,
				     EFlag *flag)
{
	g_return_if_fail (flag != NULL);

	if (e_source_get_connection_status (source) == E_SOURCE_CONNECTION_STATUS_CONNECTED)
		e_flag_set (flag);
}

static void
client_wait_for_connected_notify_online_cb (EClient *client,
					    GParamSpec *param,
					    EFlag *flag)
{
	g_return_if_fail (flag != NULL);

	if (!e_client_is_online (client))
		e_flag_set (flag);
}

/**
 * e_client_wait_for_connected_sync:
 * @client: an #EClient
 * @timeout_seconds: a timeout for the wait, in seconds
 * @cancellable: a #GCancellable; or %NULL
 * @error: a #GError to set an error, or %NULL
 *
 * Synchronously waits until the @client is connected (according
 * to @ESource::connection-status property), but not longer than @timeout_seconds.
 *
 * Note: This also calls e_client_retrieve_properties_sync() on success, to have
 *   up-to-date property values on the client side, without a delay due
 *   to property change notifcations delivery through D-Bus.
 *
 * Returns: %TRUE if successful, %FALSE otherwise.
 *
 * Since: 3.16
 **/
gboolean
e_client_wait_for_connected_sync (EClient *client,
				  guint32 timeout_seconds,
				  GCancellable *cancellable,
				  GError **error)
{
	ESource *source;
	EFlag *flag;
	gulong cancellable_handler_id = 0;
	gulong notify_connection_status_handler_id;
	gulong notify_online_handler_id;
	gint64 end_time;
	gboolean success;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	end_time = g_get_monotonic_time () + timeout_seconds * G_TIME_SPAN_SECOND;
	flag = e_flag_new ();

	if (cancellable) {
		cancellable_handler_id = g_cancellable_connect (cancellable,
			G_CALLBACK (client_wait_for_connected_cancelled_cb), flag, NULL);
	}

	source = e_client_get_source (client);

	notify_connection_status_handler_id = g_signal_connect (source, "notify::connection-status",
		G_CALLBACK (client_wait_for_connected_notify_cb), flag);
	notify_online_handler_id = g_signal_connect (client, "notify::online",
		G_CALLBACK (client_wait_for_connected_notify_online_cb), flag);

	while (success = e_source_get_connection_status (source) == E_SOURCE_CONNECTION_STATUS_CONNECTED ||
			!e_client_is_online (client),
	       !success && !g_cancellable_is_cancelled (cancellable)) {
		e_flag_clear (flag);

		if (timeout_seconds > 0) {
			if (g_get_monotonic_time () > end_time)
				break;

			e_flag_wait_until (flag, end_time);
		} else {
			e_flag_wait (flag);
		}
	}

	g_signal_handler_disconnect (source, notify_connection_status_handler_id);
	g_signal_handler_disconnect (client, notify_online_handler_id);

	if (cancellable_handler_id > 0 && cancellable)
		g_cancellable_disconnect (cancellable, cancellable_handler_id);

	e_flag_free (flag);

	success = e_source_get_connection_status (source) == E_SOURCE_CONNECTION_STATUS_CONNECTED;

	if (!success && !g_cancellable_set_error_if_cancelled (cancellable, error)) {
		g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, _("Timeout was reached"));

		/* preload properties also when in the offline mode */
		if (!e_client_is_online (client))
			e_client_retrieve_properties_sync (client, cancellable, NULL);
	} else if (success) {
		success = e_client_retrieve_properties_sync (client, cancellable, error);
	}

	return success;
}

/**
 * e_client_retrieve_properties_sync:
 * @client: an #EClient
 * @cancellable: optional #GCancellable object, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Retrieves @client properties to match server-side values, without waiting
 * for the D-Bus property change notifications delivery.
 *
 * If an error occurs, the function sets @error and returns %FALSE.
 *
 * Returns: %TRUE on success, %FALSE on error
 *
 * Since: 3.16
 **/
gboolean
e_client_retrieve_properties_sync (EClient *client,
				   GCancellable *cancellable,
				   GError **error)
{
	EClientClass *klass;

	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);

	klass = E_CLIENT_GET_CLASS (client);
	g_return_val_if_fail (klass != NULL, FALSE);
	g_return_val_if_fail (klass->retrieve_properties_sync != NULL, FALSE);

	return klass->retrieve_properties_sync (client, cancellable, error);
}

static void
client_retrieve_properties_thread (GTask *task,
				   gpointer source_object,
				   gpointer task_data,
				   GCancellable *cancellable)
{
	gboolean success;
	GError *local_error = NULL;

	success = e_client_retrieve_properties_sync (E_CLIENT (source_object), cancellable, &local_error);

	if (local_error != NULL) {
		g_task_return_error (task, local_error);
	} else {
		g_task_return_boolean (task, success);
	}
}

/**
 * e_client_retrieve_properties:
 * @client: an #EClient
 * @cancellable: optional #GCancellable object, or %NULL
 * @callback: a #GAsyncReadyCallback to call when the request is satisfied
 * @user_data: data to pass to the callback function
 *
 * Asynchronously retrieves @client properties to match server-side values,
 * without waiting for the D-Bus property change notifications delivery.
 *
 * When the operation is finished, @callback will be called. You can then
 * call e_client_retrieve_properties_finish() to get the result of the operation.
 *
 * Since: 3.16
 **/
void
e_client_retrieve_properties (EClient *client,
			      GCancellable *cancellable,
			      GAsyncReadyCallback callback,
			      gpointer user_data)
{
	GTask *task;

	g_return_if_fail (E_IS_CLIENT (client));

	task = g_task_new (client, cancellable, callback, user_data);
	g_task_set_source_tag (task, e_client_retrieve_properties);

	g_task_run_in_thread (task, client_retrieve_properties_thread);

	g_object_unref (task);
}

/**
 * e_client_retrieve_properties_finish:
 * @client: an #EClient
 * @result: a #GAsyncResult
 * @error: return location for a #GError, or %NULL
 *
 * Finishes the operation started with e_client_retrieve_properties().
 *
 * If an error occurs, the function sets @error and returns %FALSE.
 *
 * Returns: %TRUE on success, %FALSE on error
 *
 * Since: 3.16
 **/
gboolean
e_client_retrieve_properties_finish (EClient *client,
				     GAsyncResult *result,
				     GError **error)
{
	g_return_val_if_fail (E_IS_CLIENT (client), FALSE);
	g_return_val_if_fail (g_task_is_valid (result, client), FALSE);

	g_return_val_if_fail (
		g_async_result_is_tagged (
		result, e_client_retrieve_properties), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

/**
 * e_client_util_slist_to_strv:
 * @strings: (element-type utf8): a #GSList of strings (const gchar *)
 *
 * Convert a list of strings into a %NULL-terminated array of strings.
 *
 * Returns: (transfer full): Newly allocated %NULL-terminated array of strings.
 * The returned pointer should be freed with g_strfreev().
 *
 * Note: Paired function for this is e_client_util_strv_to_slist().
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_util_slist_to_strv() instead.
 **/
gchar **
e_client_util_slist_to_strv (const GSList *strings)
{
	return e_util_slist_to_strv (strings);
}

/**
 * e_client_util_strv_to_slist:
 * @strv: a %NULL-terminated array of strings (const gchar *)
 *
 * Convert a %NULL-terminated array of strings to a list of strings.
 *
 * Returns: (transfer full) (element-type utf8): Newly allocated #GSList of
 * newly allocated strings. The returned pointer should be freed with
 * e_client_util_free_string_slist().
 *
 * Note: Paired function for this is e_client_util_slist_to_strv().
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_util_strv_to_slist() instead.
 **/
GSList *
e_client_util_strv_to_slist (const gchar * const *strv)
{
	return e_util_strv_to_slist (strv);
}

/**
 * e_client_util_copy_string_slist:
 * @copy_to: (element-type utf8) (nullable) (transfer full): Where to copy; may be %NULL
 * @strings: (element-type utf8): #GSList of strings to be copied
 *
 * Copies the #GSList of strings to the end of @copy_to.
 *
 * Returns: (transfer full) (element-type utf8): New head of @copy_to.
 * The returned pointer can be freed with e_client_util_free_string_slist().
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_util_copy_string_slist() instead.
 **/
GSList *
e_client_util_copy_string_slist (GSList *copy_to,
                                 const GSList *strings)
{
	return e_util_copy_string_slist (copy_to, strings);
}

/**
 * e_client_util_copy_object_slist:
 * @copy_to: (element-type GObject) (nullable) (transfer full): Where to copy; may be %NULL
 * @objects: (element-type GObject): #GSList of #GObject<!-- -->s to be copied
 *
 * Copies a #GSList of #GObject<!-- -->s to the end of @copy_to.
 *
 * Returns: (transfer full) (element-type GObject): New head of @copy_to.
 * The returned pointer can be freed with e_client_util_free_object_slist().
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use e_util_copy_object_slist() instead.
 **/
GSList *
e_client_util_copy_object_slist (GSList *copy_to,
                                 const GSList *objects)
{
	return e_util_copy_object_slist (copy_to, objects);
}

/**
 * e_client_util_free_string_slist:
 * @strings: (element-type utf8): a #GSList of strings (gchar *)
 *
 * Frees memory previously allocated by e_client_util_strv_to_slist().
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use g_slist_free_full() instead.
 **/
void
e_client_util_free_string_slist (GSList *strings)
{
	e_util_free_string_slist (strings);
}

/**
 * e_client_util_free_object_slist:
 * @objects: (element-type GObject): a #GSList of #GObject<!-- -->s
 *
 * Calls g_object_unref() on each member of @objects and then frees @objects
 * itself.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use g_slist_free_full() instead.
 **/
void
e_client_util_free_object_slist (GSList *objects)
{
	e_util_free_object_slist (objects);
}

/**
 * e_client_util_parse_comma_strings:
 * @strings: string of comma-separated values
 *
 * Parses comma-separated list of values into #GSList.
 *
 * Returns: (transfer full) (element-type utf8): Newly allocated #GSList of
 * newly allocated strings corresponding to values parsed from @strings.
 * Free the returned pointer with e_client_util_free_string_slist().
 *
 * Since: 3.2
 **/
GSList *
e_client_util_parse_comma_strings (const gchar *strings)
{
	GSList *strs_slist = NULL;
	gchar **strs_strv = NULL;
	gint ii;

	if (!strings || !*strings)
		return NULL;

	strs_strv = g_strsplit (strings, ",", -1);
	g_return_val_if_fail (strs_strv != NULL, NULL);

	for (ii = 0; strs_strv && strs_strv[ii]; ii++) {
		gchar *str = g_strstrip (strs_strv[ii]);

		if (str && *str)
			strs_slist = g_slist_prepend (strs_slist, g_strdup (str));
	}

	g_strfreev (strs_strv);

	return g_slist_reverse (strs_slist);
}

/**
 * e_client_unwrap_dbus_error:
 * @client: an #EClient
 * @dbus_error: a #GError returned bu D-Bus
 * @out_error: a #GError variable where to store the result
 *
 * Unwraps D-Bus error to local error. @dbus_error is automatically freed.
 * @dbus_erorr and @out_error can point to the same variable.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: Use g_dbus_error_strip_remote_error() instead.
 **/
void
e_client_unwrap_dbus_error (EClient *client,
                            GError *dbus_error,
                            GError **out_error)
{
	EClientClass *class;

	g_return_if_fail (E_IS_CLIENT (client));

	class = E_CLIENT_GET_CLASS (client);
	g_return_if_fail (class != NULL);
	g_return_if_fail (class->unwrap_dbus_error != NULL);

	if (!dbus_error || !out_error) {
		if (dbus_error)
			g_error_free (dbus_error);
	} else {
		class->unwrap_dbus_error (client, dbus_error, out_error);
	}
}

/**
 * e_client_util_unwrap_dbus_error:
 * @dbus_error: DBus #GError to unwrap
 * @client_error: (out): Resulting #GError; can be %NULL
 * @known_errors: List of known errors against which try to match
 * @known_errors_count: How many items are stored in @known_errors
 * @known_errors_domain: Error domain for @known_errors
 * @fail_when_none_matched: Whether to fail when none of @known_errors matches
 *
 * The function takes a @dbus_error and tries to find a match in @known_errors
 * for it, if it is a G_IO_ERROR, G_IO_ERROR_DBUS_ERROR. If it is anything else
 * then the @dbus_error is moved to @client_error.
 *
 * The @fail_when_none_matched influences behaviour. If it's %TRUE, and none of
 * @known_errors matches, or this is not a G_IO_ERROR_DBUS_ERROR, then %FALSE
 * is returned and the @client_error is left without change. Otherwise, the
 * @fail_when_none_matched is %FALSE, the error is always processed and will
 * result in E_CLIENT_ERROR, E_CLIENT_ERROR_OTHER_ERROR if none of @known_error
 * matches.
 *
 * Returns: Whether was @dbus_error processed into @client_error.
 *
 * Note: The @dbus_error is automatically freed if returned %TRUE.
 *
 * Since: 3.2
 *
 * Deprecated: 3.8: This function is no longer used.
 **/
gboolean
e_client_util_unwrap_dbus_error (GError *dbus_error,
                                 GError **client_error,
                                 const EClientErrorsList *known_errors,
                                 guint known_errors_count,
                                 GQuark known_errors_domain,
                                 gboolean fail_when_none_matched)
{
	if (!client_error) {
		if (dbus_error)
			g_error_free (dbus_error);
		return TRUE;
	}

	if (!dbus_error) {
		*client_error = NULL;
		return TRUE;
	}

	if (dbus_error->domain == known_errors_domain) {
		*client_error = dbus_error;
		return TRUE;
	}

	if (known_errors) {
		if (g_error_matches (dbus_error, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR)) {
			gchar *name;
			gint ii;

			name = g_dbus_error_get_remote_error (dbus_error);

			for (ii = 0; ii < known_errors_count; ii++) {
				if (g_ascii_strcasecmp (known_errors[ii].name, name) == 0) {
					g_free (name);

					g_dbus_error_strip_remote_error (dbus_error);
					*client_error = g_error_new_literal (
						known_errors_domain,
						known_errors[ii].err_code,
						dbus_error->message);
					g_error_free (dbus_error);
					return TRUE;
				}
			}

			g_free (name);
		}
	}

	if (fail_when_none_matched)
		return FALSE;

	if (g_error_matches (dbus_error, G_IO_ERROR, G_IO_ERROR_DBUS_ERROR)) {
		g_dbus_error_strip_remote_error (dbus_error);
		*client_error = g_error_new_literal (
			E_CLIENT_ERROR,
			E_CLIENT_ERROR_OTHER_ERROR,
			dbus_error->message);
		g_error_free (dbus_error);
	} else {
		g_dbus_error_strip_remote_error (dbus_error);
		*client_error = dbus_error;
	}

	return TRUE;
}

/**
 * e_client_dup_bus_name:
 * @client: an #EClient
 *
 * Returns a D-Bus bus name that will be used to connect the
 * client to the backend subprocess.
 *
 * Returns: a newly-allocated string representing a D-Bus bus
 *          name that will be used to connect the client to
 *          the backend subprocess. The string should be
 *          freed by the caller using g_free().
 *
 * Since: 3.16
 **/
gchar *
e_client_dup_bus_name (EClient *client)
{
	g_return_val_if_fail (E_IS_CLIENT (client), NULL);

	return g_strdup (client->priv->bus_name);
}

/**
 * e_client_set_bus_name:
 * @client: an #EClient
 * @bus_name: a string representing a D-Bus bus name
 *
 * Sets a D-Bus bus name that will be used to connect the client
 * to the backend subprocess.
 *
 * Since: 3.16
 **/
void
e_client_set_bus_name (EClient *client,
		       const gchar *bus_name)
{
	g_return_if_fail (E_IS_CLIENT (client));
	g_return_if_fail (client->priv->bus_name == NULL);
	g_return_if_fail (bus_name != NULL && *bus_name != '\0');

	client->priv->bus_name = g_strdup (bus_name);
}
