/* This file is part of Mailfromd. -*- c -*-
Copyright (C) 2006, 2007, 2008, 2009 Sergey Poznyakoff
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
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, see . */
#define DEFAULT_DB_MODE 0640
#include
struct db_prop { /* Database properties */
char *pat; /* Database name pattern */
mode_t mode; /* File mode */
int null; /* Null byte */
};
static mu_list_t db_prop_list;
static int
db_prop_compare(const void *item, const void *data)
{
const struct db_prop *prop = item;
const char *name = data;
return strcmp(prop->pat, name);
}
static int
strtomode(char *str, mode_t *pmode)
{
mode_t mode = 0;
struct { char c; unsigned mask; } modetab[] = {
{ 'r', S_IRUSR },
{ 'w', S_IWUSR },
{ 'x', S_IXUSR },
{ 'r', S_IRGRP },
{ 'w', S_IWGRP },
{ 'x', S_IXGRP },
{ 'r', S_IROTH },
{ 'w', S_IWOTH },
{ 'x', S_IXOTH },
{ 0 }
};
int i;
for (i = 0; modetab[i].c; i++) {
if (!str[i])
return i + 1;
if (str[i] == modetab[i].c)
mode |= modetab[i].mask;
else if (str[i] != '-')
return i + 1;
}
if (str[i])
return i + 1;
*pmode = mode;
return 0;
}
/* #pragma dbprop [null] [mode] */
MF_PRAGMA(dbprop, 3, 4)
{
int null = 0;
mode_t mode = DEFAULT_DB_MODE;
struct db_prop *prop;
int rc;
char *pat;
--argc;
pat = *++argv;
while (--argc) {
char *p = *++argv;
if (strcmp (p, "null") == 0)
null = 1;
else if (c_isdigit(*p)) {
unsigned long n = strtoul(p, &p, 8);
if (*p || (mode = n) != n) {
parse_error(_("bad numeric file mode"));
return;
}
} else if (rc = strtomode(p, &mode)) {
parse_error(_("bad symbolic file mode (near %s)"),
p + rc - 1);
return;
}
}
if (!db_prop_list) {
rc = mu_list_create(&db_prop_list);
if (rc) {
parse_error(_("Cannot create list: %s"),
mu_strerror(rc));
return;
}
mu_list_set_comparator(db_prop_list, db_prop_compare);
}
if (mu_list_locate(db_prop_list, pat, (void**) &prop)) {
prop = xmalloc(sizeof(*prop));
prop->pat = xstrdup(pat);
rc = mu_list_append(db_prop_list, prop);
if (rc) {
parse_error(_("Cannot create list: %s"),
mu_strerror(rc));
return;
}
}
prop->mode = mode;
prop->null = null;
}
const struct db_prop *
db_prop_lookup(const char *name)
{
mu_iterator_t itr;
const struct db_prop *found = NULL;
if (db_prop_list && mu_list_get_iterator(db_prop_list, &itr)) {
for (mu_iterator_first(itr);
!mu_iterator_is_done(itr);
mu_iterator_next(itr)) {
const struct db_prop *p;
mu_iterator_current(itr, (void**)&p);
if (strcmp(p->pat, name) == 0) {
found = p;
break;
} else if (fnmatch(p->pat, name, 0) == 0)
found = p;
}
mu_iterator_destroy(&itr);
}
return found;
}
#define LOOKUP_NULL_BYTE 0x1
#define LOOKUP_TEST_ONLY 0x2
static int
dbmap_lookup(eval_environ_t env, char *dbname, const char *keystr,
const char *defval, int flags)
{
int rc;
DBM_FILE db;
DBM_DATUM key;
DBM_DATUM contents;
if (!defval)
defval = "";
if (mu_dbm_open(dbname, &db, MU_STREAM_READ, 0, NULL))
MF_THROW(mfe_dbfailure,
_("mu_dbm_open(%s) failed: %s"),
dbname,
mu_dbm_strerror());
memset(&key, 0, sizeof key);
memset(&contents, 0, sizeof contents);
MU_DATUM_PTR(key) = (void*) keystr;
MU_DATUM_SIZE(key) = strlen(keystr);
if (flags & LOOKUP_NULL_BYTE)
MU_DATUM_SIZE(key)++;
rc = mu_dbm_fetch(&db, key, &contents) == 0;
debug2(10, "Looking up %s: %s", keystr, rc ? "true" : "false");
if (flags & LOOKUP_TEST_ONLY)
push(env, (STKVAL)rc);
else {
if (!rc) {
if (defval)
pushs(env, (STKVAL)defval);
else
push(env, 0);
} else if (((char*)MU_DATUM_PTR(contents))[MU_DATUM_SIZE(contents)-1]) {
size_t off;
size_t len = MU_DATUM_SIZE(contents);
char *s = MF_ALLOC_HEAP(off, len + 1);
memcpy(s, MU_DATUM_PTR(contents), len);
s[len] = 0;
push(env, (STKVAL) off);
} else
pushs(env, MU_DATUM_PTR(contents));
}
mu_dbm_datum_free(&contents);
mu_dbm_close(&db);
return rc;
}
MF_DEFUN(dbmap, NUMBER, STRING dbname, STRING key, OPTIONAL, NUMBER null)
{
const struct db_prop *prop = db_prop_lookup(dbname);
dbmap_lookup(env, dbname, key, NULL,
LOOKUP_TEST_ONLY |
(MF_OPTVAL(null, prop && prop->null)
? LOOKUP_NULL_BYTE : 0));
}
END
MF_DEFUN(dbget, STRING, STRING dbname, STRING key, OPTIONAL,
STRING defval, NUMBER null)
{
const struct db_prop *prop = db_prop_lookup(dbname);
dbmap_lookup(env, dbname, key,
MF_OPTVAL(defval),
MF_OPTVAL(null, prop && prop->null)
? LOOKUP_NULL_BYTE : 0);
}
END
MF_DEFUN(dbput, VOID, STRING dbname, STRING keystr, STRING value,
OPTIONAL, NUMBER null, NUMBER mode)
{
int rc;
DBM_FILE db;
DBM_DATUM key;
DBM_DATUM contents;
const struct db_prop *prop = db_prop_lookup(dbname);
if (mu_dbm_open(dbname, &db, MU_STREAM_RDWR,
MF_OPTVAL(mode, (prop ? prop->mode : DEFAULT_DB_MODE)),
NULL))
MF_THROW(mfe_dbfailure,
_("mu_dbm_open(%s) failed: %s"),
dbname,
mu_dbm_strerror());
memset(&key, 0, sizeof key);
MU_DATUM_PTR(key) = keystr;
MU_DATUM_SIZE(key) = strlen(keystr);
if (MF_OPTVAL(null, prop && prop->null))
MU_DATUM_SIZE(key)++;
memset(&contents, 0, sizeof contents);
MU_DATUM_PTR(contents) = value;
MU_DATUM_SIZE(contents) = strlen(value) + 1;
rc = mu_dbm_insert(&db, key, contents, 1);
mu_dbm_close(&db);
MF_ASSERT(rc == 0,
mfe_dbfailure,
_("Failed to insert data to %s: %s %s: %s"),
dbname,
keystr,
value,
mu_dbm_strerror());
}
END
MF_DEFUN(dbdel, VOID, STRING dbname, STRING keystr, OPTIONAL, NUMBER null,
NUMBER mode)
{
DBM_FILE db;
DBM_DATUM key;
int rc;
const struct db_prop *prop = db_prop_lookup(dbname);
if (mu_dbm_open(dbname, &db, MU_STREAM_RDWR,
MF_OPTVAL(mode, (prop ? prop->mode : DEFAULT_DB_MODE)),
NULL))
MF_THROW(mfe_dbfailure,
_("mu_dbm_open(%s) failed: %s"),
dbname,
mu_dbm_strerror());
memset(&key, 0, sizeof key);
MU_DATUM_PTR(key) = keystr;
MU_DATUM_SIZE(key) = strlen(keystr);
if (MF_OPTVAL(null, prop && prop->null))
MU_DATUM_SIZE(key)++;
rc = mu_dbm_delete(&db, key);
mu_dbm_close(&db);
MF_ASSERT(rc == 0,
mfe_dbfailure,
_("Failed to delete data `%s' from `%s': %s"),
keystr,
dbname,
mu_dbm_strerror());
}
END
#define NUMDB 128
struct db_tab {
int used;
DBM_FILE db;
DBM_DATUM key;
};
static void *
alloc_db_tab()
{
return xcalloc(NUMDB, sizeof(struct db_tab));
}
static void
close_db_tab(struct db_tab *dbt)
{
if (dbt->used) {
mu_dbm_datum_free(&dbt->key);
mu_dbm_close(&dbt->db);
dbt->used = 0;
}
}
static void
destroy_db_tab(void *data)
{
int i;
struct db_tab *db = data;
for (i = 0; i < NUMDB; i++)
close_db_tab(db + i);
free(db);
}
MF_DECLARE_DATA(DBTAB, alloc_db_tab, destroy_db_tab);
static int
new_db_tab(struct db_tab *dbt)
{
int i;
for (i = 0; i < NUMDB; i++)
if (!dbt[i].used) {
dbt[i].used = 1;
return i;
}
return -1;
}
MF_DEFUN(dbfirst, NUMBER, STRING dbname)
{
int rc;
int n;
struct db_tab *dbt = MF_GET_DATA;
DBM_FILE db;
DBM_DATUM key;
if (mu_dbm_open(dbname, &db, MU_STREAM_READ, 0, NULL))
MF_THROW(mfe_dbfailure,
_("mu_dbm_open(%s) failed: %s"),
dbname,
mu_dbm_strerror());
rc = mu_dbm_firstkey(&db, &key);
MF_ASSERT(rc == 0, mfe_dbfailure,
_("mu_dbm_firstkey failed: %s"),
mu_dbm_strerror());
n = new_db_tab(dbt);
MF_ASSERT(n >= 0,
mfe_failure,
_("No more database entries available"));
dbt += n;
dbt->db = db;
dbt->key = key;
MF_RETURN(n);
}
END
MF_DEFUN(dbnext, NUMBER, NUMBER dn)
{
struct db_tab *dbt = MF_GET_DATA + dn;
DBM_DATUM nextkey;
int rc;
MF_ASSERT(dn >= 0 && dn < NUMDB && dbt->used,
mfe_range,
_("Invalid database descriptor"));
rc = mu_dbm_nextkey(&dbt->db, dbt->key, &nextkey);
if (rc) {
close_db_tab(dbt);
MF_RETURN(0);
}
mu_dbm_datum_free(&dbt->key);
dbt->key = nextkey;
MF_RETURN(1);
}
END
MF_DEFUN(dbkey, STRING, NUMBER dn)
{
size_t off, len;
char *s;
struct db_tab *dbt = MF_GET_DATA + dn;
MF_ASSERT(dn >= 0 && dn < NUMDB && dbt->used,
mfe_range,
_("Invalid database descriptor"));
len = MU_DATUM_SIZE(dbt->key);
s = MF_ALLOC_HEAP(off, len + 1);
memcpy(s, MU_DATUM_PTR(dbt->key), len);
s[len] = 0;
MF_RETURN(off);
}
END
MF_DEFUN(dbvalue, STRING, NUMBER dn)
{
int rc;
size_t off, len;
char *s;
struct db_tab *dbt = MF_GET_DATA + dn;
DBM_DATUM contents;
MF_ASSERT(dn >= 0 && dn < NUMDB && dbt->used,
mfe_range,
_("Invalid database descriptor"));
memset(&contents, 0, sizeof contents);
rc = mu_dbm_fetch(&dbt->db, dbt->key, &contents);
MF_ASSERT(rc == 0,
mfe_dbfailure,
_("Key not found: %s"),
mu_dbm_strerror());
len = MU_DATUM_SIZE(contents);
s = MF_ALLOC_HEAP(off, len + 1);
memcpy(s, MU_DATUM_PTR(contents), len);
s[len] = 0;
mu_dbm_datum_free(&contents);
MF_RETURN(off);
}
END
enum greylist_semantics
{
greylist_traditional,
greylist_ct
};
static enum greylist_semantics greylist_semantics = greylist_traditional;
/* #pragma greylist {traditional|gray|ct|con-tassios}*/
MF_PRAGMA(greylist, 2, 2)
{
if (strcmp(argv[1], "traditional") == 0
|| strcmp(argv[1], "gray") == 0)
greylist_semantics = greylist_traditional;
else if (strcmp(argv[1], "ct") == 0
|| strcmp(argv[1], "con-tassios") == 0)
greylist_semantics = greylist_ct;
else
/* TRANSLATORS: Do not translate keywords:
traditional, gray, ct, con-tassios */
parse_error(_("unknown semantics; allowed values are: "
"traditional (or gray) and "
"ct (or con-tassios)"));
}
static void
greylist_print_item(const char *key, size_t size, const void *content)
{
time_t timestamp = *(time_t*) content;
size--; /* Size includes the trailing nul */
printf("%*.*s ", size, size, key);
format_time_str(stdout, timestamp);
putchar('\n');
}
static int
greylist_expire_item(const void *content)
{
time_t timestamp = *(time_t*) content;
return greylist_format->expire_interval &&
time(NULL) - timestamp > greylist_format->expire_interval;
}
static struct db_format greylist_format_struct = {
"greylist",
DEFAULT_GREYLIST_DATABASE,
1,
DEFAULT_EXPIRE_INTERVAL,
greylist_print_item,
greylist_expire_item
};
struct db_format *greylist_format = &greylist_format_struct;
MF_VAR(greylist_seconds_left, NUMBER);
/* The traditional (aka gray's) greylist implementation: the greylist
database keeps the time the greylisting was activated.
*/
static int
do_greylist_traditional(eval_environ_t env, char *email, long interval)
{
int rc;
DBM_FILE db;
DBM_DATUM key;
DBM_DATUM contents;
int readonly;
time_t now;
rc = mu_dbm_open(greylist_format->dbname, &db, MU_STREAM_RDWR, 0600,
&readonly);
MF_ASSERT(rc == 0, mfe_dbfailure, _("mu_dbm_open(%s) failed: %s"),
greylist_format->dbname, mu_dbm_strerror());
memset(&key, 0, sizeof key);
memset(&contents, 0, sizeof contents);
MU_DATUM_PTR(key) = email;
MU_DATUM_SIZE(key) = strlen(email)+1;
time(&now);
if (mu_dbm_fetch(&db, key, &contents) == 0) {
time_t timestamp, diff;
MF_ASSERT(MU_DATUM_SIZE(contents) == sizeof timestamp,
mfe_dbfailure,
_("Greylist database %s has wrong data size"),
greylist_format->dbname);
timestamp = *(time_t*) MU_DATUM_PTR(contents);
diff = now - timestamp;
__DBG(20) {
char timebuf[32];
debug_log("%s entered greylist database on %s, "
"%ld seconds ago",
email,
mailfromd_timestr(timestamp, timebuf,
sizeof timebuf),
(long) diff);
}
if (diff < interval) {
diff = interval - diff;
MF_VAR_REF(greylist_seconds_left, diff);
debug2(20, "%s still greylisted (for %lu sec.)",
email,
(unsigned long) diff);
rc = 1;
} else if (diff > greylist_format->expire_interval) {
debug1(20, "greylist record for %s expired",
email);
MF_VAR_REF(greylist_seconds_left, interval);
if (!readonly) {
memcpy(MU_DATUM_PTR(contents),
&now, sizeof now);
if (mu_dbm_insert(&db, key, contents, 1))
mu_error(_("Cannot insert datum `%-.*s' in "
"greylist database %s: %s"),
MU_DATUM_SIZE(key),
(char*)MU_DATUM_PTR(key),
greylist_format->dbname,
mu_dbm_strerror());
} else
debug(20, "database opened in readonly mode: "
"not updating");
rc = 1;
} else {
debug1(20, "%s finished greylisting period",
email);
rc = 0;
}
mu_dbm_datum_free(&contents);
} else if (!readonly) {
debug1(20, "greylisting %s", email);
MF_VAR_REF(greylist_seconds_left, interval);
MU_DATUM_PTR(contents) = (void*)&now;
MU_DATUM_SIZE(contents) = sizeof now;
if (mu_dbm_insert(&db, key, contents, 1))
mu_error(_("Cannot insert datum `%-.*s' in greylist "
"database %s: %s"),
MU_DATUM_SIZE(key), (char*)MU_DATUM_PTR(key),
greylist_format->dbname,
mu_dbm_strerror());
rc = 1;
} else
rc = 0;
mu_dbm_close(&db);
return rc;
}
/* Implementation of the is_greylisted predicate has no sense for
traditional greylist databases, because greylisting interval is
not known beforehand.
FIXME: keep the reference below up to date.
*/
static int
is_greylisted_traditional(eval_environ_t env, char *email)
{
MF_THROW(mfe_failure,
_("is_greylisted is not implemented for traditional greylist databases; "
"see documentation, chapter %s %s for more info"),
"4.12.1.23", "Greylisting functions");
return 0;
}
/* New greylist implementation (by Con Tassios): the database keeps
the time the greylisting period is set to expire (`interval' seconds
from now)
*/
static int
do_greylist_ct(eval_environ_t env, char *email, long interval)
{
int rc;
DBM_FILE db;
DBM_DATUM key;
DBM_DATUM contents;
int readonly;
time_t now;
rc = mu_dbm_open(greylist_format->dbname, &db, MU_STREAM_RDWR, 0600,
&readonly);
MF_ASSERT(rc == 0, mfe_dbfailure, _("mu_dbm_open(%s) failed: %s"),
greylist_format->dbname, mu_dbm_strerror());
memset(&key, 0, sizeof key);
memset(&contents, 0, sizeof contents);
MU_DATUM_PTR(key) = email;
MU_DATUM_SIZE(key) = strlen(email)+1;
time(&now);
if (mu_dbm_fetch(&db, key, &contents) == 0) {
time_t timestamp;
MF_ASSERT(MU_DATUM_SIZE(contents) == sizeof timestamp,
mfe_dbfailure,
_("Greylist database %s has wrong data size"),
greylist_format->dbname);
timestamp = *(time_t*) MU_DATUM_PTR(contents);
if (now < timestamp) {
time_t diff = timestamp - now;
MF_VAR_REF(greylist_seconds_left, diff);
debug2(20, "%s still greylisted (for %lu sec.)",
email,
(unsigned long) diff);
rc = 1;
} else if (now - timestamp >
greylist_format->expire_interval) {
debug1(20, "greylist record for %s expired", email);
MF_VAR_REF(greylist_seconds_left, interval);
if (!readonly) {
now += interval;
memcpy(MU_DATUM_PTR(contents),
&now, sizeof now);
if (mu_dbm_insert(&db, key, contents, 1))
mu_error(_("Cannot insert datum "
"`%-.*s' in greylist "
"database %s: %s"),
MU_DATUM_SIZE(key),
(char*)MU_DATUM_PTR(key),
greylist_format->dbname,
mu_dbm_strerror());
} else
debug(20, "database opened in readonly mode: "
"not updating");
rc = 1;
} else {
debug1(20, "%s finished greylisting period", email);
rc = 0;
}
mu_dbm_datum_free(&contents);
} else if (!readonly) {
debug1(20, "greylisting %s", email);
MF_VAR_REF(greylist_seconds_left, interval);
now += interval;
MU_DATUM_PTR(contents) = (void*)&now;
MU_DATUM_SIZE(contents) = sizeof now;
if (mu_dbm_insert(&db, key, contents, 1))
mu_error(_("Cannot insert datum `%-.*s' in greylist "
"database %s: %s"),
MU_DATUM_SIZE(key), (char*)MU_DATUM_PTR(key),
greylist_format->dbname,
mu_dbm_strerror());
rc = 1;
} else
rc = 0;
mu_dbm_close(&db);
return rc;
}
/* The `is_greylisted' predicate for new databases */
static int
is_greylisted_ct(eval_environ_t env, char *email)
{
int rc;
DBM_FILE db;
DBM_DATUM key;
DBM_DATUM contents;
int readonly;
time_t now;
rc = mu_dbm_open(greylist_format->dbname, &db, MU_STREAM_RDWR, 0600,
&readonly);
MF_ASSERT(rc == 0, mfe_dbfailure, _("mu_dbm_open(%s) failed: %s"),
greylist_format->dbname, mu_dbm_strerror());
memset(&key, 0, sizeof key);
memset(&contents, 0, sizeof contents);
MU_DATUM_PTR(key) = email;
MU_DATUM_SIZE(key) = strlen(email) + 1;
time(&now);
if (mu_dbm_fetch(&db, key, &contents) == 0) {
time_t timestamp;
MF_ASSERT(MU_DATUM_SIZE(contents) == sizeof timestamp,
mfe_dbfailure,
_("Greylist database %s has wrong data size"),
greylist_format->dbname);
timestamp = *(time_t*) MU_DATUM_PTR(contents);
rc = timestamp > now;
mu_dbm_datum_free(&contents);
} else
rc = 0;
mu_dbm_close(&db);
return rc;
}
struct greylist_class {
int (*gl_fun)(eval_environ_t, char *, long);
int (*gl_pred)(eval_environ_t, char *);
};
struct greylist_class greylist_class[] = {
{ do_greylist_traditional, is_greylisted_traditional },
{ do_greylist_ct, is_greylisted_ct }
};
/* greylist(key, interval)
Returns true if the key is greylisted, false if it's OK to
deliver mail.
*/
MF_DEFUN(greylist, NUMBER, STRING email, NUMBER interval)
{
MF_RETURN(greylist_class[greylist_semantics].gl_fun(env, email,
interval));
}
END
/* is_greylisted(key)
Returns true if the key is greylisted, otherwise false
*/
MF_DEFUN(is_greylisted, NUMBER, STRING email)
{
MF_RETURN(greylist_class[greylist_semantics].gl_pred(env, email));
}
END
MF_DEFUN(db_name, STRING, STRING fmtid)
{
struct db_format *fmt = db_format_lookup(fmtid);
MF_ASSERT(fmt != NULL,
mfe_not_found,
_("No such db format: %s"), fmtid);
MF_RETURN_STRING(fmt->dbname);
}
END
MF_DEFUN(db_get_active, NUMBER, STRING fmtid)
{
struct db_format *fmt = db_format_lookup(fmtid);
MF_ASSERT(fmt != NULL,
mfe_not_found,
_("No such db format: %s"), fmtid);
MF_RETURN(fmt->enabled);
}
END
MF_DEFUN(db_set_active, VOID, STRING fmtid, NUMBER active)
{
struct db_format *fmt = db_format_lookup(fmtid);
MF_ASSERT(fmt != NULL,
mfe_not_found,
_("No such db format: %s"), fmtid);
fmt->enabled = active;
}
END
MF_DEFUN(db_expire_interval, NUMBER, STRING fmtid)
{
struct db_format *fmt = db_format_lookup(fmtid);
MF_ASSERT(fmt != NULL,
mfe_not_found,
_("No such db format: %s"), fmtid);
MF_RETURN(fmt->expire_interval);
}
END
MF_INIT