/* greylisting-spp - A qmail-spp plugin implementing greylisting
 *
 * db-bdb.c
 *
 *  Copyright (C) 2004 Peter Conrad <conrad@tivano.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License (version 2) as
 *  published by the Free Software Foundation.
 *
 *  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <db.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "db-api.h"
#include "commonstuff.h"

/* Grumble... */

#if DB_VERSION_MAJOR != 4
#error Berkeley DB interface works only for version 4.
#else
#if DB_VERSION_MINOR == 0
#define GL_EXTRA_OPEN_ARG
#else
#define GL_EXTRA_OPEN_ARG	NULL,
#endif
#endif


static DB_ENV	*db_env = NULL;
static DB	*db = NULL;
static DB_TXN	*db_txn = NULL;
static DBC	*db_crs = NULL;
static DBT	msg_key, admin_key, read_value, write_value, crs_key;

#define ERR_CANT_CREATE_DBENV	"Can't create database environment: "
#define ERR_CANT_OPEN_DBENV    "Can't open database environment in directory \""
#define ERR_CANT_CREATE_DBHDL	"Can't create database handle: "
#define ERR_CANT_OPEN_FILE	"Can't open database file \""
#define ERR_CANT_START_TXN	"Can't start transaction: "
#define ERR_CANT_CREATE_CRS	"Can't create database cursor: "
#define ERR_CANT_COMMIT_TXN	"Can't commit transaction: "
#define ERR_CANT_CLOSE_FILE	"Can't close database file: "
#define ERR_CANT_CLOSE_ENV	"Can't close database environment: "
#define ERR_CANT_READ_VERSION	"Can't read version line from file \""
#define ERR_VERSION_MISMATCH	"Version mismatch in file \""
#define ERR_WRITING_TIMESTAMP	"Couldn't write timestamp: "
#define ERR_WRITING_FLAG	"Couldn't write flag: "
#define ERR_WRITING_ENTRY	"Couldn't write entry: "
#define ERR_READING_ENTRY	"Couldn't read entry: "
#define ERR_PARSING_FAILED	"Couldn't parse entry!\n"
#define ERR_WRITING		"Couldn't write: "

#define LAST_CLEANUP_KEY	"=LastCleanup"
#define VERSION_KEY		"=Version"

#define GL_DB_FILENAME	"greylisting.bdb"

/* This file contains functions for manipulating the greylisting database.
 * The database is a Berkeley-DB style database of type HASH.
 * The keys in the database are comma-separated tripels of the ip number of
 * the sending MTA, the email envelope sender address and the envelope
 * recipient address. The corresponding values are strings of exactly 9
 * characters: a '+' or '-' followed by a hex representation of the time of the
 * last modification of the value.
 * Keys starting with '=' are reserved for internal purposes. Currently,
 * only '=LastCleanup' and '=Version' are used. The former contains the
 * 8-digit-hex-representation * of the time of the last cleanup run through
 * the database, the latter contains the version string.
 */

/** Closes cursor, transaction, db and handle.
 */
void closedb() {
int rc;

    if (msg_key.data) { free(msg_key.data); msg_key.data = NULL; }

    if (db_crs) {
	db_crs->c_close(db_crs);
	/* Don't care if it succeeds - can't do anything useful, anyway */
	db_crs = NULL;
    }

    if (db_txn) {
	db_txn->abort(db_txn);
	/* Don't care if it succeeds - can't do anything useful, anyway */
	db_txn = NULL;
    }

    if (db && (rc = db->close(db, 0))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_CLOSE_FILE, strlen(ERR_CANT_CLOSE_FILE));
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	exit(1);
    }
    db = NULL;

    if (db_env && (rc = db_env->close(db_env, 0))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_CLOSE_ENV, strlen(ERR_CANT_CLOSE_ENV));
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	exit(1);
    }
    db_env = NULL;
}

/** Write the read error message to stderr and exit with status 1. */
static void read_error(int rc) {
char *error = db_strerror(rc);

    write(STDERR_FILENO, progname, strlen(progname));
    write(STDERR_FILENO, ": ", 2);
    write(STDERR_FILENO, ERR_READING_ENTRY, strlen(ERR_READING_ENTRY));
    write(STDERR_FILENO, error, strlen(error));
    write(STDERR_FILENO, "\n" , 1);
    closedb();
    exit(1);
}

/** Write the write error message to stderr and exit with status 1. */
static void write_error(rc) {
char *error = db_strerror(rc);

    write(STDERR_FILENO, progname, strlen(progname));
    write(STDERR_FILENO, ": ", 2);
    write(STDERR_FILENO, ERR_WRITING, strlen(ERR_WRITING));
    write(STDERR_FILENO, error, strlen(error));
    write(STDERR_FILENO, "\n" , 1);
    closedb();
    exit(1);
}

