/* $Id$ */
/* vim: set sw=8 ts=8 sts=8 noexpandtab cino=(0t0\:N: */

/*-
 * Copyright (c) 2006 AIDA Shinra <shinra@j10n.org>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */


#include "config.h"

#ifdef USE_BDB_ENGINE

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>
#include <sysexits.h>
#include <sys/stat.h>
#include "engine.h"
#include <assert.h>

#include <db.h>

#include "milter-greylist.h"
#include "pending.h"
#include "autowhite.h"
#include "conf.h"
#include "dump.h"

#define UI32_GET(p) (UI32_G1_(p,0) | UI32_G1_(p,1) | UI32_G1_(p,2) | UI32_G1_(p,3))
#define UI32_G1_(p, i) ((u_int32_t)(unsigned char)(p)[i] << (24-8*(i)))
#define UI32_PUT(p, x) ((void) (UI32_P1_(p,x,0), UI32_P1_(p,x,1), UI32_P1_(p,x,2), UI32_P1_(p,x,3)))
#define UI32_P1_(p, x, i) ((p)[i] = (char) ((u_int32_t)(x) >> (24-8*(i))))

#define FORMAT_VERSION 1U
#define NETBLOCKSTRLEN (IPADDRSTRLEN + 4)
#define AUTOWHITE ((u_int32_t)(1U << 31))
#define MG_TIME_MAX ((u_int32_t)((1U << 31) - 1))
#define MG_MIN(x, y) (((x) < (y)) ? (x) : (y))
#define MG_MAX(x, y) (((x) > (y)) ? (x) : (y))

struct parsed_main_key {
	char netblock[NETBLOCKSTRLEN];
	const char *from;
	const char *rcpt;
};

static void engine_expire(time_t);
static int engine_add_any(const struct sockaddr *, socklen_t,
	const char *, const char *, u_int32_t, time_t);
static int compare_dbt2str(const DBT *, const char *);
static int time_db_callback(DB *, const DBT *, const DBT *, DBT *);
static int make_main_key(DBT *, const struct sockaddr *,
	unsigned int, const char *, const char *);
static int parse_main_key(struct parsed_main_key *, const DBT *);
#define BITSET_TEST(p, l, i) ((p)[(l)-1-(i)/8] & (char)(1U << ((i)%8)))
#define BITSET_SET(p, l, i) ((void)((p)[(l)-1-(i)/8] |= (char)(1U << ((i)%8))))
#define BITSET_CLEAR(p, l, i) ((void)((p)[(l)-1-(i)/8] &= ~(char)(1U << ((i)%8))))
#define BITSET_HITEST(p, i) ((p)[(i)/8] & (char)(1U << (7 - (i)%8)))
#define BITSET_HISET(p, i) ((void)((p)[(i)/8] |= (char)(1U << (7-(i)%8))))
#define BITSET_HICLEAR(p, i) ((void)((p)[(i)/8] &= ~(char)(1U << (7-(i)%8))))
#ifdef AF_INET6
static int bitset_ffs(const char *, size_t);
static int bitset_rshift1(char *, const char *, size_t, int);
static int bitset_lshift1(char *, const char *, size_t, int);
static void bitset_himask(char *, size_t, size_t);
#endif

/* simple "Giant lock" */
pthread_mutex_t engine_lock = PTHREAD_MUTEX_INITIALIZER;
static DB_ENV *db_env = NULL;
static DB *control_db = NULL;
static DB *main_db = NULL;
static DB *time_db = NULL;

/* config-independent initialization */
void
engine_init() {
}

