/**
 * @file
 * Postponed Email Selection Dialog
 *
 * @authors
 * Copyright (C) 2016-2023 Richard Russon <rich@flatcap.org>
 * Copyright (C) 2020-2022 Pietro Cerutti <gahr@gahr.ch>
 * Copyright (C) 2022 David Purton <dcpurton@marshwiggle.net>
 * Copyright (C) 2023 Rayford Shireman
 *
 * @copyright
 * 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, see <http://www.gnu.org/licenses/>.
 */

/**
 * @page postpone_postpone Postponed Email
 *
 * Functions to deal with Postponed Emails.
 */

#include "config.h"
#include <stdbool.h>
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "mutt/lib.h"
#include "address/lib.h"
#include "config/lib.h"
#include "email/lib.h"
#include "core/lib.h"
#include "mutt.h"
#include "lib.h"
#include "imap/lib.h"
#include "ncrypt/lib.h"
#include "send/lib.h"
#include "globals.h"
#include "handler.h"
#include "mutt_logging.h"
#include "mutt_thread.h"
#include "muttlib.h"
#include "mx.h"
#include "protos.h"
#include "rfc3676.h"

/// Number of postponed (draft) emails
short PostCount = 0;
/// When true, force a recount of the postponed (draft) emails
static bool UpdateNumPostponed = false;

/**
 * mutt_num_postponed - Return the number of postponed messages
 * @param m    currently selected mailbox
 * @param force
 * * false Use a cached value if costly to get a fresh count (IMAP)
 * * true Force check
 * @retval num Postponed messages
 */
int mutt_num_postponed(struct Mailbox *m, bool force)
{
  struct stat st = { 0 };

  static time_t LastModify = 0;
  static char *OldPostponed = NULL;

  if (UpdateNumPostponed)
  {
    UpdateNumPostponed = false;
    force = true;
  }

  const char *const c_postponed = cs_subset_string(NeoMutt->sub, "postponed");
  if (!mutt_str_equal(c_postponed, OldPostponed))
  {
    FREE(&OldPostponed);
    OldPostponed = mutt_str_dup(c_postponed);
    LastModify = 0;
    force = true;
  }

  if (!c_postponed)
    return 0;

  // We currently are in the `$postponed` mailbox so just pick the current status
  if (m && mutt_str_equal(c_postponed, m->realpath))
  {
    PostCount = m->msg_count - m->msg_deleted;
    return PostCount;
  }

  /* LastModify is useless for IMAP */
  if (imap_path_probe(c_postponed, NULL) == MUTT_IMAP)
  {
    if (force)
    {
      short newpc;

      newpc = imap_path_status(c_postponed, false);
      if (newpc >= 0)
      {
        PostCount = newpc;
        mutt_debug(LL_DEBUG3, "%d postponed IMAP messages found\n", PostCount);
      }
      else
      {
        mutt_debug(LL_DEBUG3, "using old IMAP postponed count\n");
      }
    }
    return PostCount;
  }

  if (stat(c_postponed, &st) == -1)
  {
    PostCount = 0;
    LastModify = 0;
    return 0;
  }

  if (S_ISDIR(st.st_mode))
  {
    /* if we have a maildir mailbox, we need to stat the "new" dir */
    struct Buffer *buf = buf_pool_get();

    buf_printf(buf, "%s/new", c_postponed);
    if ((access(buf_string(buf), F_OK) == 0) && (stat(buf_string(buf), &st) == -1))
    {
      PostCount = 0;
      LastModify = 0;
      buf_pool_release(&buf);
      return 0;
    }
    buf_pool_release(&buf);
  }

  if (LastModify < st.st_mtime)
  {
    int optnews = OptNews;
    LastModify = st.st_mtime;

    if (access(c_postponed, R_OK | F_OK) != 0)
      return PostCount = 0;
    if (optnews)
      OptNews = false;
    struct Mailbox *m_post = mx_path_resolve(c_postponed);
    if (mx_mbox_open(m_post, MUTT_NOSORT | MUTT_QUIET))
    {
      PostCount = m_post->msg_count;
      mx_fastclose_mailbox(m_post, false);
    }
    else
    {
      PostCount = 0;
    }
    mailbox_free(&m_post);

    if (optnews)
      OptNews = true;
  }

  return PostCount;
}

