/*
 * Copyright (C) 2000-2003 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 "url.h"
#include "pop.h"
#if defined(USE_SSL)
# include "mutt_ssl.h"
#endif

#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <netdb.h>
#include <errno.h>
#include <netinet/in.h>

/* given an POP mailbox name, return host, port, username and password */
int pop_parse_path(const char *path, ACCOUNT *acct)
{
  ciss_url_t url;
  char *c;
  struct servent *service;

  /* Defaults */
  acct->flags = 0;
  acct->type = MUTT_ACCT_TYPE_POP;
  acct->port = 0;

  c = safe_strdup(path);
  url_parse_ciss(&url, c);

  if ((url.scheme != U_POP && url.scheme != U_POPS) ||
      mutt_account_fromurl(acct, &url) < 0)
  {
    FREE(&c);
    mutt_error(_("Invalid POP URL: %s\n"), path);
    mutt_sleep(1);
    return -1;
  }

  if (url.scheme == U_POPS)
    acct->flags |= MUTT_ACCT_SSL;

  service = getservbyname(url.scheme == U_POP ? "pop3" : "pop3s", "tcp");
  if (!acct->port)
  {
    if (service)
      acct->port = ntohs(service->s_port);
    else
      acct->port = url.scheme == U_POP ? POP_PORT : POP_SSL_PORT;;
  }

  FREE(&c);
  return 0;
}

/* Copy error message to err_msg buffer */
void pop_error(POP_DATA *pop_data, char *msg)
{
  char *t, *c, *c2;

  t = strchr(pop_data->err_msg, '\0');
  c = msg;

  if (!mutt_strncmp(msg, "-ERR ", 5))
  {
    c2 = skip_email_wsp(msg + 5);

    if (*c2)
      c = c2;
  }

  strfcpy(t, c, sizeof(pop_data->err_msg) - strlen(pop_data->err_msg));
  mutt_remove_trailing_ascii_ws(pop_data->err_msg);
}

/* Parse CAPA output */
static int fetch_capa(char *line, void *data)
{
  POP_DATA *pop_data = (POP_DATA *)data;
  char *c;

  if (!ascii_strncasecmp(line, "SASL", 4))
  {
    FREE(&pop_data->auth_list);
    c = skip_email_wsp(line + 4);
    pop_data->auth_list = safe_strdup(c);
  }

  else if (!ascii_strncasecmp(line, "STLS", 4))
    pop_data->cmd_stls = 1;

  else if (!ascii_strncasecmp(line, "USER", 4))
    pop_data->cmd_user = 1;

  else if (!ascii_strncasecmp(line, "UIDL", 4))
    pop_data->cmd_uidl = 1;

  else if (!ascii_strncasecmp(line, "TOP", 3))
    pop_data->cmd_top = 1;

  return 0;
}

/* Fetch list of the authentication mechanisms */
static int fetch_auth(char *line, void *data)
{
  POP_DATA *pop_data = (POP_DATA *)data;

  if (!pop_data->auth_list)
  {
    pop_data->auth_list = safe_malloc(strlen(line) + 1);
    *pop_data->auth_list = '\0';
  }
  else
  {
    safe_realloc(&pop_data->auth_list,
                 strlen(pop_data->auth_list) + strlen(line) + 2);
    strcat(pop_data->auth_list, " ");  /* __STRCAT_CHECKED__ */
  }
  strcat(pop_data->auth_list, line);   /* __STRCAT_CHECKED__ */

  return 0;
}

/*
 * Get capabilities
 *  0 - successful,
 * -1 - connection lost,
 * -2 - execution error.
 */