/* possible config-dependent initialization */
int
engine_open_db(progname)
	const char *progname;
{
	int r;
	char dbhome[MAXPATHLEN + 1];
	char dbfile[MAXPATHLEN + 1];
	char *p;
	int dump_load_if_possible = 0;
	DB_TXN *txn = NULL;
	DBT key, value;
	char numbuf[4];

	memset(&key, 0, sizeof(DBT));
	memset(&value, 0, sizeof(DBT));

	/* Prepare paths */
	strncpy(dbhome, conf.c_dumpfile, MAXPATHLEN);
	p = strrchr(dbhome, '/');
	if (p)
		*p = '\0';
	else
		strcpy(dbhome, "/var/milter-greylist");
	strcpy(dbfile, dbhome);
	mystrlcat(dbfile, "/greylist-bdb.db", sizeof dbfile);

	/* Open environment */
	if ((r = db_env_create(&db_env, 0)) != 0) {
		mg_log(LOG_ERR, "Unable to open DB env: %s",
		       db_strerror(r));
		goto fail;
	}
	db_env->set_errpfx(db_env, progname);
	/* When we change giant lock to concurrent code,
	 * we must add DB_INIT_LOCK|DB_THREAD */
	if ((r = db_env->open(db_env, dbhome,
			      DB_INIT_LOG|DB_INIT_MPOOL|DB_INIT_TXN|
			      DB_RECOVER|DB_CREATE|DB_REGISTER,
			      0)) != 0)
	{
		mg_log(LOG_ERR, "Unable to open DB env: %s",
		       db_strerror(r));
		goto fail;
	}

	/* Begin transaction for control DB */
	if ((r = db_env->txn_begin(db_env, NULL, &txn, 0)) != 0) {
		mg_log(LOG_ERR, "Unable to open DB: %s",
		       db_strerror(r));
		goto fail;
	}

	/* Open control DB */
	if ((r = db_create(&control_db, db_env, 0)) != 0 ||
	    (r = control_db->open(control_db, txn, dbfile,
				  "control", DB_HASH,
				  DB_AUTO_COMMIT|DB_CREATE,
				  0)) != 0)
	{
		mg_log(LOG_ERR, "Unable to open DB: %s",
		       db_strerror(r));
		goto fail;
	}
	key.data = "magic";
	key.size = sizeof "magic"; /* including '\0' */
	r = control_db->get(control_db, txn, &key, &value, 0);
	if (r == DB_NOTFOUND) {
		/* Newly created database; fill control DB */
		dump_load_if_possible = 1;
		value.data = "milter-greylist";
		value.size = sizeof "milter-greylist";
		if ((r = control_db->put(control_db, txn, &key, &value, 0)) != 0) {
			mg_log(LOG_ERR, "Unable to initialize DB: %s",
			       db_strerror(r));
			goto fail;
		}

		key.data = "format_version";
		key.size = sizeof "format_version";
		UI32_PUT(numbuf, FORMAT_VERSION);
		value.data = numbuf;
		value.size = sizeof numbuf;
		if ((r = control_db->put(control_db, txn, &key, &value, 0)) != 0) {
			mg_log(LOG_ERR, "Unable to initialize DB: %s",
			       db_strerror(r));
			goto fail;
		}

		key.data = "timestamp";
		key.size = sizeof "timestamp";
		UI32_PUT(numbuf, 0); /* initial timestamp is 0 */
		value.data = numbuf;
		value.size = sizeof numbuf;
		if ((r = control_db->put(control_db, txn, &key, &value, 0)) != 0) {
			mg_log(LOG_ERR, "Unable to initialize DB: %s",
			       db_strerror(r));
			goto fail;
		}
	} else if (r == 0) {
		u_int32_t dbdate;
		struct stat dump_stat;

		/* Check control DB */
		if (compare_dbt2str(&value, "milter-greylist") != 0) {
			mg_log(LOG_ERR, "Not a milter-greylist DB");
			goto fail;
		}

		key.data = "format_version";
		key.size = sizeof "format_version";
		if ((r = control_db->get(control_db, txn, &key, &value, 0)) != 0) {
			mg_log(LOG_ERR, "Error while reading DB: %s"
			       db_strerror(r));
			goto fail;
		}
		if (value.size != 4 ||
		    UI32_GET((char *)value.data) != FORMAT_VERSION) {
			mg_log(LOG_ERR, "Incompatible DB format");
			goto fail;
		}

		key.data = "timestamp";
		key.size = sizeof "timestamp";
		if ((r = control_db->get(control_db, txn, &key, &value, 0)) != 0) {
			mg_log(LOG_ERR, "Error while reading DB: %s",
			       db_strerror(r));
			goto fail;
		}
		if (vaule.size != 4 ||
		    (dbdate = UI32_GET((char *)value.data)) > MG_TIME_MAX) {
			mg_log(LOG_ERR, "Corrupt DB");
			goto fail;
		}
		if (stat(conf.c_dumpfile, &dump_stat) == 0 &&
		    dump_stat.st_mtime >= (time_t)dbdate + 5)
			dump_load_if_possible = 1;
	} else {
		mg_log(LOG_ERR, "Unable to open DB: %s",
		       db_strerror(r));
		goto fail;
	}
	/* This version put ("mark", "") everytime opening DB.
	 * Future versions can notice that older versions opened DB
	 * and something like secondary index must be rebuilt. */
	key.data = "mark";
	key.size = sizeof "mark";
	value.data = "";
	value.size = sizeof "";
	if ((r = control_db->put(control_db, txn, &key, &value, 0)) != 0) {
		mg_log(LOG_ERR, "Error while writing to DB: %s",
		       db_strerror(r));
		goto fail;
	}

	r = txn->commit(txn, 0);
	txn = NULL;
	if (r != 0) {
		mg_log(LOG_ERR, "Error while writing to DB: %s",
		       db_strerror(r));
		goto fail;
	}

	/* Open main DB */
	if ((r = db_create(&main_db, db_env, 0)) != 0 ||
	    (r = main_db->open(main_db, txn, dbfile,
			       "main", DB_BTREE,
			       DB_AUTO_COMMIT|DB_CREATE,
			       0)) != 0)
	{
		mg_log(LOG_ERR, "Unable to open DB: %s",
		       db_strerror(r));
		goto fail;
	}

	/* Open time DB */
	if ((r = db_create(&time_db, db_env, 0)) != 0 ||
	    (r = time_db->set_flags(time_db, DB_DUP)) != 0 ||
	    (r = time_db->open(time_db, txn, dbfile,
			       "time", DB_BTREE,
			       DB_AUTO_COMMIT|DB_CREATE,
			       0)) != 0 ||
	    (r = main_db->associate(main_db, NULL, time_db,
				    &compare_time, 0)) != 0)

	{
		mg_log(LOG_ERR, "Unable to open DB: %s",
		       db_strerror(r));
		goto fail;
	}

	return dump_load_if_possible;
fail:
	if (txn)
		txn->abort(txn);
	engine_close_db();
	return -1;
}

