/* This file is part of Mailfromd. -*- c -*- Copyright (C) 2006-2019 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 . */ MF_BUILTIN_MODULE #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 */ mu_url_t hint; /* Hint to use instead of the default one */ }; 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; } static int is_url(const char *p) { for (; *p && mu_isalnum(*p); p++) ; return *p == ':'; } /* #pragma dbprop [null] [mode] [hint] At least one of the bracketed parameters must be present. Two or more parameters can be given in arbitrary order. */ MF_PRAGMA(dbprop, 3, 5) { int null = 0; mode_t mode = DEFAULT_DB_MODE; struct db_prop *prop; int rc; char *pat; mu_url_t hint = NULL; --argc; pat = *++argv; while (--argc) { char *p = *++argv; if (strcmp(p, "null") == 0) null = 1; else if (mu_isdigit(*p)) { unsigned long n = strtoul(p, &p, 8); if (*p || (mode = n) != n) { parse_error(_("bad numeric file mode")); return; } } else if (is_url(p)) { rc = mu_url_create(&hint, p); if (rc) { parse_error(_("not a valid URL: %s"), mu_strerror(rc)); 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 = mu_alloc(sizeof(*prop)); prop->pat = mu_strdup(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; prop->hint = hint; } 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) == 0) { 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; } int _open_dbm(mu_dbm_file_t *pdb, char *dbname, int access, int mode, mu_url_t hint) { mu_dbm_file_t db; int rc; mu_url_t url; if (!hint) hint = mu_dbm_get_hint(); rc = mu_url_create_hint(&url, dbname, 0, hint); if (rc) { mu_error(_("cannot create database URL for %s: %s"), dbname, mu_strerror(rc)); return rc; } rc = mu_dbm_create_from_url(url, &db, mf_file_mode_to_safety_criteria(mode)); mu_url_destroy(&url); if (rc) return rc; rc = mu_dbm_safety_check(db); if (rc && rc != ENOENT) { mu_error(_("%s fails safety check: %s"), dbname, mu_strerror(rc)); mu_dbm_destroy(&db); return rc; } rc = mu_dbm_open(db, access, mode); if (rc) { mu_dbm_destroy(&db); return rc; } *pdb = db; return rc; } #define LOOKUP_NULL_BYTE 0x1 #define LOOKUP_TEST_ONLY 0x2 MF_DSEXP_SUPPRESS([],[< static int dbmap_lookup(eval_environ_t env, char *dbname, const char *keystr, const char *defval, const struct db_prop *prop, int flags) { int rc; mu_dbm_file_t db; struct mu_dbm_datum key; struct mu_dbm_datum contents; if (!defval) defval = ""; rc = _open_dbm(&db, dbname, MU_STREAM_READ, prop ? prop->mode : DEFAULT_DB_MODE, prop ? prop->hint : NULL); if (rc) MF_THROW(mfe_dbfailure, _("mf_dbm_open(%s) failed: %s"), dbname, mu_strerror(rc)); memset(&key, 0, sizeof key); memset(&contents, 0, sizeof contents); key.mu_dptr = (void*) keystr; key.mu_dsize = strlen(keystr); if (flags & LOOKUP_NULL_BYTE) key.mu_dsize++; rc = mu_dbm_fetch(db, &key, &contents); if (rc && rc != MU_ERR_NOENT) { mu_dbm_destroy(&db); MF_THROW(mfe_dbfailure, _("cannot fetch data: %s"), mu_dbm_strerror(db)); } MF_DEBUG(MU_DEBUG_TRACE5, ("Looking up %s: %s", keystr, rc ? "false" : "true")); if (flags & LOOKUP_TEST_ONLY) push(env, (STKVAL) (rc == 0)); else { if (rc) { if (defval) pushs(env, defval); else push(env, (STKVAL) 0L); } else if (((char*)contents.mu_dptr)[contents.mu_dsize-1]) { size_t off; size_t len = contents.mu_dsize; char *s = MF_ALLOC_HEAP(off, len + 1); memcpy(s, contents.mu_dptr, len); s[len] = 0; push(env, (STKVAL) off); } else pushs(env, contents.mu_dptr); } mu_dbm_datum_free(&contents); mu_dbm_destroy(&db); return rc; } >]) MF_DSEXP 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, prop, 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), prop, 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; mu_dbm_file_t db; struct mu_dbm_datum key; struct mu_dbm_datum contents; const struct db_prop *prop = db_prop_lookup(dbname); rc = _open_dbm(&db, dbname, MU_STREAM_RDWR, MF_OPTVAL(mode, (prop ? prop->mode : DEFAULT_DB_MODE)), prop ? prop->hint : NULL); if (rc) MF_THROW(mfe_dbfailure, _("mf_dbm_open(%s) failed: %s"), dbname, mu_strerror(rc)); memset(&key, 0, sizeof key); key.mu_dptr = keystr; key.mu_dsize = strlen(keystr); if (MF_OPTVAL(null, prop && prop->null)) key.mu_dsize++; memset(&contents, 0, sizeof contents); contents.mu_dptr = value; contents.mu_dsize = strlen(value) + 1; rc = mu_dbm_store(db, &key, &contents, 1); if (rc) { const char *errstr = mu_dbm_strerror(db); mu_dbm_destroy(&db); MF_THROW(mfe_dbfailure, _("failed to insert data to %s: %s %s: %s"), dbname, keystr, value, errstr); } mu_dbm_destroy(&db); } END MF_DEFUN(dbinsert, VOID, STRING dbname, STRING keystr, STRING value, OPTIONAL, NUMBER replace, NUMBER null, NUMBER mode) { int rc; mu_dbm_file_t db; struct mu_dbm_datum key; struct mu_dbm_datum contents; const struct db_prop *prop = db_prop_lookup(dbname); const char *errstr; rc = _open_dbm(&db, dbname, MU_STREAM_RDWR, MF_OPTVAL(mode, (prop ? prop->mode : DEFAULT_DB_MODE)), prop ? prop->hint : NULL); if (rc) MF_THROW(mfe_dbfailure, _("mf_dbm_open(%s) failed: %s"), dbname, mu_strerror(rc)); memset(&key, 0, sizeof key); key.mu_dptr = keystr; key.mu_dsize = strlen(keystr); if (MF_OPTVAL(null, prop && prop->null)) key.mu_dsize++; memset(&contents, 0, sizeof contents); contents.mu_dptr = value; contents.mu_dsize = strlen(value) + 1; rc = mu_dbm_store(db, &key, &contents, MF_OPTVAL(replace)); if (rc && rc != MU_ERR_EXISTS) errstr = mu_dbm_strerror(db); mu_dbm_destroy(&db); if (rc == MU_ERR_EXISTS) MF_THROW(mfe_exists, _("record already exists")); MF_ASSERT(rc == 0, mfe_dbfailure, _("failed to insert data to %s: %s %s: %s"), dbname, keystr, value, errstr); } END MF_DEFUN(dbdel, VOID, STRING dbname, STRING keystr, OPTIONAL, NUMBER null, NUMBER mode) { mu_dbm_file_t db; struct mu_dbm_datum key; int rc; const struct db_prop *prop = db_prop_lookup(dbname); rc = _open_dbm(&db, dbname, MU_STREAM_RDWR, MF_OPTVAL(mode, (prop ? prop->mode : DEFAULT_DB_MODE)), prop ? prop->hint : NULL); MF_ASSERT(rc == 0, mfe_dbfailure, _("mf_dbm_open(%s) failed: %s"), dbname, mu_strerror(rc)); memset(&key, 0, sizeof key); key.mu_dptr = keystr; key.mu_dsize = strlen(keystr); if (MF_OPTVAL(null, prop && prop->null)) key.mu_dsize++; rc = mu_dbm_delete(db, &key); mu_dbm_destroy(&db); MF_ASSERT(rc == 0 || rc == MU_ERR_NOENT, mfe_dbfailure, _("failed to delete data `%s' from `%s': %s"), keystr, dbname, mu_strerror(rc)); } END #define NUMDB 128 struct db_tab { int used; mu_dbm_file_t db; struct mu_dbm_datum key; }; static void * alloc_db_tab() { return mu_calloc(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_destroy(&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; mu_dbm_file_t db; struct mu_dbm_datum key; const struct db_prop *prop = db_prop_lookup(dbname); rc = _open_dbm(&db, dbname, MU_STREAM_READ, prop ? prop->mode : DEFAULT_DB_MODE, prop ? prop->hint : NULL); MF_ASSERT(rc == 0, mfe_dbfailure, _("mf_dbm_open(%s) failed: %s"), dbname, mu_strerror(rc)); memset(&key, 0, sizeof key); rc = mu_dbm_firstkey(db, &key); if (rc) { if (rc == MU_ERR_NOENT) { mu_dbm_destroy(&db); MF_RETURN(0); } else if (rc) { mu_dbm_destroy(&db); MF_THROW(mfe_dbfailure, _("mf_dbm_firstkey failed: %s"), mu_strerror(rc)); } } 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; struct mu_dbm_datum nextkey; int rc; MF_ASSERT(dn >= 0 && dn < NUMDB && dbt->used, mfe_range, _("invalid database descriptor")); memset (&nextkey, 0, sizeof nextkey); rc = mu_dbm_nextkey(dbt->db, &nextkey); if (rc) { if (rc == MU_ERR_FAILURE) mu_error(_("mu_dbm_nextkey: %s"), mu_dbm_strerror(dbt->db)); close_db_tab(dbt); MF_RETURN(0); } mu_dbm_datum_free(&nextkey); 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 = dbt->key.mu_dsize; s = MF_ALLOC_HEAP(off, len + 1); memcpy(s, dbt->key.mu_dptr, len); s[len] = 0; MF_RETURN(off, size); } END MF_DEFUN(dbvalue, STRING, NUMBER dn) { int rc; size_t off, len; char *s; struct db_tab *dbt = MF_GET_DATA + dn; struct mu_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, _("cannot fetch key: %s"), rc == MU_ERR_FAILURE ? mu_dbm_strerror(dbt->db) : mu_strerror (rc)); len = contents.mu_dsize; s = MF_ALLOC_HEAP(off, len + 1); memcpy(s, contents.mu_dptr, len); s[len] = 0; mu_dbm_datum_free(&contents); MF_RETURN(off, size); } 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)")); } /* FIXME: Duplicated in lib/cache.c */ static char * format_timestr(time_t timestamp, char *timebuf, size_t bufsize) { struct tm tm; gmtime_r(×tamp, &tm); strftime(timebuf, bufsize, "%c", &tm); return timebuf; } 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; mu_dbm_file_t db; struct mu_dbm_datum key; struct mu_dbm_datum contents; int readonly = 0; time_t now; rc = _open_dbm(&db, greylist_format->dbname, MU_STREAM_RDWR, 0600, NULL); if (rc) { rc = _open_dbm(&db, greylist_format->dbname, MU_STREAM_READ, 0600, NULL); readonly = 1; } MF_ASSERT(rc == 0, mfe_dbfailure, _("mf_dbm_open(%s) failed: %s"), greylist_format->dbname, mu_strerror(rc)); memset(&key, 0, sizeof key); memset(&contents, 0, sizeof contents); key.mu_dptr = email; key.mu_dsize = strlen(email)+1; time(&now); rc = mu_dbm_fetch(db, &key, &contents); if (rc == 0) { time_t timestamp, diff; MF_ASSERT(contents.mu_dsize == sizeof timestamp, mfe_dbfailure, _("greylist database %s has wrong data size"), greylist_format->dbname); timestamp = *(time_t*) contents.mu_dptr; diff = now - timestamp; if (mu_debug_level_p(debug_handle, MU_DEBUG_TRACE5)) { char timebuf[32]; mu_debug_log("%s entered greylist database on %s, " "%ld seconds ago", email, format_timestr(timestamp, timebuf, sizeof timebuf), (long) diff); } if (diff < interval) { diff = interval - diff; MF_VAR_REF(greylist_seconds_left, ulong, diff);//FIXME MF_DEBUG(MU_DEBUG_TRACE6, ("%s still greylisted (for %lu sec.)", email, (unsigned long) diff)); rc = 1; } else if (diff > greylist_format->expire_interval) { MF_DEBUG(MU_DEBUG_TRACE6, ("greylist record for %s expired", email)); MF_VAR_REF(greylist_seconds_left, long, interval); if (!readonly) { memcpy(contents.mu_dptr, &now, sizeof now); rc = mu_dbm_store(db, &key, &contents, 1); if (rc) mu_error(_("cannot insert datum `%-.*s' in " "greylist database %s: %s"), (int)key.mu_dsize, (char*)key.mu_dptr, greylist_format->dbname, rc == MU_ERR_FAILURE ? mu_dbm_strerror(db) : mu_strerror(rc)); } else MF_DEBUG(MU_DEBUG_TRACE6, ("database opened in readonly mode: " "not updating")); rc = 1; } else { MF_DEBUG(MU_DEBUG_TRACE6, ("%s finished greylisting period", email)); rc = 0; } mu_dbm_datum_free(&contents); } else if (!readonly) { MF_DEBUG(MU_DEBUG_TRACE6, ("greylisting %s", email)); MF_VAR_REF(greylist_seconds_left, long, interval); contents.mu_dptr = (void*)&now; contents.mu_dsize = sizeof now; rc = mu_dbm_store(db, &key, &contents, 1); if (rc) mu_error(_("Cannot insert datum `%-.*s' in greylist " "database %s: %s"), (int)key.mu_dsize, (char*)key.mu_dptr, greylist_format->dbname, rc == MU_ERR_FAILURE ? mu_dbm_strerror(db) : mu_strerror(rc)); rc = 1; } else rc = 0; mu_dbm_destroy(&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; mu_dbm_file_t db; struct mu_dbm_datum key; struct mu_dbm_datum contents; int readonly = 0; time_t now; rc = _open_dbm(&db, greylist_format->dbname, MU_STREAM_RDWR, 0600, NULL); if (rc) { rc = _open_dbm(&db, greylist_format->dbname, MU_STREAM_READ, 0600, NULL); readonly = 1; } MF_ASSERT(rc == 0, mfe_dbfailure, _("mf_dbm_open(%s) failed: %s"), greylist_format->dbname, mu_strerror(rc)); memset(&key, 0, sizeof key); memset(&contents, 0, sizeof contents); key.mu_dptr = email; key.mu_dsize = strlen(email) + 1; time(&now); rc = mu_dbm_fetch(db, &key, &contents); if (rc == 0) { time_t timestamp; MF_ASSERT(contents.mu_dsize == sizeof timestamp, mfe_dbfailure, _("greylist database %s has wrong data size"), greylist_format->dbname); timestamp = *(time_t*) contents.mu_dptr; if (now < timestamp) { time_t diff = timestamp - now; MF_VAR_REF(greylist_seconds_left, long, diff); MF_DEBUG(MU_DEBUG_TRACE6, ("%s still greylisted (for %lu sec.)", email, (unsigned long) diff)); rc = 1; } else if (now - timestamp > greylist_format->expire_interval) { MF_DEBUG(MU_DEBUG_TRACE6, ("greylist record for %s expired", email)); MF_VAR_REF(greylist_seconds_left, long, interval); if (!readonly) { now += interval; memcpy(contents.mu_dptr, &now, sizeof now); rc = mu_dbm_store(db, &key, &contents, 1); if (rc) mu_error(_("Cannot insert datum " "`%-.*s' in greylist " "database %s: %s"), (int)key.mu_dsize, (char*)key.mu_dptr, greylist_format->dbname, rc == MU_ERR_FAILURE ? mu_dbm_strerror(db) : mu_strerror(rc)); } else MF_DEBUG(MU_DEBUG_TRACE6, ("database opened in readonly mode: " "not updating")); rc = 1; } else { MF_DEBUG(MU_DEBUG_TRACE6, ("%s finished greylisting period", email)); rc = 0; } mu_dbm_datum_free(&contents); } else if (!readonly) { MF_DEBUG(MU_DEBUG_TRACE6, ("greylisting %s", email)); MF_VAR_REF(greylist_seconds_left, long, interval); now += interval; contents.mu_dptr = (void*)&now; contents.mu_dsize = sizeof now; rc = mu_dbm_store(db, &key, &contents, 1); if (rc) mu_error(_("Cannot insert datum `%-.*s' in greylist " "database %s: %s"), (int)key.mu_dsize, (char*)key.mu_dptr, greylist_format->dbname, rc == MU_ERR_FAILURE ? mu_dbm_strerror(db) : mu_strerror(rc)); rc = 1; } else rc = 0; mu_dbm_destroy(&db); return rc; } /* The `is_greylisted' predicate for new databases */ static int is_greylisted_ct(eval_environ_t env, char *email) { int rc; mu_dbm_file_t db; struct mu_dbm_datum key; struct mu_dbm_datum contents; time_t now; rc = _open_dbm(&db, greylist_format->dbname, MU_STREAM_RDWR, 0600, NULL); if (rc) rc = _open_dbm(&db, greylist_format->dbname, MU_STREAM_READ, 0600, NULL); MF_ASSERT(rc == 0, mfe_dbfailure, _("mf_dbm_open(%s) failed: %s"), greylist_format->dbname, mu_strerror(rc)); memset(&key, 0, sizeof key); memset(&contents, 0, sizeof contents); key.mu_dptr = email; key.mu_dsize = strlen(email) + 1; time(&now); rc = mu_dbm_fetch(db, &key, &contents); if (rc == 0) { time_t timestamp; MF_ASSERT(contents.mu_dsize == sizeof timestamp, mfe_dbfailure, _("greylist database %s has wrong data size"), greylist_format->dbname); timestamp = *(time_t*) contents.mu_dptr; rc = timestamp > now; if (rc) MF_VAR_REF(greylist_seconds_left, long, timestamp - now); mu_dbm_datum_free(&contents); } else rc = 0; mu_dbm_destroy(&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(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