/*
 * Copyright (C) 2000-2001 Vsevolod Volkov <vvv@mutt.org.ua>
 *
 *     This program 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 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 General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program; if not, write to the Free Software
 *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#if HAVE_CONFIG_H
# include "config.h"
#endif

#include "mutt.h"
#include "mx.h"
#include "md5.h"
#include "pop.h"

#include <string.h>
#include <unistd.h>

#ifdef USE_SASL_CYRUS
#include <sasl/sasl.h>
#include <sasl/saslutil.h>

#include "mutt_sasl.h"
#endif

#ifdef USE_SASL_GNU
#include "mutt_sasl_gnu.h"
#include <gsasl.h>
#endif

#ifdef USE_SASL_CYRUS
/* SASL authenticator */
static pop_auth_res_t pop_auth_sasl (POP_DATA *pop_data, const char *method)
{
  sasl_conn_t *saslconn;
  sasl_interact_t *interaction = NULL;
  int rc;
  char *buf = NULL;
  size_t bufsize = 0;
  char inbuf[LONG_STRING];
  const char* mech;
  const char *pc = NULL;
  unsigned int len, olen, client_start;

  if (mutt_account_getpass (&pop_data->conn->account) ||
      !pop_data->conn->account.pass[0])
    return POP_A_FAILURE;

  if (mutt_sasl_client_new (pop_data->conn, &saslconn) < 0)
  {
    dprint (1, (debugfile, "pop_auth_sasl: Error allocating SASL connection.\n"));
    return POP_A_FAILURE;
  }

  if (!method)
    method = pop_data->auth_list;

  FOREVER
  {
    rc = sasl_client_start(saslconn, method, &interaction, &pc, &olen, &mech);
    if (rc != SASL_INTERACT)
      break;
    mutt_sasl_interact (interaction);
  }

  if (rc != SASL_OK && rc != SASL_CONTINUE)
  {
    dprint (1, (debugfile, "pop_auth_sasl: Failure starting authentication exchange. No shared mechanisms?\n"));

    /* SASL doesn't support suggested mechanisms, so fall back */
    sasl_dispose (&saslconn);
    return POP_A_UNAVAIL;
  }

  /* About client_start:  If sasl_client_start() returns data via pc/olen,
   * the client is expected to send this first (after the AUTH string is sent).
   * sasl_client_start() may in fact return SASL_OK in this case.
   */
  client_start = olen;

  mutt_message _("Authenticating (SASL)...");

  bufsize = ((olen * 2) > LONG_STRING) ? (olen * 2) : LONG_STRING;
  buf = safe_malloc (bufsize);

  snprintf (buf, bufsize, "AUTH %s", mech);
  olen = strlen (buf);

  /* looping protocol */
  FOREVER
  {
    strfcpy (buf + olen, "\r\n", bufsize - olen);
    mutt_socket_write (pop_data->conn, buf);
    if (mutt_socket_readln (inbuf, sizeof (inbuf), pop_data->conn) < 0)
    {
      sasl_dispose (&saslconn);
      pop_data->status = POP_DISCONNECTED;
      FREE (&buf);
      return POP_A_SOCKET;
    }

    /* Note we don't exit if rc==SASL_OK when client_start is true.
     * This is because the first loop has only sent the AUTH string, we
     * need to loop at least once more to send the pc/olen returned
     * by sasl_client_start().
     */
    if (!client_start && rc != SASL_CONTINUE)
      break;

    if (!mutt_strncmp (inbuf, "+ ", 2)
        && sasl_decode64 (inbuf+2, strlen (inbuf+2), buf, bufsize - 1, &len) != SASL_OK)
    {
      dprint (1, (debugfile, "pop_auth_sasl: error base64-decoding server response.\n"));
      goto bail;
    }

    if (!client_start)
      FOREVER
      {
	rc = sasl_client_step (saslconn, buf, len, &interaction, &pc, &olen);
	if (rc != SASL_INTERACT)
	  break;
	mutt_sasl_interact (interaction);
      }
    else
    {
      olen = client_start;
      client_start = 0;
    }

    /* Even if sasl_client_step() returns SASL_OK, we should send at
     * least one more line to the server.  See #3862.
     */
    if (rc != SASL_CONTINUE && rc != SASL_OK)
      break;

    /* send out response, or line break if none needed */
    if (pc)
    {
      if ((olen * 2) > bufsize)
      {
        bufsize = olen * 2;
        safe_realloc (&buf, bufsize);
      }
      if (sasl_encode64 (pc, olen, buf, bufsize, &olen) != SASL_OK)
      {
	dprint (1, (debugfile, "pop_auth_sasl: error base64-encoding client response.\n"));
	goto bail;
      }
    }
  }

  if (rc != SASL_OK)
    goto bail;

  if (!mutt_strncmp (inbuf, "+OK", 3))
  {
    mutt_sasl_setup_conn (pop_data->conn, saslconn);
    FREE (&buf);
    return POP_A_SUCCESS;
  }

bail:
  sasl_dispose (&saslconn);

  /* terminate SASL session if the last response is not +OK nor -ERR */
  if (!mutt_strncmp (inbuf, "+ ", 2))
  {
    snprintf (buf, bufsize, "*\r\n");
    if (pop_query (pop_data, buf, sizeof (buf)) == -1)
    {
      FREE (&buf);
      return POP_A_SOCKET;
    }
  }

  FREE (&buf);
  mutt_error _("SASL authentication failed.");
  mutt_sleep (2);

  return POP_A_FAILURE;
}
#endif


