/*++
/* NAME
/*	postscreen_tests 3
/* SUMMARY
/*	postscreen tests timestamp/flag bulk support
/* SYNOPSIS
/*	#include <postscreen.h>
/*
/*	void	PSC_INIT_TESTS(state)
/*	PSC_STATE *state;
/*
/*	void	psc_new_tests(state)
/*	PSC_STATE *state;
/*
/*	void	psc_parse_tests(state, stamp_text, time_value)
/*	PSC_STATE *state;
/*	const char *stamp_text;
/*	time_t time_value;
/*
/*	void	psc_todo_tests(state, time_value)
/*	PSC_STATE *state;
/*	const char *stamp_text;
/*	time_t time_value;
/*
/*	char	*psc_print_tests(buffer, state)
/*	VSTRING	*buffer;
/*	PSC_STATE *state;
/*
/*	char	*psc_print_grey_key(buffer, client, helo, sender, rcpt)
/*	VSTRING	*buffer;
/*	const char *client;
/*	const char *helo;
/*	const char *sender;
/*	const char *rcpt;
/*
/*	const char *psc_test_name(tindx)
/*	int	tindx;
/* DESCRIPTION
/*	The functions in this module overwrite the per-test expiration
/*	time stamps and all flags bits.  Some functions are implemented
/*	as unsafe macros, meaning they evaluate one or more arguments
/*	multiple times.
/*
/*	PSC_INIT_TESTS() is an unsafe macro that sets the per-test
/*	expiration time stamps to PSC_TIME_STAMP_INVALID, and that
/*	zeroes all the flags bits. These values are not meant to
/*	be stored into the postscreen(8) cache.
/*
/*	PSC_INIT_TEST_FLAGS_ONLY() zeroes all the flag bits.  It
/*	should be used when the time stamps are already initialized.
/*
/*	psc_new_tests() sets all test expiration time stamps to
/*	PSC_TIME_STAMP_NEW, and invokes psc_todo_tests().
/*
/*	psc_parse_tests() parses a cache file record and invokes
/*	psc_todo_tests().
/*
/*	psc_todo_tests() overwrites all per-session flag bits, and
/*	populates the flags based on test expiration time stamp
/*	information.  Tests are considered "expired" when they
/*	would be expired at the specified time value. Only enabled
/*	tests are flagged as "expired"; the object is flagged as
/*	"new" if some enabled tests have "new" time stamps.
/*
/*	psc_print_tests() creates a cache file record for the
/*	specified flags and per-test expiration time stamps.
/*	This may modify the time stamps for disabled tests.
/*
/*	psc_print_grey_key() prints a greylist lookup key.
/*
/*	psc_test_name() returns the name for the specified text
/*	index.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	IBM T.J. Watson Research
/*	P.O. Box 704
/*	Yorktown Heights, NY 10598, USA
/*
/*	Wietse Venema
/*	Google, Inc.
/*	111 8th Avenue
/*	New York, NY 10011, USA
/*--*/

/* System library. */

#include <sys_defs.h>
#include <stdio.h>			/* sscanf */

/* Utility library. */

#include <msg.h>
#include <name_code.h>
#include <sane_strtol.h>

/* Global library. */

#include <mail_params.h>

/* Application-specific. */

#include <postscreen.h>

 /*
  * Kludge to detect if some test is enabled.
  */
#define PSC_PREGR_TEST_ENABLE()	(*var_psc_pregr_banner != 0)
#define PSC_DNSBL_TEST_ENABLE()	(*var_psc_dnsbl_sites != 0)

 /*
  * Format of a persistent cache entry (which is almost but not quite the
  * same as the in-memory representation).
  * 
  * Each cache entry has one time stamp for each test.
  * 
  * - A time stamp of PSC_TIME_STAMP_INVALID must never appear in the cache. It
  * is reserved for in-memory objects that are still being initialized.
  * 
  * - A time stamp of PSC_TIME_STAMP_NEW indicates that the test never passed.
  * Postscreen will log the client with "pass new" when it passes the final
  * test.
  * 
  * - A time stamp of PSC_TIME_STAMP_DISABLED indicates that the test never
  * passed, and that the test was disabled when the cache entry was written.
  * 
  * - Otherwise, the test was passed, and the time stamp indicates when that
  * test result expires.
  * 
  * A cache entry is expired when the time stamps of all passed tests are
  * expired.
  */

/* psc_new_tests - initialize new test results from scratch */

