--- /dev/null
+commit 2357aa78ccd7182cad14307eb89cb1065f078356
+Author: Jeremy Harris <jgh146exb@wizmail.org>
+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=<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);
+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:<tester@test.ex>
++RCPT TO:<a1@test.ex>
++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@test.ex>
++RCPT TO:<a1@test.ex>
++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@test.ex>
++RCPT TO:<a1@test.ex>
++QUIT
++****
++# default key should change with ip
++exim -DOPT='-1s' -bh HOSTIPV4
++HELO test
++MAIL FROM:<tester@test.ex>
++RCPT TO:<a1@test.ex>
++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@test.ex>
++RCPT TO:<a1@test.ex>
++QUIT
++****
++dump seen
++# check refresh
++sleep 1
++exim -DOPT='-1s / refresh=1s' -bh 127.0.0.1
++HELO test
++MAIL FROM:<tester@test.ex>
++RCPT TO:<a1@test.ex>
++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@test.ex>
++RCPT TO:<a1@test.ex>
++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@test.ex>
++RCPT TO:<a1@test.ex>
++QUIT
++****
++# having updated, should pass
++exim -DOPT='1s / key=${sender_host_address}_foo' -bh 127.0.0.1
++HELO test
++MAIL FROM:<tester@test.ex>
++RCPT TO:<a1@test.ex>
++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=<tester@test.ex> rejected RCPT <a1@test.ex>
++>>> 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@test.ex> rejected RCPT <a1@test.ex>
++>>> 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@test.ex> rejected RCPT <a1@test.ex>
++>>> 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@test.ex> rejected RCPT <a1@test.ex>
++>>> 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\r
++250 test.ex Hello test [127.0.0.1]\r
++250 OK\r
++550 Administrative prohibition\r
++221 test.ex closing connection\r
+++++++++++++++++++++++++++++
++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\r
++250 test.ex Hello test [127.0.0.1]\r
++250 OK\r
++250 Accepted\r
++221 test.ex closing connection\r
++
++**** 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\r
++250 test.ex Hello test [127.0.0.1]\r
++250 OK\r
++250 Accepted\r
++221 test.ex closing connection\r
++
++**** 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\r
++250 test.ex Hello test [ip4.ip4.ip4.ip4]\r
++250 OK\r
++550 Administrative prohibition\r
++221 test.ex closing connection\r
+++++++++++++++++++++++++++++
++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\r
++250 test.ex Hello test [127.0.0.1]\r
++250 OK\r
++550 Administrative prohibition\r
++221 test.ex closing connection\r
+++++++++++++++++++++++++++++
++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\r
++250 test.ex Hello test [127.0.0.1]\r
++250 OK\r
++250 Accepted\r
++221 test.ex closing connection\r
++
++**** 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\r
++250 test.ex Hello test [127.0.0.1]\r
++250 OK\r
++250 Accepted\r
++221 test.ex closing connection\r
++
++**** 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\r
++250 test.ex Hello test [127.0.0.1]\r
++250 OK\r
++550 Administrative prohibition\r
++221 test.ex closing connection\r
++
++**** 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\r
++250 test.ex Hello test [127.0.0.1]\r
++250 OK\r
++250 Accepted\r
++221 test.ex closing connection\r