/**
 * mutt_update_num_postponed - Force the update of the number of postponed messages
 */
void mutt_update_num_postponed(void)
{
  UpdateNumPostponed = true;
}

/**
 * hardclose - Try hard to close a mailbox
 * @param m Mailbox to close
 */
static void hardclose(struct Mailbox *m)
{
  /* messages might have been marked for deletion.
   * try once more on reopen before giving up. */
  enum MxStatus rc = mx_mbox_close(m);
  if (rc != MX_STATUS_ERROR && rc != MX_STATUS_OK)
    rc = mx_mbox_close(m);
  if (rc != MX_STATUS_OK)
    mx_fastclose_mailbox(m, false);
}

/**
 * mutt_parse_crypt_hdr - Parse a crypto header string
 * @param p                Header string to parse
 * @param set_empty_signas Allow an empty "Sign as"
 * @param crypt_app App, e.g. #APPLICATION_PGP
 * @retval num SecurityFlags, see #SecurityFlags
 */
SecurityFlags mutt_parse_crypt_hdr(const char *p, bool set_empty_signas, SecurityFlags crypt_app)
{
  char smime_cryptalg[1024] = { 0 };
  char sign_as[1024] = { 0 };
  char *q = NULL;
  SecurityFlags flags = SEC_NO_FLAGS;

  if (!WithCrypto)
    return SEC_NO_FLAGS;

  p = mutt_str_skip_email_wsp(p);
  for (; p[0] != '\0'; p++)
  {
    switch (p[0])
    {
      case 'c':
      case 'C':
        q = smime_cryptalg;

        if (p[1] == '<')
        {
          for (p += 2; (p[0] != '\0') && (p[0] != '>') &&
                       (q < (smime_cryptalg + sizeof(smime_cryptalg) - 1));
               *q++ = *p++)
          {
          }

          if (p[0] != '>')
          {
            mutt_error(_("Illegal S/MIME header"));
            return SEC_NO_FLAGS;
          }
        }

        *q = '\0';
        break;

      case 'e':
      case 'E':
        flags |= SEC_ENCRYPT;
        break;

      case 'i':
      case 'I':
        flags |= SEC_INLINE;
        break;

      /* This used to be the micalg parameter.
       *
       * It's no longer needed, so we just skip the parameter in order
       * to be able to recall old messages.  */
      case 'm':
      case 'M':
        if (p[1] != '<')
          break;

        for (p += 2; (p[0] != '\0') && (p[0] != '>'); p++)
          ; // do nothing

        if (p[0] != '>')
        {
          mutt_error(_("Illegal crypto header"));
          return SEC_NO_FLAGS;
        }
        break;

      case 'o':
      case 'O':
        flags |= SEC_OPPENCRYPT;
        break;

      case 'a':
      case 'A':
#ifdef USE_AUTOCRYPT
        flags |= SEC_AUTOCRYPT;
#endif
        break;

      case 'z':
      case 'Z':
#ifdef USE_AUTOCRYPT
        flags |= SEC_AUTOCRYPT_OVERRIDE;
#endif
        break;

      case 's':
      case 'S':
        flags |= SEC_SIGN;
        q = sign_as;

        if (p[1] == '<')
        {
          for (p += 2;
               (p[0] != '\0') && (*p != '>') && (q < (sign_as + sizeof(sign_as) - 1));
               *q++ = *p++)
          {
          }

          if (p[0] != '>')
          {
            mutt_error(_("Illegal crypto header"));
            return SEC_NO_FLAGS;
          }
        }

        q[0] = '\0';
        break;

      default:
        mutt_error(_("Illegal crypto header"));
        return SEC_NO_FLAGS;
    }
  }

  /* the cryptalg field must not be empty */
  if (((WithCrypto & APPLICATION_SMIME) != 0) && *smime_cryptalg)
  {
    struct Buffer *errmsg = buf_pool_get();
    int rc = cs_subset_str_string_set(NeoMutt->sub, "smime_encrypt_with",
                                      smime_cryptalg, errmsg);

    if ((CSR_RESULT(rc) != CSR_SUCCESS) && !buf_is_empty(errmsg))
      mutt_error("%s", buf_string(errmsg));

    buf_pool_release(&errmsg);
  }

  /* Set {Smime,Pgp}SignAs, if desired. */

  if (((WithCrypto & APPLICATION_PGP) != 0) && (crypt_app == APPLICATION_PGP) &&
      (flags & SEC_SIGN) && (set_empty_signas || *sign_as))
  {
    cs_subset_str_string_set(NeoMutt->sub, "pgp_sign_as", sign_as, NULL);
  }

  if (((WithCrypto & APPLICATION_SMIME) != 0) && (crypt_app == APPLICATION_SMIME) &&
      (flags & SEC_SIGN) && (set_empty_signas || *sign_as))
  {
    cs_subset_str_string_set(NeoMutt->sub, "smime_sign_as", sign_as, NULL);
  }

  return flags;
}

