/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/***************************************************************************
 *            camel-kolab-session.c
 *
 *  Tue Aug 10 15:04:38 2010
 *  Copyright  2010  Christian Hilberg
 *  <hilberg@kernelconcepts.de>
 ****************************************************************************/

/*
 * This program 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; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This program 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 main.c; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA 02110-1301,  USA
 */

/*----------------------------------------------------------------------------*/

/*
 * The CamelSession class for Kolab access. To be instantiated once for
 * each IMAPX Camel.Provider we have. Within EDS, this is one for address book
 * and one for calendar access. Within Evolution, there should be one more
 * for email.
 */

/*----------------------------------------------------------------------------*/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <glib/gi18n-lib.h>

#include <pk11func.h>
#include <prtypes.h>

#include <libedataserver/e-account.h>
#include <libedataserverui/e-passwords.h>

#include <libekolabutil/camel-system-headers.h>

/* Kolab error reporting */
#include <libekolabutil/kolab-util-error.h>

#include "camel-kolab-imapx-store.h"
#include "camel-kolab-session.h"

/*----------------------------------------------------------------------------*/

static gchar *nss_tok_pin = NULL; /* FIXME better solution for this */

/*----------------------------------------------------------------------------*/

typedef struct _CamelKolabSessionPrivate CamelKolabSessionPrivate;
struct _CamelKolabSessionPrivate {
	/* TODO these should be retrieved
	 * from CamelKolabSettings instead
	 */
	gchar *passwd;

	/* TODO get rid of this workaround */
	gchar *nss_tok_pwd;

	gboolean is_initialized;
};

#define CAMEL_KOLAB_SESSION_PRIVATE(obj)  (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CAMEL_TYPE_KOLAB_SESSION, CamelKolabSessionPrivate))

G_DEFINE_TYPE (CamelKolabSession, camel_kolab_session, CAMEL_TYPE_SESSION)

/*----------------------------------------------------------------------------*/

static void
camel_kolab_session_init (CamelKolabSession *self)
{
	CamelKolabSessionPrivate *priv = NULL;

	g_assert (CAMEL_IS_KOLAB_SESSION (self));
	priv = CAMEL_KOLAB_SESSION_PRIVATE (self);

	priv->nss_tok_pwd = NULL;
	priv->passwd = NULL;
	priv->is_initialized = FALSE;
}

