/* greylisting-spp - A qmail-spp plugin implementing greylisting
 *
 * db-sqlite3.c
 *
 *  Copyright (C) 2011 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 <sqlite3.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "db-api.h"
#include "commonstuff.h"

static sqlite3	*db = NULL;
static char *prev_ip, *prev_sender, *prev_recipient;

#define ERR_CANT_OPEN_DB	"Can't open database file \""
#define ERR_CANT_CREATE_DB_FN	"Can't create DB function!\n"
#define ERR_VERSION_MISMATCH	"Version mismatch in file \""
#define ERR_WRITING_ENTRY	"Couldn't write entry: "
#define ERR_READING_ENTRY	"Couldn't read entry: "

/* This file contains functions for manipulating the greylisting database.
 * The database is an SQLite style database (see http://sqlite.org/ ).
 * It consists of two simple tables:
 *  - GL_MGMT contains some key/value pairs for internal use
 *  - GL_GREYLIST contains greylisting data (i. e. IP, sender, recipient,
 *    timestamp and flag).
 */

/** Closes cursor, transaction, db and handle.
 */
void closedb() {
    if (db) {
	sqlite3_close(db);
	db = NULL;
    }
}

/** Write the read error message to stderr and exit with status 1. */
static void read_error(const char *error) {
    write_error_string(progname);
    write_error_string(": ");
    write_error_string(ERR_READING_ENTRY);
    if (error) {
	write_error_string(error);
    } else {
	write_error_string("NULL");
    }
    write_error_string("\n");
    closedb();
    exit(1);
}

/** Write the write error message to stderr and exit with status 1. */
static void write_error(const char *error) {
    write_error_string(progname);
    write_error_string(": ");
    write_error_string(ERR_WRITING_ENTRY);
    if (error) {
	write_error_string(error);
    } else {
	write_error_string("<<NULL>>");
    }
    write_error_string("\n");
    closedb();
    exit(1);
}

/** Writes to the given buffer 10 hex digits representing the given time.
 */
static void timeAsString(char *ts, time_t time) {
int i = 9;

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

/** Writes to the given buffer 10 hex digits representing the current time.
 */
static void currentTimeAsString(char *ts) {
    timeAsString(ts, time(NULL));
}

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

    for (i = 0; i < 10; 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;
}

static void single_arg_exec(const char *sql, const char *arg) {
sqlite3_stmt *stmt;

    if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
	write_error(sqlite3_errmsg(db));
    }
    if (arg != NULL && sqlite3_bind_text(stmt, 1, arg, -1, SQLITE_TRANSIENT) != SQLITE_OK) {
	write_error(sqlite3_errmsg(db));
    }
    if (sqlite3_step(stmt) != SQLITE_DONE) {
	write_error(sqlite3_errmsg(db));
    }
    sqlite3_finalize(stmt);
}

static int time_callback(void *pArg, int argc, char **argv,
			 char **columnNames) {
    if (!argv) {
	((char*) pArg)[0] = 0;
	return 0;
    }
    if (argc != 1) {
	return -1; /* Expecting 1 result column, the timestamp */
    }
    strncpy((char*) pArg, argv[0], 10);
    ((char*) pArg)[10] = 0;
    return 0;
}

static int count_callback(void *pArg, int argc, char **argv,
			  char **columnNames) {
    *((int*)pArg) = atoi(argv[0]);
    return 0;
}

/** Opens the given database file. Checks if required tables exist and creates
 *  them if not. Checks the value of 'Version' and 'LastCleanup' and executes
 * a cleanup statement if it's more than max_wait seconds in the past.
 */