/** Writes to the given buffer 8 hex digits representing the current time.
 */
static void currentTimeAsString(char *ts) {
time_t now = time(NULL);
int i = 7;

    while (now) {
	int digit = now & 0xf;
	if (digit < 10) {
	    ts[i] = '0' + digit;
	} else {
	    ts[i] = 'a' + digit - 10;
	}
	i--;
	now >>= 4;
    }
    while (i >= 0) { ts[i--] = '0'; }
}

/** Parses the given buffer as a hex string representing a timestamp.
 *  The buffer must contain at least 8 hex digits.
 *  Returns -1 in case of an error.
 */
static time_t parseTimeString(char *buf) {
int i;
time_t ts = 0;

    for (i = 0; i < 8; i++) {
	ts <<= 4;
	if (buf[i] >= '0' && buf[i] <= '9') {
	    ts += buf[i] - '0';
	} else if (buf[i] >= 'a' && buf[i] <= 'f') {
	    ts += buf[i] - 'a' + 10;
	} else {
	    return -1;
	}
    }

    return ts;
}

/** Opens the database in the given directory. Checks the value of '=Version'
 *  and '=LastCleanup'. Performs a cleanup run if it's more than max_wait
 *  seconds in the past.
 */
void opendb(char *path) {
char buf[8];
int rc;

    if ((rc = db_env_create(&db_env, 0))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_CREATE_DBENV,
	      strlen(ERR_CANT_CREATE_DBENV));
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	exit(1);
    }

    if ((rc = db_env->open(db_env, path, DB_INIT_LOCK | DB_INIT_MPOOL
					 | DB_INIT_TXN | DB_RECOVER | DB_CREATE,
			   S_IRUSR | S_IWUSR))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_OPEN_DBENV, strlen(ERR_CANT_OPEN_DBENV));
	write(STDERR_FILENO, path, strlen(path));
	write(STDERR_FILENO, "\": ", 2);
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	closedb();
	exit(1);
    }

    if ((rc = db_create(&db, db_env, 0))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_CREATE_DBHDL,
	      strlen(ERR_CANT_CREATE_DBHDL));
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	closedb();
	exit(1);
    }

    if ((rc = db->open(db, GL_EXTRA_OPEN_ARG GL_DB_FILENAME, NULL, DB_HASH,
		       DB_CREATE, S_IRUSR | S_IWUSR))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_OPEN_FILE, strlen(ERR_CANT_OPEN_FILE));
	write(STDERR_FILENO, path, strlen(path));
	write(STDERR_FILENO, "/", 1);
	write(STDERR_FILENO, GL_DB_FILENAME, strlen(GL_DB_FILENAME));
	write(STDERR_FILENO, "\": ", 3);
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	closedb();
	exit(1);
    }

    if ((rc = db_env->txn_begin(db_env, NULL, &db_txn, DB_TXN_NOSYNC))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_START_TXN, strlen(ERR_CANT_START_TXN));
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	closedb();
	exit(1);
    }

    /* FIXME: check version field */

    admin_key.data = LAST_CLEANUP_KEY;
    admin_key.size = strlen(LAST_CLEANUP_KEY);
    rc = db->get(db, db_txn, &admin_key, &read_value, 0);
    if (rc && rc != DB_NOTFOUND) { read_error(rc); }
    if (rc == 0) {
	time_t last = parseTimeString(read_value.data), now = time(NULL);
	if (now - last > max_wait) {
	    /* Walk through all key / value pairs, deleting expired entries */
	    if ((rc = db->cursor(db, db_txn, &db_crs, 0))) {
		char *error = db_strerror(rc);
		write(STDERR_FILENO, progname, strlen(progname));
		write(STDERR_FILENO, ": ", 2);
		write(STDERR_FILENO, ERR_CANT_CREATE_CRS,
		      strlen(ERR_CANT_CREATE_CRS));
		write(STDERR_FILENO, error, strlen(error));
		write(STDERR_FILENO, "\n" , 1);
		closedb();
		exit(1);
	    }
	    rc = db_crs->c_get(db_crs, &crs_key, &read_value, DB_FIRST);
	    while (!rc) {
		if (((char*) crs_key.data)[0] != '=') {
		    last = parseTimeString(&((char *)read_value.data)[1]);
		    if ((now - last > max_wait && ((char *)read_value.data)[0] =='-')
			    || (now - last > accept_good
			        && ((char *)read_value.data)[0] == '+')) {
			db_crs->c_del(db_crs, 0);
			/* FIXME: check return value */
		    }
		}
		rc = db_crs->c_get(db_crs, &crs_key, &read_value, DB_NEXT);
	    }
	    if (rc != DB_NOTFOUND) { read_error(rc); }
	    rc = DB_NOTFOUND; /* Force update of last_cleanup_key */
	    db_crs->c_close(db_crs);
	    /* Hm. Should we check the return value here? */
	    db_crs = NULL;
	}
    }

    if (rc == DB_NOTFOUND) {
	currentTimeAsString(buf);
	write_value.size = 8;
	write_value.data = buf;
	if ((rc = db->put(db, db_txn, &admin_key, &write_value, 0))) {
	    write_error(rc);
	 }
    }

    if ((rc = db_txn->commit(db_txn, 0))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_COMMIT_TXN, strlen(ERR_CANT_COMMIT_TXN));
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	closedb();
	exit(1);
    }
    db_txn = NULL;
}

