/*
 * SPDX-FileCopyrightText: (C) 2015 Red Hat, Inc. (www.redhat.com)
 * SPDX-License-Identifier: LGPL-2.0-or-later
 */

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

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

#include <camel/camel.h>
#include <libedataserver/libedataserver.h>

#include "e-credentials-prompter.h"

/* built-in credentials prompter implementations */
#include "e-credentials-prompter-impl-password.h"
#include "e-credentials-prompter-impl-oauth2.h"

typedef struct _ProcessPromptData {
	ECredentialsPrompterImpl *prompter_impl;
	ESource *auth_source;
	ESource *cred_source;
	ESourceConnectionStatus connection_status; /* of the auth_source */
	gboolean remember_password; /* of the cred_source, to check for changes */
	gulong notify_handler_id;
	gchar *error_text;
	ENamedParameters *credentials;
	gboolean allow_source_save;
	GTask *task;
} ProcessPromptData;

struct _ECredentialsPrompterPrivate {
	ESourceRegistry *registry;
	ESourceCredentialsProvider *provider;
	gboolean auto_prompt;
	GCancellable *cancellable;

	GMutex disabled_auto_prompt_lock;
	GHashTable *disabled_auto_prompt; /* gchar *source_uid ~> 1; Source UIDs for which the auto-prompt is disabled */

	GMutex prompters_lock;
	GHashTable *prompters; 		/* gchar *method ~> ECredentialsPrompterImpl *impl */
	GHashTable *known_prompters;	/* gpointer [ECredentialsPrompterImpl] ~> UINT known instances; the prompter_impl is not referenced */

	GRecMutex queue_lock;		/* guards all queue and schedule related properties */
	GSList *queue;			/* ProcessPromptData * */
	ProcessPromptData *processing_prompt;
	guint schedule_idle_id;
};

enum {
	PROP_0,
	PROP_AUTO_PROMPT,
	PROP_REGISTRY,
	PROP_PROVIDER,
	N_PROPS
};

enum {
	GET_DIALOG_PARENT,
	GET_DIALOG_PARENT_FULL,
	LAST_SIGNAL
};

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

static guint signals[LAST_SIGNAL];

G_DEFINE_TYPE_WITH_CODE (ECredentialsPrompter, e_credentials_prompter, G_TYPE_OBJECT,
	G_ADD_PRIVATE (ECredentialsPrompter)
	G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL))

static void
process_prompt_data_free (gpointer ptr)
{
	ProcessPromptData *ppd = ptr;

	if (ppd) {
		g_clear_signal_handler (&ppd->notify_handler_id, ppd->auth_source);

		if (ppd->task) {
			g_task_return_new_error (ppd->task, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Credentials prompt was cancelled"));
			g_clear_object (&ppd->task);
		}

		g_clear_object (&ppd->prompter_impl);
		g_clear_object (&ppd->auth_source);
		g_clear_object (&ppd->cred_source);
		g_clear_pointer (&ppd->error_text, g_free);
		g_clear_pointer (&ppd->credentials, e_named_parameters_free);
		g_slice_free (ProcessPromptData, ppd);
	}
}

typedef struct _LookupSourceDetailsData {
	ESource *auth_source; /* an ESource which asked for credentials */
	ESource *cred_source; /* this might be auth_source or a parent collection source, if applicable, from where the credentials come */
	ENamedParameters *credentials; /* actual stored credentials */
} LookupSourceDetailsData;

static void
lookup_source_details_data_free (gpointer ptr)
{
	LookupSourceDetailsData *data = ptr;

	if (data) {
		g_clear_object (&data->auth_source);
		g_clear_object (&data->cred_source);
		g_clear_pointer (&data->credentials, e_named_parameters_free);
		g_slice_free (LookupSourceDetailsData, data);
	}
}

static void
credentials_prompter_lookup_source_details_thread (GTask *task,
						   gpointer source_object,
						   gpointer task_data,
						   GCancellable *cancellable)
{
	ESource *source, *cred_source = NULL;
	ECredentialsPrompter *prompter;
	ESourceCredentialsProvider *provider;
	ENamedParameters *credentials = NULL;
	GError *local_error = NULL;

	g_return_if_fail (E_IS_SOURCE (source_object));

	source = E_SOURCE (source_object);

	prompter = g_weak_ref_get (task_data);
	if (!prompter)
		return;

	provider = e_credentials_prompter_get_provider (prompter);
	cred_source = e_source_credentials_provider_ref_credentials_source (provider, source);

	e_source_credentials_provider_lookup_sync (provider, cred_source ? cred_source : source, cancellable, &credentials, &local_error);

	/* Interested only in the cancelled error, which means the prompter is freed. */
	if (local_error != NULL && g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		g_task_return_error (task, g_steal_pointer (&local_error));
	} else {
		LookupSourceDetailsData *data;

		data = g_slice_new0 (LookupSourceDetailsData);
		data->auth_source = g_object_ref (source);
		data->cred_source = g_object_ref (cred_source ? cred_source : source); /* always set both, for simplicity */
		data->credentials = g_steal_pointer (&credentials); /* NULL for no credentials available */

		g_task_return_pointer (task, data, lookup_source_details_data_free);
	}

	g_clear_pointer (&credentials, e_named_parameters_free);
	g_clear_object (&cred_source);
	g_clear_object (&prompter);
	g_clear_error (&local_error);
}

static void
credentials_prompter_lookup_source_details (ESource *source,
					    ECredentialsPrompter *prompter,
					    GAsyncReadyCallback callback,
					    gpointer user_data)
{
	GTask *task;

	g_return_if_fail (E_IS_SOURCE (source));
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	task = g_task_new (source, prompter->priv->cancellable, callback, user_data);
	g_task_set_source_tag (task, credentials_prompter_lookup_source_details_thread);
	g_task_set_task_data (task, e_weak_ref_new (prompter), (GDestroyNotify) e_weak_ref_free);

	g_task_run_in_thread (task, credentials_prompter_lookup_source_details_thread);

	g_object_unref (task);
}

static gboolean
credentials_prompter_lookup_source_details_finish (ESource *source,
						   GAsyncResult *result,
						   ECredentialsPrompter **out_prompter, /* will be referenced, if not NULL */
						   LookupSourceDetailsData **out_data,
						   GError **error)
{
	LookupSourceDetailsData *data;

	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
	g_return_val_if_fail (out_prompter != NULL, FALSE);
	g_return_val_if_fail (out_data != NULL, FALSE);
	g_return_val_if_fail (g_task_is_valid (result, source), FALSE);

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

	data = g_task_propagate_pointer (G_TASK (result), error);
	if (!data)
		return FALSE;

	*out_data = data;
	*out_prompter = g_weak_ref_get (g_task_get_task_data (G_TASK (result)));

	return TRUE;
}

static void
credentials_prompter_invoke_authenticate_cb (GObject *source_object,
					     GAsyncResult *result,
					     gpointer user_data)
{
	GError *error = NULL;

	if (!e_source_invoke_authenticate_finish (E_SOURCE (source_object), result, &error) &&
	    !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		g_debug ("%s: Failed to invoke authenticate: %s", G_STRFUNC, error ? error->message : "Unknown error");
	}

	g_clear_error (&error);
}

typedef struct _CredentialsPromptData {
	ESource *source;
	gchar *error_text;
	ECredentialsPrompterPromptFlags flags;
	GTask *task;
} CredentialsPromptData;

static void
credentials_prompt_data_free (gpointer ptr)
{
	CredentialsPromptData *data = ptr;

	if (data) {
		if (data->task) {
			g_task_return_new_error (data->task,
				G_IO_ERROR, G_IO_ERROR_CANCELLED, "%s", _("Credentials prompt was cancelled"));
			g_clear_object (&data->task);
		}

		g_clear_object (&data->source);
		g_clear_pointer (&data->error_text, g_free);
		g_slice_free (CredentialsPromptData, data);
	}
}

typedef struct _CredentialsResultData {
	ESource *source;
	ENamedParameters *credentials;
} CredentialsResultData;

static void
credentials_result_data_free (gpointer ptr)
{
	CredentialsResultData *data = ptr;

	if (data) {
		g_clear_object (&data->source);
		e_named_parameters_free (data->credentials);
		g_slice_free (CredentialsResultData, data);
	}
}

static void
credentials_prompter_maybe_process_next_prompt (ECredentialsPrompter *prompter)
{
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	g_rec_mutex_lock (&prompter->priv->queue_lock);

	/* Already processing one */
	if (prompter->priv->processing_prompt) {
		g_rec_mutex_unlock (&prompter->priv->queue_lock);
		return;
	}

	if (prompter->priv->queue) {
		ProcessPromptData *ppd = prompter->priv->queue->data;

		g_warn_if_fail (ppd != NULL);

		prompter->priv->queue = g_slist_remove (prompter->priv->queue, ppd);
		prompter->priv->processing_prompt = ppd;

		e_credentials_prompter_impl_prompt (ppd->prompter_impl, ppd, ppd->auth_source,
			ppd->cred_source, ppd->error_text, ppd->credentials);
	}

	g_rec_mutex_unlock (&prompter->priv->queue_lock);
}

static gboolean
credentials_prompter_process_next_prompt_idle_cb (gpointer user_data)
{
	ECredentialsPrompter *prompter = user_data;

	if (g_source_is_destroyed (g_main_current_source ()))
		return FALSE;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), FALSE);

	g_rec_mutex_lock (&prompter->priv->queue_lock);

	if (g_source_get_id (g_main_current_source ()) == prompter->priv->schedule_idle_id) {
		prompter->priv->schedule_idle_id = 0;

		credentials_prompter_maybe_process_next_prompt (prompter);
	}

	g_rec_mutex_unlock (&prompter->priv->queue_lock);

	return FALSE;
}