#ifdef USE_SASL_GNU
static pop_auth_res_t pop_auth_gsasl (POP_DATA *pop_data, const char *method)
{
  Gsasl_session *gsasl_session = NULL;
  const char *chosen_mech, *pop_auth_data;
  BUFFER *output_buf = NULL, *input_buf = NULL;
  char *gsasl_step_output = NULL;
  int rc = POP_A_FAILURE, gsasl_rc = GSASL_OK;

  chosen_mech = mutt_gsasl_get_mech (method, pop_data->auth_list);
  if (!chosen_mech)
  {
    dprint (2, (debugfile, "mutt_gsasl_get_mech() returned no usable mech\n"));
    return POP_A_UNAVAIL;
  }

  dprint (2, (debugfile, "pop_auth_gsasl: using mech %s\n", chosen_mech));

  if (mutt_gsasl_client_new (pop_data->conn, chosen_mech, &gsasl_session) < 0)
  {
    dprint (1, (debugfile,
                "pop_auth_gsasl: Error allocating GSASL connection.\n"));
    return POP_A_UNAVAIL;
  }

  mutt_message (_("Authenticating (%s)..."), chosen_mech);

  output_buf = mutt_buffer_pool_get ();
  input_buf = mutt_buffer_pool_get ();
  mutt_buffer_printf (output_buf, "AUTH %s\r\n", chosen_mech);

  do
  {
    if (mutt_socket_write (pop_data->conn, mutt_b2s (output_buf)) < 0)
    {
      pop_data->status = POP_DISCONNECTED;
      rc = POP_A_SOCKET;
      goto fail;
    }

    if (mutt_socket_buffer_readln (input_buf, pop_data->conn) < 0)
    {
      pop_data->status = POP_DISCONNECTED;
      rc = POP_A_SOCKET;
      goto fail;
    }

    if (mutt_strncmp (mutt_b2s (input_buf), "+ ", 2))
      break;

    pop_auth_data = mutt_b2s (input_buf) + 2;
    gsasl_rc = gsasl_step64 (gsasl_session, pop_auth_data, &gsasl_step_output);
    if (gsasl_rc == GSASL_NEEDS_MORE || gsasl_rc == GSASL_OK)
    {
      mutt_buffer_strcpy (output_buf, gsasl_step_output);
      mutt_buffer_addstr (output_buf, "\r\n");
      gsasl_free (gsasl_step_output);
    }
    else
    {
      dprint (1, (debugfile, "gsasl_step64() failed (%d): %s\n", gsasl_rc,
                  gsasl_strerror (gsasl_rc)));
    }
  }
  while (gsasl_rc == GSASL_NEEDS_MORE || gsasl_rc == GSASL_OK);

  if (!mutt_strncmp (mutt_b2s (input_buf), "+ ", 2))
  {
    mutt_socket_write (pop_data->conn, "*\r\n");
    goto fail;
  }

  if (!mutt_strncmp (mutt_b2s (input_buf), "+OK", 3) && (gsasl_rc == GSASL_OK))
    rc = POP_A_SUCCESS;

fail:
  mutt_buffer_pool_release (&input_buf);
  mutt_buffer_pool_release (&output_buf);
  mutt_gsasl_client_finish (&gsasl_session);

  if (rc == POP_A_FAILURE)
  {
    dprint (2, (debugfile, "pop_auth_gsasl: %s failed\n", chosen_mech));
    mutt_error _("SASL authentication failed.");
    mutt_sleep (2);
  }

  return rc;
}
#endif


