summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJeremy Harris <jgh146exb@wizmail.org>2021-08-01 18:15:39 +0100
committerJeremy Harris <jgh146exb@wizmail.org>2021-08-01 18:15:39 +0100
commit2357aa78ccd7182cad14307eb89cb1065f078356 (patch)
tree480b8913a6d153400e8f85b418263f18b25e6f86 /src
parent38089ca5c8f4c06092324099fc38494f8491b53c (diff)
ACL: "seen" condition
Diffstat (limited to 'src')
-rw-r--r--src/src/acl.c143
-rw-r--r--src/src/dbstuff.h6
-rw-r--r--src/src/exim_dbutil.c12
3 files changed, 160 insertions, 1 deletions
diff --git a/src/src/acl.c b/src/src/acl.c
index f47259ca0..be17b5768 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -103,6 +103,7 @@ enum { ACLC_ACL,
ACLC_REGEX,
#endif
ACLC_REMOVE_HEADER,
+ ACLC_SEEN,
ACLC_SENDER_DOMAINS,
ACLC_SENDERS,
ACLC_SET,
@@ -288,6 +289,7 @@ static condition_def conditions[] = {
ACL_BIT_MIME | ACL_BIT_NOTSMTP |
ACL_BIT_NOTSMTP_START),
},
+ [ACLC_SEEN] = { US"seen", TRUE, FALSE, 0 },
[ACLC_SENDER_DOMAINS] = { US"sender_domains", FALSE, FALSE,
ACL_BIT_AUTH | ACL_BIT_CONNECT |
ACL_BIT_HELO |
@@ -2816,6 +2818,143 @@ return rc;
/*************************************************
+* Handle a check for previously-seen *
+*************************************************/
+
+/*
+ACL clauses like: seen = -5m / key=$foo / readonly
+
+Return is true for condition-true - but the semantics
+depend heavily on the actual use-case.
+
+Negative times test for seen-before, positive for seen-more-recently-than
+(the given interval before current time).
+
+All are subject to history not having been cleaned from the DB.
+
+Default for seen-before is to create if not present, and to
+update if older than 10d (with the seen-test time).
+Default for seen-since is to always create or update.
+
+Options:
+ key=value. Default key is $sender_host_address
+ readonly
+ write
+ refresh=<interval>: update an existing DB entry older than given
+ amount. Default refresh lacking this option is 10d.
+ The update sets the record timestamp to the seen-test time.
+
+XXX do we need separate nocreate, noupdate controls?
+
+Arguments:
+ arg the option string for seen=
+ where ACL_WHERE_xxxx indicating which ACL this is
+ log_msgptr for error messages
+
+Returns: OK - Condition is true
+ FAIL - Condition is false
+ DEFER - Problem opening history database
+ ERROR - Syntax error in options
+*/
+
+static int
+acl_seen(const uschar * arg, int where, uschar ** log_msgptr)
+{
+enum { SEEN_DEFAULT, SEEN_READONLY, SEEN_WRITE };
+
+const uschar * list = arg;
+int slash = '/', equal = '=', interval, mode = SEEN_DEFAULT, yield = FAIL;
+BOOL before;
+int refresh = 10 * 24 * 60 * 60; /* 10 days */
+const uschar * ele, * key = sender_host_address;
+open_db dbblock, * dbm;
+dbdata_seen * dbd;
+time_t now;
+
+/* Parse the first element, the time-relation. */
+
+if (!(ele = string_nextinlist(&list, &slash, NULL, 0)))
+ goto badparse;
+if ((before = *ele == '-'))
+ ele++;
+if ((interval = readconf_readtime(ele, 0, FALSE)) < 0)
+ goto badparse;
+
+/* Remaining elements are options */
+
+while ((ele = string_nextinlist(&list, &slash, NULL, 0)))
+ if (Ustrncmp(ele, "key=", 4) == 0)
+ key = ele + 4;
+ else if (Ustrcmp(ele, "readonly") == 0)
+ mode = SEEN_READONLY;
+ else if (Ustrcmp(ele, "write") == 0)
+ mode = SEEN_WRITE;
+ else if (Ustrncmp(ele, "refresh=", 8) == 0)
+ {
+ if ((refresh = readconf_readtime(ele + 8, 0, FALSE)) < 0)
+ goto badparse;
+ }
+ else
+ goto badopt;
+
+if (!(dbm = dbfn_open(US"seen", O_RDWR, &dbblock, TRUE, TRUE)))
+ {
+ HDEBUG(D_acl) debug_printf_indent("database for 'seen' not available\n");
+ *log_msgptr = US"database for 'seen' not available";
+ return DEFER;
+ }
+
+dbd = dbfn_read_with_length(dbm, key, NULL);
+now = time(NULL);
+if (dbd) /* an existing record */
+ {
+ time_t diff = now - dbd->time_stamp; /* time since the record was written */
+
+ if (before ? diff >= interval : diff < interval)
+ yield = OK;
+
+ if (mode == SEEN_READONLY)
+ { HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n"); }
+ else if (mode == SEEN_WRITE || !before)
+ {
+ dbd->time_stamp = now;
+ dbfn_write(dbm, key, dbd, sizeof(*dbd));
+ HDEBUG(D_acl) debug_printf_indent("seen db written (update)\n");
+ }
+ else if (diff >= refresh)
+ {
+ dbd->time_stamp = now - interval;
+ dbfn_write(dbm, key, dbd, sizeof(*dbd));
+ HDEBUG(D_acl) debug_printf_indent("seen db written (refresh)\n");
+ }
+ }
+else
+ { /* No record found, yield always FAIL */
+ if (mode != SEEN_READONLY)
+ {
+ dbdata_seen d = {.time_stamp = now};
+ dbfn_write(dbm, key, &d, sizeof(*dbd));
+ HDEBUG(D_acl) debug_printf_indent("seen db written (create)\n");
+ }
+ else
+ HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n");
+ }
+
+dbfn_close(dbm);
+return yield;
+
+
+badparse:
+ *log_msgptr = string_sprintf("failed to parse '%s'", arg);
+ return ERROR;
+badopt:
+ *log_msgptr = string_sprintf("unrecognised option '%s' in '%s'", ele, arg);
+ return ERROR;
+}
+
+
+
+/*************************************************
* The udpsend ACL modifier *
*************************************************/
@@ -3740,6 +3879,10 @@ for (; cb; cb = cb->next)
setup_remove_header(arg);
break;
+ case ACLC_SEEN:
+ rc = acl_seen(arg, where, log_msgptr);
+ break;
+
case ACLC_SENDER_DOMAINS:
{
uschar *sdomain;
diff --git a/src/src/dbstuff.h b/src/src/dbstuff.h
index 2f00dffb4..94db7f7fd 100644
--- a/src/src/dbstuff.h
+++ b/src/src/dbstuff.h
@@ -788,6 +788,12 @@ typedef struct {
uschar bloom[40]; /* Bloom filter which may be larger than this */
} dbdata_ratelimit_unique;
+
+/* For "seen" ACL condition */
+typedef struct {
+ time_t time_stamp;
+} dbdata_seen;
+
#ifndef DISABLE_PIPE_CONNECT
/* This structure records the EHLO responses, cleartext and crypted,
for an IP, as bitmasks (cf. OPTION_TLS). For LIMITS, also values
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 13f74540e..45b778fc0 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -21,7 +21,9 @@ argument is the name of the database file. The available names are:
misc: miscellaneous hints data
wait-<t>: message waiting information; <t> is a transport name
callout: callout verification cache
+ ratelimit: ACL 'ratelimit' condition
tls: TLS session resumption cache
+ seen: ACL 'seen' condition
There are a number of common subroutines, followed by three main programs,
whose inclusion is controlled by -D on the compilation command. */
@@ -38,6 +40,7 @@ whose inclusion is controlled by -D on the compilation command. */
#define type_callout 4
#define type_ratelimit 5
#define type_tls 6
+#define type_seen 7
/* This is used by our cut-down dbfn_open(). */
@@ -126,7 +129,7 @@ static void
usage(uschar *name, uschar *options)
{
printf("Usage: exim_%s%s <spool-directory> <database-name>\n", name, options);
-printf(" <database-name> = retry | misc | wait-<transport-name> | callout | ratelimit | tls\n");
+printf(" <database-name> = retry | misc | wait-<transport-name> | callout | ratelimit | tls | seen\n");
exit(1);
}
@@ -150,6 +153,7 @@ if (argc == 3)
if (Ustrcmp(argv[2], "callout") == 0) return type_callout;
if (Ustrcmp(argv[2], "ratelimit") == 0) return type_ratelimit;
if (Ustrcmp(argv[2], "tls") == 0) return type_tls;
+ if (Ustrcmp(argv[2], "seen") == 0) return type_seen;
}
usage(name, options);
return -1; /* Never obeyed */
@@ -581,6 +585,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
dbdata_ratelimit *ratelimit;
dbdata_ratelimit_unique *rate_unique;
dbdata_tls_session *session;
+ dbdata_seen *seen;
int count_bad = 0;
int length;
uschar *t;
@@ -720,6 +725,11 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
session = (dbdata_tls_session *)value;
printf(" %s %.*s\n", keybuffer, length, session->session);
break;
+
+ case type_seen:
+ seen = (dbdata_seen *)value;
+ printf("%s\t%s\n", keybuffer, print_time(seen->time_stamp));
+ break;
}
}
store_reset(reset_point);