static int pop_capabilities(POP_DATA *pop_data, int mode)
{
  char buf[LONG_STRING];

  /* don't check capabilities on reconnect */
  if (pop_data->capabilities)
    return 0;

  /* init capabilities */
  if (mode == 0)
  {
    pop_data->cmd_capa = 0;
    pop_data->cmd_stls = 0;
    pop_data->cmd_user = 0;
    pop_data->cmd_uidl = 0;
    pop_data->cmd_top = 0;
    pop_data->resp_codes = 0;
    pop_data->expire = 1;
    pop_data->login_delay = 0;
    FREE(&pop_data->auth_list);
  }

  /* Execute CAPA command */
  if (mode == 0 || pop_data->cmd_capa)
  {
    strfcpy(buf, "CAPA\r\n", sizeof(buf));
    switch (pop_fetch_data(pop_data, buf, NULL, fetch_capa, pop_data))
    {
      case 0:
      {
        pop_data->cmd_capa = 1;
        break;
      }
      case -1:
        return -1;
    }
  }

  /* CAPA not supported, use defaults */
  if (mode == 0 && !pop_data->cmd_capa)
  {
    pop_data->cmd_user = 2;
    pop_data->cmd_uidl = 2;
    pop_data->cmd_top = 2;

    strfcpy(buf, "AUTH\r\n", sizeof(buf));
    if (pop_fetch_data(pop_data, buf, NULL, fetch_auth, pop_data) == -1)
      return -1;
  }

  /* Check capabilities */
  if (mode == 2)
  {
    char *msg = NULL;

    if (!pop_data->expire)
      msg = _("Unable to leave messages on server.");
    if (!pop_data->cmd_top)
      msg = _("Command TOP is not supported by server.");
    if (!pop_data->cmd_uidl)
      msg = _("Command UIDL is not supported by server.");
    if (msg && pop_data->cmd_capa)
    {
      mutt_error(msg);
      return -2;
    }
    pop_data->capabilities = 1;
  }

  return 0;
}

/*
 * Open connection
 *  0 - successful,
 * -1 - connection lost,
 * -2 - invalid response.
 */
int pop_connect(POP_DATA *pop_data)
{
  char buf[LONG_STRING];

  pop_data->status = POP_NONE;
  if (mutt_socket_open(pop_data->conn) < 0 ||
      mutt_socket_readln(buf, sizeof(buf), pop_data->conn) < 0)
  {
    mutt_error(_("Error connecting to server: %s"), pop_data->conn->account.host);
    return -1;
  }

  pop_data->status = POP_CONNECTED;

  if (mutt_strncmp(buf, "+OK", 3))
  {
    *pop_data->err_msg = '\0';
    pop_error(pop_data, buf);
    mutt_error("%s", pop_data->err_msg);
    return -2;
  }

  pop_apop_timestamp(pop_data, buf);

  return 0;
}

/*
 * Open connection and authenticate
 *  0 - successful,
 * -1 - connection lost,
 * -2 - invalid command or execution error,
 * -3 - authentication canceled.
 */