static void
credentials_prompter_schedule_process_next_prompt (ECredentialsPrompter *prompter)
{
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	g_rec_mutex_lock (&prompter->priv->queue_lock);

	/* Already processing one */
	if (prompter->priv->processing_prompt ||
	    prompter->priv->schedule_idle_id) {
		g_rec_mutex_unlock (&prompter->priv->queue_lock);
		return;
	}

	prompter->priv->schedule_idle_id = g_idle_add_full (G_PRIORITY_HIGH_IDLE,
		credentials_prompter_process_next_prompt_idle_cb,
		prompter, NULL);

	g_rec_mutex_unlock (&prompter->priv->queue_lock);
}

static void
credentials_prompter_connection_status_changed_cb (ESource *source,
						   GParamSpec *param,
						   ECredentialsPrompter *prompter)
{
	g_return_if_fail (E_IS_SOURCE (source));
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	/* Do not cancel the prompt when the source is still waiting for the credentials. */
	if (e_source_get_connection_status (source) == E_SOURCE_CONNECTION_STATUS_AWAITING_CREDENTIALS)
		return;

	g_rec_mutex_lock (&prompter->priv->queue_lock);

	if (prompter->priv->processing_prompt &&
	    e_source_equal (prompter->priv->processing_prompt->auth_source, source)) {
		e_credentials_prompter_impl_cancel_prompt (prompter->priv->processing_prompt->prompter_impl, prompter->priv->processing_prompt);
	} else {
		GSList *link;

		for (link = prompter->priv->queue; link; link = g_slist_next (link)) {
			ProcessPromptData *ppd = link->data;

			g_warn_if_fail (ppd != NULL);

			if (ppd && e_source_equal (ppd->auth_source, source)) {
				if (ppd->connection_status != e_source_get_connection_status (source)) {
					prompter->priv->queue = g_slist_remove (prompter->priv->queue, ppd);
					process_prompt_data_free (ppd);
				}
				break;
			}
		}
	}

	g_rec_mutex_unlock (&prompter->priv->queue_lock);
}

static gboolean
e_credentials_prompter_eval_remember_password (ESource *source)
{
	gboolean remember_password = FALSE;

	if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION))
		remember_password = e_source_authentication_get_remember_password (
			e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION));

	return remember_password;
}

static void
e_credentials_prompter_manage_impl_prompt (ECredentialsPrompter *prompter,
					   ECredentialsPrompterImpl *prompter_impl,
					   ESource *auth_source,
					   ESource *cred_source,
					   const gchar *error_text,
					   const ENamedParameters *credentials,
					   gboolean allow_source_save,
					   GTask *task)
{
	GSList *link;
	gboolean success = TRUE;

	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER_IMPL (prompter_impl));
	g_return_if_fail (E_IS_SOURCE (auth_source));
	g_return_if_fail (E_IS_SOURCE (cred_source));
	g_return_if_fail (credentials != NULL);

	g_rec_mutex_lock (&prompter->priv->queue_lock);

	for (link = prompter->priv->queue; link; link = g_slist_next (link)) {
		ProcessPromptData *ppd = link->data;

		g_warn_if_fail (ppd != NULL);

		if (ppd && e_source_equal (ppd->auth_source, auth_source)) {
			break;
		}
	}

	if (link != NULL || (prompter->priv->processing_prompt &&
	    e_source_equal (prompter->priv->processing_prompt->auth_source, auth_source))) {
		/* have queued or already asking for credentials for this source */
		success = FALSE;
	} else {
		ProcessPromptData *ppd;

		ppd = g_slice_new0 (ProcessPromptData);
		ppd->prompter_impl = g_object_ref (prompter_impl);
		ppd->auth_source = g_object_ref (auth_source);
		ppd->cred_source = g_object_ref (cred_source);
		ppd->connection_status = e_source_get_connection_status (ppd->auth_source);
		ppd->remember_password = e_credentials_prompter_eval_remember_password (ppd->cred_source);
		ppd->error_text = g_strdup (error_text);
		ppd->credentials = e_named_parameters_new_clone (credentials);
		ppd->allow_source_save = allow_source_save;
		ppd->task = task ? g_object_ref (task) : NULL;

		/* If the prompter doesn't auto-prompt, then it should not auto-close the prompt as well. */
		if (e_credentials_prompter_get_auto_prompt (prompter)) {
			ppd->notify_handler_id = g_signal_connect (ppd->auth_source, "notify::connection-status",
				G_CALLBACK (credentials_prompter_connection_status_changed_cb), prompter);
		} else {
			ppd->notify_handler_id = 0;
		}

		prompter->priv->queue = g_slist_append (prompter->priv->queue, ppd);

		credentials_prompter_schedule_process_next_prompt (prompter);
	}

	g_rec_mutex_unlock (&prompter->priv->queue_lock);

	if (!success && task) {
		g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Credentials prompt was cancelled"));
	}
}

static void
credentials_prompter_store_credentials_cb (GObject *source_object,
					   GAsyncResult *result,
					   gpointer user_data)
{
	GError *error = NULL;

	if (!e_source_credentials_provider_store_finish (E_SOURCE_CREDENTIALS_PROVIDER (source_object), result, &error) &&
	    !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		g_warning ("%s: Failed to store source credentials: %s", G_STRFUNC, error ? error->message : "Unknown error");
	}

	g_clear_error (&error);
}

static void
credentials_prompter_source_write_cb (GObject *source_object,
				      GAsyncResult *result,
				      gpointer user_data)
{
	ESource *source = E_SOURCE (source_object);
	GError *error = NULL;

	if (!e_source_write_finish (source, result, &error) &&
	    !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
		g_warning ("%s: Failed to write source '%s' (%s) changes: %s", G_STRFUNC,
			e_source_get_uid (source),
			e_source_get_display_name (source),
			error ? error->message : "Unknown error");
	}

	g_clear_error (&error);
}