/**
 * create_tmp_files_for_attachments - Create temporary files for all attachments
 * @param fp_body           file containing the template
 * @param file              Allocated buffer for temporary file name
 * @param e_new             The new email template header
 * @param body              First body in email or group
 * @param protected_headers MIME headers for email template
 * @retval  0 Success
 * @retval -1 Error
 */
static int create_tmp_files_for_attachments(FILE *fp_body, struct Buffer *file,
                                            struct Email *e_new, struct Body *body,
                                            struct Envelope *protected_headers)
{
  struct Body *b = NULL;
  struct State state = { 0 };

  state.fp_in = fp_body;

  for (b = body; b; b = b->next)
  {
    if (b->type == TYPE_MULTIPART)
    {
      if (create_tmp_files_for_attachments(fp_body, file, e_new, b->parts, protected_headers) < 0)
      {
        return -1;
      }
    }
    else
    {
      buf_reset(file);
      if (b->filename)
      {
        buf_strcpy(file, b->filename);
        b->d_filename = mutt_str_dup(b->filename);
      }
      else
      {
        /* avoid Content-Disposition: header with temporary filename */
        b->use_disp = false;
      }

      /* set up state flags */

      state.flags = 0;

      if (b->type == TYPE_TEXT)
      {
        if (mutt_istr_equal("yes", mutt_param_get(&b->parameter, "x-mutt-noconv")))
        {
          b->noconv = true;
        }
        else
        {
          state.flags |= STATE_CHARCONV;
          b->noconv = false;
        }

        mutt_param_delete(&b->parameter, "x-mutt-noconv");
      }

      mutt_adv_mktemp(file);
      state.fp_out = mutt_file_fopen(buf_string(file), "w");
      if (!state.fp_out)
        return -1;

      SecurityFlags sec_type = SEC_NO_FLAGS;
      if (((WithCrypto & APPLICATION_PGP) != 0) && sec_type == SEC_NO_FLAGS)
        sec_type = mutt_is_application_pgp(b);
      if (((WithCrypto & APPLICATION_SMIME) != 0) && sec_type == SEC_NO_FLAGS)
        sec_type = mutt_is_application_smime(b);
      if (sec_type & (SEC_ENCRYPT | SEC_SIGN))
      {
        if (sec_type & SEC_ENCRYPT)
        {
          if (!crypt_valid_passphrase(sec_type))
          {
            mutt_file_fclose(&state.fp_out);
            return -1;
          }
          if (sec_type & APPLICATION_SMIME)
            crypt_smime_getkeys(e_new->env);
          mutt_message(_("Decrypting message..."));
        }

        if (mutt_body_handler(b, &state) < 0)
        {
          mutt_error(_("Decryption failed"));
          mutt_file_fclose(&state.fp_out);
          return -1;
        }

        /* Is this the first body part? Then save the headers. */
        if ((b == body) && !protected_headers)
        {
          protected_headers = b->mime_headers;
          b->mime_headers = NULL;
        }

        e_new->security |= sec_type;
        b->type = TYPE_TEXT;
        mutt_str_replace(&b->subtype, "plain");
        if (sec_type & APPLICATION_PGP)
          mutt_param_delete(&b->parameter, "x-action");
      }
      else
      {
        mutt_decode_attachment(b, &state);
      }

      if (mutt_file_fclose(&state.fp_out) != 0)
        return -1;

      mutt_str_replace(&b->filename, buf_string(file));
      b->unlink = true;

      mutt_stamp_attachment(b);

      mutt_body_free(&b->parts);
      if (b->email)
        b->email->body = NULL; /* avoid dangling pointer */
    }
  }

