/* This file is part of Mailfromd.
   Copyright (C) 2005-2025 Sergey Poznyakoff

   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 3, 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/>. */

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

#include <mailutils/mailutils.h>
#include <string.h>
#include <sysexits.h>
#include <sys/stat.h>

#include "libmf.h"
#include "mfdb.h"

int mf_database_mode = 0600;
int ignore_failed_reads_option;//FIXME;

struct db_fmt_list {
	struct db_fmt_list *next;
	struct db_format fmt;
};

static struct db_fmt_list *db_fmt_list;
static mu_debug_handle_t debug_handle;

void
db_format_enumerate(dbfmt_enumerator_t fp, void *data)
{
	struct db_fmt_list *p;
	
	for (p = db_fmt_list; p; p = p->next)
		fp(&p->fmt, data);
}

struct db_format *
db_format_install(struct db_format *fmt)
{
	struct db_fmt_list *p;

	if (!fmt)
		return NULL;
	
	p = mu_alloc(sizeof *p);
	p->fmt = *fmt;
	mf_namefixup_register(&p->fmt.dbname, p->fmt.dbname);
	/* FIXME: not used so far */
	p->fmt.debug_handle = mu_debug_register_category(p->fmt.name);
	p->next = db_fmt_list;
	db_fmt_list = p;
	
	return &p->fmt;
}

struct db_format *
db_format_lookup(const char *name)
{
	struct db_fmt_list *p;
	for (p = db_fmt_list; p; p = p->next)
		if (strcmp(p->fmt.name, name) == 0)
			return &p->fmt;
	return NULL;
}


void
db_format_setup()
{
	debug_handle = mu_debug_register_category("db");
	cache_format = db_format_install(cache_format);
	rate_format = db_format_install(rate_format);
	tbf_rate_format = db_format_install(tbf_rate_format);
	greylist_format = db_format_install(greylist_format);
}

int
mf_file_mode_to_safety_criteria(int mode)
{
	int fl = 0;
	
	if (!(mode & 0002))
		fl |= MU_FILE_SAFETY_WORLD_WRITABLE;
	if (!(mode & 0004))
		fl |= MU_FILE_SAFETY_WORLD_READABLE;
	if (!(mode & 0020))
		fl |= MU_FILE_SAFETY_GROUP_WRITABLE;
	if (!(mode & 0040))
		fl |= MU_FILE_SAFETY_GROUP_READABLE;
	return fl;
}

mu_dbm_file_t 
mf_dbm_open(char *dbname, int access)
{
	mu_dbm_file_t db;
	int rc;
	
	rc = mu_dbm_create(dbname, &db, MU_FILE_SAFETY_LINKED_WRDIR |
			   MU_FILE_SAFETY_DIR_IWOTH|
			   mf_file_mode_to_safety_criteria(mf_database_mode));
	if (rc)	{
		mu_error(_("unable to create database %s: %s"),
			 dbname, mu_strerror (rc));
		return NULL;
	}

	rc = mu_dbm_safety_check(db);
	if (rc && rc != ENOENT) {
		mu_error(_("%s fails safety check: %s"),
			 dbname, mu_strerror(rc));
		mu_dbm_destroy(&db);
		return NULL;
	}

	rc = mu_dbm_open(db, access, mf_database_mode);
	if (rc) {
		mu_error(_("mu_dbm_open(%s) failed: %s (%s)"), dbname,
			 mu_strerror (rc), mu_strerror (errno));
		mu_dbm_destroy(&db);
		return NULL;
	}
	return db;
}


int
db_list_item(char *dbname, char *email, db_item_printer_t fun)
{	
	mu_dbm_file_t db;
	struct mu_dbm_datum key, contents;
	int rc = 1;

	db = mf_dbm_open(dbname, MU_STREAM_READ);
	if (!db)
		return 1;
	memset(&contents, 0, sizeof contents);
	memset(&key, 0, sizeof key);
	key.mu_dptr = email;
	key.mu_dsize = strlen (email) + 1;
	rc = mu_dbm_fetch(db, &key, &contents);
	if (rc == 0) {
		fun(&key, &contents);
	} else if (rc != MU_ERR_NOENT) {
		mu_error(_("cannot fetch record `%s' from `%s': %s"), 
		         email, dbname, mu_dbm_strerror(db));
		rc = 0;
	}
	mu_dbm_destroy(&db);
	return rc;
}

typedef int (*db_enum_func)(struct mu_dbm_datum *key,
			    struct mu_dbm_datum *cnt, void *data);

