/*
 * $Id: 4e417b53c1180179ff23c024f86bcdbea89d22e1 $
 *
 * This file is part of nsmtp.
 *
 * Copyright (c) 2007, 2008 Niklas Olmes <niklas@noxa.de>
 *                          http://www.nevm.de/nsmtp/
 *                          NEVM-RIPE
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include <sys/types.h>
#include <sys/file.h>
#include <fcntl.h>
#include <limits.h>
#if defined(WITH_DB3)
#include <db3/db_185.h>
#else
#include <db.h>
#endif
#include <string.h>
#include <errno.h>
#include <time.h>

#include "ntl.h"
#include "log.h"
#include "check.h"
#include "alloc.h"
#include "greylist.h"
#include "config.h"

static const char rcsid[] = "$Id: 4e417b53c1180179ff23c024f86bcdbea89d22e1 $";

struct greylist_info {
	char status;
	time_t first;
	time_t last;
} greylist_info;

/*
 * return codes:
 * -1 - unable to determine atm
 * 0 - ahoi!
 * 1 - nah!
 */
int
want_greylisting(const char *address, NTL_ATTR **d_attrs, NTL_ATTR **u_attrs, int ret)
{
	const char *attrs[] = { "greylisting", NULL };
	int e;

	if ((e = want_generic("greylisting", address, attrs, d_attrs, u_attrs, ret)) == 2)
		return 0;
	return e;
}

int
greylist_rcpt(const char *ip, const char *from, const char *to)
{
	char key[512], iprange[16];
	char *p;

	DEBUG(("greylist_rcpt() => called for triple ip=[%s] from=<%s> to=<%s>", ip, from, to));

	/*
	 * NOTE: We use ip/24, not the exact ip address for greylisting.
	 *       Many provider, like web.de, use a range of servers to
	 *       deliver their outbound mail.
	 *
	 * This raises our chances to autowhitelist legitimate senders
	 * faster.  (Instead of waiting for every possible combination
	 * of server ip and sender recipient tuple.)
	 */
	(void)snprintf(iprange, sizeof(iprange), "%s", ip);
	if ((p = strrchr(iprange, '.')) != NULL)
		*p = '\0';
	(void)snprintf(key, sizeof(key), "%s|%s|%s", iprange, from, to);

	return greylist_generic("rcpt", nsmc_greylist_rcpt_db, key, nsmc_greylist_rcpt_wait);
}

void
cleanup_greylist_rcpt(void)
{

	cleanup_greylist_generic("rcpt", nsmc_greylist_rcpt_db, nsmc_greylist_rcpt_autowhite_timeout, nsmc_greylist_rcpt_whitelist_timeout);
}

int
greylist_spf(const char *ip, const char *from)
{
	char key[256], iprange[16];
	char *p;

	DEBUG(("greylist_spf() => called for tuple ip=[%s] from=<%s>", ip, from));

	/*
	 * NOTE: As for greylist_rcpt() above.
	 */
	(void)snprintf(iprange, sizeof(iprange), "%s", ip);
	if ((p = strrchr(iprange, '.')) != NULL)
		*p = '\0';
	(void)snprintf(key, sizeof(key), "%s|%s", iprange, from);

	return greylist_generic("spf", nsmc_greylist_spf_db, key, nsmc_greylist_spf_wait);
}

void
cleanup_greylist_spf(void)
{

	cleanup_greylist_generic("spf", nsmc_greylist_spf_db, nsmc_greylist_spf_autowhite_timeout, nsmc_greylist_spf_whitelist_timeout);
}

int
whitelist_host(const char *ip)
{
	char key[16];
	char *p;
	int e;

	(void)snprintf(key, sizeof(key), "%s", ip);

	if ((e = greylist_generic("host", nsmc_whitelist_host_db, key, -1)) == 0) { /* a.b.c.d */
		if ((p = strrchr(key, '.')) != NULL)
			*p = '\0';
		if ((e = greylist_generic("host", nsmc_whitelist_host_db, key, -1)) == 0) { /* a.b.c */
			if ((p = strrchr(key, '.')) != NULL)
				*p = '\0';
			if ((e = greylist_generic("host", nsmc_whitelist_host_db, key, -1)) == 0) { /* a.b */
				if ((p = strrchr(key, '.')) != NULL)
					*p = '\0';
				return greylist_generic("host", nsmc_whitelist_host_db, key, -1); /* a */
			}
		}
	}
	return e;
}