static void
credentials_prompter_update_username_for_children (ESourceRegistry *registry,
						   ESource *collection_source,
						   gboolean allow_source_save,
						   const gchar *old_username,
						   const gchar *new_username,
						   GCancellable *cancellable)
{
	GList *sources, *link;
	const gchar *parent_uid;
	gchar *collection_host;
	gboolean username_changed;

	g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
	g_return_if_fail (E_IS_SOURCE (collection_source));

	parent_uid = e_source_get_uid (collection_source);
	if (!parent_uid || !*parent_uid)
		return;

	collection_host = e_source_authentication_dup_host (e_source_get_extension (collection_source, E_SOURCE_EXTENSION_AUTHENTICATION));
	username_changed = g_strcmp0 (old_username, new_username) != 0;
	sources = e_source_registry_list_sources (registry, NULL);

	for (link = sources; link; link = g_list_next (link)) {
		ESource *child = link->data;

		if (g_strcmp0 (e_source_get_parent (child), parent_uid) == 0 &&
		    e_source_get_writable (child) && e_source_has_extension (child, E_SOURCE_EXTENSION_AUTHENTICATION)) {
			ESourceAuthentication *auth_extension;
			gchar *child_username, *child_host;

			auth_extension = e_source_get_extension (child, E_SOURCE_EXTENSION_AUTHENTICATION);
			child_username = e_source_authentication_dup_user (auth_extension);
			child_host = e_source_authentication_dup_host (auth_extension);

			if ((!child_host || !*child_host || !collection_host || !*collection_host ||
			    g_ascii_strcasecmp (child_host, collection_host) == 0) &&
			    (!child_username || !*child_username || !old_username || !*old_username ||
			    (username_changed && g_strcmp0 (child_username, old_username) == 0))) {
				e_source_authentication_set_user (auth_extension, new_username);

				if (allow_source_save) {
					e_source_write (child, cancellable,
						credentials_prompter_source_write_cb, NULL);
				}
			}

			g_free (child_username);
			g_free (child_host);
		}
	}

	g_list_free_full (sources, g_object_unref);
	g_free (collection_host);
}

static void
e_credentials_prompter_prompt_finish_for_source (ECredentialsPrompter *prompter,
						 ProcessPromptData *ppd,
						 const ENamedParameters *credentials)
{
	ESource *cred_source;
	gboolean changed = FALSE;

	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));
	g_return_if_fail (ppd != NULL);

	if (!credentials)
		return;

	cred_source = ppd->cred_source;

	if (e_source_has_extension (cred_source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
		ESourceAuthentication *auth_extension = e_source_get_extension (cred_source, E_SOURCE_EXTENSION_AUTHENTICATION);
		gboolean could_use_collection;

		could_use_collection = e_source_has_extension (cred_source, E_SOURCE_EXTENSION_COLLECTION);

		if (e_source_get_writable (cred_source)) {
			const gchar *username;

			username = e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME);
			if (username && *username) {
				gchar *old_username;

				old_username = e_source_authentication_dup_user (auth_extension);

				/* Sync the changed user name to the child sources of the collection as well */
				if (ppd->auth_source == cred_source && e_source_has_extension (cred_source, E_SOURCE_EXTENSION_COLLECTION)) {
					credentials_prompter_update_username_for_children (
						e_credentials_prompter_get_registry (prompter),
						cred_source,
						ppd->allow_source_save,
						old_username,
						username,
						prompter->priv->cancellable);

					/* Update the collection source as the last, due to tests for the old
					   username in the credentials_prompter_update_username_for_children(). */
					if (g_strcmp0 (username, old_username) != 0) {
						e_source_authentication_set_user (auth_extension, username);
						changed = TRUE;
					}
				} else if (g_strcmp0 (username, old_username) != 0) {
					if (ppd->auth_source != cred_source &&
					    e_source_has_extension (cred_source, E_SOURCE_EXTENSION_COLLECTION)) {
						auth_extension = e_source_get_extension (ppd->auth_source, E_SOURCE_EXTENSION_AUTHENTICATION);
						e_source_authentication_set_user (auth_extension, username);

						if (ppd->allow_source_save && e_source_get_writable (ppd->auth_source)) {
							e_source_write (ppd->auth_source, prompter->priv->cancellable,
								credentials_prompter_source_write_cb, NULL);
						}
					} else {
						e_source_authentication_set_user (auth_extension, username);
						changed = TRUE;
					}
				}

				g_free (old_username);
			}
		}

		if (could_use_collection && !e_util_can_use_collection_as_credential_source (cred_source, ppd->auth_source)) {
			/* Copy also the remember-password flag */
			e_source_authentication_set_remember_password (e_source_get_extension (ppd->auth_source, E_SOURCE_EXTENSION_AUTHENTICATION),
				e_credentials_prompter_eval_remember_password (cred_source));

			cred_source = ppd->auth_source;
		}
	}

	if (e_source_has_extension (cred_source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
		ESourceAuthentication *auth_extension = e_source_get_extension (cred_source, E_SOURCE_EXTENSION_AUTHENTICATION);

		if (e_source_credentials_provider_can_store (e_credentials_prompter_get_provider (prompter), cred_source)) {
			e_source_credentials_provider_store (e_credentials_prompter_get_provider (prompter), cred_source, credentials,
				e_source_authentication_get_remember_password (auth_extension),
				prompter->priv->cancellable,
				credentials_prompter_store_credentials_cb, NULL);
		}
	}

	if (ppd->allow_source_save && e_source_get_writable (cred_source) &&
	    (changed || (ppd->remember_password ? 1 : 0) != (e_credentials_prompter_eval_remember_password (cred_source) ? 1 : 0))) {
		e_source_write (cred_source, prompter->priv->cancellable,
			credentials_prompter_source_write_cb, NULL);
	}

	if (ppd->task) {
		CredentialsResultData *result;

		result = g_slice_new0 (CredentialsResultData);
		result->source = g_object_ref (ppd->auth_source);
		result->credentials = e_named_parameters_new_clone (credentials);

		g_task_return_pointer (ppd->task, result, credentials_result_data_free);

		/* To not be completed multiple times */
		g_clear_object (&ppd->task);
	} else {
		e_source_invoke_authenticate (ppd->auth_source, credentials, prompter->priv->cancellable,
			credentials_prompter_invoke_authenticate_cb, NULL);
	}
}

static void
credentials_prompter_prompt_finished_cb (ECredentialsPrompterImpl *prompter_impl,
					 gpointer prompt_id,
					 const ENamedParameters *credentials,
					 ECredentialsPrompter *prompter)
{
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER_IMPL (prompter_impl));
	g_return_if_fail (prompt_id != NULL);
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	g_rec_mutex_lock (&prompter->priv->queue_lock);

	if (prompt_id == prompter->priv->processing_prompt) {
		ProcessPromptData *ppd = prompter->priv->processing_prompt;
		GSList *link, *to_remove = NULL;

		prompter->priv->processing_prompt = NULL;

		e_credentials_prompter_prompt_finish_for_source (prompter, ppd, credentials);

		/* Finish also any other pending prompts for the same credentials source
		   as was finished this one. This can be relevant to collection sources. */
		for (link = prompter->priv->queue; link; link = g_slist_next (link)) {
			ProcessPromptData *sub_ppd = link->data;

			if (sub_ppd && sub_ppd->cred_source && e_source_equal (sub_ppd->cred_source, ppd->cred_source)) {
				to_remove = g_slist_prepend (to_remove, sub_ppd);
			}
		}

		for (link = to_remove; link; link = g_slist_next (link)) {
			ProcessPromptData *sub_ppd = link->data;

			if (sub_ppd) {
				prompter->priv->queue = g_slist_remove (prompter->priv->queue, sub_ppd);
				e_credentials_prompter_prompt_finish_for_source (prompter, sub_ppd, credentials);
			}
		}

		g_slist_free_full (to_remove, process_prompt_data_free);
		process_prompt_data_free (ppd);

		credentials_prompter_schedule_process_next_prompt (prompter);
	} else {
		g_warning ("%s: Unknown prompt_id %p", G_STRFUNC, prompt_id);
	}

	g_rec_mutex_unlock (&prompter->priv->queue_lock);
}