/* Get the server timestamp for APOP authentication */
void pop_apop_timestamp (POP_DATA *pop_data, char *buf)
{
  char *p1, *p2;

  FREE (&pop_data->timestamp);

  if ((p1 = strchr (buf, '<')) && (p2 = strchr (p1, '>')))
  {
    p2[1] = '\0';
    pop_data->timestamp = safe_strdup (p1);
  }
}

/* APOP authenticator */
static pop_auth_res_t pop_auth_apop (POP_DATA *pop_data, const char *method)
{
  struct md5_ctx ctx;
  unsigned char digest[16];
  char hash[33];
  char buf[LONG_STRING];
  size_t i;

  if (mutt_account_getpass (&pop_data->conn->account) ||
      !pop_data->conn->account.pass[0])
    return POP_A_FAILURE;

  if (!pop_data->timestamp)
    return POP_A_UNAVAIL;

  if (rfc822_valid_msgid (pop_data->timestamp) < 0)
  {
    mutt_error _("POP timestamp is invalid!");
    mutt_sleep (2);
    return POP_A_UNAVAIL;
  }

  mutt_message _("Authenticating (APOP)...");

  /* Compute the authentication hash to send to the server */
  md5_init_ctx (&ctx);
  md5_process_bytes (pop_data->timestamp, strlen (pop_data->timestamp), &ctx);
  md5_process_bytes (pop_data->conn->account.pass,
		     strlen (pop_data->conn->account.pass), &ctx);
  md5_finish_ctx (&ctx, digest);

  for (i = 0; i < sizeof (digest); i++)
    sprintf (hash + 2 * i, "%02x", digest[i]);

  /* Send APOP command to server */
  snprintf (buf, sizeof (buf), "APOP %s %s\r\n", pop_data->conn->account.user, hash);

  switch (pop_query (pop_data, buf, sizeof (buf)))
  {
    case 0:
      return POP_A_SUCCESS;
    case -1:
      return POP_A_SOCKET;
  }

  mutt_error _("APOP authentication failed.");
  mutt_sleep (2);

  return POP_A_FAILURE;
}