int pop_open_connection(POP_DATA *pop_data)
{
  int ret;
  unsigned int n, size;
  char buf[LONG_STRING];

  ret = pop_connect(pop_data);
  if (ret < 0)
  {
    mutt_sleep(2);
    return ret;
  }

  ret = pop_capabilities(pop_data, 0);
  if (ret == -1)
    goto err_conn;
  if (ret == -2)
  {
    mutt_sleep(2);
    return -2;
  }

#if defined(USE_SSL)
  /* Attempt STLS if available and desired. */
  if (!pop_data->conn->ssf && (pop_data->cmd_stls || option(OPTSSLFORCETLS)))
  {
    if (option(OPTSSLFORCETLS))
      pop_data->use_stls = 2;
    if (pop_data->use_stls == 0)
    {
      ret = query_quadoption(OPT_SSLSTARTTLS,
                             _("Secure connection with TLS?"));
      if (ret == -1)
        return -2;
      pop_data->use_stls = 1;
      if (ret == MUTT_YES)
        pop_data->use_stls = 2;
    }
    if (pop_data->use_stls == 2)
    {
      strfcpy(buf, "STLS\r\n", sizeof(buf));
      ret = pop_query(pop_data, buf, sizeof(buf));
      if (ret == -1)
        goto err_conn;
      if (ret != 0)
      {
        mutt_error("%s", pop_data->err_msg);
        mutt_sleep(2);
      }
      else if (mutt_ssl_starttls(pop_data->conn))
      {
        mutt_error(_("Could not negotiate TLS connection"));
        mutt_sleep(2);
        return -2;
      }
      else
      {
        /* recheck capabilities after STLS completes */
        ret = pop_capabilities(pop_data, 1);
        if (ret == -1)
          goto err_conn;
        if (ret == -2)
        {
          mutt_sleep(2);
          return -2;
        }
      }
    }
  }

  if (option(OPTSSLFORCETLS) && !pop_data->conn->ssf)
  {
    mutt_error _("Encrypted connection unavailable");
    mutt_sleep(1);
    return -2;
  }
#endif

  ret = pop_authenticate(pop_data);
  if (ret == -1)
    goto err_conn;
  if (ret == -3)
    mutt_clear_error();
  if (ret != 0)
    return ret;

  /* recheck capabilities after authentication */
  ret = pop_capabilities(pop_data, 2);
  if (ret == -1)
    goto err_conn;
  if (ret == -2)
  {
    mutt_sleep(2);
    return -2;
  }

  /* get total size of mailbox */
  strfcpy(buf, "STAT\r\n", sizeof(buf));
  ret = pop_query(pop_data, buf, sizeof(buf));
  if (ret == -1)
    goto err_conn;
  if (ret == -2)
  {
    mutt_error("%s", pop_data->err_msg);
    mutt_sleep(2);
    return ret;
  }

  sscanf(buf, "+OK %u %u", &n, &size);
  pop_data->size = size;
  return 0;

err_conn:
  pop_data->status = POP_DISCONNECTED;
  mutt_error _("Server closed connection!");
  mutt_sleep(2);
  return -1;
}

/* logout from POP server */
void pop_logout(CONTEXT *ctx)
{
  int ret = 0;
  char buf[LONG_STRING];
  POP_DATA *pop_data = (POP_DATA *)ctx->data;

  if (pop_data->status == POP_CONNECTED)
  {
    mutt_message _("Closing connection to POP server...");

    if (ctx->readonly)
    {
      strfcpy(buf, "RSET\r\n", sizeof(buf));
      ret = pop_query(pop_data, buf, sizeof(buf));
    }

    if (ret != -1)
    {
      strfcpy(buf, "QUIT\r\n", sizeof(buf));
      pop_query(pop_data, buf, sizeof(buf));
    }

    mutt_clear_error();
  }

  pop_data->status = POP_DISCONNECTED;
  return;
}

/*
 * Send data from buffer and receive answer to the same buffer
 *  0 - successful,
 * -1 - connection lost,
 * -2 - invalid command or execution error.
 */
int pop_query_d(POP_DATA *pop_data, char *buf, size_t buflen, char *msg)
{
  int dbg = MUTT_SOCK_LOG_CMD;
  char *c;

  if (pop_data->status != POP_CONNECTED)
    return -1;

#ifdef DEBUG
  /* print msg instead of real command */
  if (msg)
  {
    dbg = MUTT_SOCK_LOG_FULL;
    muttdbg(MUTT_SOCK_LOG_CMD, "> %s", msg);
  }
#endif

  mutt_socket_write_d(pop_data->conn, buf, -1, dbg);

  c = strpbrk(buf, " \r\n");
  *c = '\0';
  snprintf(pop_data->err_msg, sizeof(pop_data->err_msg), "%s: ", buf);

  if (mutt_socket_readln(buf, buflen, pop_data->conn) < 0)
  {
    pop_data->status = POP_DISCONNECTED;
    return -1;
  }
  if (!mutt_strncmp(buf, "+OK", 3))
    return 0;

  pop_error(pop_data, buf);
  return -2;
}

