/* GNU Mailutils -- a suite of utilities for electronic mail
   Copyright (C) 2003, 2004 Free Software Foundation, Inc.

   GNU Mailutils is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.

   GNU Mailutils 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GNU Mailutils; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA  */

#include "imap4d.h"
#include <gsasl.h>
#include <mailutils/gsasl.h>

static Gsasl_ctx *ctx;   
static Gsasl_session_ctx *sess_ctx; 

static void auth_gsasl_capa_init __P((int disable));

static int
create_gsasl_stream (stream_t *newstr, stream_t transport, int flags)
{
  int rc;
  
  rc = gsasl_stream_create (newstr, transport, sess_ctx, flags);
  if (rc)
    {
      syslog (LOG_ERR, _("cannot create SASL stream: %s"),
	      mu_strerror (rc));
      return RESP_NO;
    }

  if ((rc = stream_open (*newstr)) != 0)
    {
      const char *p;
      if (stream_strerror (*newstr, &p))
	p = mu_strerror (rc);
      syslog (LOG_ERR, _("cannot open SASL input stream: %s"), p);
      return RESP_NO;
    }

  return RESP_OK;
}

int
gsasl_replace_streams (void *self, void *data)
{
  stream_t *s = data;

  util_set_input (s[0]);
  util_set_output (s[1]);
  free (s);
  util_event_remove (self);
  free (self);
  return 0;
}

static void
finish_session (void)
{
  gsasl_server_finish (sess_ctx);
}

static int
auth_gsasl (struct imap4d_command *command,
	    char *auth_type, char *arg, char **username)
{
  char *input = NULL;
  char *output;
  char *s;
  int rc;

  input = util_getword (arg, &s);
  util_unquote (&input);
  
  rc = gsasl_server_start (ctx, auth_type, &sess_ctx);
  if (rc != GSASL_OK)
    {
      syslog (LOG_NOTICE, _("SASL gsasl_server_start: %s"),
	      gsasl_strerror(rc));
      return 0;
    }

  gsasl_server_application_data_set (sess_ctx, username);

  output = NULL;
  while ((rc = gsasl_step64 (sess_ctx, input, &output)) == GSASL_NEEDS_MORE)
    {
      util_send ("+ %s\r\n", output);
      input = imap4d_readline_ex ();
    }
  
  if (rc != GSASL_OK)
    {
      syslog (LOG_NOTICE, _("GSASL error: %s"), gsasl_strerror (rc));
      free (output);
      return RESP_NO;
    }

  /* Some SASL mechanisms output data when GSASL_OK is returned */
  if (output[0])
    util_send ("+ %s\r\n", output);
  
  free (output);

  if (*username == NULL)
    {
      syslog (LOG_NOTICE, _("GSASL %s: cannot get username"), auth_type);
      return RESP_NO;
    }

  if (sess_ctx)
    {
      stream_t tmp, new_in, new_out;
      stream_t *s;

      util_get_input (&tmp);
      if (create_gsasl_stream (&new_in, tmp, MU_STREAM_READ))
	return RESP_NO;
      util_get_output (&tmp);
      if (create_gsasl_stream (&new_out, tmp, MU_STREAM_WRITE))
	{
	  stream_destroy (&new_in, stream_get_owner (new_in));
	  return RESP_NO;
	}

      s = calloc (2, sizeof (stream_t));
      s[0] = new_in;
      s[1] = new_out;
      util_register_event (STATE_NONAUTH, STATE_AUTH,
			   gsasl_replace_streams, s);
      util_atexit (finish_session);
    }
  
  auth_gsasl_capa_init (1);
  return RESP_OK;
}

static void
auth_gsasl_capa_init (int disable)
{
  int rc;
  char *listmech, *name, *s;

  rc =  gsasl_server_mechlist (ctx, &listmech);
  if (rc != GSASL_OK)
    return;

  for (name = strtok_r (listmech, " ", &s); name;
       name = strtok_r (NULL, " ", &s))
    {
      if (disable)
	auth_remove (name);
      else
	auth_add (strdup (name), auth_gsasl);
    }
      
  free (listmech);
}

/* This is for DIGEST-MD5 */
static int
cb_realm (Gsasl_session_ctx *ctx, char *out, size_t *outlen, size_t nth)
{
  char *realm = util_localname ();

  if (nth > 0)
    return GSASL_NO_MORE_REALMS;

  if (out)
    {
      if (*outlen < strlen (realm))
	return GSASL_TOO_SMALL_BUFFER;
      memcpy (out, realm, strlen (realm));
    }

  *outlen = strlen (realm);

  return GSASL_OK;
}

static int
cb_validate (Gsasl_session_ctx *ctx,
	     const char *authorization_id,
	     const char *authentication_id,
	     const char *password)
{
  char **username = gsasl_server_application_data_get (ctx);
  *username = strdup (authentication_id ?
		      authentication_id : authorization_id);
  return GSASL_OK;
}

#define GSSAPI_SERVICE "imap"

static int
cb_service (Gsasl_session_ctx *ctx, char *srv, size_t *srvlen,
	    char *host, size_t *hostlen)
{
  char *hostname = util_localname ();

  if (srv)
    {
      if (*srvlen < strlen (GSSAPI_SERVICE))
       return GSASL_TOO_SMALL_BUFFER;

      memcpy (srv, GSSAPI_SERVICE, strlen (GSSAPI_SERVICE));
    }

  if (srvlen)
    *srvlen = strlen (GSSAPI_SERVICE);

  if (host)
    {
      if (*hostlen < strlen (hostname))
       return GSASL_TOO_SMALL_BUFFER;

      memcpy (host, hostname, strlen (hostname));
    }

  if (hostlen)
    *hostlen = strlen (hostname);

  return GSASL_OK;
}

/* This gets called when SASL mechanism EXTERNAL is invoked */
static int
cb_external (Gsasl_session_ctx *ctx)
{
  return GSASL_AUTHENTICATION_ERROR;
}

/* This gets called when SASL mechanism CRAM-MD5 or DIGEST-MD5 is invoked */

static int
cb_retrieve (Gsasl_session_ctx *ctx,
	     const char *authentication_id,
	     const char *authorization_id,
	     const char *realm,
	     char *key,
	     size_t *keylen)
{
  char **username = gsasl_server_application_data_get (ctx);

  if (username && authentication_id)
    *username = strdup (authentication_id);

  return gsasl_md5pwd_get_password (gsasl_cram_md5_pwd, authentication_id,
				    key, keylen);
}

void
auth_gsasl_init ()
{
  int rc;

  rc = gsasl_init (&ctx);
  if (rc != GSASL_OK)
    {
      syslog (LOG_NOTICE, _("cannot initialize libgsasl: %s"),
	      gsasl_strerror (rc));
    }

  gsasl_server_callback_realm_set (ctx, cb_realm);
  gsasl_server_callback_external_set (ctx, cb_external);
  gsasl_server_callback_validate_set (ctx, cb_validate);
  gsasl_server_callback_service_set (ctx, cb_service);

  if (gsasl_cram_md5_pwd && access (gsasl_cram_md5_pwd, R_OK) == 0)
    {
      gsasl_server_callback_retrieve_set (ctx, cb_retrieve);
    }
  
  auth_gsasl_capa_init (0);
}