static gboolean
credentials_prompter_prompt_with_source_details (ECredentialsPrompter *prompter,
						 LookupSourceDetailsData *data,
						 const gchar *error_text,
						 ECredentialsPrompterPromptFlags flags,
						 GTask *task)
{
	ECredentialsPrompterImpl *prompter_impl = NULL;
	gchar *method = NULL;
	gboolean success = TRUE;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), FALSE);
	g_return_val_if_fail (data != NULL, FALSE);

	if (e_source_has_extension (data->cred_source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
		ESourceAuthentication *authentication = e_source_get_extension (data->cred_source, E_SOURCE_EXTENSION_AUTHENTICATION);

		method = e_source_authentication_dup_method (authentication);
	}

	g_mutex_lock (&prompter->priv->prompters_lock);

	prompter_impl = g_hash_table_lookup (prompter->priv->prompters, method ? method : "");
	if (!prompter_impl && method && *method)
		prompter_impl = g_hash_table_lookup (prompter->priv->prompters, "");

	if (prompter_impl)
		g_object_ref (prompter_impl);

	g_mutex_unlock (&prompter->priv->prompters_lock);

	if (prompter_impl) {
		ENamedParameters *credentials;

		credentials = e_named_parameters_new ();
		if (data->credentials)
			e_named_parameters_assign (credentials, data->credentials);

		if (task && data->credentials && (flags & E_CREDENTIALS_PROMPTER_PROMPT_FLAG_ALLOW_STORED_CREDENTIALS) != 0) {
			CredentialsResultData *result;

			result = g_slice_new0 (CredentialsResultData);
			result->source = g_object_ref (data->auth_source);
			result->credentials = e_named_parameters_new_clone (credentials);

			g_task_return_pointer (task, result, credentials_result_data_free);
		} else if (!e_source_credentials_provider_can_prompt (prompter->priv->provider, data->auth_source)) {
			/* This source cannot be asked for credentials, thus end with a 'not supported' error. */
			if (task) {
				g_task_return_new_error (task,
					G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
					_("Source “%s” doesn’t support prompt for credentials"),
					e_source_get_display_name (data->cred_source));
			}
		} else {
			e_credentials_prompter_manage_impl_prompt (prompter, prompter_impl,
				data->auth_source, data->cred_source, error_text, credentials,
				!task || (flags & E_CREDENTIALS_PROMPTER_PROMPT_FLAG_ALLOW_SOURCE_SAVE) != 0,
				task);
		}

		e_named_parameters_free (credentials);
	} else {
		/* Shoud not happen, because the password prompter is added as the default prompter. */
		g_warning ("%s: No prompter impl found for an authentication method '%s'", G_STRFUNC, method ? method : "");
		success = FALSE;
	}

	g_clear_object (&prompter_impl);
	g_free (method);

	return success;
}

static void
credentials_prompter_lookup_source_details_before_prompt_cb (GObject *source_object,
							     GAsyncResult *result,
							     gpointer user_data)
{
	CredentialsPromptData *prompt_data = user_data;
	ECredentialsPrompter *prompter = NULL;
	LookupSourceDetailsData *data = NULL;
	GError *error = NULL;

	g_return_if_fail (prompt_data != NULL);
	g_return_if_fail (E_IS_SOURCE (source_object));

	if (!credentials_prompter_lookup_source_details_finish (E_SOURCE (source_object), result, &prompter, &data, &error)) {
		g_clear_error (&error);
		credentials_prompt_data_free (prompt_data);
		return;
	}

	if (credentials_prompter_prompt_with_source_details (prompter, data, prompt_data->error_text,
		prompt_data->flags, prompt_data->task)) {
		/* To not finish the task multiple times */
		g_clear_object (&prompt_data->task);
	}

	g_clear_object (&prompter);

	credentials_prompt_data_free (prompt_data);
	lookup_source_details_data_free (data);
}

static void
credentials_prompter_lookup_source_details_cb (GObject *source_object,
					       GAsyncResult *result,
					       gpointer user_data)
{
	LookupSourceDetailsData *data = NULL;
	ECredentialsPrompter *prompter = NULL;
	ESource *source;
	GError *error = NULL;

	g_return_if_fail (E_IS_SOURCE (source_object));

	source = E_SOURCE (source_object);

	if (!credentials_prompter_lookup_source_details_finish (source, result, &prompter, &data, &error)) {
		g_clear_error (&error);
		return;
	}

	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));
	g_return_if_fail (data != NULL);

	if (data->credentials) {
		e_source_invoke_authenticate (E_SOURCE (data->auth_source), data->credentials, prompter->priv->cancellable, credentials_prompter_invoke_authenticate_cb, NULL);
	} else {
		credentials_prompter_prompt_with_source_details (prompter, data, NULL, 0, NULL);
	}

	lookup_source_details_data_free (data);
	g_clear_object (&prompter);
}

static void
credentials_prompter_credentials_required_cb (ESourceRegistry *registry,
					      ESource *source,
					      ESourceCredentialsReason reason,
					      const gchar *certificate_pem,
					      GTlsCertificateFlags certificate_errors,
					      const GError *op_error,
					      ECredentialsPrompter *prompter)
{
	ESource *cred_source;

	g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
	g_return_if_fail (E_IS_SOURCE (source));
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	/* Only these two reasons are meant to be used to prompt the user for credentials. */
	if (reason != E_SOURCE_CREDENTIALS_REASON_REQUIRED &&
	    reason != E_SOURCE_CREDENTIALS_REASON_REJECTED) {
		return;
	}

	if (!e_source_registry_check_enabled (prompter->priv->registry, source))
		return;

	cred_source = e_source_credentials_provider_ref_credentials_source (e_credentials_prompter_get_provider (prompter), source);

	/* Global auto-prompt or the source's auto-prompt is disabled. */
	if (!e_credentials_prompter_get_auto_prompt (prompter) ||
	    (e_credentials_prompter_get_auto_prompt_disabled_for (prompter, source) &&
	    (!cred_source || e_credentials_prompter_get_auto_prompt_disabled_for (prompter, cred_source)))) {
		g_clear_object (&cred_source);
		return;
	}

	g_clear_object (&cred_source);

	/* This is a re-prompt, but the source cannot be prompted for credentials. */
	if (reason == E_SOURCE_CREDENTIALS_REASON_REJECTED &&
	    !e_source_credentials_provider_can_prompt (prompter->priv->provider, source)) {
		return;
	}

	if (reason == E_SOURCE_CREDENTIALS_REASON_REQUIRED) {
		credentials_prompter_lookup_source_details (source, prompter,
			credentials_prompter_lookup_source_details_cb, NULL);
		return;
	}

	e_credentials_prompter_prompt (prompter, source, op_error ? op_error->message : NULL, 0, NULL, NULL);
}

static gboolean
credentials_prompter_get_dialog_parent_accumulator (GSignalInvocationHint *ihint,
						    GValue *return_accu,
						    const GValue *handler_return,
						    gpointer data)
{
	if (handler_return && g_value_get_object (handler_return) != NULL) {
		g_value_set_object (return_accu, g_value_get_object (handler_return));
		return FALSE;
	}

	return TRUE;
}

static void
credentials_prompter_set_registry (ECredentialsPrompter *prompter,
				   ESourceRegistry *registry)
{
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));
	g_return_if_fail (E_IS_SOURCE_REGISTRY (registry));
	g_return_if_fail (prompter->priv->registry == NULL);

	prompter->priv->registry = g_object_ref (registry);
	prompter->priv->provider = e_source_credentials_provider_new (prompter->priv->registry);

	g_signal_connect (prompter->priv->registry, "credentials-required",
		G_CALLBACK (credentials_prompter_credentials_required_cb), prompter);
}