/*
 * This function calls  funct(*line, *data)  for each received line,
 * funct(NULL, *data)  if  rewind(*data)  needs, exits when fail or done.
 * Returned codes:
 *  0 - successful,
 * -1 - connection lost,
 * -2 - invalid command or execution error,
 * -3 - error in funct(*line, *data)
 */
int pop_fetch_data(POP_DATA *pop_data, char *query, progress_t *progressbar,
                   int (*funct)(char *, void *), void *data)
{
  char buf[LONG_STRING];
  char *inbuf;
  char *p;
  int ret, chunk = 0;
  long pos = 0;
  size_t lenbuf = 0;

  strfcpy(buf, query, sizeof(buf));
  ret = pop_query(pop_data, buf, sizeof(buf));
  if (ret < 0)
    return ret;

  inbuf = safe_malloc(sizeof(buf));

  FOREVER
  {
    chunk = mutt_socket_readln_d(buf, sizeof(buf), pop_data->conn, MUTT_SOCK_LOG_HDR);
    if (chunk < 0)
    {
      pop_data->status = POP_DISCONNECTED;
      ret = -1;
      break;
    }

    p = buf;
    if (!lenbuf && buf[0] == '.')
    {
      if (buf[1] != '.')
        break;
      p++;
    }

    strfcpy(inbuf + lenbuf, p, sizeof(buf));
    pos += chunk;

    /* cast is safe since we break out of the loop when chunk<=0 */
    if ((size_t)chunk >= sizeof(buf))
    {
      lenbuf += strlen(p);
    }
    else
    {
      if (progressbar)
        mutt_progress_update(progressbar, pos, -1);
      if (ret == 0 && funct(inbuf, data) < 0)
        ret = -3;
      lenbuf = 0;
    }

    safe_realloc(&inbuf, lenbuf + sizeof(buf));
  }

  FREE(&inbuf);
  return ret;
}

/* find message with this UIDL and set refno */
static int check_uidl(char *line, void *data)
{
  int i;
  unsigned int index;
  CONTEXT *ctx = (CONTEXT *)data;
  char *endp;

  errno = 0;
  index = strtoul(line, &endp, 10);
  if (errno)
    return -1;
  while (*endp == ' ')
    endp++;
  memmove(line, endp, strlen(endp) + 1);

  for (i = 0; i < ctx->msgcount; i++)
  {
    if (!mutt_strcmp(ctx->hdrs[i]->data, line))
    {
      ctx->hdrs[i]->refno = index;
      break;
    }
  }

  return 0;
}

/* reconnect and verify idnexes if connection was lost */
int pop_reconnect(CONTEXT *ctx)
{
  int ret;
  POP_DATA *pop_data = (POP_DATA *)ctx->data;
  progress_t progressbar;

  if (pop_data->status == POP_CONNECTED)
    return 0;
  if (pop_data->status == POP_BYE)
    return -1;

  FOREVER
  {
    mutt_socket_close(pop_data->conn);

    ret = pop_open_connection(pop_data);
    if (ret == 0)
    {
      int i;

      mutt_progress_init(&progressbar, _("Verifying message indexes..."),
                         MUTT_PROGRESS_SIZE, NetInc, 0);

      for (i = 0; i < ctx->msgcount; i++)
        ctx->hdrs[i]->refno = -1;

      ret = pop_fetch_data(pop_data, "UIDL\r\n", &progressbar, check_uidl, ctx);
      if (ret == -2)
      {
        mutt_error("%s", pop_data->err_msg);
        mutt_sleep(2);
      }
    }
    if (ret == 0)
      return 0;

    pop_logout(ctx);

    if (ret < -1)
      return -1;

    if (query_quadoption(OPT_POPRECONNECT,
                         _("Connection lost. Reconnect to POP server?")) != MUTT_YES)
      return -1;
  }
}
