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

   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; either
   version 2 of the License, or (at your option) any later version.

   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, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA  */

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

#ifdef WITH_GSASL

#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <unistd.h>
#include <mailutils/argp.h>
#include <mailutils/error.h>
#include <mailutils/errno.h>
#include <mailutils/mu_auth.h>
#include <mailutils/nls.h>
#include <mailutils/stream.h>

#include <gsasl.h>
#include <lbuf.h>

char *gsasl_cram_md5_pwd = SITE_CRAM_MD5_PWD;

#define ARG_CRAM_PASSWD 1

static struct argp_option _gsasl_argp_options[] = {
  {NULL, 0, NULL, 0, N_("GSASL options"), 0},
  {"cram-passwd", ARG_CRAM_PASSWD, N_("FILE"), 0,
   N_("Specify password file for CRAM-MD5 authentication"), 0},
  { NULL,      0, NULL, 0, NULL, 0 }
};

static error_t
_gsasl_argp_parser (int key, char *arg, struct argp_state *state)
{
  switch (key)
    {
    case ARG_CRAM_PASSWD:
      gsasl_cram_md5_pwd = arg;
      break;

    default:
      return ARGP_ERR_UNKNOWN;
    }
  return 0;
}

static struct argp _gsasl_argp = {
  _gsasl_argp_options,
  _gsasl_argp_parser
};

static struct argp_child _gsasl_argp_child = {
  &_gsasl_argp,
  0,
  NULL,
  0
};

void
mu_gsasl_init_argp ()
{
  if (mu_register_capa ("gsasl", &_gsasl_argp_child))
    {
      mu_error (_("INTERNAL ERROR: cannot register argp capability gsasl"));
      abort ();
    }
}


struct _gsasl_stream {
  Gsasl_session_ctx *sess_ctx; /* Context */
  int last_err;        /* Last Gsasl error code */
  
  stream_t stream;     /* I/O stream */
  struct _line_buffer *lb; 
};

static void
_gsasl_destroy (stream_t stream)
{
  int flags;
  struct _gsasl_stream *s = stream_get_owner (stream);
  stream_get_flags (stream, &flags);
  if (!(flags & MU_STREAM_NO_CLOSE))
    stream_destroy (&s->stream, stream_get_owner (s->stream));
  _auth_lb_destroy (&s->lb);
}

static int
_gsasl_readline (stream_t stream, char *optr, size_t osize,
		 off_t offset, size_t *nbytes)
{
  struct _gsasl_stream *s = stream_get_owner (stream);
  int rc;
  size_t len, sz;
  char *bufp;
  
  if (_auth_lb_level (s->lb))
    {
      len = _auth_lb_readline (s->lb, optr, osize-1);
      optr[len] = 0;
      if (nbytes)
	*nbytes = len;
      return 0;
    }
  
  do
    {
      char buf[80];
      size_t sz;
      int status;
      
      status = stream_sequential_read (s->stream, buf, sizeof (buf), &sz);
      if (status == EINTR)
	continue;
      else if (status)
	return status;
      
      rc = _auth_lb_grow (s->lb, buf, sz);
      if (rc)
	return rc;

      len = UINT_MAX; /* override the bug in libgsasl */
      rc = gsasl_decode (s->sess_ctx,
			 _auth_lb_data (s->lb),
			 _auth_lb_level (s->lb),
			 NULL, &len);
    }
  while (rc == GSASL_NEEDS_MORE);

  if (rc != GSASL_OK)
    {
      s->last_err = rc;
      return EIO;
    }
      
  bufp = malloc (len + 1);
  if (!bufp)
    return ENOMEM;
  rc = gsasl_decode (s->sess_ctx,
		     _auth_lb_data (s->lb), _auth_lb_level (s->lb), bufp, &len);
  if (rc != GSASL_OK)
    {
      s->last_err = rc;
      return EIO;
    }
  bufp[len++] = '\0';

  sz = len > osize ? osize : len;
  
  if (len > osize)
    {
      memcpy (optr, bufp, osize);
      _auth_lb_drop (s->lb);
      _auth_lb_grow (s->lb, bufp + osize, len - osize);
      len = osize;
    }
  else
    {
      _auth_lb_drop (s->lb);
      memcpy (optr, bufp, len);
    }
  
  if (nbytes)
    *nbytes = len;
  
  free (bufp);

  return 0;
}