static void
credentials_prompter_set_property (GObject *object,
				   guint property_id,
				   const GValue *value,
				   GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_REGISTRY:
			credentials_prompter_set_registry (
				E_CREDENTIALS_PROMPTER (object),
				g_value_get_object (value));
			return;

		case PROP_AUTO_PROMPT:
			e_credentials_prompter_set_auto_prompt (
				E_CREDENTIALS_PROMPTER (object),
				g_value_get_boolean (value));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
credentials_prompter_get_property (GObject *object,
				   guint property_id,
				   GValue *value,
				   GParamSpec *pspec)
{
	switch (property_id) {
		case PROP_REGISTRY:
			g_value_set_object (value,
				e_credentials_prompter_get_registry (
				E_CREDENTIALS_PROMPTER (object)));
			return;

		case PROP_PROVIDER:
			g_value_set_object (value,
				e_credentials_prompter_get_provider (
				E_CREDENTIALS_PROMPTER (object)));
			return;

		case PROP_AUTO_PROMPT:
			g_value_set_boolean (value,
				e_credentials_prompter_get_auto_prompt (
				E_CREDENTIALS_PROMPTER (object)));
			return;
	}

	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
credentials_prompter_constructed (GObject *object)
{
	/* Chain up to parent's method. */
	G_OBJECT_CLASS (e_credentials_prompter_parent_class)->constructed (object);

	e_extensible_load_extensions (E_EXTENSIBLE (object));
}

static void
credentials_prompter_dispose (GObject *object)
{
	ECredentialsPrompter *prompter = E_CREDENTIALS_PROMPTER (object);
	GHashTableIter iter;
	gpointer key, value;

	if (prompter->priv->cancellable) {
		g_cancellable_cancel (prompter->priv->cancellable);
		g_clear_object (&prompter->priv->cancellable);
	}

	if (prompter->priv->registry) {
		g_signal_handlers_disconnect_by_data (prompter->priv->registry, prompter);
		g_clear_object (&prompter->priv->registry);
	}

	g_rec_mutex_lock (&prompter->priv->queue_lock);

	g_clear_handle_id (&prompter->priv->schedule_idle_id, g_source_remove);

	g_rec_mutex_unlock (&prompter->priv->queue_lock);

	g_clear_object (&prompter->priv->provider);

	g_mutex_lock (&prompter->priv->prompters_lock);

	g_hash_table_iter_init (&iter, prompter->priv->prompters);
	while (g_hash_table_iter_next (&iter, &key, &value)) {
		ECredentialsPrompterImpl *prompter_impl = value;

		g_signal_handlers_disconnect_by_func (prompter_impl, credentials_prompter_prompt_finished_cb, prompter);
	}

	g_hash_table_remove_all (prompter->priv->prompters);
	g_hash_table_remove_all (prompter->priv->known_prompters);
	g_mutex_unlock (&prompter->priv->prompters_lock);

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

static void
credentials_prompter_finalize (GObject *object)
{
	ECredentialsPrompter *prompter = E_CREDENTIALS_PROMPTER (object);

	g_hash_table_destroy (prompter->priv->prompters);
	g_hash_table_destroy (prompter->priv->known_prompters);
	g_mutex_clear (&prompter->priv->prompters_lock);

	g_hash_table_destroy (prompter->priv->disabled_auto_prompt);
	g_mutex_clear (&prompter->priv->disabled_auto_prompt_lock);

	g_rec_mutex_clear (&prompter->priv->queue_lock);

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

static void
e_credentials_prompter_class_init (ECredentialsPrompterClass *class)
{
	GObjectClass *object_class;

	object_class = G_OBJECT_CLASS (class);
	object_class->set_property = credentials_prompter_set_property;
	object_class->get_property = credentials_prompter_get_property;
	object_class->constructed = credentials_prompter_constructed;
	object_class->dispose = credentials_prompter_dispose;
	object_class->finalize = credentials_prompter_finalize;

	/**
	 * ECredentialsPrompter:auto-prompt:
	 *
	 * Whether the #ECredentialsPrompter can response to credential
	 * requests automatically.
	 *
	 * Since: 3.16
	 **/
	properties[PROP_AUTO_PROMPT] =
		g_param_spec_boolean (
			"auto-prompt",
			NULL, NULL,
			TRUE,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT |
			G_PARAM_EXPLICIT_NOTIFY |
			G_PARAM_STATIC_STRINGS);

	/**
	 * ECredentialsPrompter:registry:
	 *
	 * The #ESourceRegistry object, to whose credential requests the prompter listens.
	 *
	 * Since: 3.16
	 **/
	properties[PROP_REGISTRY] =
		g_param_spec_object (
			"registry",
			NULL, NULL,
			E_TYPE_SOURCE_REGISTRY,
			G_PARAM_READWRITE |
			G_PARAM_CONSTRUCT_ONLY |
			G_PARAM_STATIC_STRINGS);

	/**
	 * ECredentialsPrompter:provider:
	 *
	 * The #ESourceCredentialsProvider object, which the prompter uses.
	 *
	 * Since: 3.16
	 **/
	properties[PROP_PROVIDER] =
		g_param_spec_object (
			"provider",
			NULL, NULL,
			E_TYPE_SOURCE_CREDENTIALS_PROVIDER,
			G_PARAM_READABLE |
			G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties (object_class, N_PROPS, properties);

	/**
	 * ECredentialsPrompter::get-dialog-parent:
	 * @prompter: the #ECredentialsPrompter which emitted the signal
	 *
	 * Emitted when a new dialog will be shown, to get the right parent
	 * window for it. If the result of the call is %NULL, then it tries
	 * to get the window from the default GtkApplication.
	 *
	 * Returns: (transfer none) (nullable): a #GtkWindow, to be used as a
	 * dialog parent, or %NULL.
	 *
	 * Since: 3.16
	 **/
	signals[GET_DIALOG_PARENT] = g_signal_new (
		"get-dialog-parent",
		G_OBJECT_CLASS_TYPE (object_class),
		G_SIGNAL_RUN_LAST,
		G_STRUCT_OFFSET (ECredentialsPrompterClass, get_dialog_parent),
		credentials_prompter_get_dialog_parent_accumulator, NULL, NULL,
		GTK_TYPE_WINDOW, 0, G_TYPE_NONE);

	/**
	 * ECredentialsPrompter::get-dialog-parent-full:
	 * @prompter: the #ECredentialsPrompter which emitted the signal
	 * @auth_source: (nullable): an #ESource, for which to show the credentials prompt
	 *
	 * Emitted when a new dialog will be shown, to get the right parent
	 * window for it. If the result of the call is %NULL, then it tries
	 * to get the window from the default GtkApplication.
	 *
	 * Returns: (transfer none) (nullable): a #GtkWindow, to be used as a
	 * dialog parent, or %NULL.
	 *
	 * Since: 3.42
	 **/
	signals[GET_DIALOG_PARENT_FULL] = g_signal_new (
		"get-dialog-parent-full",
		G_OBJECT_CLASS_TYPE (object_class),
		G_SIGNAL_RUN_LAST,
		/*G_STRUCT_OFFSET (ECredentialsPrompterClass, get_dialog_parent_full)*/ 0,
		credentials_prompter_get_dialog_parent_accumulator, NULL, NULL,
		GTK_TYPE_WINDOW, 1, E_TYPE_SOURCE);

	/* Ensure built-in credential providers implementation types */
	g_type_ensure (E_TYPE_CREDENTIALS_PROMPTER_IMPL_PASSWORD);
	g_type_ensure (E_TYPE_CREDENTIALS_PROMPTER_IMPL_OAUTH2);
}

static void
e_credentials_prompter_init (ECredentialsPrompter *prompter)
{
	prompter->priv = e_credentials_prompter_get_instance_private (prompter);

	prompter->priv->auto_prompt = TRUE;
	prompter->priv->provider = NULL;
	prompter->priv->cancellable = g_cancellable_new ();

	g_mutex_init (&prompter->priv->prompters_lock);
	prompter->priv->prompters = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, g_object_unref);
	prompter->priv->known_prompters = g_hash_table_new (g_direct_hash, g_direct_equal);

	g_mutex_init (&prompter->priv->disabled_auto_prompt_lock);
	prompter->priv->disabled_auto_prompt = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

	g_rec_mutex_init (&prompter->priv->queue_lock);
}

/**
 * e_credentials_prompter_new:
 * @registry: an #ESourceRegistry to have the prompter listen to
 *
 * Creates a new #ECredentialsPrompter, which listens for credential requests
 * from @registry.
 *
 * Returns: (transfer full): a new #ECredentialsPrompter
 *
 * Since: 3.16
 **/
ECredentialsPrompter *
e_credentials_prompter_new (ESourceRegistry *registry)
{
	return g_object_new (E_TYPE_CREDENTIALS_PROMPTER,
		"registry", registry,
		NULL);
}

/**
 * e_credentials_prompter_get_registry:
 * @prompter: an #ECredentialsPrompter
 *
 * Returns an #ESourceRegistry, to which the @prompter listens.
 *
 * Returns: (transfer none): an #ESourceRegistry, to which the @prompter listens.
 *
 * Since: 3.16
 **/
ESourceRegistry *
e_credentials_prompter_get_registry (ECredentialsPrompter *prompter)
{
	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), NULL);

	return prompter->priv->registry;
}

/**
 * e_credentials_prompter_get_provider:
 * @prompter: an #ECredentialsPrompter
 *
 * Returns an #ESourceCredentialsProvider, which the @prompter uses.
 *
 * Returns: (transfer none): an #ESourceCredentialsProvider, which the @prompter uses.
 *
 * Since: 3.16
 **/
ESourceCredentialsProvider *
e_credentials_prompter_get_provider (ECredentialsPrompter *prompter)
{
	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), NULL);
	g_return_val_if_fail (prompter->priv->provider != NULL, NULL);

	return prompter->priv->provider;
}