void
engine_close_db()
{
	if (time_db)
		time_db->close(time_db, 0);
	if (main_db)
		main_db->close(main_db, 0);
	if (control_db)
		control_db->close(control_db, 0);
	if (db_env)
		db_env->close(db_env, 0);
}

void
engine_bump_timestamp(now)
	time_t now;
{
	DBT key, value;
	char timebuf[4];

	memset(key, 0, sizeof(DBT));
	memset(value, 0, sizeof(DBT));

	key.data = "timestamp";
	key.size = sizeof "timestamp";
	UI32_PUT(timebuf, (u_int32_t)now);
	value.data = timebuf;
	value.size = 4;
	if ((r = control_db->put(control_db, NULL, &key, &value, 0)) != 0) {
		mg_log(LOG_INFO, "Error while bumping timestamp: %s",
		       db_strerror(r));
	}
}

int
engine_textdump(stream, greylisted_count, whitelisted_count)
	FILE *stream;
	int *greylisted_count;
	int *whitelisted_count;
{
	int r;
	DBT key, pkey, value;
	DBC *curs = NULL;
	u_int32_t accepted, expiry;
	struct parsed_main_key pmk;
	char *slash;

	memset(&key, 0, sizeof(DBT));
	memset(&pkey, 0, sizeof(DBT));
	memset(&value, 0, sizeof(DBT));

	*greylisted_count = *whitelisted_count = 0;
	if ((r = time_db->cursor(time_db, NULL, &curs, 0)) != 0)
		goto xfail;

	fprintf(stream, "\n\n#\n# greylisted tuples\n#\n");
	for (rr = curs->c_pget(curs, &key, &pkey, &value, DB_FIRST);
	     rr == 0; rr = curs->c_pget(curs, &key, &pkey, &value, DB_NEXT)) {
		if (key.size != 4 || value.size < 8)
			continue;
		accepted = UI32_GET((char *)value.data);
		expiry = UI32_GET((char *)value.data + 4);
		if (accepted >= AUTOWHITE || expiry >= AUTOWHITE)
			continue;
		if (parse_main_key(&pmk, &pkey) != 0 ||
		    strcmp(pmk.netblock, "???") == 0 ||
		    (slash = strchr(pmk.netblock, '/')) == NULL)
			continue;
		*slash = 0;
		if (conf.c_dump_no_time_translation) {
			fprintf(stream, "%s\t%s\t%s\t%ld # /%s\n", 
			    pmk.netblock, pmk.from, pmk.rcpt,
			    (long)expiry, slash + 1);
		} else {
			time_t expiry2 = (time_t)expiry;
			localtime_r((time_t *)&expiry2, &tm);
			strftime(textdate, DATELEN, "%Y-%m-%d %T", &tm);
		
			fprintf(stream, "%s\t%s\t%s\t%ld # %s /%s\n", 
			    pmk.netblock, pmk.from, pmk.rcpt,
			    (long)expiry, textdate, slash);
		}
	}
}

