commit 2357aa78ccd7182cad14307eb89cb1065f078356 Author: Jeremy Harris Date: Sun Aug 1 18:15:39 2021 +0100 ACL: "seen" condition 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 | @@ -2815,6 +2817,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=: 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-: message waiting information; 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 \n", name, options); -printf(" = retry | misc | wait- | callout | ratelimit | tls\n"); +printf(" = retry | misc | wait- | 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); diff --git a/test/confs/0626 b/test/confs/0626 new file mode 100644 index 000000000..872c4b20a --- /dev/null +++ b/test/confs/0626 @@ -0,0 +1,31 @@ +# Exim test configuration 0626 +# ACL seen condition + +.include DIR/aux-var/std_conf_prefix + + +# ----- Main settings ----- + +primary_hostname = test.ex +queue_only + +acl_smtp_rcpt = chk_rcpt + +# ----- ACL ----- + +begin acl + +chk_rcpt: + accept seen = OPT + +# seen = never / $sender_host_addreee / per_call +# seen = before=10s +# seen = before=10s / write +# seen = since / readonly +# +# seen = -10s +# seen = -10s / readonly +# seen = 2s +# seen = 0s / update=20d +# +# End diff --git a/test/scripts/0000-Basic/0626 b/test/scripts/0000-Basic/0626 new file mode 100644 index 000000000..6da58ee48 --- /dev/null +++ b/test/scripts/0000-Basic/0626 @@ -0,0 +1,82 @@ +# ACL 'seen' condition +# +exim -DOPT='-1s' -bh 127.0.0.1 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** +# Check that a hints DB was created. +# Only the key is useful thanks to munging; should match the IP used above. +dump seen +# +sleep 1 +# should now see old-enough record +exim -DOPT='-1s' -bh 127.0.0.1 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** +# force an update (visible via debug output in stdout for -bh) +exim -DOPT='-1s / write' -bh 127.0.0.1 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** +# default key should change with ip +exim -DOPT='-1s' -bh HOSTIPV4 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** +dump seen +# explicit key (also checking expansion) +exim -DOPT='-1s / key=${sender_host_address}_foo' -bh 127.0.0.1 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** +dump seen +# check refresh +sleep 1 +exim -DOPT='-1s / refresh=1s' -bh 127.0.0.1 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** +# +# +# +# +# +# test for seen-more-recently-than +# that previous one should be no older than 5s, so this should pass +# do not update +# check list-parsing spaceless while we're here +exim -DOPT='5s/key=${sender_host_address}_foo/readonly' -bh 127.0.0.1 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** +# check the above no-update by waiting longer than the later-than interval; should fail +# should update +sleep 2 +exim -DOPT='1s / key=${sender_host_address}_foo' -bh 127.0.0.1 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** +# having updated, should pass +exim -DOPT='1s / key=${sender_host_address}_foo' -bh 127.0.0.1 +HELO test +MAIL FROM: +RCPT TO: +QUIT +**** diff --git a/test/stderr/0626 b/test/stderr/0626 new file mode 100644 index 000000000..25e96bc4e --- /dev/null +++ b/test/stderr/0626 @@ -0,0 +1,142 @@ +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = -1s +>>> seen db written (create) +>>> accept: condition test failed in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": implicit DENY +LOG: H=(test) [127.0.0.1] F= rejected RCPT +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = -1s +>>> accept: condition test succeeded in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": ACCEPT +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = -1s / write +>>> seen db written (update) +>>> accept: condition test succeeded in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": ACCEPT +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = -1s +>>> seen db written (create) +>>> accept: condition test failed in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": implicit DENY +LOG: H=(test) [ip4.ip4.ip4.ip4] F= rejected RCPT +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = -1s / key=${sender_host_address}_foo +>>> = -1s / key=127.0.0.1_foo +>>> seen db written (create) +>>> accept: condition test failed in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": implicit DENY +LOG: H=(test) [127.0.0.1] F= rejected RCPT +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = -1s / refresh=1s +>>> seen db written (refresh) +>>> accept: condition test succeeded in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": ACCEPT +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = 5s/key=${sender_host_address}_foo/readonly +>>> = 5s/key=127.0.0.1_foo/readonly +>>> seen db not written (readonly) +>>> accept: condition test succeeded in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": ACCEPT +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = 1s / key=${sender_host_address}_foo +>>> = 1s / key=127.0.0.1_foo +>>> seen db written (update) +>>> accept: condition test failed in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": implicit DENY +LOG: H=(test) [127.0.0.1] F= rejected RCPT +>>> host in hosts_connection_nolog? no (option unset) +>>> host in host_lookup? no (option unset) +>>> host in host_reject_connection? no (option unset) +>>> host in sender_unqualified_hosts? no (option unset) +>>> host in recipient_unqualified_hosts? no (option unset) +>>> host in helo_verify_hosts? no (option unset) +>>> host in helo_try_verify_hosts? no (option unset) +>>> host in helo_accept_junk_hosts? no (option unset) +>>> test in helo_lookup_domains? no (end of list) +>>> using ACL "chk_rcpt" +>>> processing "accept" (TESTSUITE/test-config 19) +>>> check seen = 1s / key=${sender_host_address}_foo +>>> = 1s / key=127.0.0.1_foo +>>> seen db written (update) +>>> accept: condition test succeeded in ACL "chk_rcpt" +>>> end of ACL "chk_rcpt": ACCEPT diff --git a/test/stdout/0626 b/test/stdout/0626 new file mode 100644 index 000000000..44b481f31 --- /dev/null +++ b/test/stdout/0626 @@ -0,0 +1,99 @@ + +**** SMTP testing session as if from host 127.0.0.1 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [127.0.0.1] +250 OK +550 Administrative prohibition +221 test.ex closing connection ++++++++++++++++++++++++++++ +127.0.0.1 07-Mar-2000 12:21:52 + +**** SMTP testing session as if from host 127.0.0.1 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [127.0.0.1] +250 OK +250 Accepted +221 test.ex closing connection + +**** SMTP testing session as if from host 127.0.0.1 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [127.0.0.1] +250 OK +250 Accepted +221 test.ex closing connection + +**** SMTP testing session as if from host ip4.ip4.ip4.ip4 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [ip4.ip4.ip4.ip4] +250 OK +550 Administrative prohibition +221 test.ex closing connection ++++++++++++++++++++++++++++ +ip4.ip4.ip4.ip4 07-Mar-2000 12:21:52 +127.0.0.1 07-Mar-2000 12:21:52 + +**** SMTP testing session as if from host 127.0.0.1 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [127.0.0.1] +250 OK +550 Administrative prohibition +221 test.ex closing connection ++++++++++++++++++++++++++++ +127.0.0.1_foo 07-Mar-2000 12:21:52 +ip4.ip4.ip4.ip4 07-Mar-2000 12:21:52 +127.0.0.1 07-Mar-2000 12:21:52 + +**** SMTP testing session as if from host 127.0.0.1 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [127.0.0.1] +250 OK +250 Accepted +221 test.ex closing connection + +**** SMTP testing session as if from host 127.0.0.1 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [127.0.0.1] +250 OK +250 Accepted +221 test.ex closing connection + +**** SMTP testing session as if from host 127.0.0.1 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [127.0.0.1] +250 OK +550 Administrative prohibition +221 test.ex closing connection + +**** SMTP testing session as if from host 127.0.0.1 +**** but without any ident (RFC 1413) callback. +**** This is not for real! + +220 test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000 +250 test.ex Hello test [127.0.0.1] +250 OK +250 Accepted +221 test.ex closing connection