/**
 * e_credentials_prompter_get_auto_prompt:
 * @prompter: an #ECredentialsPrompter
 *
 * Returns, whether can respond to credential prompts automatically.
 * Default value is %TRUE.
 *
 * This property does not influence direct calls of e_credentials_prompter_prompt().
 *
 * Returns: Whether can respond to credential prompts automatically.
 *
 * Since: 3.16
 **/
gboolean
e_credentials_prompter_get_auto_prompt (ECredentialsPrompter *prompter)
{
	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), FALSE);

	return prompter->priv->auto_prompt;
}

/**
 * e_credentials_prompter_set_auto_prompt:
 * @prompter: an #ECredentialsPrompter
 * @auto_prompt: new value of the auto-prompt property
 *
 * Sets whether can respond to credential prompts automatically. That means that
 * whenever any ESource will ask for credentials, it'll try to provide them.
 *
 * Use e_credentials_prompter_set_auto_prompt_disabled_for() to influence
 * auto-prompt per an #ESource.
 *
 * This property does not influence direct calls of e_credentials_prompter_prompt().
 *
 * Since: 3.16
 **/
void
e_credentials_prompter_set_auto_prompt (ECredentialsPrompter *prompter,
					gboolean auto_prompt)
{
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	if ((prompter->priv->auto_prompt ? 1 : 0) == (auto_prompt ? 1 : 0))
		return;

	prompter->priv->auto_prompt = auto_prompt;

	g_object_notify_by_pspec (G_OBJECT (prompter), properties[PROP_AUTO_PROMPT]);
}

/**
 * e_credentials_prompter_set_auto_prompt_disabled_for:
 * @prompter: an #ECredentialsPrompter
 * @source: an #ESource
 * @is_disabled: whether the auto-prompt should be disabled for this @source
 *
 * Sets whether the auto-prompt should be disabled for the given @source.
 * All sources can be auto-prompted by default. This is a complementary
 * value for the ECredentialsPrompter::auto-prompt property.
 *
 * This value does not influence direct calls of e_credentials_prompter_prompt().
 *
 * Since: 3.16
 **/
void
e_credentials_prompter_set_auto_prompt_disabled_for (ECredentialsPrompter *prompter,
						     ESource *source,
						     gboolean is_disabled)
{
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));
	g_return_if_fail (E_IS_SOURCE (source));
	g_return_if_fail (e_source_get_uid (source) != NULL);

	g_mutex_lock (&prompter->priv->disabled_auto_prompt_lock);

	if (is_disabled)
		g_hash_table_insert (prompter->priv->disabled_auto_prompt, g_strdup (e_source_get_uid (source)), GINT_TO_POINTER (1));
	else
		g_hash_table_remove (prompter->priv->disabled_auto_prompt, e_source_get_uid (source));

	g_mutex_unlock (&prompter->priv->disabled_auto_prompt_lock);
}

/**
 * e_credentials_prompter_get_auto_prompt_disabled_for:
 * @prompter: an #ECredentialsPrompter
 * @source: an #ESource
 *
 * Returns whether the auto-prompt is disabled for the given @source.
 * All sources can be auto-prompted by default. This is a complementary
 * value for the ECredentialsPrompter::auto-prompt property.
 *
 * This value does not influence direct calls of e_credentials_prompter_prompt().
 *
 * Returns: Whether the auto-prompt is disabled for the given @source
 *
 * Since: 3.16
 **/
gboolean
e_credentials_prompter_get_auto_prompt_disabled_for (ECredentialsPrompter *prompter,
						     ESource *source)
{
	gboolean is_disabled;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), TRUE);
	g_return_val_if_fail (E_IS_SOURCE (source), TRUE);
	g_return_val_if_fail (e_source_get_uid (source) != NULL, TRUE);

	g_mutex_lock (&prompter->priv->disabled_auto_prompt_lock);

	is_disabled = g_hash_table_contains (prompter->priv->disabled_auto_prompt, e_source_get_uid (source));

	g_mutex_unlock (&prompter->priv->disabled_auto_prompt_lock);

	return is_disabled;
}

static GtkWindow *
credentials_prompter_guess_dialog_parent (ECredentialsPrompter *prompter)
{
	GApplication *app;

	app = g_application_get_default ();
	if (!app)
		return NULL;

	if (GTK_IS_APPLICATION (app))
		return gtk_application_get_active_window (GTK_APPLICATION (app));

	return NULL;
}

/**
 * e_credentials_prompter_get_dialog_parent:
 * @prompter: an #ECredentialsPrompter
 *
 * Returns a #GtkWindow, which should be used as a dialog parent. This is determined
 * by an ECredentialsPrompter::get-dialog-parent signal emission. If there is no callback
 * registered or the current callbacks don't have any suitable window, then there's
 * chosen the last active window from the default GApplication, if any available.
 *
 * Returns: (transfer none) (nullable): a #GtkWindow, to be used as a dialog parent,
 *    or %NULL.
 *
 * Since: 3.16
 **/
GtkWindow *
e_credentials_prompter_get_dialog_parent (ECredentialsPrompter *prompter)
{
	GtkWindow *parent = NULL;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), NULL);

	g_signal_emit (prompter, signals[GET_DIALOG_PARENT], 0, &parent);

	if (!parent)
		parent = credentials_prompter_guess_dialog_parent (prompter);

	return parent;
}

/**
 * e_credentials_prompter_get_dialog_parent_full:
 * @prompter: an #ECredentialsPrompter
 * @auth_source: (nullable): an #ESource
 *
 * Returns a #GtkWindow, which should be used as a dialog parent for the @auth_source.
 *
 * This is determined by an ECredentialsPrompter::get-dialog-parent-full signal emission
 * and an ECredentialsPrompter::get-dialog-parent when the first doesn't return anything.
 * If there is no callback registered or the current callbacks don't have any suitable
 * window, then there's chosen the last active window from the default GApplication,
 * if any available.
 *
 * Returns: (transfer none) (nullable): a #GtkWindow, to be used as a dialog parent,
 *    or %NULL.
 *
 * Since: 3.42
 **/
GtkWindow *
e_credentials_prompter_get_dialog_parent_full (ECredentialsPrompter *prompter,
					       ESource *auth_source)
{
	GtkWindow *parent = NULL;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), NULL);
	if (auth_source)
		g_return_val_if_fail (E_IS_SOURCE (auth_source), NULL);

	g_signal_emit (prompter, signals[GET_DIALOG_PARENT_FULL], 0, auth_source, &parent);

	if (!parent)
		g_signal_emit (prompter, signals[GET_DIALOG_PARENT], 0, &parent);

	if (!parent)
		parent = credentials_prompter_guess_dialog_parent (prompter);

	return parent;
}