engine_result_t
engine_check(sa, salen, from, rcpt, remaining, elapsed, queueid, now,
	     delay, timeout, aw)
	const struct sockaddr *sa;
	socklen_t salen;
	const char *from;
	const char *rcpt;
	time_t *remaining;
	time_t *elapsed;
	const char *queueid;
	time_t now;
	time_t delay;
	time_t timeout;
	time_t aw;
{
	unsigned int prefixlen;
	DBT key, value;
	int r;
	u_int32_t best_accepted, accepted, expiry;
	engine_result_t result = ER_NEW;
	char timebuf[4];

	memset(&key, 0, sizeof(DBT));
	memset(&value, 0, sizeof(DBT));

	engine_expire(now); /* Expiration */
	if (remaining != NULL)
		*remaining = 0;
	if (elapsed != NULL)
		*elapsed = 0;
	best_accepted = MG_MIN((u_int32_t)(now + delay), MG_TIME_MAX);

	/* Try every prefixlen and find the most permissive entry */
	switch (sa->sa_family) {
	case AF_INET:
		prefixlen = 32;
		break;
#ifdef AF_INET6
	case AF_INET6:
		prefixlen = 128;
		break;
#endif
	default:
		mg_log(LOG_ERR, "engine_check: unexpected sa_family");
		goto fail;
	}

	if (make_main_key(&key, sa, prefixlen, from, rcpt) != 0)
		goto fail;
	do {
		if ((r = main_db->get(main_db, NULL, &key, &value, 0))
		    == DB_NOTFOUND) {
			continue;
		} else if (r != 0) {
			mg_log(LOG_INFO, "Error reading DB: %s",
			       db_strerror(r));
			continue;
		}
		if (value.size < 8 ||
		    (accepted = UI32_GET((char *)value.data)) > AUTOWHITE ||
		    (expiry = UI32_GET((char *)value.data)) > MG_TIME_MAX) {
			mg_log(LOG_DEBUG, "engine_check: warning: "
			       "skipping a corrupt record");
			continue;
		}
		if (expiry > (u_int32_t)now) /* outdated entry */
			continue;
		if (accepted == AUTOWHITE) {
			result = ER_AUTOWHITE;
			break;
		}
		if (result == ER_NEW ||
		    (result == ER_PENDING && accepted < best_accepted)) {
			result = ER_PENDING;
			best_accepted = accepted;
		}
	} while (prefixlen-- != 0 &&
		 (BITSET_HICLEAR((char *)key.data + 1, prefixlen + 8),
		  BITSET_HISET((char *)key.data + 1, prefixlen + 7),
		  1));
	if (result == ER_PENDING && best_accepted <= (u_int32_t)now)
		result = ER_ACCEPT;

	/* Store the results */
	if (result != ER_AUTOWHITE) {
		if (remaining != NULL && result != ER_ACCEPT)
			*remaining = best_accepted - now;
		if (elapsed != NULL)
			*elapsed = now - (best_accepted - delay);
	}

	/* Update the DB */
	switch (result) {
	case ER_NEW:
		accepted = best_accepted;
		expiry = MG_MIN(best_accepted + (u_int32_t)timeout,
				MG_TIME_MAX);
		break;
	case ER_ACCEPT: /* FALLTHROUGH */
	case ER_AUTOWHITE:
		accepted = AUTOWHITE;
		expiry = MG_MIN((u_int32_t)(now + aw), MG_TIME_MAX);
		break;
	default:
		assert(result == ER_PENDING);
		goto last;
		break;
	}
	if (engine_add_any(sa, salen, from, rcpt, queueid, now,
			   accepted, expiry) == 0)
		goto last;
fail:
	result = ER_ERROR;
last:
	if (value.flags & DB_DBT_MALLOC)
		free(value.data);
	free(key.data);
	return result;
}

static void
engine_expire(now)
	time_t now;
{
	DBC *curs = NULL, *prev = NULL;
	int r, rr;
	char start[4], upto[4];
	DBT key, pkey, pkey2, value;
	void *swaptmp;
	int logexpired = conf.c_debug || conf.c_logexpired;
	int dirty = 0;

	memset(&key, 0, sizeof(DBT));
	memset(&pkey, 0, sizeof(DBT));
	memset(&pkey2, 0, sizeof(DBT));
	memset(&value, 0, sizeof(DBT));
	pkey.flags = DB_DBT_REALLOC;

	if ((r = time_db->cursor(time_db, NULL, &curs, 0)) != 0)
		goto xfail;

	UI32_PUT(upto, (u_int32_t)(now));
	rr = curs->c_pget(curs, &key, &pkey, &value, DB_FIRST);
	while (rr == 0 && memcmp(key.data, upto, MG_MIN(key.size, 4)) < 0) {
		u_int32_t accepted = (key.size != 4 || value.size < 8 ||
				      ((char *)value.data)[4] & 0x80) ?
			AUTOWHITE + 1 : UI32_GET((char *)value.data);

		swaptmp = pkey2.data;
		pkey2.data = pkey.data;
		pkey.data = swaptmp;
		pkey2.size = pkey.size;
		if ((r = curs->c_dup(curs, &prev, DB_POSITION)) != 0)
			goto xfail;
		rr = curs->c_pget(curs, &key, &pkey, &value, DB_NEXT);
		if ((r = prev->c_del(prev, 0)) != 0)
			goto xfail;
		dirty = 1;
		if (accepted >= AUTOWHITE || logexpired) {
			int level;
			const char *type;
			struct parsed_main_key pmk;

			if (accepted > AUTOWHITE) {
				level = LOG_INFO;
				type = "corrupt";
			} else if (accepted == AUTOWHITE) {
				level = LOG_INFO;
				type = "autowhitelisted";
			} else {
				level = LOG_DEBUG;
				type = "greylisted";
			}
			if (parse_main_key(&pmk, &pkey2) != 0) {
				strcpy(pmk.netblock, "???");
				pmk.from = pmk.rcpt = "???";
			}
			mg_log(level, "(local): %s from %s rcpt %s: "
			       "%s entry expired",
			       pmk.netblock, pmk.from, pmk.rcpt, type);
		}
		r = prev->c_close(prev);
		prev = NULL;
		if (r)
			goto xfail;
	}
	r = rr;
	if (r == 0 || r == DB_NOTFOUND)
		goto last;
xfail:
	mg_log(LOG_INFO, "engine_expire: %s", db_strerror(r));
last:
	if (dirty)
		bump_db_timestamp(now);
	if (prev)
		cursor->c_close(prev);
	if (cursor)
		cursor->c_close(cursor);
	free(pkey2.data);
	free(pkey.data);
}