  return 0;
}

/**
 * mutt_prepare_template - Prepare a message template
 * @param fp      If not NULL, file containing the template
 * @param m       If fp is NULL, the Mailbox containing the header with the template
 * @param e_new   The template is read into this Header
 * @param e       Email to recall/resend
 * @param resend  Set if resending (as opposed to recalling a postponed msg)
 *                Resent messages enable header weeding, and also
 *                discard any existing Message-ID and Mail-Followup-To
 * @retval  0 Success
 * @retval -1 Error
 */
int mutt_prepare_template(FILE *fp, struct Mailbox *m, struct Email *e_new,
                          struct Email *e, bool resend)
{
  struct Message *msg = NULL;
  struct Body *b = NULL;
  FILE *fp_body = NULL;
  int rc = -1;
  struct Envelope *protected_headers = NULL;
  struct Buffer *file = NULL;

  if (!fp && !(msg = mx_msg_open(m, e)))
    return -1;

  if (!fp)
    fp = msg->fp;

  fp_body = fp;

  /* parse the message header and MIME structure */

  if (!mutt_file_seek(fp, e->offset, SEEK_SET))
  {
    return -1;
  }
  e_new->offset = e->offset;
  /* enable header weeding for resent messages */
  e_new->env = mutt_rfc822_read_header(fp, e_new, true, resend);
  e_new->body->length = e->body->length;
  mutt_parse_part(fp, e_new->body);

  /* If resending a message, don't keep message_id or mail_followup_to.
   * Otherwise, we are resuming a postponed message, and want to keep those
   * headers if they exist.  */
  if (resend)
  {
    FREE(&e_new->env->message_id);
    mutt_addrlist_clear(&e_new->env->mail_followup_to);
  }

  SecurityFlags sec_type = SEC_NO_FLAGS;
  if (((WithCrypto & APPLICATION_PGP) != 0) && sec_type == SEC_NO_FLAGS)
    sec_type = mutt_is_multipart_encrypted(e_new->body);
  if (((WithCrypto & APPLICATION_SMIME) != 0) && sec_type == SEC_NO_FLAGS)
    sec_type = mutt_is_application_smime(e_new->body);
  if (sec_type != SEC_NO_FLAGS)
  {
    e_new->security |= sec_type;
    if (!crypt_valid_passphrase(sec_type))
      goto bail;

    mutt_message(_("Decrypting message..."));
    int ret = -1;
    if (sec_type & APPLICATION_PGP)
      ret = crypt_pgp_decrypt_mime(fp, &fp_body, e_new->body, &b);
    else if (sec_type & APPLICATION_SMIME)
      ret = crypt_smime_decrypt_mime(fp, &fp_body, e_new->body, &b);
    if ((ret == -1) || !b)
    {
      mutt_error(_("Could not decrypt postponed message"));
      goto bail;
    }

    /* throw away the outer layer and keep only the (now decrypted) inner part
     * with its headers. */
    mutt_body_free(&e_new->body);
    e_new->body = b;

    if (b->mime_headers)
    {
      protected_headers = b->mime_headers;
      b->mime_headers = NULL;
    }

    mutt_clear_error();
  }

