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

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

   You should have received a copy of the GNU Lesser General Public
   License along with GNU Mailutils.  If not, see
   <http://www.gnu.org/licenses/>. */

/* The "uidnew" test.
 *
 * Syntax:   require [ "test-uidnew" ];
 *
 *           if uidnew [ :mailbox <name: string> ]
 *                     [ :db <name: string> ]
 *             {
 *                action;
 *             }
 *
 * The "uidnew" test makes sure each message in the mailbox is processed
 * exactly once.  The test evaluates to true, if the UID of the current
 * message has not yet been seen, and false otherwise.  To that effect,
 * it keeps a DBM database where it stores, for each mailbox, the values
 * of uidvalidity and last processed UID.  By default, the GNU DBM file
 * ".uidnew.db" is stored in the user home directory.  The following tags
 * modify the test behavior:
 *
 *    :db        Sets the name of the DBM file to use instead of the
 *               default.  The argument is either a filename (in which
 *		 case GDBM format is assumed), or a full DBM URL.
 *    :mailbox   Name or URL of the mailbox to use as a key to the database.
 *               By default, URL of the current mailbox is used.
 */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <mailutils/sieve.h>
#include <mailutils/dbm.h>
#include <mailutils/diag.h>

static char *defdbname = "~/.uidnew.db";
static mu_assoc_t dbtab;

static int
db_close (char const *name, void *data, void *cdata)
{
  mu_dbm_file_t db = data;
  mu_dbm_close (db);
  mu_dbm_destroy (&db);
  return 0;
}

static void
dbtab_close (void *ptr)
{
  mu_assoc_foreach (dbtab, db_close, NULL);
  mu_assoc_destroy (&dbtab);
}

static int
tempdb (mu_sieve_machine_t mach, mu_url_t url, mu_dbm_file_t *pdb)
{
  int rc;
  char const *path;
  mu_stream_t src, dst;
  int tempfd;
  char *tempname;
  mu_dbm_file_t db;

  if ((rc = mu_url_sget_path (url, &path)) != 0)
    {
      mu_sieve_error (mach,
		      _("%s: cannot get pathname from url: %s"),
		      mu_url_to_string (url),
		      mu_strerror (rc));
      return 1;
    }

  if ((rc = mu_tempfile (NULL, 0, &tempfd, &tempname)) != 0)
    {
      mu_sieve_error (mach,
		      _("cannot create temporary file: %s"),
		      mu_strerror (rc));
      return 1;
    }

  if ((rc = mu_file_stream_create (&src, path, MU_STREAM_READ)) == 0)
    {
      rc = mu_fd_stream_create (&dst, tempname, tempfd, MU_STREAM_WRITE);
      if (rc)
	{
	  mu_sieve_error (mach,
			  _("cannot create temporary file stream: %s"),
			  mu_strerror (rc));
	  mu_stream_destroy (&src);
	  close (tempfd);
	  goto err;
	}

      rc = mu_stream_copy (dst, src, 0, NULL);

      mu_stream_destroy (&src);
      mu_stream_destroy (&dst);
      close (tempfd);

      if (rc)
	{
	  mu_sieve_error (mach,
			  _("error copying to temporary file stream: %s"),
			  mu_strerror (rc));
	  goto err;
	}
    }
  else
    {
      mu_sieve_error (mach,
		      _("%s: cannot create file stream: %s"),
		      path, mu_strerror (rc));
      close (tempfd);
      goto err;
    }

  mu_url_set_path (url, tempname);

  if ((rc = mu_dbm_create_from_url (url, &db, MU_FILE_SAFETY_ALL)) != 0)
    {
      mu_diag_funcall (MU_DIAG_ERROR, "mu_dbm_create_from_url",
		       mu_url_to_string (url), rc);
    }
  else if ((rc = mu_dbm_open (db, MU_STREAM_RDWR, 0600)) != 0)
    {
      mu_dbm_destroy (&db);
      mu_diag_funcall (MU_DIAG_ERROR, "mu_dbm_open",
		       mu_url_to_string (url), rc);
    }
  else
    *pdb = db;

 err:
  unlink (tempname);
  free (tempname);

  return rc;
}