/**
 * e_credentials_prompter_register_impl:
 * @prompter: an #ECredentialsPrompter
 * @authentication_method: (nullable): an authentication method to registr @prompter_impl for; or %NULL
 * @prompter_impl: an #ECredentialsPrompterImpl
 *
 * Registers a prompter implementation for a given authentication method. If there is
 * registered a prompter for the same @authentication_method, then the function does
 * nothing, otherwise it adds its own reference on the @prompter_impl, and uses it
 * for that authentication method. One @prompter_impl can be registered for multiple
 * authentication methods.
 *
 * A special value %NULL can be used for the @authentication_method, which means
 * a default credentials prompter, that is to be used when there is no prompter
 * registered for the exact authentication method.
 *
 * Returns: %TRUE on success, %FALSE on failure or when there was another prompter
 * implementation registered for the given authentication method.
 *
 * Since: 3.16
 **/
gboolean
e_credentials_prompter_register_impl (ECredentialsPrompter *prompter,
				      const gchar *authentication_method,
				      ECredentialsPrompterImpl *prompter_impl)
{
	guint known_prompters;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), FALSE);
	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER_IMPL (prompter_impl), FALSE);

	if (!authentication_method)
		authentication_method = "";

	g_mutex_lock (&prompter->priv->prompters_lock);

	if (g_hash_table_lookup (prompter->priv->prompters, authentication_method) != NULL) {
		g_mutex_unlock (&prompter->priv->prompters_lock);
		return FALSE;
	}

	g_hash_table_insert (prompter->priv->prompters, g_strdup (authentication_method), g_object_ref (prompter_impl));

	known_prompters = GPOINTER_TO_UINT (g_hash_table_lookup (prompter->priv->known_prompters, prompter_impl));
	if (!known_prompters) {
		g_signal_connect (prompter_impl, "prompt-finished", G_CALLBACK (credentials_prompter_prompt_finished_cb), prompter);
	}
	g_hash_table_insert (prompter->priv->known_prompters, prompter_impl, GUINT_TO_POINTER (known_prompters + 1));

	g_mutex_unlock (&prompter->priv->prompters_lock);

	return TRUE;
}

/**
 * e_credentials_prompter_unregister_impl:
 * @prompter: an #ECredentialsPrompter
 * @authentication_method: (nullable): an authentication method to registr @prompter_impl for; or %NULL
 * @prompter_impl: an #ECredentialsPrompterImpl
 *
 * Unregisters previously registered @prompter_impl for the given @autnetication_method with
 * e_credentials_prompter_register_impl(). Function does nothing, if no such authentication
 * method is registered or if it has set a different prompter implementation.
 *
 * Since: 3.16
 **/
void
e_credentials_prompter_unregister_impl (ECredentialsPrompter *prompter,
					const gchar *authentication_method,
					ECredentialsPrompterImpl *prompter_impl)
{
	ECredentialsPrompterImpl *current_prompter_impl;

	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	if (!authentication_method)
		authentication_method = "";

	g_mutex_lock (&prompter->priv->prompters_lock);

	current_prompter_impl = g_hash_table_lookup (prompter->priv->prompters, authentication_method);
	if (current_prompter_impl == prompter_impl) {
		guint known_prompters;

		known_prompters = GPOINTER_TO_UINT (g_hash_table_lookup (prompter->priv->known_prompters, prompter_impl));
		if (known_prompters == 1) {
			g_signal_handlers_disconnect_by_func (prompter_impl, credentials_prompter_prompt_finished_cb, prompter);
			g_hash_table_remove (prompter->priv->known_prompters, prompter_impl);
		} else {
			known_prompters--;
			g_hash_table_insert (prompter->priv->known_prompters, prompter_impl, GUINT_TO_POINTER (known_prompters + 1));
		}

		g_hash_table_remove (prompter->priv->prompters, authentication_method);
	}

	g_mutex_unlock (&prompter->priv->prompters_lock);
}

static void
credentials_prompter_get_last_credentials_required_arguments_cb (GObject *source_object,
								 GAsyncResult *result,
								 gpointer user_data)
{
	ECredentialsPrompter *prompter = user_data;
	ESource *source;
	ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_UNKNOWN;
	gchar *certificate_pem = NULL;
	GTlsCertificateFlags certificate_errors = 0;
	GError *op_error = NULL;
	GError *error = NULL;

	g_return_if_fail (E_IS_SOURCE (source_object));

	source = E_SOURCE (source_object);

	if (!e_source_get_last_credentials_required_arguments_finish (source, result,
		&reason, &certificate_pem, &certificate_errors, &op_error, &error)) {
		if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			g_warning ("%s: Failed to get last credential values: %s", G_STRFUNC, error ? error->message : "Unknown error");
		}

		g_clear_error (&error);
		return;
	}

	/* Can check only now, when know the operation was not cancelled and the prompter freed. */
	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	/* Check once again, as this was called asynchronously and anything could change meanwhile. */
	if (e_source_get_connection_status (source) == E_SOURCE_CONNECTION_STATUS_AWAITING_CREDENTIALS) {
		credentials_prompter_credentials_required_cb (prompter->priv->registry,
			source, reason, certificate_pem, certificate_errors, op_error, prompter);
	}

	g_free (certificate_pem);
	g_clear_error (&op_error);
}

/**
 * e_credentials_prompter_process_awaiting_credentials:
 * @prompter: an #ECredentialsPrompter
 *
 * Process all enabled sources with connection state #E_SOURCE_CONNECTION_STATUS_AWAITING_CREDENTIALS,
 * like if they just asked for its credentials for the first time.
 *
 * Since: 3.16
 **/
void
e_credentials_prompter_process_awaiting_credentials (ECredentialsPrompter *prompter)
{
	GList *sources, *link;

	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));

	sources = e_source_registry_list_enabled (prompter->priv->registry, NULL);
	for (link = sources; link; link = g_list_next (link)) {
		ESource *source = link->data;

		if (!source)
			continue;

		if (e_source_get_connection_status (source) == E_SOURCE_CONNECTION_STATUS_AWAITING_CREDENTIALS) {
			/* Check what failed the last time */
			e_credentials_prompter_process_source (prompter, source);
		}
	}

	g_list_free_full (sources, g_object_unref);
}

/**
 * e_credentials_prompter_process_source:
 * @prompter: an #ECredentialsPrompter
 * @source: an #ESource
 *
 * Continues a credential prompt for @source. Returns, whether anything will be done.
 * The %FALSE either means that the @source<!-- -->'s connection status is not
 * the %E_SOURCE_CONNECTION_STATUS_AWAITING_CREDENTIALS or it is disabled.
 *
 * Returns: Whether continues with the credentials prompt.
 *
 * Since: 3.16
 **/
gboolean
e_credentials_prompter_process_source (ECredentialsPrompter *prompter,
				       ESource *source)
{
	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), FALSE);
	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);

	if (e_source_get_connection_status (source) != E_SOURCE_CONNECTION_STATUS_AWAITING_CREDENTIALS ||
	    !e_source_registry_check_enabled (prompter->priv->registry, source))
		return FALSE;

	e_source_get_last_credentials_required_arguments (source, prompter->priv->cancellable,
		credentials_prompter_get_last_credentials_required_arguments_cb, prompter);

	return TRUE;
}

/**
 * e_credentials_prompter_prompt:
 * @prompter: an #ECredentialsPrompter
 * @source: an #ESource, which prompt the credentials for
 * @error_text: (nullable): Additional error text to show to a user, or %NULL
 * @flags: a bit-or of #ECredentialsPrompterPromptFlags
 * @callback: a callback to call when the credentials are ready, or %NULL
 * @user_data: user data passed into @callback
 *
 * Asks the @prompter to prompt for credentials, which are returned
 * to the caller through @callback, when available.The @flags are ignored,
 * when the @callback is %NULL; the credentials are passed to the @source
 * with e_source_invoke_authenticate() directly, in this case.
 * Call e_credentials_prompter_prompt_finish() in @callback to get to
 * the provided credentials.
 *
 * Since: 3.16
 **/