/* USER authenticator */
static pop_auth_res_t pop_auth_user (POP_DATA *pop_data, const char *method)
{
  char buf[LONG_STRING];
  int ret;

  if (!pop_data->cmd_user)
    return POP_A_UNAVAIL;

  if (mutt_account_getpass (&pop_data->conn->account) ||
      !pop_data->conn->account.pass[0])
    return POP_A_FAILURE;

  mutt_message _("Logging in...");

  snprintf (buf, sizeof (buf), "USER %s\r\n", pop_data->conn->account.user);
  ret = pop_query (pop_data, buf, sizeof (buf));

  if (pop_data->cmd_user == 2)
  {
    if (ret == 0)
    {
      pop_data->cmd_user = 1;

      dprint (1, (debugfile, "pop_auth_user: set USER capability\n"));
    }

    if (ret == -2)
    {
      pop_data->cmd_user = 0;

      dprint (1, (debugfile, "pop_auth_user: unset USER capability\n"));
      snprintf (pop_data->err_msg, sizeof (pop_data->err_msg), "%s",
                _("Command USER is not supported by server."));
    }
  }

  if (ret == 0)
  {
    snprintf (buf, sizeof (buf), "PASS %s\r\n", pop_data->conn->account.pass);
    ret = pop_query_d (pop_data, buf, sizeof (buf),
#ifdef DEBUG
                       /* don't print the password unless we're at the ungodly debugging level */
                       debuglevel < MUTT_SOCK_LOG_FULL ? "PASS *\r\n" :
#endif
                       NULL);
  }

  switch (ret)
  {
    case 0:
      return POP_A_SUCCESS;
    case -1:
      return POP_A_SOCKET;
  }

  mutt_error ("%s %s", _("Login failed."), pop_data->err_msg);
  mutt_sleep (2);

  return POP_A_FAILURE;
}

/* OAUTHBEARER/XOAUTH2 authenticator */
static pop_auth_res_t pop_auth_oauth (POP_DATA *pop_data, int xoauth2)
{
  int rc = POP_A_FAILURE;
  BUFFER *bearertoken = NULL, *authline = NULL;
  const char *authtype;
  char decoded_err[LONG_STRING];
  char *err = NULL;
  int ret, len;

  authtype = xoauth2 ? "XOAUTH2" : "OAUTHBEARER";

  mutt_message (_("Authenticating (%s)..."), authtype);

  bearertoken = mutt_buffer_pool_get ();
  authline = mutt_buffer_pool_get ();

  if (mutt_account_getoauthbearer (&pop_data->conn->account, bearertoken, xoauth2))
    goto cleanup;

  mutt_buffer_printf (authline, "AUTH %s\r\n", authtype);
  ret = pop_query (pop_data, authline->data, authline->dsize);

  if (ret == 0 ||
      (ret == -2 && !mutt_strncmp (authline->data, "+", 1)))
  {
    mutt_buffer_printf (authline, "%s\r\n", mutt_b2s (bearertoken));

    ret = pop_query_d (pop_data, authline->data, authline->dsize,
#ifdef DEBUG
                       /* don't print the bearer token unless we're at the ungodly debugging level */
                       debuglevel < MUTT_SOCK_LOG_FULL ?
                       (xoauth2 ? "*\r\n" : "*\r\n")
                       :
#endif
                       NULL);
  }

  switch (ret)
  {
    case 0:
      rc = POP_A_SUCCESS;
      goto cleanup;
    case -1:
      rc = POP_A_SOCKET;
      goto cleanup;
  }

  /* The error response was a SASL continuation, so "continue" it.
   * See RFC 7628 3.2.3.  "AQ==" is Base64 encoded ^A (0x01) .
   */
  err = pop_data->err_msg;
  len = mutt_from_base64 (decoded_err, pop_data->err_msg, sizeof(decoded_err) - 1);
  if (len >= 0)
  {
    decoded_err[len] = '\0';
    err = decoded_err;
  }
  mutt_buffer_strcpy (authline, "AQ==\r\n");
  pop_query_d (pop_data, authline->data, authline->dsize, NULL);
  mutt_error ("%s %s", _("Authentication failed."), err);
  mutt_sleep (2);

cleanup:
  mutt_buffer_pool_release (&bearertoken);
  mutt_buffer_pool_release (&authline);
  return rc;
}


/* OAUTHBEARER/XOAUTH2 authenticator */
static pop_auth_res_t pop_auth_oauthbearer (POP_DATA *pop_data, const char *method)
{
  if (!PopOauthRefreshCmd)
    return POP_A_UNAVAIL;

  return pop_auth_oauth (pop_data, 0);
}