int
engine_add_pending(sa, salen, from, rcpt, queueid, now, date, timeout)
	struct sockaddr *sa;
	socklen_t salen;
	const char *from;
	const char *rcpt;
	const char *queueid;
	time_t now;
	time_t date;
	time_t timeout;
{
	return engine_add_any(sa, salen, from, rcpt, queueid, now,
			      (u_int32_t)date,
			      MG_MIN((u_int32_t)(date + timeout), MG_TIME_MAX));
}

int
engine_add_autowhite(sa, salen, from, rcpt, queueid, now, date)
	struct sockaddr *sa;
	socklen_t salen;
	const char *from;
	const char *rcpt;
	const char *queueid;
	time_t now;
	time_t date;
{
	return engine_add_any(sa, salen, from, rcpt, queueid, now,
			      AUTOWHITE, (time_t)date);
}

static int
engine_add_any(sa, salen, from, rcpt, queueid, now, accepted, expiry)
	struct sockaddr *sa;
	socklen_t salen;
	const char *from;
	const char *rcpt;
	const char *queueid;
	time_t now;
	u_int32_t accepted;
	u_int32_t expiry;
{
	DBT key, value, value2;
	int result = -1;
	int r;
	char buf[8];
	u_int32_t naccepted = accepted, nexpiry = expiry;
	char addr[IPADDRSTRLEN];
	unsigned int h, mn, s;
	int newentry = 0;

	memset(&key, 0, sizeof(DBT));
	memset(&value, 0, sizeof(DBT));
	memset(&value2, 0, sizeof(DBT));
	value.flags = DB_DBT_MALLOC;

	if (expiry < now)
		return 0;
	h = (expiry - now) / 3600;
	mn = (((expiry - now) % 3600) / 60);
	s = ((expiry - now) % 3600) % 60;

	switch (sa->sa_family) {
	case AF_INET:
		prefixlen = 33 - ffs(ntohl(conf.c_match_mask.s_addr));
		if (prefixlen == 33)
			prefixlen = 0;
		break;
#ifdef AF_INET6
	case AF_INET6:
		prefixlen = 129 - bitset_ffs((char *)&conf.c_match_mask6, 16);
		if (prefixlen == 129)
			prefixlen = 0;
		break;
#endif
	default:
		mg_log(LOG_ERR, "engine_add_any: unexpected sa_family");
		goto fail;
	}
	if (make_main_key(&key, sa, prefixlen, from, rcpt) != 0)
		goto fail;
	if (!iptostring(sa, salen, addr, sizeof(addr)))
		strcpy(addr, "???");

	if ((r = main_db->get(main_db, NULL, &key, &value, 0)) == 0) {
		u_int32_t oaccepted, oexpiry;

		if (value.size < 8 ||
		    (oaccepted = UI32_GET((char *)value.data)) > AUTOWHITE ||
		    (oexpiry = UI32_GET((char *)value.data + 4)) > MG_TIME_MAX) {
			mg_log(LOG_INFO,
			       "warning: overwriting a corrupt record: "
			       "addr %s/%u from %s rcpt %s",
			       addr, prefixlen, from, rcpt);
			newentry = 1;
		} else if (oexpiry < (u_int32_t)now) {
			if (oaccepted == AUTOWHITE) {
				mg_log(LOG_INFO,
				       "(local): %s/%u from %s rcpt %s: "
				       "autowhitelisted entry expired",
				       addr, prefixlen, from, rcpt);
			} else if (conf.c_debug || conf.c_logexpired) {
				mg_log(LOG_DEBUG,
				       "(local): %s/%u from %s rcpt %s: "
				       "greylisted entry expired",
				       addr, prefixlen, from, rcpt);
			}
			newentry = 1;
		} else if (oaccepted == AUTOWHITE) {
			if (oexpiry >= expiry)
				goto ok; /* no need to update */
			mg_log(LOG_INFO, "%s: %s/%u from %s rcpt %s: "
			       "autowhitelisted for more %02d:%02d:%02d", 
			       queueid, addr, prefixlen, from, rcpt, h, mn, s);
			naccepted = AUTOWHITE;
		} else if (accepted == AUTOWHITE) {
			if (oexpiry >= expiry) {
				h = (oexpiry - now) / 3600;
				mn = (((oexpiry - now) % 3600) / 60);
				s = ((oexpiry - now) % 3600) % 60;
				nexpiry = oexpiry;
			}
			mg_log(LOG_INFO, "%s: %s/%u from %s rcpt %s: "
			       "autowhitelisted for %02d:%02d:%02d", 
			       queueid, addr, prefixlen, from, rcpt, h, mn, s);
		} else {
			if (oexpiry >= expiry && oaccepted <= accepted) {
				goto ok; /* no need to update */
			} else if (oexpiry >= expiry) {
				h = (oexpiry - now) / 3600;
				mn = (((oexpiry - now) % 3600) / 60);
				s = ((oexpiry - now) % 3600) % 60;
				nexpiry = oexpiry;
			} else if (oaccepted <= accepted) {
				naccepted = oaccepted;
			}
			if (conf.c_debug) {
				mg_log(LOG_DEBUG, "%s: %s/%u from %s rcpt %s: "
				       "greylisted entry modified: "
				       "%lds %02d:%02d:%02d",
				       queueid, addr, prefixlen, from, rcpt,
				       (long)naccepted - (long)now, h, mn, s);
			}
		}
	} else if (r == DB_NOTFOUND) {
		newentry = 1;
	} else {
		mg_log(LOG_INFO, "Error reading DB: %s",
		       db_strerror(r));
		goto fail;
	}
	if (newentry) {
		if (accepted == AUTOWHITE) {
			mg_log(LOG_INFO, "%s: %s/%u from %s rcpt %s: "
			    "autowhitelisted for %02d:%02d:%02d", 
			    queueid, addr, prefixlen, from, rcpt, h, mn, s);
		} else if (conf.c_debug) {
			mg_log(LOG_DEBUG, "%s: %s/%u from %s rcpt %s: "
			       "greylisted for %lds",
			       queueid, addr, prefixlen, from, rcpt,
			       (long)naccepted - (long)now);
		}
		value2.data = buf;
		value2.size = 8;
	} else {
		value2.data = value.data;
		value2.size = value.size;
	}
	UI32_PUT((char *)value2.data, naccepted);
	UI32_PUT((char *)value2.data + 4, nexpiry);
	if ((r = main_db->put(main_db, NULL, &key, &value2, 0)) != 0) {
		mg_log(LOG_INFO, "Error writing DB: %s",
		       db_strerror(r));
		goto fail;
	}
	bump_db_timestamp(now);
ok:
	result = 0;
fail:
	free(value.data);
	free(key.data);
	return result;
}