void
e_credentials_prompter_prompt (ECredentialsPrompter *prompter,
			       ESource *source,
			       const gchar *error_text,
			       ECredentialsPrompterPromptFlags flags,
			       GAsyncReadyCallback callback,
			       gpointer user_data)
{
	CredentialsPromptData *prompt_data;

	g_return_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter));
	g_return_if_fail (E_IS_SOURCE (source));

	prompt_data = g_slice_new0 (CredentialsPromptData);
	prompt_data->source = g_object_ref (source);
	prompt_data->error_text = g_strdup (error_text);
	prompt_data->flags = flags;
	if (callback) {
		prompt_data->task = g_task_new (prompter, NULL, callback, user_data);
		g_task_set_source_tag (prompt_data->task, e_credentials_prompter_prompt);
	}

	/* Just it can be shown in the UI as a prefilled value and the right source (collection) is used. */
	credentials_prompter_lookup_source_details (source, prompter,
		credentials_prompter_lookup_source_details_before_prompt_cb, prompt_data);
}

/**
 * e_credentials_prompter_prompt_finish:
 * @prompter: an #ECredentialsPrompter
 * @result: a #GAsyncResult
 * @out_source: (transfer full) (out) (optional) (nullable): optionally set to an #ESource, on which the prompt was started; can be %NULL
 * @out_credentials: (transfer full) (out) (nullable): set to an #ENamedParameters with provided credentials
 * @error: return location for a #GError, or %NULL
 *
 * Finishes a credentials prompt previously started with e_credentials_prompter_prompt().
 * The @out_source will have set a referenced #ESource, for which the prompt
 * was started. Unref it, when  no longer needed. Similarly the @out_credentials
 * will have set a newly allocated #ENamedParameters structure with provided credentials,
 * which should be freed with e_named_credentials_free() when no longer needed.
 * Both output arguments will be set to %NULL on error and %FALSE will be returned.
 *
 * Returns: %TRUE on success, %FALSE otherwise.
 *
 * Since: 3.16
 **/
gboolean
e_credentials_prompter_prompt_finish (ECredentialsPrompter *prompter,
				      GAsyncResult *result,
				      ESource **out_source,
				      ENamedParameters **out_credentials,
				      GError **error)
{
	CredentialsResultData *data;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), FALSE);
	g_return_val_if_fail (g_task_is_valid (result, prompter), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (result, e_credentials_prompter_prompt), FALSE);
	g_return_val_if_fail (out_credentials, FALSE);

	if (out_source)
		*out_source = NULL;
	*out_credentials = NULL;

	data = g_task_propagate_pointer (G_TASK (result), error);
	if (!data)
		return FALSE;

	if (data->credentials) {
		if (out_source)
			*out_source = g_object_ref (data->source);
		*out_credentials = e_named_parameters_new_clone (data->credentials);
	} else {
		g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Credentials prompt was cancelled"));
		g_clear_pointer (&data, credentials_result_data_free);
		return FALSE;
	}

	g_clear_pointer (&data, credentials_result_data_free);
	return TRUE;
}

static gboolean
credentials_prompter_prompt_sync (ECredentialsPrompter *prompter,
				  ESource *source,
				  gboolean is_retry,
				  ECredentialsPrompterPromptFlags *flags,
				  const gchar *error_text,
				  ENamedParameters **out_credentials,
				  GCancellable *cancellable,
				  GError **error)
{
	gboolean res = FALSE;
	ESourceCredentialsProvider *credentials_provider;
	ENamedParameters *credentials = NULL;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), FALSE);
	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
	g_return_val_if_fail (flags != NULL, FALSE);
	g_return_val_if_fail (out_credentials != NULL, FALSE);

	if (g_cancellable_set_error_if_cancelled (cancellable, error))
		return FALSE;

	credentials_provider = e_credentials_prompter_get_provider (prompter);

	if (!is_retry) {
		ESource *cred_source;
		GError *local_error = NULL;

		cred_source = e_source_credentials_provider_ref_credentials_source (credentials_provider, source);

		if (e_source_credentials_provider_lookup_sync (credentials_provider, cred_source ? cred_source : source,
			cancellable, &credentials, &local_error)) {
			res = TRUE;
		} else if (!g_cancellable_is_cancelled (cancellable)) {
			/* To prompt for the password directly */
			is_retry = TRUE;
			g_clear_error (&local_error);
		} else {
			g_propagate_error (error, local_error);
		}

		g_clear_object (&cred_source);
	}

	if (is_retry) {
		EAsyncClosure *closure;
		GAsyncResult *result;

		*flags = (*flags) & (~E_CREDENTIALS_PROMPTER_PROMPT_FLAG_ALLOW_STORED_CREDENTIALS);

		closure = e_async_closure_new ();

		e_credentials_prompter_prompt (prompter, source, error_text, *flags,
			e_async_closure_callback, closure);

		result = e_async_closure_wait (closure);

		if (e_credentials_prompter_prompt_finish (prompter, result, NULL, &credentials, error)) {
			res = TRUE;
		}

		e_async_closure_free (closure);
	}

	if (res && credentials)
		*out_credentials = e_named_parameters_new_clone (credentials);

	e_named_parameters_free (credentials);

	return res;
}

/**
 * e_credentials_prompter_loop_prompt_sync:
 * @prompter: an #ECredentialsPrompter
 * @source: an #ESource to be prompted credentials for
 * @flags: a bit-or of #ECredentialsPrompterPromptFlags initial flags
 * @func: (scope call): an #ECredentialsPrompterLoopPromptFunc user function to call to check provided credentials
 * @user_data: user data to pass to @func
 * @cancellable: an optional #GCancellable, or %NULL
 * @error: a #GError, to store any errors to, or %NULL
 *
 * Runs a credentials prompt loop for @source, as long as the @func doesn't
 * indicate that the provided credentials can be used to successfully
 * authenticate against @source<!-- -->'s server, or that the @func
 * returns %FALSE. The loop is also teminated when a used cancels
 * the credentials prompt or the @cancellable is cancelled, though
 * not sooner than the credentials prompt dialog is closed.
 *
 * Note: The function doesn't return until the loop is terminated, either
 *    successfully or unsuccessfully. The function can be called from any
 *    thread, though a dedicated thread is preferred.
 *
 * Returns: %TRUE, when the credentials were provided successfully and they
 *   can be used to authenticate the @source; %FALSE otherwise.
 *
 * Since: 3.16
 **/
gboolean
e_credentials_prompter_loop_prompt_sync (ECredentialsPrompter *prompter,
					 ESource *source,
					 ECredentialsPrompterPromptFlags flags,
					 ECredentialsPrompterLoopPromptFunc func,
					 gpointer user_data,
					 GCancellable *cancellable,
					 GError **error)
{
	gboolean is_retry, authenticated;
	ENamedParameters *credentials = NULL;

	g_return_val_if_fail (E_IS_CREDENTIALS_PROMPTER (prompter), FALSE);
	g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
	g_return_val_if_fail (func != NULL, FALSE);

	is_retry = FALSE;
	authenticated = FALSE;

	while (!authenticated && !g_cancellable_is_cancelled (cancellable)) {
		GError *local_error = NULL;

		e_named_parameters_free (credentials);
		credentials = NULL;

		if (!credentials_prompter_prompt_sync (prompter, source, is_retry, &flags, NULL,
			&credentials, cancellable, error))
			break;

		if (g_cancellable_set_error_if_cancelled (cancellable, error))
			break;

		g_clear_error (&local_error);

		if (!func (prompter, source, credentials, &authenticated, user_data, cancellable, &local_error)) {
			if (local_error)
				g_propagate_error (error, local_error);
			break;
		}

		is_retry = TRUE;
	}

	e_named_parameters_free (credentials);

	return authenticated;
}