void    psc_new_tests(PSC_STATE *state)
{
    time_t *expire_time = state->client_info->expire_time;

    /*
     * Give all tests a PSC_TIME_STAMP_NEW time stamp, so that we can later
     * recognize cache entries that haven't passed all enabled tests. When we
     * write a cache entry to the database, any new-but-disabled tests will
     * get a PSC_TIME_STAMP_DISABLED time stamp.
     */
    expire_time[PSC_TINDX_PREGR] = PSC_TIME_STAMP_NEW;
    expire_time[PSC_TINDX_DNSBL] = PSC_TIME_STAMP_NEW;
    expire_time[PSC_TINDX_PIPEL] = PSC_TIME_STAMP_NEW;
    expire_time[PSC_TINDX_NSMTP] = PSC_TIME_STAMP_NEW;
    expire_time[PSC_TINDX_BARLF] = PSC_TIME_STAMP_NEW;

    /*
     * Determine what tests need to be completed.
     */
    psc_todo_tests(state, PSC_TIME_STAMP_NEW + 1);
}

/* psc_parse_tests - parse test results from cache */

void    psc_parse_tests(PSC_STATE *state,
			        const char *stamp_str,
			        time_t time_value)
{
    const char *start = stamp_str;
    char   *cp;
    time_t *time_stamps = state->client_info->expire_time;
    time_t *sp;

    /*
     * Parse the cache entry, and allow for older postscreen versions that
     * implemented fewer tests. We pretend that the newer tests were disabled
     * at the time that the cache entry was written.
     */
    for (sp = time_stamps; sp < time_stamps + PSC_TINDX_COUNT; sp++) {
	*sp = sane_strtoul(start, &cp, 10);
	if (*start == 0 || (*cp != '\0' && *cp != ';') || errno == ERANGE)
	    *sp = PSC_TIME_STAMP_DISABLED;
	if (msg_verbose)
	    msg_info("%s -> %lu", start, (unsigned long) *sp);
	if (*cp == ';')
	    start = cp + 1;
	else
	    start = cp;
    }

    /*
     * Determine what tests need to be completed.
     */
    psc_todo_tests(state, time_value);
}

/* psc_todo_tests - determine what tests to perform */

void    psc_todo_tests(PSC_STATE *state, time_t time_value)
{
    time_t *expire_time = state->client_info->expire_time;
    time_t *sp;

    /*
     * Reset all per-session flags.
     */
    state->flags = 0;

    /*
     * Flag the tests as "new" when the cache entry has fields for all
     * enabled tests, but the remote SMTP client has not yet passed all those
     * tests.
     */
    for (sp = expire_time; sp < expire_time + PSC_TINDX_COUNT; sp++) {
	if (*sp == PSC_TIME_STAMP_NEW)
	    state->flags |= PSC_STATE_FLAG_NEW;
    }

    /*
     * Don't flag disabled tests as "todo", because there would be no way to
     * make those bits go away.
     */
    if (PSC_PREGR_TEST_ENABLE() && time_value > expire_time[PSC_TINDX_PREGR])
	state->flags |= PSC_STATE_FLAG_PREGR_TODO;
    if (PSC_DNSBL_TEST_ENABLE() && time_value > expire_time[PSC_TINDX_DNSBL])
	state->flags |= PSC_STATE_FLAG_DNSBL_TODO;
    if (var_psc_pipel_enable && time_value > expire_time[PSC_TINDX_PIPEL])
	state->flags |= PSC_STATE_FLAG_PIPEL_TODO;
    if (var_psc_nsmtp_enable && time_value > expire_time[PSC_TINDX_NSMTP])
	state->flags |= PSC_STATE_FLAG_NSMTP_TODO;
    if (var_psc_barlf_enable && time_value > expire_time[PSC_TINDX_BARLF])
	state->flags |= PSC_STATE_FLAG_BARLF_TODO;

    /*
     * If any test has expired, proactively refresh tests that will expire
     * soon. This can increase the occurrence of client-visible delays, but
     * avoids questions about why a client can pass some test and then fail
     * within seconds. The proactive refresh time is really a surrogate for
     * the user's curiosity level, and therefore hard to choose optimally.
     */
#ifdef VAR_PSC_REFRESH_TIME
    if ((state->flags & PSC_STATE_MASK_ANY_TODO) != 0
	&& var_psc_refresh_time > 0) {
	time_t  refresh_time = time_value + var_psc_refresh_time;

	if (PSC_PREGR_TEST_ENABLE() && refresh_time > expire_time[PSC_TINDX_PREGR])
	    state->flags |= PSC_STATE_FLAG_PREGR_TODO;
	if (PSC_DNSBL_TEST_ENABLE() && refresh_time > expire_time[PSC_TINDX_DNSBL])
	    state->flags |= PSC_STATE_FLAG_DNSBL_TODO;
	if (var_psc_pipel_enable && refresh_time > expire_time[PSC_TINDX_PIPEL])
	    state->flags |= PSC_STATE_FLAG_PIPEL_TODO;
	if (var_psc_nsmtp_enable && refresh_time > expire_time[PSC_TINDX_NSMTP])
	    state->flags |= PSC_STATE_FLAG_NSMTP_TODO;
	if (var_psc_barlf_enable && refresh_time > expire_time[PSC_TINDX_BARLF])
	    state->flags |= PSC_STATE_FLAG_BARLF_TODO;
    }
#endif

    /*
     * Gratuitously make postscreen logging more useful by turning on all
     * enabled pre-handshake tests when any pre-handshake test is turned on.
     * 
     * XXX Don't enable PREGREET gratuitously before the test expires. With a
     * short TTL for DNSBL allowlisting, turning on PREGREET would force a
     * full postscreen_greet_wait too frequently.
     */
#if 0
    if (state->flags & PSC_STATE_MASK_EARLY_TODO) {
	if (PSC_PREGR_TEST_ENABLE())
	    state->flags |= PSC_STATE_FLAG_PREGR_TODO;
	if (PSC_DNSBL_TEST_ENABLE())
	    state->flags |= PSC_STATE_FLAG_DNSBL_TODO;
    }
#endif
}