static int
db_open (mu_sieve_machine_t mach, char const *name, mu_dbm_file_t *retdb)
{
  int rc;
  mu_dbm_file_t *pdb;

  if (!dbtab)
    {
      if ((rc = mu_assoc_create (&dbtab, 0)) != 0)
	{
	  mu_diag_funcall (MU_DIAG_ERROR, "mu_assoc_create", NULL, rc);
	  return -1;
	}
      mu_sieve_machine_add_destructor (mach, dbtab_close, NULL);
    }
  rc = mu_assoc_install_ref2 (dbtab, name, &pdb, NULL);

  if (rc == MU_ERR_EXISTS)
    {
      *retdb = *pdb;
      return 0;
    }
  else if (rc == 0)
    {
      mu_url_t url, hint;
      mu_dbm_file_t db;

      mu_url_create_null (&hint);
      mu_url_set_scheme (hint, "gdbm");
      rc = mu_url_create_hint (&url, name, MU_URL_PARSE_LOCAL, hint);
      mu_url_destroy (&hint);
      if (rc)
	{
	  mu_diag_funcall (MU_DIAG_ERROR, "mu_url_create_hint", name, rc);
	  return 1;
	}

      if ((rc = mu_dbm_create_from_url (url, &db, MU_FILE_SAFETY_ALL)) != 0)
	{
	  mu_diag_funcall (MU_DIAG_ERROR, "mu_dbm_create", name, rc);
	  return 1;
	}
      if ((rc = mu_dbm_open (db, MU_STREAM_RDWR, 0600)) != 0)
	{
	  mu_diag_funcall (MU_DIAG_ERROR, "mu_dbm_open", name, rc);
	  return 1;
	}

      if (mu_sieve_is_dry_run (mach))
	{
	  mu_dbm_destroy (&db);
	  rc = tempdb (mach, url, &db);
	}

      mu_url_destroy (&url);

      if (rc == 0)
	*retdb = *pdb = db;

      return rc;
    }
  else
    {
      mu_diag_funcall (MU_DIAG_ERROR, "mu_assoc_install_ref2", NULL, rc);
      return rc;
    }
  return 0;
}

struct uidstat
{
  unsigned long uidvalidity;
  size_t uidnew;
};

static int
sieve_test_uidnew (mu_sieve_machine_t mach)
{
  int rc;
  mu_mailbox_t mbx = mu_sieve_get_mailbox (mach);
  mu_message_t msg = mu_sieve_get_message (mach);
  struct mu_dbm_datum key, datum;
  struct uidstat uidstat, cur;
  size_t uid;
  char *dbname, *name;
  mu_dbm_file_t db;

  mu_sieve_log_action (mach, "UIDNEW", NULL);

  if ((rc = mu_message_get_uid (msg, &uid)) != 0)
    {
      mu_sieve_error (mach,
		      _("%lu: cannot get message uid: %s"),
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_strerror (rc));
      mu_sieve_abort (mach);
    }

  if (mu_sieve_get_tag (mach, "mailbox", SVT_STRING, &name) == 0)
    {
      mu_url_t url;

      if (mbx == NULL)
	{
	  mu_sieve_error (mach,
			  _("no mailbox; use the ':mailbox' tag"));
	  mu_sieve_abort (mach);
	}

      if ((rc = mu_mailbox_get_url (mbx, &url)) != 0)
	{
	  mu_sieve_error (mach,
			  _("cannot get mailbox url: %s"),
			  mu_strerror (rc));
	  mu_sieve_abort (mach);
	}
      name = (char*) mu_url_to_string (url);
    }

  if (mu_sieve_get_tag (mach, "db", SVT_STRING, &dbname) == 0)
    dbname = defdbname;

  if (db_open (mach, dbname, &db))
    mu_sieve_abort (mach);

  key.mu_dptr = name;
  key.mu_dsize = strlen (key.mu_dptr) + 1;
  memset (&datum, 0, sizeof (datum));
  rc = mu_dbm_fetch (db, &key, &datum);
  if (rc == 0 && datum.mu_dsize == sizeof (struct uidstat))
    {
      uidstat = *(struct uidstat *)datum.mu_dptr;
      mu_dbm_datum_free (&datum);
    }
  else
    memset (&uidstat, 0, sizeof (uidstat));

  mu_mailbox_uidvalidity (mbx, &cur.uidvalidity);
  if (cur.uidvalidity == uidstat.uidvalidity)
    {
      if (uid <= uidstat.uidnew)
	return 0;
    }
  else
    {
      uidstat.uidvalidity = cur.uidvalidity;
    }
  uidstat.uidnew = uid;

  datum.mu_dsize = sizeof (uidstat);
  datum.mu_dptr = (char*) &uidstat;
  if ((rc = mu_dbm_store (db, &key, &datum, 1)) != 0)
    {
      mu_sieve_error (mach,
		      _("%lu: error storing datum: %s"),
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_dbm_strerror (db));
      mu_sieve_abort (mach);
    }
  return 1;
}

/* Tagged arguments: */
static mu_sieve_tag_def_t uidnew_tags[] = {
  { "mailbox", SVT_STRING },
  { "db", SVT_STRING },
  { NULL }
};

static mu_sieve_tag_group_t uidnew_tag_groups[] = {
  { uidnew_tags, NULL },
  { NULL }
};

int
SIEVE_EXPORT (uidnew, init) (mu_sieve_machine_t mach)
{
  mu_sieve_register_test (mach, "uidnew", sieve_test_uidnew,
			  NULL, uidnew_tag_groups, 1);
  return 0;
}