void
cleanup_whitelist_host(void)
{

	cleanup_greylist_generic("host", nsmc_whitelist_host_db, nsmc_whitelist_host_timeout, nsmc_whitelist_host_timeout);
}

void
cleanup_greylist_generic(const char *name, const char *db, int autowhite_to, int whitelist_to)
{
	DB *d;
	DBT dbt_k, dbt_d;
	struct greylist_info *info;
	time_t t;
	int fd;
	int e;
	unsigned long count, count_del_a, count_del_w;

	if ((d = dbopen(db, O_RDWR, 0660, DB_HASH, NULL)) == NULL) {
		xfatal("fatal: cleanup_greylist_%s() => unable to dbopen(%s) greylisting db: \"%s\"", name, db, strerror(errno));
		return;
	}

	fd = d->fd(d);
	if (flock(fd, LOCK_EX) != 0) {
		xwarn("warn: cleanup_greylist_%s() => unable to flock(%s) greylisting db: \"%s\"", name, db, strerror(errno));
		(void)d->close(d);
		return;
	}

	#define ZERO()	(void)memset(&dbt_k, 0, sizeof(DBT)); \
			(void)memset(&dbt_d, 0, sizeof(DBT));

	ZERO();
	count = count_del_a = count_del_w = 0LU;
	t = time(NULL);

	while ((e = d->seq(d, &dbt_k, &dbt_d, R_NEXT)) == 0) {
		++count;

		if (dbt_d.size != sizeof(struct greylist_info)) {
			xfatal("fatal: cleanup_greylist_%s() => result size is != sizeof(struct greylist_info), did you import the greylist_db=<%s> from a different host?", name, db);
			break;
		}
		info = (struct greylist_info *)dbt_d.data;

		switch (info->status) {
		case 2:
			break;
		case 1:
			if (info->last + whitelist_to < t && ++count_del_w && d->del(d, &dbt_k, R_CURSOR) != 0)
				xwarn("warn: cleanup_greylist_%s() => del() (reason: whitelist timeout) failed for key \"%s\" in greylisting db: \"%s\"", name, dbt_k.data, strerror(errno));
			break;
		default:
			if (info->first + autowhite_to < t && ++count_del_a && d->del(d, &dbt_k, R_CURSOR) != 0)
				xwarn("warn: cleanup_greylist_%s() => del() (reason: autowhite timeout) failed for key \"%s\" in greylisting db: \"%s\"", name, dbt_k.data, strerror(errno));
			break;
		}

		ZERO();
	}

	if (flock(fd, LOCK_UN) != 0) {
		xfatal("fatal: cleanup_greylist_%s() => unable to un-flock(%s) greylisting db: \"%s\"", name, db, strerror(errno));
		(void)d->close(d);
		return;
	}
	(void)d->close(d);

	if (e != 1)
		xwarn("warn: cleanup_greylist_%s() => cleanup loop terminated prematurely on seq(R_NEXT): \"%s\"", name, strerror(errno));
	else
		xinfo("info: cleanup_greylist_%s() => done, run took (%lu) seconds.  total=(%lu) del_a=(%lu) del_w(%lu)", name, time(NULL) - t, count, count_del_a, count_del_w);
}

/*
 * return codes:
 * -1 - unable to determine atm
 *  0 - greylisted
 *  1 - okay to send
 */