/** Searches the database for the given entry.
 *  Returns a negative number if no matching entry was found.
 *  Returns 0 if a matching entry was found but the min_reject interval has
 *  not yet expired.
 *  Otherwise, a positive number is returned.
 */
int find_entry(char *ip, char *sender, char *recipient) {
int rc;
int iplen = strlen(ip), senderlen = strlen(sender), rcptlen = strlen(recipient);
time_t now, then;

    msg_key.size = iplen + 1 + senderlen + 1 + rcptlen;
    msg_key.data = (char *) malloc(msg_key.size);
    if (!msg_key.data) { err_memory(); }

    memcpy(msg_key.data, ip, iplen);
    ((char *)msg_key.data)[iplen] = ',';
    memcpy(&((char *)msg_key.data)[iplen + 1], sender, senderlen);
    ((char *)msg_key.data)[iplen + 1 + senderlen] = ',';
    memcpy(&((char *)msg_key.data)[iplen + 1 + senderlen + 1], recipient,
	   rcptlen);

    if ((rc = db_env->txn_begin(db_env, NULL, &db_txn, DB_TXN_NOSYNC))) {
	char *error = db_strerror(rc);
	write(STDERR_FILENO, progname, strlen(progname));
	write(STDERR_FILENO, ": ", 2);
	write(STDERR_FILENO, ERR_CANT_START_TXN, strlen(ERR_CANT_START_TXN));
	write(STDERR_FILENO, error, strlen(error));
	write(STDERR_FILENO, "\n" , 1);
	closedb();
	exit(1);
    }

    rc = db->get(db, db_txn, &msg_key, &read_value, 0);
    if (rc == DB_NOTFOUND) {
	db_txn->abort(db_txn);
	/* Don't care if it succeeds - can't do anything useful, anyway */
	db_txn = NULL;
	return -1;
    } else if (rc) {
	read_error(rc);
    }

    then = parseTimeString(&((char *)read_value.data)[1]);
    now = time(NULL);
    if ((now - then > max_wait && ((char *)read_value.data)[0] == '-')
	    || (now - then > accept_good && ((char *)read_value.data)[0] == '+')) {
	/* Expired -> delete */
	rc = db->del(db, db_txn, &msg_key, 0);
	if (rc && rc != DB_NOTFOUND) { write_error(rc); }
	if ((rc = db_txn->commit(db_txn, 0))) { write_error(rc); }
	db_txn = NULL;
	return -1;
    }

    return ((char *)read_value.data)[0] == '+' || now - then >= min_reject;
}

/** Updates the entry for our msg_key.
 */
void update_entry() {
char buf[9];
int rc;

    write_value.size = 9;
    write_value.data = buf;
    ((char *)write_value.data)[0] = '+';
    currentTimeAsString(&((char *)write_value.data)[1]);
    if ((rc = db->put(db, db_txn, &msg_key, &write_value, 0))) {
	write_error(rc);
    }
    if ((rc = db_txn->commit(db_txn, 0))) { write_error(rc); }
    db_txn = NULL;
}

/** Deletes the entry for our msg_key.
 */
void delete_entry() {
int rc;

    if ((rc = db->del(db, db_txn, &msg_key, 0))) {
	write_error(rc);
    }
    if ((rc = db_txn->commit(db_txn, 0))) { write_error(rc); }
    db_txn = NULL;
}

/** Adds a new entry to the file.
 */
void add_entry(char *ip, char *sender, char *recipient) {
int rc;
char buf[9];

    write_value.size = 9;
    write_value.data = buf;
    ((char *)write_value.data)[0] = '-';
    currentTimeAsString(&((char *)write_value.data)[1]);
    if ((rc = db->put(db, NULL, &msg_key, &write_value, 0))) {
	write_error(rc);
    }
}

/* Do not change the following line:
 * arch-tag: 10d13bf1-7962-4d70-b780-2348df681f93
 */
