Gitweb:
https://git.exim.org/exim.git/commitdiff/2357aa78ccd7182cad14307eb89cb1065f078356
Commit: 2357aa78ccd7182cad14307eb89cb1065f078356
Parent: 38089ca5c8f4c06092324099fc38494f8491b53c
Author: Jeremy Harris <jgh146exb@???>
AuthorDate: Sun Aug 1 18:15:39 2021 +0100
Committer: Jeremy Harris <jgh146exb@???>
CommitDate: Sun Aug 1 18:15:39 2021 +0100
ACL: "seen" condition
---
doc/doc-docbook/spec.xfpt | 61 ++++++++++++++++++
doc/doc-txt/NewStuff | 6 ++
src/src/acl.c | 143 +++++++++++++++++++++++++++++++++++++++++++
src/src/dbstuff.h | 6 ++
src/src/exim_dbutil.c | 12 +++-
test/confs/0626 | 31 ++++++++++
test/scripts/0000-Basic/0626 | 82 +++++++++++++++++++++++++
test/stderr/0626 | 142 ++++++++++++++++++++++++++++++++++++++++++
test/stdout/0626 | 99 ++++++++++++++++++++++++++++++
9 files changed, 581 insertions(+), 1 deletion(-)
diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index cea21a1..397096d 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -32315,6 +32315,14 @@ content-scanning extension, and is available only in the DATA, MIME, and
non-SMTP ACLs. It causes the incoming message to be scanned for a match with
any of the regular expressions. For details, see chapter &<<CHAPexiscan>>&.
+.new
+.vitem &*seen&~=&~*&<&'parameters'&>
+.cindex "&%sseen%& ACL condition"
+This condition can be used to test if a situation has been previously met,
+for example for greylisting.
+Details are given in section &<<SECTseen>>&.
+.wen
+
.vitem &*sender_domains&~=&~*&<&'domain&~list'&>
.cindex "&%sender_domains%& ACL condition"
.cindex "sender" "ACL checking"
@@ -33039,6 +33047,59 @@ address you should specify alternate list separators for both the outer
dnslists = <; dnsbl.example.com/<|$acl_m_addrslist
.endd
+
+.new
+.section "Previously seen user and hosts" "SECTseen"
+.cindex "&%sseen%& ACL condition"
+.cindex greylisting
+The &%seen%& ACL condition can be used to test whether a
+situation has been previously met.
+It uses a hints database to record a timestamp against a key.
+host. The syntax of the condition is:
+.display
+&`seen =`& <&'time interval'&> &`/`& <&'options'&>
+.endd
+
+For example,
+.code
+defer seen = -5m / key=${sender_host_address}_$local_part@$domain
+.endd
+in a RCPT ACL will implement simple greylisting.
+
+The parameters for the condition
+are an interval followed, slash-separated, by a list of options.
+The interval is taken as an offset before the current time,
+and used for the test.
+If the interval is preceded by a minus sign then the condition returns
+whether a record is found which is before the test time.
+Otherwise, the condition returns whether one is found which is since the
+test time.
+
+Options are read in order with later ones overriding earlier ones.
+
+The default key is &$sender_host_address$&.
+An explicit key can be set using a &%key=value%& option.
+
+If a &%readonly%& option is given then
+no record create or update is done.
+If a &%write%& option is given then
+a record create or update is always done.
+An update is done if the test is for &"since"&.
+
+Creates and updates are marked with the current time.
+
+Finally, a &"before"& test which succeeds, and for which the record
+is old enough, will be refreshed with a timstamp of the test time.
+This can prevent tidying of the database from removing the entry.
+The interval for this is, by default, 10 days.
+An explicit interval can be set using a
+&%refresh=value%& option.
+
+Note that &"seen"& should be added to the list of hints databases
+for maintenance if this ACL condition is used.
+.wen
+
+
.section "Rate limiting incoming messages" "SECTratelimiting"
.cindex "rate limiting" "client sending"
.cindex "limiting client sending rates"
diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff
index 6f3d4b3..478446b 100644
--- a/doc/doc-txt/NewStuff
+++ b/doc/doc-txt/NewStuff
@@ -6,6 +6,12 @@ Before a formal release, there may be quite a lot of detail so that people can
test from the snapshots or the Git before the documentation is updated. Once
the documentation is updated, this file is reduced to a short list.
+Version 4.96
+------------
+
+ 1. A new ACL condition: seen. Records/tests a timestamp against a key.
+
+
Version 4.95
------------
diff --git a/src/src/acl.c b/src/src/acl.c
index f47259c..be17b57 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 2f00dff..94db7f7 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 13f7454..45b778f 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);
diff --git a/test/confs/0626 b/test/confs/0626
new file mode 100644
index 0000000..872c4b2
--- /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 0000000..6da58ee
--- /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:<tester@???>
+RCPT TO:<a1@???>
+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:<tester@???>
+RCPT TO:<a1@???>
+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:<tester@???>
+RCPT TO:<a1@???>
+QUIT
+****
+# default key should change with ip
+exim -DOPT='-1s' -bh HOSTIPV4
+HELO test
+MAIL FROM:<tester@???>
+RCPT TO:<a1@???>
+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:<tester@???>
+RCPT TO:<a1@???>
+QUIT
+****
+dump seen
+# check refresh
+sleep 1
+exim -DOPT='-1s / refresh=1s' -bh 127.0.0.1
+HELO test
+MAIL FROM:<tester@???>
+RCPT TO:<a1@???>
+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:<tester@???>
+RCPT TO:<a1@???>
+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:<tester@???>
+RCPT TO:<a1@???>
+QUIT
+****
+# having updated, should pass
+exim -DOPT='1s / key=${sender_host_address}_foo' -bh 127.0.0.1
+HELO test
+MAIL FROM:<tester@???>
+RCPT TO:<a1@???>
+QUIT
+****
diff --git a/test/stderr/0626 b/test/stderr/0626
new file mode 100644
index 0000000..25e96bc
--- /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=<tester@???> rejected RCPT <a1@???>
+>>> 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=<tester@???> rejected RCPT <a1@???>
+>>> 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=<tester@???> rejected RCPT <a1@???>
+>>> 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=<tester@???> rejected RCPT <a1@???>
+>>> 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 0000000..44b481f
--- /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