  /* remove a potential multipart/signed layer - useful when
   * resending messages */
  if ((WithCrypto != 0) && mutt_is_multipart_signed(e_new->body))
  {
    e_new->security |= SEC_SIGN;
    if (((WithCrypto & APPLICATION_PGP) != 0) &&
        mutt_istr_equal(mutt_param_get(&e_new->body->parameter, "protocol"), "application/pgp-signature"))
    {
      e_new->security |= APPLICATION_PGP;
    }
    else if (WithCrypto & APPLICATION_SMIME)
    {
      e_new->security |= APPLICATION_SMIME;
    }

    /* destroy the signature */
    mutt_body_free(&e_new->body->parts->next);
    e_new->body = mutt_remove_multipart(e_new->body);

    if (e_new->body->mime_headers)
    {
      mutt_env_free(&protected_headers);
      protected_headers = e_new->body->mime_headers;
      e_new->body->mime_headers = NULL;
    }
  }

  /* We don't need no primary multipart/mixed. */
  if ((e_new->body->type == TYPE_MULTIPART) && mutt_istr_equal(e_new->body->subtype, "mixed"))
    e_new->body = mutt_remove_multipart(e_new->body);

  file = buf_pool_get();

  /* create temporary files for all attachments */
  if (create_tmp_files_for_attachments(fp_body, file, e_new, e_new->body, protected_headers) < 0)
  {
    goto bail;
  }

  const bool c_crypt_protected_headers_read = cs_subset_bool(NeoMutt->sub, "crypt_protected_headers_read");
  if (c_crypt_protected_headers_read && protected_headers && protected_headers->subject &&
      !mutt_str_equal(e_new->env->subject, protected_headers->subject))
  {
    mutt_env_set_subject(e_new->env, protected_headers->subject);
  }
  mutt_env_free(&protected_headers);

  /* Fix encryption flags. */

  /* No inline if multipart. */
  if ((WithCrypto != 0) && (e_new->security & SEC_INLINE) && e_new->body->next)
    e_new->security &= ~SEC_INLINE;

  /* Do we even support multiple mechanisms? */
  e_new->security &= WithCrypto | ~(APPLICATION_PGP | APPLICATION_SMIME);

  /* Theoretically, both could be set. Take the one the user wants to set by default. */
  if ((e_new->security & APPLICATION_PGP) && (e_new->security & APPLICATION_SMIME))
  {
    const bool c_smime_is_default = cs_subset_bool(NeoMutt->sub, "smime_is_default");
    if (c_smime_is_default)
      e_new->security &= ~APPLICATION_PGP;
    else
      e_new->security &= ~APPLICATION_SMIME;
  }

  mutt_rfc3676_space_unstuff(e_new);

  rc = 0;

bail:

  /* that's it. */
  buf_pool_release(&file);
  if (fp_body != fp)
    mutt_file_fclose(&fp_body);
  if (msg)
    mx_msg_close(m, &msg);

  if (rc == -1)
  {
    mutt_env_free(&e_new->env);
    mutt_body_free(&e_new->body);
  }

  return rc;
}

/**
 * mutt_get_postponed - Recall a postponed message
 * @param[in]  m_cur   Current mailbox
 * @param[in]  hdr     envelope/attachment info for recalled message
 * @param[out] cur     if message was a reply, 'cur' is set to the message which 'hdr' is in reply to
 * @param[in]  fcc     fcc for the recalled message
 * @retval -1         Error/no messages
 * @retval 0          Normal exit
 * @retval #SEND_REPLY Recalled message is a reply
 */
int mutt_get_postponed(struct Mailbox *m_cur, struct Email *hdr,
                       struct Email **cur, struct Buffer *fcc)
{
  const char *const c_postponed = cs_subset_string(NeoMutt->sub, "postponed");
  if (!c_postponed)
    return -1;

  struct Email *e = NULL;
  int rc = SEND_POSTPONED;
  const char *p = NULL;

  struct Mailbox *m = mx_path_resolve(c_postponed);
  if (m_cur != m)
  {
    if (!mx_mbox_open(m, MUTT_NOSORT))
    {
      PostCount = 0;
      mutt_error(_("No postponed messages"));
      mailbox_free(&m);
      return -1;
    }
  }

  mx_mbox_check(m);

  if (m->msg_count == 0)
  {
    PostCount = 0;
    mutt_error(_("No postponed messages"));
    if (m_cur != m)
    {
      mx_fastclose_mailbox(m, false);
      mailbox_free(&m);
    }
    return -1;
  }