int
write_chunk (void *data, char *start, char *end)
{
  struct _gsasl_stream *s = data;
  size_t chunk_size = end - start + 1;
  size_t len;
  char *buf = NULL;
  int status;
    
  len = UINT_MAX; /* override the bug in libgsasl */
  gsasl_encode (s->sess_ctx, start, chunk_size, NULL, &len);
  buf = malloc (len);
  if (!buf)
    return ENOMEM;

  gsasl_encode (s->sess_ctx, start, chunk_size, buf, &len);

  status = stream_sequential_write (s->stream, buf, len);
  
  free (buf);

  return status;
}


static int
_gsasl_write (stream_t stream, const char *iptr, size_t isize,
	      off_t offset, size_t *nbytes)
{
  int rc;
  struct _gsasl_stream *s = stream_get_owner (stream);
  
  rc = _auth_lb_grow (s->lb, iptr, isize);
  if (rc)
    return rc;

  return _auth_lb_writelines (s->lb, iptr, isize, offset,
			      write_chunk, s, nbytes);      
}

static int
_gsasl_flush (stream_t stream)
{
  struct _gsasl_stream *s = stream_get_owner (stream);
  return stream_flush (s->stream);
}

static int
_gsasl_close (stream_t stream)
{
  int flags;
  struct _gsasl_stream *s = stream_get_owner (stream);

  stream_get_flags (stream, &flags);
  if (!(flags & MU_STREAM_NO_CLOSE))
    stream_close (s->stream);
  return 0;
}

static int
_gsasl_open (stream_t stream)
{
  /* Nothing to do */
  return 0;
}

int
_gsasl_strerror (stream_t stream, const char **pstr)
{
  struct _gsasl_stream *s = stream_get_owner (stream);
  *pstr = gsasl_strerror (s->last_err);
  return 0;
}

int
_gsasl_get_transport2 (stream_t stream, mu_transport_t *pt, mu_transport_t *pt2)
{
  struct _gsasl_stream *s = stream_get_owner (stream);
  *pt2 = NULL; /* FIXME 1 */
  *pt = (mu_transport_t) s->stream;
  return 0;
}

int
_gsasl_wait (stream_t stream, int *pflags, struct timeval *tvp)
{
  int flags;
  struct _gsasl_stream *s = stream_get_owner (stream);

  stream_get_flags (stream, &flags);
  if (((*pflags & MU_STREAM_READY_RD) && !(flags & MU_STREAM_READ))
      || ((*pflags & MU_STREAM_READY_WR) && !(flags & MU_STREAM_WRITE)))
    return EINVAL; 
  return stream_wait (s->stream, pflags, tvp);
}

int
gsasl_stream_create (stream_t *stream, stream_t transport,
		     Gsasl_session_ctx *ctx, int flags)
{
  struct _gsasl_stream *s;
  int rc;
    
  if (stream == NULL)
    return MU_ERR_OUT_PTR_NULL;

  if ((flags & ~(MU_STREAM_READ|MU_STREAM_WRITE))
      || (flags & (MU_STREAM_READ|MU_STREAM_WRITE)) ==
          (MU_STREAM_READ|MU_STREAM_WRITE))
    return EINVAL;
  
  s = calloc (1, sizeof (*s));
  if (s == NULL)
    return ENOMEM;

  s->stream = transport;
  s->sess_ctx = ctx;
  
  rc = stream_create (stream, flags|MU_STREAM_NO_CHECK, s);
  if (rc)
    {
      free (s);
      return rc;
    }

  stream_set_open (*stream, _gsasl_open, s);
  stream_set_close (*stream, _gsasl_close, s);
  stream_set_flush (*stream, _gsasl_flush, s);
  stream_set_destroy (*stream, _gsasl_destroy, s);
  stream_set_strerror (*stream, _gsasl_strerror, s);
  stream_set_wait (*stream, _gsasl_wait, s);
  stream_set_get_transport2 (*stream, _gsasl_get_transport2, s);
  if (flags & MU_STREAM_READ)
    stream_set_readline (*stream, _gsasl_readline, s);
  else
    stream_set_write (*stream, _gsasl_write, s);

  _auth_lb_create (&s->lb);
  
  return 0;
}
  
#endif