/* OAUTHBEARER/XOAUTH2 authenticator */
static pop_auth_res_t pop_auth_xoauth2 (POP_DATA *pop_data, const char *method)
{
  /* If they did not explicitly request XOAUTH2 then fail quietly */
  if (!(method && PopOauthRefreshCmd))
    return POP_A_UNAVAIL;

  return pop_auth_oauth (pop_data, 1);
}


static const pop_auth_t pop_authenticators[] = {
  { pop_auth_oauthbearer, "oauthbearer" },
  { pop_auth_xoauth2, "xoauth2" },
#ifdef USE_SASL_CYRUS
  { pop_auth_sasl, NULL },
#endif
#ifdef USE_SASL_GNU
  { pop_auth_gsasl, NULL },
#endif
  { pop_auth_apop, "apop" },
  { pop_auth_user, "user" },
  { NULL,	   NULL }
};

/*
 * Authentication
 *  0 - successful,
 * -1 - connection lost,
 * -2 - login failed,
 * -3 - authentication canceled.
 */
int pop_authenticate (POP_DATA* pop_data)
{
  ACCOUNT *acct = &pop_data->conn->account;
  const pop_auth_t* authenticator;
  char* methods;
  char* comma;
  char* method;
  int attempts = 0;
  int ret = POP_A_UNAVAIL;

  if (mutt_account_getuser (acct) || !acct->user[0])
    return -3;

  if (PopAuthenticators)
  {
    /* Try user-specified list of authentication methods */
    methods = safe_strdup (PopAuthenticators);
    method = methods;

    while (method)
    {
      comma = strchr (method, ':');
      if (comma)
	*comma++ = '\0';
      dprint (2, (debugfile, "pop_authenticate: Trying method %s\n", method));
      authenticator = pop_authenticators;

      while (authenticator->authenticate)
      {
	if (!authenticator->method ||
	    !ascii_strcasecmp (authenticator->method, method))
	{
	  ret = authenticator->authenticate (pop_data, method);
	  if (ret == POP_A_SOCKET)
	    switch (pop_connect (pop_data))
	    {
	      case 0:
	      {
		ret = authenticator->authenticate (pop_data, method);
		break;
	      }
	      case -2:
		ret = POP_A_FAILURE;
	    }

	  if (ret != POP_A_UNAVAIL)
	    attempts++;
	  if (ret == POP_A_SUCCESS || ret == POP_A_SOCKET ||
	      (ret == POP_A_FAILURE && !option (OPTPOPAUTHTRYALL)))
	  {
	    comma = NULL;
	    break;
	  }
	}
	authenticator++;
      }

      method = comma;
    }

    FREE (&methods);
  }
  else
  {
    /* Fall back to default: any authenticator */
    dprint (2, (debugfile, "pop_authenticate: Using any available method.\n"));
    authenticator = pop_authenticators;

    while (authenticator->authenticate)
    {
      ret = authenticator->authenticate (pop_data, NULL);
      if (ret == POP_A_SOCKET)
	switch (pop_connect (pop_data))
	{
	  case 0:
	  {
	    ret = authenticator->authenticate (pop_data, NULL);
	    break;
	  }
	  case -2:
	    ret = POP_A_FAILURE;
	}

      if (ret != POP_A_UNAVAIL)
	attempts++;
      if (ret == POP_A_SUCCESS || ret == POP_A_SOCKET ||
	  (ret == POP_A_FAILURE && !option (OPTPOPAUTHTRYALL)))
	break;

      authenticator++;
    }
  }

  switch (ret)
  {
    case POP_A_SUCCESS:
      return 0;
    case POP_A_SOCKET:
      return -1;
    case POP_A_UNAVAIL:
      if (!attempts)
	mutt_error (_("No authenticators available"));
  }

  return -2;
}