  /* avoid the "purge deleted messages" prompt */
  const enum QuadOption c_delete = cs_subset_quad(NeoMutt->sub, "delete");
  cs_subset_str_native_set(NeoMutt->sub, "delete", MUTT_YES, NULL);

  if (m->msg_count == 1)
  {
    /* only one message, so just use that one. */
    e = m->emails[0];
  }
  else if (!(e = dlg_postponed(m)))
  {
    rc = -1;
    goto cleanup;
  }

  if (mutt_prepare_template(NULL, m, hdr, e, false) < 0)
  {
    rc = -1;
    goto cleanup;
  }

  /* finished with this message, so delete it. */
  mutt_set_flag(m, e, MUTT_DELETE, true, true);
  mutt_set_flag(m, e, MUTT_PURGE, true, true);

  /* update the count for the status display */
  PostCount = m->msg_count - m->msg_deleted;

  struct ListNode *np = NULL, *tmp = NULL;
  STAILQ_FOREACH_SAFE(np, &hdr->env->userhdrs, entries, tmp)
  {
    size_t plen = 0;
    // Check for header names: most specific first
    if ((plen = mutt_istr_startswith(np->data, "X-Mutt-References:")) ||
        (plen = mutt_istr_startswith(np->data, "Mutt-References:")))
    {
      /* if a mailbox is currently open, look to see if the original message
       * the user attempted to reply to is in this mailbox */
      if (m_cur)
      {
        p = mutt_str_skip_email_wsp(np->data + plen);
        if (!m_cur->id_hash)
          m_cur->id_hash = mutt_make_id_hash(m_cur);
        *cur = mutt_hash_find(m_cur->id_hash, p);

        if (*cur)
          rc |= SEND_REPLY;
      }
    }
    // Check for header names: most specific first
    else if ((plen = mutt_istr_startswith(np->data, "X-Mutt-Fcc:")) ||
             (plen = mutt_istr_startswith(np->data, "Mutt-Fcc:")))
    {
      p = mutt_str_skip_email_wsp(np->data + plen);
      buf_strcpy(fcc, p);
      buf_pretty_mailbox(fcc);

      /* note that mutt-fcc was present.  we do this because we want to add a
       * default fcc if the header was missing, but preserve the request of the
       * user to not make a copy if the header field is present, but empty. */
      rc |= SEND_POSTPONED_FCC;
    }
    // Check for header names: most specific first
    else if (((WithCrypto & APPLICATION_PGP) != 0) &&
             ((plen = mutt_istr_startswith(np->data, "X-Mutt-PGP:")) ||
              (plen = mutt_istr_startswith(np->data, "Mutt-PGP:")) ||
              (plen = mutt_istr_startswith(np->data, "Pgp:"))))
    {
      hdr->security = mutt_parse_crypt_hdr(np->data + plen, true, APPLICATION_PGP);
      hdr->security |= APPLICATION_PGP;
    }
    // Check for header names: most specific first
    else if (((WithCrypto & APPLICATION_SMIME) != 0) &&
             ((plen = mutt_istr_startswith(np->data, "X-Mutt-SMIME:")) ||
              (plen = mutt_istr_startswith(np->data, "Mutt-SMIME:"))))
    {
      hdr->security = mutt_parse_crypt_hdr(np->data + plen, true, APPLICATION_SMIME);
      hdr->security |= APPLICATION_SMIME;
    }
    else
    {
      // skip header removal
      continue;
    }

    // remove the header
    STAILQ_REMOVE(&hdr->env->userhdrs, np, ListNode, entries);
    FREE(&np->data);
    FREE(&np);
  }

  const bool c_crypt_opportunistic_encrypt = cs_subset_bool(NeoMutt->sub, "crypt_opportunistic_encrypt");
  if (c_crypt_opportunistic_encrypt)
    crypt_opportunistic_encrypt(hdr);

cleanup:
  if (m_cur != m)
  {
    hardclose(m);
    mailbox_free(&m);
  }

  cs_subset_str_native_set(NeoMutt->sub, "delete", c_delete, NULL);
  return rc;
}