void
engine_del_addr(sa, salen, queueid, acl_line)
	const struct sockaddr *sa;
	socklen_t salen;
	const char *queueid;
	int acl_line;
{
	time_t now = time(NULL);
	DBC *curs = NULL, *prev = NULL;
	int r, rr;
	DBT key0, key, key2, value;
	char addr[IPADDRSTRLEN];
	unsigned int prefixlen;
	void *swaptmp;
	unsigned int count_pending = 0, count_white = 0;
        char aclstr[16];
	int logexpired = conf.c_debug || conf.c_logexpired;
	int dirty = 0;

	if (now < 0)
		return;
	memset(&key0, 0, sizeof(DBT));
	memset(&key, 0, sizeof(DBT));
	memset(&key2, 0, sizeof(DBT));
	memset(&value, 0, sizeof(DBT));
	key.flags = DB_DBT_MALLOC;

	switch (sa->sa_family) {
	case AF_INET:
		prefixlen = 32;
		break;
#ifdef AF_INET6
	case AF_INET6:
		prefixlen = 128;
		break;
#endif
	default:
		mg_log(LOG_ERR, "engine_del_addr: unexpected sa_family");
		goto last;
	}
	if (make_main_key(&key0, sa, prefixlen, "", "") != 0)
		goto last;
	key0.size -= 2; /* strip \0\0 at the end */
	if (!iptostring(sa, salen, addr, sizeof(addr)))
		strcpy(addr, "???");
	if ((r = main_db->cursor(main_db, NULL, &curs, 0)) != 0)
		goto xfail;

	key.data = key0.data;
	key.size = key0.size;
	if ((rr = curs->c_get(curs, &key, &value, DB_SET_RANGE)) == 0)
		key.flags = DB_DBT_REALLOC;
	while (rr == 0 && key.size >= key0.size &&
	       memcmp(key.data, key0.data, key0.size) == 0) {
		u_int32_t accepted =
			(value.size < 8 || ((char *)value.data)[4] & 0x80) ?
			AUTOWHITE + 1 : UI32_GET((char *)value.data);

		swaptmp = pkey2.data;
		pkey2.data = pkey.data;
		pkey.data = swaptmp;
		pkey2.size = pkey.size;
		if ((r = curs->c_dup(curs, &prev, DB_POSITION)) != 0)
			goto xfail;
		rr = curs->c_pget(curs, &key, &pkey, &value, DB_NEXT);
		if ((r = prev->c_del(prev, 0)) != 0)
			goto xfail;
		dirty = 1;
		if (accepted >= AUTOWHITE || logexpired) {
			int level;
			const char *type;
			struct parsed_main_key pmk;

			if (accepted > AUTOWHITE) {
				level = LOG_INFO;
				type = "corrupt";
			} else if (accepted == AUTOWHITE) {
				level = LOG_INFO;
				type = "autowhitelisted";
				++count_white;
			} else {
				level = LOG_DEBUG;
				type = "greylisted";
				++count_pending;
			}
			if (parse_main_key(&pmk, &key2) != 0) {
				strcpy(pmk.netblock, "???");
				pmk.from = pmk.rcpt = "???";
			}
			mg_log(level, "%s: %s from %s rcpt %s: "
			       "%s entry removed",
			       queueid, pmk.netblock, pmk.from, pmk.rcpt, type);
		}
		r = prev->c_close(prev);
		prev = NULL;
		if (r)
			goto xfail;
	}
	r = rr;
	if (r != 0 && r != DB_NOTFOUND)
		goto xfail;

	*aclstr = '\0';
	if (acl_line != 0)
		snprintf(aclstr, sizeof(aclstr), " (ACL %d)", acl_line);
	mg_log(LOG_INFO,
	       "%s: addr %s flushed, removed %u grey and %u autowhite%s",
		queueid, addr, count_pending, count_white, aclstr);
	result = 0;
	goto last;
xfail:
	mg_log(LOG_INFO, "engine_del_addr: %s", db_strerror(r));
last:
	if (dirty)
		bump_db_timestamp(now);
	if (prev)
		cursor->c_close(prev);
	if (cursor)
		cursor->c_close(cursor);
	if (key.flags == DB_DBT_REALLOC) {
		free(key2.data);
		free(key.data);
	}
	free(key0.data);
}