static void
camel_kolab_session_dispose (GObject *object)
{
	g_assert (CAMEL_IS_KOLAB_SESSION (object));

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

static void
camel_kolab_session_finalize (GObject *object)
{
	CamelKolabSession *self = NULL;
	CamelKolabSessionPrivate *priv = NULL;

	self = CAMEL_KOLAB_SESSION (object);
	priv = CAMEL_KOLAB_SESSION_PRIVATE (self);

	if (priv->nss_tok_pwd != NULL)
		g_free (priv->nss_tok_pwd);
	if (priv->passwd)
		g_free (priv->passwd);

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

/*----------------------------------------------------------------------------*/
/* local statics */

/* NSS token pin callback */
static gchar* PR_CALLBACK
pk11_password (PK11SlotInfo *slot,
               PRBool retry,
               gpointer arg)
{
	(void)slot;
	(void)arg;

	/* as recommended by PK11_SetPasswordFunc docs */
	if (retry)
		return NULL;

	return PL_strdup (nss_tok_pin);
}

/*----------------------------------------------------------------------------*/
/* class functions */

static gchar*
kolab_session_get_password (CamelSession *self,
                            CamelService *service,
                            const gchar *prompt,
                            const gchar *item,
                            guint32 flags,
                            GError **err)
{
	/* TODO rework to CamelSettings */

	CamelKolabSession *myself = NULL;
	CamelKolabSessionPrivate *priv = NULL;

	g_assert (CAMEL_IS_KOLAB_SESSION (self));
	g_assert (CAMEL_IS_KOLAB_IMAPX_STORE (service));
	g_return_val_if_fail (err == NULL || *err == NULL, NULL);

	(void)prompt;
	(void)item;
	(void)flags;

	myself = CAMEL_KOLAB_SESSION (self);
	priv = CAMEL_KOLAB_SESSION_PRIVATE (myself);

	return g_strdup (priv->passwd);
}

static void
kolab_session_forget_password (CamelSession *self,
                               CamelService *service,
                               const gchar *item,
                               GError **err)
{
	/* TODO rework to CamelSettings */

	CamelKolabSession *myself = NULL;
	CamelKolabSessionPrivate *priv = NULL;

	g_assert (CAMEL_IS_KOLAB_SESSION (self));
	g_assert (CAMEL_IS_KOLAB_IMAPX_STORE (service));
	g_return_if_fail (err == NULL || *err == NULL);

	(void)item;

	myself = CAMEL_KOLAB_SESSION (self);
	priv = CAMEL_KOLAB_SESSION_PRIVATE (myself);

	if (priv->passwd != NULL) {
		g_free (priv->passwd);
		priv->passwd = NULL;
	}
}

static gboolean
kolab_session_authenticate_sync (CamelSession *session,
                                 CamelService *service,
                                 const gchar *mechanism,
                                 GCancellable *cancellable,
                                 GError **error)
{
	CamelServiceAuthType *authtype = NULL;
	CamelAuthenticationResult result;
	CamelProvider *provider;
	CamelSettings *settings;
	const gchar *password;
	guint32 password_flags;
	GError *local_error = NULL;

	/* this is a dupe of mail_session_authenticate_sync()
	 * from e-mail-session.c in evolution's libemail-engine.
	 *
	 * Once libemail-engine becomes part of E-D-S, we
	 * might be able to just drop this dupe and use
	 * the original function from there directly
	 */

	/* Do not chain up.  Camel's default method is only an example for
	 * subclasses to follow.  Instead we mimic most of its logic here. */

	provider = camel_service_get_provider (service);
	settings = camel_service_get_settings (service);

	/* APOP is one case where a non-SASL mechanism name is passed, so
	 * don't bail if the CamelServiceAuthType struct comes back NULL. */
	if (mechanism != NULL)
		authtype = camel_sasl_authtype (mechanism);

	/* If the SASL mechanism does not involve a user
	 * password, then it gets one shot to authenticate. */
	if (authtype != NULL && !authtype->need_password) {
		result = camel_service_authenticate_sync (service, mechanism, cancellable, error);
		if (result == CAMEL_AUTHENTICATION_REJECTED)
			g_set_error (error, CAMEL_SERVICE_ERROR,
			             CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE,
			             _("%s authentication failed"), mechanism);
		return (result == CAMEL_AUTHENTICATION_ACCEPTED);
	}

	/* Some SASL mechanisms can attempt to authenticate without a
	 * user password being provided (e.g. single-sign-on credentials),
	 * but can fall back to a user password.  Handle that case next. */
	if (mechanism != NULL) {
		CamelProvider *provider;
		CamelSasl *sasl;
		const gchar *service_name;
		gboolean success = FALSE;

		provider = camel_service_get_provider (service);
		service_name = provider->protocol;

		/* XXX Would be nice if camel_sasl_try_empty_password_sync()
		 *     returned CamelAuthenticationResult so it's easier to
		 *     detect errors. */
		sasl = camel_sasl_new (service_name, mechanism, service);
		if (sasl != NULL) {
			success = camel_sasl_try_empty_password_sync (sasl,
			                                              cancellable,
			                                              &local_error);
			g_object_unref (sasl);
		}

		if (success)
			return TRUE;
	}

	/* Abort authentication if we got cancelled.
	 * Otherwise clear any errors and press on. */
	if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
		return FALSE;

	g_clear_error (&local_error);

	password_flags = CAMEL_SESSION_PASSWORD_SECRET;

 retry:
	password = camel_service_get_password (service);

	if (password == NULL) {
		CamelNetworkSettings *network_settings;
		const gchar *host;
		const gchar *user;
		gchar *prompt;
		gchar *new_passwd;

		network_settings = CAMEL_NETWORK_SETTINGS (settings);
		host = camel_network_settings_get_host (network_settings);
		user = camel_network_settings_get_user (network_settings);

		prompt = camel_session_build_password_prompt (provider->name,
		                                              user,
		                                              host);

		new_passwd = camel_session_get_password (session, service, prompt, "password",
		                                         password_flags, &local_error);
		camel_service_set_password (service, new_passwd);
		password = camel_service_get_password (service);
		g_free (new_passwd);

		g_free (prompt);

		if (local_error != NULL) {
			g_propagate_error (error, local_error);
			return FALSE;
		}

		if (password == NULL) {
			g_set_error (error, CAMEL_SERVICE_ERROR,
			             CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE,
			             _("No password was provided"));
			return FALSE;
		}
	}

	result = camel_service_authenticate_sync (service, mechanism, cancellable, error);

	if (result == CAMEL_AUTHENTICATION_REJECTED) {
		password_flags |= CAMEL_SESSION_PASSWORD_REPROMPT;
		camel_service_set_password (service, NULL);
		goto retry;
	}

	return (result == CAMEL_AUTHENTICATION_ACCEPTED);
}

/*----------------------------------------------------------------------------*/
/* class init */

static void
camel_kolab_session_class_init (CamelKolabSessionClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	CamelSessionClass *session_class = CAMEL_SESSION_CLASS (klass);
	CamelSessionClass *parent_class = CAMEL_SESSION_CLASS (camel_kolab_session_parent_class);

	g_type_class_add_private (klass, sizeof (CamelKolabSessionPrivate));

	object_class->set_property = G_OBJECT_CLASS (parent_class)->set_property;
	object_class->get_property = G_OBJECT_CLASS (parent_class)->get_property;
	object_class->dispose = camel_kolab_session_dispose;
	object_class->finalize = camel_kolab_session_finalize;

	session_class->add_service = parent_class->add_service;
	session_class->get_password = kolab_session_get_password;
	session_class->forget_password = kolab_session_forget_password;
#if 0
	session_class->alert_user = kolab_session_alert_user;
	session_class->get_filter_driver = kolab_session_get_filter_driver;
	session_class->lookup_addressbook = kolab_session_lookup_addressbook;
	session_class->forward_to = kolab_session_forward_to;
	session_class->get_socks_proxy = kolab_session_get_socks_proxy;
#endif
	session_class->authenticate_sync = kolab_session_authenticate_sync;
}

/*----------------------------------------------------------------------------*/
/* public API */

gboolean
camel_kolab_session_bringup (CamelKolabSession *self,
                             GCancellable *cancellable,
                             GError **err)
{
	/* TODO rework to use GInitable */

	CamelKolabSessionPrivate *priv = NULL;

	g_assert (CAMEL_IS_KOLAB_SESSION (self));
	(void)cancellable; /* FIXME */ /* cancellable may be NULL */
	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	priv = CAMEL_KOLAB_SESSION_PRIVATE (self);

	if (priv->is_initialized)
		return TRUE;

	/* TODO need to supply (parts of) storage paths here so the imap
	 *	objects for the backends will store their files in their
	 *	own respective directories
	 */

	/* TODO junk settings */

	/* setup further NSS bits here */
	PK11_SetPasswordFunc (pk11_password);

	priv->is_initialized = TRUE;
	g_debug ("%s: camel session initialized",
	         __func__);

	return TRUE;
}

gboolean
camel_kolab_session_shutdown (CamelKolabSession *self,
                              GCancellable *cancellable,
                              GError **err)
{
	CamelKolabSessionPrivate *priv = NULL;

	g_assert (CAMEL_IS_KOLAB_SESSION (self));
	(void)cancellable; /* FIXME */ /* cancellable may be NULL */
	g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

	priv = CAMEL_KOLAB_SESSION_PRIVATE (self);

	if (! priv->is_initialized)
		return TRUE;

	g_debug ("%s: camel session shut down",
	         __func__);

	return TRUE;
}

void
camel_kolab_session_set_token_pin (CamelKolabSession *self,
                                   const gchar *pin)
{
	/* TODO rework
	 *
	 * The entire NSS token handling needs
	 * to be rewritten (it is a mere demonstrator
	 * presently, and should not be relied upon
	 * in critical environments
	 *
	 */

	g_assert (CAMEL_IS_KOLAB_SESSION (self));

	if (nss_tok_pin != NULL)
		g_free (nss_tok_pin);

	nss_tok_pin = g_strdup (pin);
}

void
camel_kolab_session_set_password (CamelKolabSession *self,
                                  const gchar *passwd)
{
	/* TODO rework to CamelSettings */

	CamelKolabSessionPrivate *priv = NULL;

	g_assert (CAMEL_IS_KOLAB_SESSION (self));
	priv = CAMEL_KOLAB_SESSION_PRIVATE (self);

	if (priv->passwd != NULL)
		g_free (priv->passwd);

	priv->passwd = g_strdup (passwd);
}

/*----------------------------------------------------------------------------*/