int
db_enumerate(mu_dbm_file_t db, db_enum_func fun, void *data)
{
	int rc = 0;
	int ret = 0;
	struct mu_dbm_datum key, contents;
	const char *name = "";

	mu_dbm_get_name(db, &name);

	memset(&key, 0, sizeof key);
	memset(&contents, 0, sizeof contents);
	for (rc = mu_dbm_firstkey(db, &key); rc == 0;
	     rc = mu_dbm_nextkey(db, &key)) {
		int res = mu_dbm_fetch(db, &key, &contents);
		if (res == 0) {
			res = fun(&key, &contents, data);
			if (res) {
				mu_dbm_datum_free(&key);
				break;
			}
			mu_dbm_datum_free(&contents);
		} else {
 			mu_error(_("cannot fetch data `%*.*s' from `%s': %s"),
				 (int) key.mu_dsize, (int) key.mu_dsize,
				 key.mu_dptr,
				 name,
	                         mu_dbm_strerror(db));
			ret = 1;
		}
	}
	if (rc != MU_ERR_NOENT) {
		mu_error(_("unexpected error scanning database %s: %s"),
			 name, mu_dbm_strerror(db));
		ret = 1;
	}
	return ret;
}

static int
db_list_func(struct mu_dbm_datum *key, struct mu_dbm_datum *contents,
	     void *data)
{
	db_item_printer_t fun = data;
	fun(key, contents);
	return 0;
}

int
db_list(char *dbname, db_item_printer_t fun)
{
	mu_dbm_file_t db = mf_dbm_open(dbname, MU_STREAM_READ);
	if (!db)
		return 1;
	db_enumerate(db, db_list_func, fun);
	mu_dbm_destroy(&db);
	return 1;
}

struct expire_data {
	db_expire_t fun;
	mu_opool_t pool;
	size_t key_count;
};

static int
db_expire_func(struct mu_dbm_datum *key, struct mu_dbm_datum *contents,
	       void *data)
{
	struct expire_data *dp = data;
	if (dp->fun(contents)) {
		mu_opool_append(dp->pool, &key->mu_dsize,
				sizeof key->mu_dsize);
		mu_opool_append(dp->pool, key->mu_dptr, key->mu_dsize);
		dp->key_count++;
	}
	return 0;
}

int
db_expire(char *dbname, db_expire_t fun)
{
	mu_dbm_file_t db;
	struct mu_dbm_datum key;
	struct expire_data ed;
	size_t size;
	char *base, *p;
	size_t i;
	int rc = 0;

	db = mf_dbm_open(dbname, MU_STREAM_RDWR);
	if (!db)
		return 1;

	ed.fun = fun;
	mu_opool_create(&ed.pool, MU_OPOOL_ENOMEMABRT);
	ed.key_count = 0;
	mu_debug(debug_handle, MU_DEBUG_TRACE0,
		 ("Expiring %s: mark phase", dbname));
	if (db_enumerate(db, db_expire_func, &ed))
		rc = !ignore_failed_reads_option;
	
	size = 0;
	mu_opool_append(ed.pool, &size, sizeof size);

	mu_debug(debug_handle, MU_DEBUG_TRACE0, 
              ("Number of keys to expire: %lu, memory used: %u",
	       (unsigned long) ed.key_count,
	       (unsigned) mu_opool_size(ed.pool)));

	mu_debug(debug_handle, MU_DEBUG_TRACE0,
		 ("Expiring %s: sweep phase", dbname));

	base = mu_opool_finish(ed.pool, NULL);
	memset(&key, 0, sizeof key);
	for (i = 0, p = base; i < ed.key_count; i++) {
		size = *(size_t*)p;
		p += sizeof(size_t);
		
		mu_debug(debug_handle, MU_DEBUG_TRACE5,
			 ("Remove: %*.*s", (int)size, (int)size, p));
		key.mu_dptr = p;
		key.mu_dsize = size;
		rc = mu_dbm_delete(db, &key);
		if (rc && rc != MU_ERR_NOENT) {
			mu_error (_("cannot remove record `%*.*s' from `%s': %s"),
				  (int)size, (int)size,
				  p, dbname, mu_dbm_strerror(db));
			rc = 1;
		}
		p += size;
	}

	mu_debug(debug_handle, MU_DEBUG_TRACE0,
		 ("Finished expiring %s", dbname));
	mu_opool_destroy(&ed.pool);
	mu_dbm_destroy(&db);
	return rc;
}

int
db_delete(char *dbname, char *id)
{
        mu_dbm_file_t db;
	struct mu_dbm_datum key;
	int rc;
	
	db = mf_dbm_open(dbname, MU_STREAM_RDWR);
	if (!db)
		return 1;
	memset(&key, 0, sizeof key);
	key.mu_dptr = id;
	key.mu_dsize = strlen (id) + 1;
	rc = mu_dbm_delete(db, &key); 
	if (rc) {
		mu_error(_("cannot remove record `%s' from `%s': %s"), id, 
	                 dbname, mu_dbm_strerror(db));
	}
	mu_dbm_destroy(&db);
	return rc;
}	