int
greylist_generic(const char *name, const char *db, const char *key, int wait)
{
	DB *d;
	DBT dbt_k, dbt_d;
	DBT dbt_i;
	struct greylist_info *info;
	struct greylist_info newinfo;
	int fd;
	int r, e;

	info = NULL;
	DEBUG(("greylist_%s() => called for greylist_db=<%s> key=<%s>", name, db, key));

	/*
	 * (0) Open and Lock Greylist DB
	 */
	if ((d = dbopen(db, O_CREAT | O_RDWR, 0660, DB_HASH, NULL)) == NULL) {
		xfatal("fatal: greylist_%s() => unable to dbopen(%s) greylisting db: \"%s\"", name, db, strerror(errno));
		return -1;
	}

	fd = d->fd(d);
	if (flock(fd, LOCK_EX) != 0) {
		xwarn("warn: greylist_%s() => unable to flock(%s) greylisting db: \"%s\"", name, db, strerror(errno));
		(void)d->close(d);
		return -1;
	}

	(void)memset(&dbt_k, 0, sizeof(DBT));
	(void)memset(&dbt_d, 0, sizeof(DBT));
	(void)memset(&dbt_i, 0, sizeof(DBT));
	(void)memset(&newinfo, 0, sizeof(struct greylist_info));

	dbt_k.data = nsm_strdup(key);
	dbt_k.size = strlen(key) + 1;

	#define PUT()	dbt_i.data = &newinfo; \
			dbt_i.size = sizeof(struct greylist_info); \
			if (d->put(d, &dbt_k, &dbt_i, 0) != 0) \
				xfatal("fatal: greylist_%s() => put() on greylist_db=<%s> returned error: \"%s\"", name, db, strerror(errno));

	/*
	 * (1) Try to Read Entry
	 */
	if ((e = d->get(d, &dbt_k, &dbt_d, 0)) < 0) {
		xwarn("warn: greylist_%s() => get() on greylist_db=<%s> returned error: \"%s\"", name, db, strerror(errno));
		r = -1;
	} else if (e == 0) {
		/*
		 * (1a) Entry is already in database, interpret data.
		 */
		DEBUG(("greylist_%s() => entry for key found in DB", name));

		if (dbt_d.size != sizeof(struct greylist_info)) {
			xfatal("fatal: greylist_%s() => result size is != sizeof(struct greylist_info), did you import the greylist_db=<%s> from a different host?", name, db);
			r = -1;
			goto out;
		}
		info = (struct greylist_info *)dbt_d.data;

		if (info->status == 2) {
			/*
			 * (1a)(a) Permanent Whitelist
			 */
			r = 1;
		} else if (info->status == 1) {
			/*
			 * (1a)(b) Auto Whitelisted
			 *
			 * We update the info->last timestamp.
			 */
			r = 1;
			newinfo.status = info->status;
			newinfo.first = info->first;
			newinfo.last = time(NULL);
			PUT();
		} else if (wait == -1) {
			r = 0;
		} else {
			/*
			 * (1a)(c) Greylisted
			 *
			 * Auto whitelist if we're over the waiting time.
			 */
			r = 0;
			newinfo.status = 0;
			newinfo.first = info->first;
			newinfo.last = time(NULL);
			if (info->first + wait < time(NULL)) {
				xinfo("info: greylist_%s() => auto whitelist key=<%s>, first_seen=(%d)", name, key, info->first);
				r = 1;
				newinfo.status = 1;
			}
			PUT();
		}
	} else {
		if (wait == -1) {
			r = 0;
		} else {
			/*
			 * (1b) Entry is not in database, add
			 */
			xinfo("info: greylist_%s() => greylist key=<%s> for (%d) seconds", name, key, wait);
			r = 0;
			newinfo.status = 0;
			newinfo.first = time(NULL);
			newinfo.last = time(NULL);
			PUT();
		}
	}

out:
	nsm_free(dbt_k.data);

	/*
	 * (2) Unlock and Return Status
	 */
	if (flock(fd, LOCK_UN) != 0) {
		xfatal("fatal: greylist_%s() => unable to un-flock(%s) greylisting db: \"%s\"", name, db, strerror(errno));
		(void)d->close(d);
		return -1;
	}
	(void)d->close(d);

	DEBUG(("greylist_%s() => returning greylist status (%d) for key=<%s>, first_seen=(%lu) last_seen=(%lu)", name, r, key, info != NULL ? info->first : 0, info != NULL ? info->last : 0));

	return r;
}