void opendb(char *path) {
time_t now, then;
int result_count;
char buf[11];
char *errmsg = NULL;

    if (sqlite3_open(path, &db) != SQLITE_OK) {
	write_error_string(progname);
	write_error_string(": ");
	write_error_string(ERR_CANT_OPEN_DB);
	errmsg = sqlite3_errmsg(db);
	if (errmsg) {
	    write_error_string(errmsg);
	} else {
	    write_error_string("NULL");
	}
	write_error_string("\n");
	exit(1);
    }

    /* Locking + transactions are handled by sqlite - or so the documentation
     * says... */
    sqlite3_busy_timeout(db, 5000); /* 5 seconds should be more than enough. */

    /* Check if table GL_MGMT exists */
    if (sqlite3_exec(db, "SELECT COUNT(*) FROM SQLite_Master "
			 "WHERE Type = 'table' AND name = 'GL_MGMT'",
		     &count_callback, &result_count, &errmsg) != SQLITE_OK) {
	read_error(errmsg);
    }
    if (result_count == 0) {
	/* If not -> create tables */
	if (sqlite3_exec(db, "CREATE TABLE GL_MGMT ("
			     "Name TEXT NOT NULL PRIMARY KEY, "
			     "Value TEXT NOT NULL)",
			 NULL, NULL, &errmsg) != SQLITE_OK) {
	    write_error(errmsg);
	}
	if (sqlite3_exec(db, "INSERT INTO GL_MGMT (Name, Value) "
			     "VALUES ('Version', '1.0')",
			 NULL, NULL, &errmsg) != SQLITE_OK) {
	    write_error(errmsg);
	}
	currentTimeAsString(buf);
	buf[10] = 0;
	single_arg_exec("INSERT INTO GL_MGMT (Name, Value) "
			"VALUES ('LastCleanup', :time)", buf);
	if (sqlite3_exec(db, "CREATE TABLE GL_GREYLIST ("
			     "IP TEXT NOT NULL, "
			     "Sender TEXT NOT NULL, "
			     "Recipient TEXT NOT NULL, "
			     "Flag CHAR(1) NOT NULL, "
			     "Timestamp CHAR(10) NOT NULL, "
			     "PRIMARY KEY (IP, Sender, Recipient))",
			 NULL, NULL, &errmsg) != SQLITE_OK) {
	    write_error(errmsg);
	}
	if (sqlite3_exec(db, "CREATE INDEX I_GL_FLAG_TIME "
			     "ON GL_GREYLIST (Flag, Timestamp)",
			 NULL, NULL, &errmsg) != SQLITE_OK) {
	    write_error(errmsg);
	}
    } else {
	/* else check Version, check LastCleanupRun */
	if (sqlite3_exec(db, "SELECT COUNT(*) FROM GL_MGMT "
			     "WHERE Name = 'Version' AND Value = '1.0'",
			 &count_callback, &result_count, &errmsg) != SQLITE_OK) {
	    read_error(errmsg);
	}
	if (result_count == 0) {
	    write_error_string(progname);
	    write_error_string(": ");
	    write_error_string(ERR_VERSION_MISMATCH);
	    write_error_string(path);
	    write_error_string("\n");
	    closedb();
	    exit(1);
	}
	buf[0] = 0;
	if (sqlite3_exec(db, "SELECT Value FROM GL_MGMT "
			     "WHERE Name = 'LastCleanup'",
			 &time_callback, buf, &errmsg) != SQLITE_OK) {
	    read_error(errmsg);
	}
	if (!buf[0]) {
	    currentTimeAsString(buf);
	    buf[10] = 0;
	    single_arg_exec("INSERT INTO GL_MGMT (Name, Value) "
			    "VALUES ('LastCleanup', :time)", buf);
	} else {
	    then = parseTimeString(buf);
	    now = time(NULL);
	    if (now - then > max_wait) {
		buf[10] = 0;
		timeAsString(buf, now - max_wait);
		single_arg_exec("DELETE FROM GL_GREYLIST "
				"WHERE Flag = '-' AND Timestamp < :time", buf);
		timeAsString(buf, now - accept_good);
		single_arg_exec("DELETE FROM GL_GREYLIST "
				"WHERE Flag = '+' AND Timestamp < :time", buf);
		timeAsString(buf, now);
		single_arg_exec("UPDATE GL_MGMT SET Value = :time "
				"WHERE Name = 'LastCleanup'", buf);
	    }
	}
    }
}