static char *
make_tmp_name(const char *dbname)
{
	char *suf = strrchr(dbname, '.');
	char *p = strrchr(dbname, '/');
	size_t len;
	char *newname;
	char buf[80];
	
	if (suf)
		len = suf - dbname;
	else if (p)
		len = p - dbname;
	else
		len = strlen(dbname);
	snprintf(buf, sizeof buf, "%lu", (unsigned long) getpid());
	newname = mu_alloc(len + 1 + strlen(buf) + 1);
	memcpy(newname, dbname, len);
	newname[len] = '.';
	strcpy(newname + len + 1, buf);
	return newname;
}

struct compact_data {
	int rc;
	db_expire_t fun;
	mu_dbm_file_t ndb;
};

static int
db_compact_func(struct mu_dbm_datum *key, struct mu_dbm_datum *contents,
		void *data)
{
        struct compact_data *dp = data;
	int rc;
	const char *name;
	
	mu_dbm_get_name(dp->ndb, &name);
				
	if (!dp->fun || dp->fun(contents) == 0) {
		if (((char*)key->mu_dptr)[key->mu_dsize - 1]) {
			/* Old database format. Convert. */
			char *p;
			struct mu_dbm_datum newkey;
			
			p = malloc(key->mu_dsize + 1);
			if (!p) {
				mu_error(_("not enough memory"));
				return 1;
			}
			memcpy(p, key->mu_dptr, key->mu_dsize);
			p[key->mu_dsize] = 0;
			
			memset(&newkey, 0, sizeof newkey);
			newkey.mu_dptr = p;
			newkey.mu_dsize = key->mu_dsize + 1;
			rc = mu_dbm_store(dp->ndb, &newkey, contents, 1);
			if (rc) {
				mu_error(_("cannot insert datum `%s' into `%s': %s"),
					 p, name,
					 rc == MU_ERR_FAILURE ?
					  mu_dbm_strerror(dp->ndb) :
					  mu_strerror(rc)); 
				dp->rc = 1;
			}
			free(p);
		} else if ((rc = mu_dbm_store(dp->ndb, key, contents, 1))) {
			mu_error(_("cannot insert datum `%*.*s' into `%s': %s"),
			         (int)key->mu_dsize, (int)key->mu_dsize,
		                 (char*) key->mu_dsize,
	                         name,
				 rc == MU_ERR_FAILURE ?
				  mu_dbm_strerror(dp->ndb) :
				  mu_strerror(rc));
			dp->rc = 1;
		}
	}
	return 0;
}	

int
db_compact(char *dbname, db_expire_t fun)
{
	mu_dbm_file_t odb;
	char *tmpname;
	struct compact_data dat;
	const char *file_name;
	int flags, rc;
	int fd;
	struct stat st;
	
	mu_debug(debug_handle, MU_DEBUG_TRACE0,
		 ("Compacting database `%s'", dbname));
       
	odb = mf_dbm_open(dbname, MU_STREAM_READ);
	if (!odb)
		return 1;
	rc = mu_dbm_get_fd(odb, &fd, NULL);
	if (rc) {
		mu_dbm_destroy(&odb);
		mu_diag_funcall(MU_DIAG_ERROR, "mu_dbm_get_fd", NULL, rc);
		return 1;
	}
	if (fstat(fd, &st)) {
		mu_diag_funcall(MU_DIAG_ERROR, "fstat", NULL, errno);
		mu_dbm_destroy(&odb);
		return 1;
	}

	if (getuid() != st.st_uid && getgid() != st.st_gid) {
		if (switch_to_privs(st.st_uid, st.st_gid, NULL))
			exit(EX_SOFTWARE);
	}
	umask(0777 & ~ st.st_mode);
	mu_dbm_get_name(odb, &file_name);
	tmpname = make_tmp_name(file_name);
	
	mu_dbm_safety_get_flags(odb, &flags);
	rc = mu_dbm_create(tmpname, &dat.ndb, flags); 
	if (rc)	{
		mu_error(_("unable to create database %s: %s"),
			 tmpname, mu_strerror (rc));
		return 1;
	}
	mu_dbm_safety_set_owner(dat.ndb, st.st_uid);

	rc = mu_dbm_open(dat.ndb, MU_STREAM_CREAT, mf_database_mode);
	if (rc) {
		mu_error(_("unable to open database %s: %s"),
			 tmpname, mu_strerror (rc));
		mu_dbm_destroy(&odb);
		return 1;
	}
		
	dat.rc = 0;
	dat.fun = fun;
	
	db_enumerate(odb, db_compact_func, &dat);

	mu_dbm_close(dat.ndb);
	mu_dbm_close(odb);

	if (dat.rc == 0) {
		if (rename(tmpname, file_name)) {
			mu_error(_("cannot rename %s to %s: %s"),
				 tmpname, file_name, mu_strerror(errno));
			dat.rc = 1;
		}
	}

	mu_dbm_destroy(&dat.ndb);
	mu_dbm_destroy(&odb);
	
	free(tmpname);
	return dat.rc;
}