time_t
time_checked(void)
{
	time_t now = time(NULL);

	if (now == (time_t)-1) {
		mg_log(LOG_ERR, "Cannot get current time: %s",
		       strerror(errno));
		return (time_t)-1;
	} else if (now & ~(time_t)MG_TIME_MAX) {
		/* Before UNIX epoch or far future */
		mg_log(LOG_ERR, "System clock looks bad");
		return (time_t)-1;
	} else {
		return now;
	}
}

static int
compare_dbt2str(dbt, str)
	const DBT *dbt;
	const char *str;
{
	size_t rhssize = strlen(str) + 1;
	int r1, r2;
	
	r1 = (dbt->size < rhssize) ? -1 : (dbt->size > rhssize) ? 1 : 0;
	r2 = memcmp(dbt->data, str, (r1 < 0) ? dbt->size : rhssize);
	return r2 ? r2 : r1;
}

static int
time_db_callback(db, key, value, result)
	DB *db;
	const DBT *key;
	const DBT *value;
	DBT *result;
{
	if (value->size < 8) /* corrupt record... */
		return DB_DONOTINDEX;
	result->data = (char *)value->data + 4;
	result->size = 4;
	return 0;
}

static int
make_main_key(result, sa, prefixlen, from, rcpt)
	DBT *result;
	const struct sockaddr *sa;
	unsigned int prefixlen;
	const char *from;
	const char *rcpt;
{
	size_t addrlen, fromlen = strlen(from), rcptlen = strlen(rcpt);
	char *newptr;
	size_t newsize;

	switch (sa->sa_family) {
	case AF_INET:
		addrlen = 5;
		break;
#ifdef AF_INET6
	case AF_INET6:
		addrlen = 17;
		break;
#endif
	default:
		mg_log(LOG_ERR, "make_main_key: unexpected sa_family");
		return -1;
	}
	newsize = addrlen + fromlen + rcptlen + 3;
	if ((newptr = realloc(result->data, newsize)) == NULL) {
		mg_log(LOG_ERR, "make_main_key: out of memory");
		return -1;
	}
	result->data = newptr;
	result->size = newsize;

	newptr[0] = (char)addrlen;
	switch (sa->sa_family) {
	case AF_INET:
		if (prefixlen == 0) {
			newptr[1] = 1;
			UI32_PUT(newptr + 2, 0);
		} else {
			u_int32_t tmpaddr = ntohl(SADDR4(sa)->s_addr);
			newptr[1] = (tmpaddr & (1U << 31)) ? 1 : 0;
			tmpaddr = (tmpaddr << 1) | (1U << (32 - prefixlen));
			tmpaddr &= (u_int32_t)(-(int)(1U << (32 - prefixlen)));
			UI32_PUT(newptr + 2, tmpaddr);
		}
		break;
#ifdef AF_INET6
	case AF_INET6:
		newptr[1] = (char)bitset_lshift1(newptr + 2,
						 (char *)SADDR6(sa), 16, 0);
		BITSET_RSET(newptr + 1, prefixlen + 7);
		bitset_himask(newptr + 1, 17, prefixlen + 7);
		break;
#endif
	}
	memcpy(newptr + addrlen + 1, from, fromlen + 1);
	memcpy(newptr + fromlen + 2, rcpt, rcptlen + 1);
	return 0;
}