/* psc_print_tests - print postscreen cache record */

char   *psc_print_tests(VSTRING *buf, PSC_STATE *state)
{
    const char *myname = "psc_print_tests";
    time_t *expire_time = state->client_info->expire_time;

    /*
     * Sanity check.
     */
    if ((state->flags & PSC_STATE_MASK_ANY_UPDATE) == 0)
	msg_panic("%s: attempt to save a no-update record", myname);

    /*
     * Give disabled tests a dummy time stamp so that we don't log a client
     * with "pass new" when some disabled test becomes enabled at some later
     * time.
     */
    if (PSC_PREGR_TEST_ENABLE() == 0 && expire_time[PSC_TINDX_PREGR] == PSC_TIME_STAMP_NEW)
	expire_time[PSC_TINDX_PREGR] = PSC_TIME_STAMP_DISABLED;
    if (PSC_DNSBL_TEST_ENABLE() == 0 && expire_time[PSC_TINDX_DNSBL] == PSC_TIME_STAMP_NEW)
	expire_time[PSC_TINDX_DNSBL] = PSC_TIME_STAMP_DISABLED;
    if (var_psc_pipel_enable == 0 && expire_time[PSC_TINDX_PIPEL] == PSC_TIME_STAMP_NEW)
	expire_time[PSC_TINDX_PIPEL] = PSC_TIME_STAMP_DISABLED;
    if (var_psc_nsmtp_enable == 0 && expire_time[PSC_TINDX_NSMTP] == PSC_TIME_STAMP_NEW)
	expire_time[PSC_TINDX_NSMTP] = PSC_TIME_STAMP_DISABLED;
    if (var_psc_barlf_enable == 0 && expire_time[PSC_TINDX_BARLF] == PSC_TIME_STAMP_NEW)
	expire_time[PSC_TINDX_BARLF] = PSC_TIME_STAMP_DISABLED;

    vstring_sprintf(buf, "%lu;%lu;%lu;%lu;%lu",
		    (unsigned long) expire_time[PSC_TINDX_PREGR],
		    (unsigned long) expire_time[PSC_TINDX_DNSBL],
		    (unsigned long) expire_time[PSC_TINDX_PIPEL],
		    (unsigned long) expire_time[PSC_TINDX_NSMTP],
		    (unsigned long) expire_time[PSC_TINDX_BARLF]);
    return (STR(buf));
}

/* psc_print_grey_key - print postscreen cache record */

char   *psc_print_grey_key(VSTRING *buf, const char *client,
			           const char *helo, const char *sender,
			           const char *rcpt)
{
    return (STR(vstring_sprintf(buf, "%s/%s/%s/%s",
				client, helo, sender, rcpt)));
}

/* psc_test_name - map test index to symbolic name */

const char *psc_test_name(int tindx)
{
    const char *myname = "psc_test_name";
    const NAME_CODE test_name_map[] = {
	PSC_TNAME_PREGR, PSC_TINDX_PREGR,
	PSC_TNAME_DNSBL, PSC_TINDX_DNSBL,
	PSC_TNAME_PIPEL, PSC_TINDX_PIPEL,
	PSC_TNAME_NSMTP, PSC_TINDX_NSMTP,
	PSC_TNAME_BARLF, PSC_TINDX_BARLF,
	0, -1,
    };
    const char *result;

    if ((result = str_name_code(test_name_map, tindx)) == 0)
	msg_panic("%s: bad index %d", myname, tindx);
    return (result);
}