/** 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) {
sqlite3_stmt *stmt;
time_t now, then;
int rc;

    prev_ip = ip; prev_sender = sender; prev_recipient = recipient;
    if (sqlite3_prepare_v2(db, "SELECT Timestamp, Flag FROM GL_GREYLIST "
				"WHERE IP = :ip "
					"AND Sender = :sender "
					"AND Recipient = :rcpt ",
			   -1, &stmt, NULL) != SQLITE_OK) {
	read_error(sqlite3_errmsg(db));
    }
    if (sqlite3_bind_text(stmt, 1, ip, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 2, sender, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 3, recipient, -1, SQLITE_TRANSIENT) != SQLITE_OK) {
	read_error(sqlite3_errmsg(db));
    }

    rc = sqlite3_step(stmt);
    if (rc != SQLITE_ROW && rc != SQLITE_DONE) {
	read_error(sqlite3_errmsg(db));
    }

    if (rc == SQLITE_DONE) {
	rc = -1;
    } else {
	char result_flag = sqlite3_column_text(stmt, 1)[0];
	then = parseTimeString(sqlite3_column_text(stmt, 0));
	now = time(NULL);
	if ((now - then > max_wait && result_flag == '-')
		|| (now - then > accept_good && result_flag == '+')) {
	    /* Expired -> delete */
	    delete_entry();
	    rc = -1;
	} else {
	    rc = (result_flag == '+' || now - then >= min_reject);
	}
    }
    sqlite3_finalize(stmt);

    return rc;
}

/** Updates the entry for our msg_key.
 */
void update_entry() {
sqlite3_stmt *stmt;
char buf[11];

    currentTimeAsString(buf);
    buf[10] = 0;
    if (sqlite3_prepare_v2(db, "UPDATE GL_GREYLIST "
				"SET Timestamp = :time, Flag = '+' "
				"WHERE IP = :ip "
			 		"AND Sender = :sender "
					"AND Recipient = :rcpt ",
			   -1, &stmt, NULL) != SQLITE_OK) {
	write_error(sqlite3_errmsg(db));
    }
    if (sqlite3_bind_text(stmt, 2, prev_ip, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 3, prev_sender, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 4, prev_recipient, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 1, buf, 10, SQLITE_TRANSIENT) != SQLITE_OK) {
	write_error(sqlite3_errmsg(db));
    }
    if (sqlite3_step(stmt) != SQLITE_DONE) {
	write_error(sqlite3_errmsg(db));
    }
    sqlite3_finalize(stmt);
}

/** Deletes the entry for our msg_key.
 */
void delete_entry() {
sqlite3_stmt *stmt;

    if (sqlite3_prepare_v2(db, "DELETE FROM GL_GREYLIST "
				"WHERE IP = :ip "
					"AND Sender = :sender "
					"AND Recipient = :rcpt ",
			   -1, &stmt, NULL) != SQLITE_OK) {
	write_error(sqlite3_errmsg(db));
    }
    if (sqlite3_bind_text(stmt, 1, prev_ip, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 2, prev_sender, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 3, prev_recipient, -1, SQLITE_TRANSIENT) != SQLITE_OK) {
	write_error(sqlite3_errmsg(db));
    }
    if (sqlite3_step(stmt) != SQLITE_DONE) {
	write_error(sqlite3_errmsg(db));
    }
    sqlite3_finalize(stmt);
}

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

    currentTimeAsString(buf);
    buf[10] = 0;
    if (sqlite3_prepare_v2(db,"INSERT INTO GL_GREYLIST "
				"(IP, Sender, Recipient, Timestamp, Flag) "
			      "VALUES (:ip, :sender, :rcpt, :time, '-')",
			   -1, &stmt, NULL) != SQLITE_OK) {
	write_error(sqlite3_errmsg(db));
    }
    if (sqlite3_bind_text(stmt, 1, ip, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 2, sender, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 3, recipient, -1, SQLITE_TRANSIENT) != SQLITE_OK
	    || sqlite3_bind_text(stmt, 4, buf, 10, SQLITE_TRANSIENT) != SQLITE_OK) {
	write_error(sqlite3_errmsg(db));
    }
    if (sqlite3_step(stmt) != SQLITE_DONE) {
	write_error(sqlite3_errmsg(db));
    }
    sqlite3_finalize(stmt);
}

/* Do not change the following line:
 * arch-tag: bb23ab97-3a11-47eb-8703-e11640421918
 */