static int
parse_main_key(result, key)
	struct parsed_main_key *result;
	const DBT *key;
{
	const char *p = (const char *)key->data, *endp = p + key->size;
	unsigned int prefixlen;

	if (p == endp || (result->from = p + 1 + (unsigned char)p[0]) > endp)
		return -1;
	p = (const char *)memchr(result->from, 0, endp - result->from);
	if (p == NULL)
		return -1;
	result->rcpt = p + 1;
	if (memchr(result->rcpt, 0, endp - result->rcpt) != endp - 1)
		return -1;

	p = (const char *)key->data;
	if (p[0] == 5 && (p[1] & (char)0xfe) == 0) {
		in_addr_t addr;
		u_int32_t tmp;
		unsigned int prefixlen;
		int carry;

		tmp = UI32_GET(p + 2);
		carry = tmp & 1;
		tmp = (tmp >> 1) | ((u_int32_t)(p[1] & 1) << 31);
		if (carry) {
			prefixlen = 32;
		} else {
			prefixlen = 32 - ffs(tmp);
			if (prefixlen == 32) /* all zero */
				goto unknown;
			tmp &= ~(1U << (31 - prefixlen));
		}
		addr = (in_addr_t)htonl(tmp);
		if (!inet_ntop(AF_INET, &addr,
			       result->netblock, sizeof result->netblock - 3))
			goto unknown;
		snprintf(result->netblock + strlen(result->netblock), 4,
			 "/%u", prefixlen);
		return 0;
	}
#ifdef AF_INET6
	else if (p[0] == 17 && (p[1] & (char)0xfe) == 0) {
		struct in6_addr addr;
		unsigned int prefixlen;
		int carry;

		carry = bitset_rshift1((char *)addr, p + 2, 16, p[1]);
		if (carry) {
			prefixlen = 128;
		} else {
			prefixlen = 128 - bitset_ffs((char *)addr, 16);
			if (prefixlen == 128)
				goto unknown;
			BITSET_HICLEAR((char *)addr, 16, prefixlen);
		}
		if (!inet_ntop(AF_INET6, &addr,
			       result->netblock, sizeof result->netblock - 4))
			goto unknown;
		snprintf(result->netblock + strlen(result->netblock), 5,
			 "/%u", prefixlen);
		return 0;
	}
#endif
unknown:
	strcpy(result->netblock, "???");
	return 0;
}

#ifdef AF_INET6
static int
bitset_ffs(p, len)
	const char *p;
	size_t len;
{
	const char *q;
	unsigned char mask;
	int r = 0;

	for (q = p + len - 1; q >= p; --q) {
		for (mask = 1; mask; mask <<= 1) {
			++r;
			if (mask & (unsigned char)*q)
				return r;
		}
	}
	return 0;
}

static int
bitset_rshift1(dst, src, len, carry)
	char *dst;
	const char *src;
	size_t len;
	int carry;
{
	size_t i;
	unsigned char c = carry ? 1 : 0;

	for (i = 0; i != len; ++i) {
		dst[i] = (char)((c << 7) | ((unsigned char)src[i] >> 1));
		c = src[i] & 1;
	}
	return c;
}

static int
bitset_lshift1(dst, src, len, carry)
	char *dst;
	const char *src;
	size_t len;
	int carry;
{
	size_t i = len;
	unsigned char c = carry ? 1 : 0;

	while (i != 0) {
		--i;
		dst[i] = (char)(c | ((unsigned char)src[i] << 1));
		c = (unsigned char)(src[i] & 0x80) >> 7;
	}
	return c;
}

static void
bitset_himask(p, len, nbits)
	char *p;
	size_t len;
	size_t nbits;
{
	size_t i;

	assert(nbits <= len * 8);
	i = nbits / 8;
	if (nbits % 8)
		p[i++] &= (char)(0xff00U >> (nbits % 8));
	for (; i != len; ++i)
		p[i] = 0;
}
#endif /* AF_INET6 */

#endif /* USE_BDB_ENGINE */
