[exim-cvs] MySQL, pgsql: per-query server options outside t…

Top Page
Delete this message
Reply to this message
Author: Exim Git Commits Mailing List
Date:  
To: exim-cvs
Subject: [exim-cvs] MySQL, pgsql: per-query server options outside the lookup string. Bug 2546
Gitweb: https://git.exim.org/exim.git/commitdiff/0b4dfe7aa1f12214abdfa1037497d6c49a471612
Commit:     0b4dfe7aa1f12214abdfa1037497d6c49a471612
Parent:     ba74fb8d95d2e9af2122e0a95c4d5334b4f0466c
Author:     Jeremy Harris <jgh146exb@???>
AuthorDate: Mon Apr 6 00:28:06 2020 +0100
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Mon Apr 6 13:14:05 2020 +0100


    MySQL, pgsql: per-query server options outside the lookup string.  Bug 2546
---
 doc/doc-docbook/spec.xfpt       |  29 +++++-
 doc/doc-txt/NewStuff            |   7 +-
 src/src/acl.c                   |   9 +-
 src/src/lookups/lf_functions.h  |   5 +-
 src/src/lookups/lf_sqlperform.c | 118 +++++++++++++++--------
 src/src/lookups/mysql.c         |  31 +++---
 src/src/lookups/pgsql.c         |   5 +-
 src/src/lookups/redis.c         |   5 +-
 src/src/string.c                |   4 -
 src/src/verify.c                |   7 +-
 test/confs/2610                 |  27 +++++-
 test/confs/2620                 |  26 ++++-
 test/stderr/2610                | 155 +++++++++++++++++++++++++-----
 test/stderr/2620                | 206 ++++++++++++++++++++++++++++++++++------
 14 files changed, 499 insertions(+), 135 deletions(-)


diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index 9a7f911..8e7cb4d 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -8016,12 +8016,14 @@ The &%quote_redis%& expansion operator
escapes whitespace and backslash characters with a backslash.

.section "Specifying the server in the query" "SECTspeserque"
+.new
For MySQL, PostgreSQL and Redis lookups (but not currently for Oracle and InterBase),
it is possible to specify a list of servers with an individual query. This is
-done by starting the query with
+done by appending a comma-separated option to the query type:
.display
-&`servers=`&&'server1:server2:server3:...'&&`;`&
.endd
+&`,servers=`&&'server1:server2:server3:...'&
+.wen
Each item in the list may take one of two forms:
.olist
If it contains no slashes it is assumed to be just a host name. The appropriate
@@ -8046,14 +8048,25 @@ mysql_servers = slave1/db/name/pw:\
.endd
In an updating lookup, you could then write:
.code
-${lookup mysql{servers=master; UPDATE ...} }
+${lookup mysql,servers=master {UPDATE ...} }
.endd
That query would then be sent only to the master server. If, on the other hand,
the master is not to be used for reading, and so is not present in the global
option, you can still update it by a query of this form:
.code
-${lookup pgsql{servers=master/db/name/pw; UPDATE ...} }
+${lookup pgsql,servers=master/db/name/pw {UPDATE ...} }
+.endd
+
+.new
+An older syntax places the servers speciification before the qury,
+semicolon separated:
+.code
+${lookup mysql{servers=master; UPDATE ...} }
.endd
+The new version avoids potential issues with tainted
+arguments in the query, for explicit expansion.
+&*Note*&: server specifications in list-style lookups are still problematic.
+.wen


 .section "Special MySQL features" "SECID73"
@@ -8619,6 +8632,14 @@ whether or not the query succeeds. However, when a lookup is used for the
 &%domains%& option on a router, the data is preserved in the &$domain_data$&
 variable and can be referred to in other options.
 .next
+.new
+If the pattern starts with the name of a lookup type
+of either kind (single-key or query-style) it may be
+followed by a command and options,
+The options are lookup-type specific and consist of a comma-separated list.
+Each item starts with a tag and and equals "=".
+.wen
+.next
 .cindex "domain list" "matching literal domain name"
 If none of the above cases apply, a caseless textual comparison is made
 between the pattern and the domain.
diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff
index 1573f34..80d3525 100644
--- a/doc/doc-txt/NewStuff
+++ b/doc/doc-txt/NewStuff
@@ -47,10 +47,13 @@ Version 4.94
     lookup string.  The older method fails when tainted variables are used
     in the lookup, as the filename becomes tainted.  The new method keeps the
     filename separate.
-12. An option on the dsearch lookup, to return the full path.
-12. Options on the dsearch lookup, to return the full path and to filter
+
+13. Options on the dsearch lookup, to return the full path and to filter
     filetypes for matching.


+14. Options on pgsql and mysql lookups, to specify server separate from the
+    lookup string.
+



 Version 4.93
diff --git a/src/src/acl.c b/src/src/acl.c
index 02251b1..3ea8df1 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -3477,13 +3477,13 @@ for (; cb; cb = cb->next)
       {
       uschar *endcipher = NULL;
       uschar *cipher = Ustrchr(tls_in.cipher, ':');
-      if (cipher == NULL) cipher = tls_in.cipher; else
+      if (!cipher) cipher = tls_in.cipher; else
         {
         endcipher = Ustrchr(++cipher, ':');
-        if (endcipher != NULL) *endcipher = 0;
+        if (endcipher) *endcipher = 0;
         }
       rc = match_isinlist(cipher, &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-      if (endcipher != NULL) *endcipher = ':';
+      if (endcipher) *endcipher = ':';
       }
     break;


@@ -3496,8 +3496,7 @@ for (; cb; cb = cb->next)

     case ACLC_HOSTS:
     rc = verify_check_this_host(&arg, sender_host_cache, NULL,
-      (sender_host_address == NULL)? US"" : sender_host_address,
-      CUSS &host_data);
+      sender_host_address ? sender_host_address : US"", CUSS &host_data);
     if (rc == DEFER) *log_msgptr = search_error_message;
     if (host_data) host_data = string_copy_perm(host_data, TRUE);
     break;
diff --git a/src/src/lookups/lf_functions.h b/src/src/lookups/lf_functions.h
index 4d9ae95..7e7ac4d 100644
--- a/src/src/lookups/lf_functions.h
+++ b/src/src/lookups/lf_functions.h
@@ -12,7 +12,8 @@ extern int     lf_check_file(int, const uschar *, int, int, uid_t *, gid_t *,
 extern gstring *lf_quote(uschar *, uschar *, int, gstring *);
 extern int     lf_sqlperform(const uschar *, const uschar *, const uschar *,
          const uschar *, uschar **,
-                 uschar **, uint *, int(*)(const uschar *, uschar *, uschar **,
-                 uschar **, BOOL *, uint *));
+                 uschar **, uint *, const uschar *,
+         int(*)(const uschar *, uschar *, uschar **,
+                 uschar **, BOOL *, uint *, const uschar *));


 /* End of lf_functions.h */
diff --git a/src/src/lookups/lf_sqlperform.c b/src/src/lookups/lf_sqlperform.c
index cc894e0..e2636bf 100644
--- a/src/src/lookups/lf_sqlperform.c
+++ b/src/src/lookups/lf_sqlperform.c
@@ -36,55 +36,35 @@ Returns:         the return from the lookup function, or DEFER
 int
 lf_sqlperform(const uschar *name, const uschar *optionname,
   const uschar *optserverlist, const uschar *query,
-  uschar **result, uschar **errmsg, uint *do_cache,
-  int(*fn)(const uschar *, uschar *, uschar **, uschar **, BOOL *, uint *))
+  uschar **result, uschar **errmsg, uint *do_cache, const uschar * opts,
+  int(*fn)(const uschar *, uschar *, uschar **, uschar **, BOOL *, uint *, const uschar *))
 {
-int sep, rc;
+int rc;
 uschar *server;
-const uschar *serverlist;
-uschar buffer[512];
 BOOL defer_break = FALSE;


-DEBUG(D_lookup) debug_printf_indent("%s query: %s\n", name, query);
-
-/* Handle queries that do not have server information at the start. */
-
-if (Ustrncmp(query, "servers", 7) != 0)
-  {
-  sep = 0;
-  serverlist = optserverlist;
-  while ((server = string_nextinlist(&serverlist, &sep, buffer,
-          sizeof(buffer))) != NULL)
-    {
-    rc = (*fn)(query, server, result, errmsg, &defer_break, do_cache);
-    if (rc != DEFER || defer_break) return rc;
-    }
-  if (optserverlist == NULL)
-    *errmsg = string_sprintf("no %s servers defined (%s option)", name,
-      optionname);
-  }
+DEBUG(D_lookup) debug_printf_indent("%s query: \"%s\" opts '%s'\n", name, query, opts);


/* Handle queries that do have server information at the start. */

-else
+if (Ustrncmp(query, "servers", 7) == 0)
{
- int qsep;
+ int qsep = 0;
const uschar *s, *ss;
const uschar *qserverlist;
uschar *qserver;
- uschar qbuffer[512];

   s = query + 7;
-  while (isspace(*s)) s++;
+  skip_whitespace(&s);
   if (*s++ != '=')
     {
     *errmsg = string_sprintf("missing = after \"servers\" in %s lookup", name);
     return DEFER;
     }
-  while (isspace(*s)) s++;
+  skip_whitespace(&s);


   ss = Ustrchr(s, ';');
-  if (ss == NULL)
+  if (!ss)
     {
     *errmsg = string_sprintf("missing ; after \"servers=\" in %s lookup",
       name);
@@ -99,27 +79,21 @@ else
     }


qserverlist = string_sprintf("%.*s", (int)(ss - s), s);
- qsep = 0;

-  while ((qserver = string_nextinlist(&qserverlist, &qsep, qbuffer,
-           sizeof(qbuffer))) != NULL)
+  while ((qserver = string_nextinlist(&qserverlist, &qsep, NULL, 0)))
     {
-    if (Ustrchr(qserver, '/') != NULL)
+    if (Ustrchr(qserver, '/'))
       server = qserver;
     else
       {
       int len = Ustrlen(qserver);
+      const uschar * serverlist = optserverlist;


-      sep = 0;
-      serverlist = optserverlist;
-      while ((server = string_nextinlist(&serverlist, &sep, buffer,
-              sizeof(buffer))) != NULL)
-        {
+      for (int sep = 0; server = string_nextinlist(&serverlist, &sep, NULL, 0);)
         if (Ustrncmp(server, qserver, len) == 0 && server[len] == '/')
           break;
-        }


-      if (server == NULL)
+      if (!server)
         {
         *errmsg = string_sprintf("%s server \"%s\" not found in %s", name,
           qserver, optionname);
@@ -127,11 +101,73 @@ else
         }
       }


-    rc = (*fn)(ss+1, server, result, errmsg, &defer_break, do_cache);
+    if (is_tainted(server))
+      {
+      *errmsg = string_sprintf("%s server \"%s\" is tainted", name, server);
+      return DEFER;
+      }
+
+    rc = (*fn)(ss+1, server, result, errmsg, &defer_break, do_cache, opts);
     if (rc != DEFER || defer_break) return rc;
     }
   }


+/* Handle queries that do not have server information at the start. */
+
+else
+  {
+  const uschar * serverlist = NULL;
+
+  /* If options are present, scan for a server definition.  Default to
+  the "optserverlist" srgument. */
+
+  if (opts)
+    {
+    uschar * ele;
+    for (int sep = ','; ele = string_nextinlist(&opts, &sep, NULL, 0); )
+      if (Ustrncmp(ele, "servers=", 8) == 0)
+    { serverlist = ele + 8; break; }
+    }
+
+  if (!serverlist)
+    serverlist = optserverlist;
+  if (!serverlist)
+    *errmsg = string_sprintf("no %s servers defined (%s option)", name,
+      optionname);
+  else
+    for (int d = 0; (server = string_nextinlist(&serverlist, &d, NULL, 0)); )
+      {
+      /* If not a full spec assume from options; scan main list for matching
+      hostname */
+
+      if (!Ustrchr(server, '/'))
+    {
+    int len = Ustrlen(server);
+    const uschar * slist = optserverlist;
+    uschar * ele;
+    for (int sep = 0; ele = string_nextinlist(&slist, &sep, NULL, 0); )
+      if (Ustrncmp(ele, server, len) == 0 && ele[len] == '/')
+        break;
+    if (!ele)
+      {
+      *errmsg = string_sprintf("%s server \"%s\" not found in %s", name,
+        server, optionname);
+      return DEFER;
+      }
+    server = ele;
+    }
+
+      if (is_tainted(server))
+        {
+        *errmsg = string_sprintf("%s server \"%s\" is tainted", name, server);
+        return DEFER;
+        }
+
+      rc = (*fn)(query, server, result, errmsg, &defer_break, do_cache, opts);
+      if (rc != DEFER || defer_break) return rc;
+      }
+  }
+
 return DEFER;
 }


diff --git a/src/src/lookups/mysql.c b/src/src/lookups/mysql.c
index 7651353..220aba1 100644
--- a/src/src/lookups/mysql.c
+++ b/src/src/lookups/mysql.c
@@ -122,6 +122,7 @@ Arguments:
   errmsg       where to point an error message
   defer_break  TRUE if no more servers are to be tried after DEFER
   do_cache     set zero if data is changed
+  opts           options


 The server string is of the form "host/dbname/user/password". The host can be
 host:port. This string is in a nextinlist temporary buffer, so can be
@@ -132,7 +133,7 @@ Returns:       OK, FAIL, or DEFER


 static int
 perform_mysql_search(const uschar *query, uschar *server, uschar **resultptr,
-  uschar **errmsg, BOOL *defer_break, uint *do_cache)
+  uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
 {
 MYSQL *mysql_handle = NULL;        /* Keep compilers happy */
 MYSQL_RES *mysql_result = NULL;
@@ -155,7 +156,7 @@ has the password removed. This copy is also used for debugging output. */
 for (int i = 3; i > 0; i--)
   {
   uschar *pp = Ustrrchr(server, '/');
-  if (pp == NULL)
+  if (!pp)
     {
     *errmsg = string_sprintf("incomplete MySQL server data: %s",
       (i == 3)? server : server_copy);
@@ -172,10 +173,7 @@ sdata[0] = server;   /* What's left at the start */


 for (cn = mysql_connections; cn; cn = cn->next)
   if (Ustrcmp(cn->server, server_copy) == 0)
-    {
-    mysql_handle = cn->handle;
-    break;
-    }
+    { mysql_handle = cn->handle; break; }


/* If no cached connection, we must set one up. Mysql allows for a host name
and port to be specified. It also allows the name of a Unix socket to be used.
@@ -283,7 +281,7 @@ up. Setting do_cache zero requests this. */

 if (!(mysql_result = mysql_use_result(mysql_handle)))
   {
-  if ( mysql_field_count(mysql_handle) == 0 )
+  if (mysql_field_count(mysql_handle) == 0)
     {
     DEBUG(D_lookup) debug_printf_indent("MYSQL: query was not one that returns data\n");
     result = string_cat(result,
@@ -328,18 +326,15 @@ while ((mysql_row_data = mysql_fetch_row(mysql_result)))
    we don't expect any more results. */


 while((i = mysql_next_result(mysql_handle)) >= 0)
-  {
-  if(i == 0)    /* Just ignore more results */
+  if(i != 0)
     {
-    DEBUG(D_lookup) debug_printf_indent("MYSQL: got unexpected more results\n");
-    continue;
+    *errmsg = string_sprintf(
+      "MYSQL: lookup result error when checking for more results: %s\n",
+      mysql_error(mysql_handle));
+    goto MYSQL_EXIT;
     }
-
-  *errmsg = string_sprintf(
-    "MYSQL: lookup result error when checking for more results: %s\n",
-    mysql_error(mysql_handle));
-  goto MYSQL_EXIT;
-  }
+  else    /* just ignore more results */
+    DEBUG(D_lookup) debug_printf_indent("MYSQL: got unexpected more results\n");


/* If result is NULL then no data has been found and so we return FAIL.
Otherwise, we must terminate the string which has been built; string_cat()
@@ -394,7 +389,7 @@ mysql_find(void * handle, const uschar * filename, const uschar * query,
const uschar * opts)
{
return lf_sqlperform(US"MySQL", US"mysql_servers", mysql_servers, query,
- result, errmsg, do_cache, perform_mysql_search);
+ result, errmsg, do_cache, opts, perform_mysql_search);
}


diff --git a/src/src/lookups/pgsql.c b/src/src/lookups/pgsql.c
index 90dda60..505f5e5 100644
--- a/src/src/lookups/pgsql.c
+++ b/src/src/lookups/pgsql.c
@@ -113,13 +113,14 @@ Arguments:
   errmsg       where to point an error message
   defer_break  set TRUE if no more servers are to be tried after DEFER
   do_cache     set FALSE if data is changed
+  opts           options list


 Returns:       OK, FAIL, or DEFER
 */


static int
perform_pgsql_search(const uschar *query, uschar *server, uschar **resultptr,
- uschar **errmsg, BOOL *defer_break, uint *do_cache)
+ uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
{
PGconn *pg_conn = NULL;
PGresult *pg_result = NULL;
@@ -386,7 +387,7 @@ pgsql_find(void * handle, const uschar * filename, const uschar * query,
const uschar * opts)
{
return lf_sqlperform(US"PostgreSQL", US"pgsql_servers", pgsql_servers, query,
- result, errmsg, do_cache, perform_pgsql_search);
+ result, errmsg, do_cache, opts, perform_pgsql_search);
}


diff --git a/src/src/lookups/redis.c b/src/src/lookups/redis.c
index 84a2dc6..337fdae 100644
--- a/src/src/lookups/redis.c
+++ b/src/src/lookups/redis.c
@@ -63,6 +63,7 @@ single server.
       errmsg       where to point an error message
       defer_break  TRUE if no more servers are to be tried after DEFER
       do_cache     set false if data is changed
+      opts       options


     The server string is of the form "host/dbnumber/password". The host can be
     host:port. This string is in a nextinlist temporary buffer, so can be
@@ -73,7 +74,7 @@ single server.


 static int
 perform_redis_search(const uschar *command, uschar *server, uschar **resultptr,
-  uschar **errmsg, BOOL *defer_break, uint *do_cache)
+  uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
 {
 redisContext *redis_handle = NULL;        /* Keep compilers happy */
 redisReply *redis_reply = NULL;
@@ -380,7 +381,7 @@ redis_find(void * handle __attribute__((unused)),
   uint * do_cache, const uschar * opts)
 {
 return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command,
-  result, errmsg, do_cache, perform_redis_search);
+  result, errmsg, do_cache, opts, perform_redis_search);
 }



diff --git a/src/src/string.c b/src/src/string.c
index 80cf49f..1192a55 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -574,18 +574,14 @@ uschar *ss = yield = store_get(Ustrlen(s) + 1, is_tainted(s));
 while (*s != 0)
   {
   if (*s != '\\')
-    {
     *ss++ = *s++;
-    }
   else if (isdigit(s[1]))
     {
     *ss++ = (s[1] - '0')*100 + (s[2] - '0')*10 + s[3] - '0';
     s += 4;
     }
   else if (*(++s) != 0)
-    {
     *ss++ = *s++;
-    }
   }


*ss = 0;
diff --git a/src/src/verify.c b/src/src/verify.c
index cd9df1f..dda51a5 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -2909,7 +2909,6 @@ provided that host name matching is permitted; if it's "@[]" match against the
local host's IP addresses. */

 if (*ss == '@')
-  {
   if (ss[1] == 0)
     {
     if (isiponly) return ERROR;
@@ -2921,7 +2920,6 @@ if (*ss == '@')
       if (Ustrcmp(ip->address, cb->host_address) == 0) return OK;
     return FAIL;
     }
-  }


/* If the pattern is an IP address, optionally followed by a bitmask count, do
a (possibly masked) comparison with the current IP address. */
@@ -2951,6 +2949,11 @@ if (*t == 0 || (*t == '/' && t != ss))

if ((semicolon = Ustrchr(ss, ';')))
endname = (opts = Ustrchr(ss, ',')) ? opts : semicolon;
+if (opts)
+ {
+ opts++;
+ opts = string_copyn(opts, semicolon - opts);
+ }

/* If we are doing an IP address only match, then all lookups must be IP
address lookups, even if there is no "net-". */
diff --git a/test/confs/2610 b/test/confs/2610
index 9e22172..5a96427 100644
--- a/test/confs/2610
+++ b/test/confs/2610
@@ -11,7 +11,9 @@ hostlist relay_hosts = net-mysql;select * from them where id='$sender_host_add

acl_smtp_rcpt = check_recipient

-mysql_servers = 127.0.0.1::PORT_N/test/root/
+PARTIAL = 127.0.0.1::PORT_N
+SSPEC = PARTIAL/test/root/
+mysql_servers = SSPEC


# ----- ACL -----
@@ -19,6 +21,29 @@ mysql_servers = 127.0.0.1::PORT_N/test/root/
begin acl

 check_recipient:
+      # Tainted-data checks
+  warn
+      # taint only in lookup string
+      set acl_m0 =    ok:   ${lookup mysql                    {select name from them where id = '$local_part'}}
+      # option on lookup type unaffected
+      set acl_m0 =    ok:   ${lookup mysql,servers=SSPEC      {select name from them where id = '$local_part'}}
+      # partial server-spec, indexing main-option, works
+      set acl_m0 =    ok:   ${lookup mysql,servers=PARTIAL    {select name from them where id = '$local_part'}}
+      # oldstyle server spec, prepended to lookup string, fails with taint
+      set acl_m0 =    FAIL: ${lookup mysql     {servers=SSPEC; select name from them where id = '$local_part'}}
+
+      # In list-stle lookup, tainted lookup string is ok if server spec comes from main-option
+  warn      set acl_m0 =    ok:   hostlist
+      hosts =    net-mysql;select * from them where id='$local_part'
+      # ... but setting a per-query servers spec fails due to the taint
+  warn      set acl_m0 =    FAIL: hostlist
+      hosts =    <& net-mysql;servers=SSPEC; select * from them where id='$local_part'
+
+      # The newer server-list-as-option-to-lookup-type is not a solution to tainted data in the lookup, because
+      # string-expansion is done before list-expansion so the taint contaminates the entire list.
+  warn      set acl_m0 =    FAIL: hostlist
+      hosts =    <& net-mysql,servers=SSPEC; select * from them where id='$local_part'
+
   accept  domains = +local_domains
   accept  hosts = +relay_hosts
   deny    message = relay not permitted
diff --git a/test/confs/2620 b/test/confs/2620
index 009e74f..e63fca2 100644
--- a/test/confs/2620
+++ b/test/confs/2620
@@ -1,6 +1,7 @@
 # Exim test configuration 2620


-SERVERS=localhost::PORT_N/test/CALLER/
+PARTIAL=localhost::PORT_N
+SERVERS=PARTIAL/test/CALLER/

.include DIR/aux-var/std_conf_prefix

@@ -22,6 +23,29 @@ pgsql_servers = SERVERS
begin acl

 check_recipient:
+      # Tainted-data checks
+  warn
+      # taint only in lookup string
+      set acl_m0 =    ok:   ${lookup pgsql                    {select name from them where id = '$local_part'}}
+      # option on lookup type unaffected
+      set acl_m0 =    ok:   ${lookup pgsql,servers=SSPEC      {select name from them where id = '$local_part'}}
+      # partial server-spec, indexing main-option, works
+      set acl_m0 =    ok:   ${lookup pgsql,servers=PARTIAL    {select name from them where id = '$local_part'}}
+      # oldstyle server spec, prepended to lookup string, fails with taint
+      set acl_m0 =    FAIL: ${lookup pgsql     {servers=SSPEC; select name from them where id = '$local_part'}}
+
+      # In list-stle lookup, tainted lookup string is ok if server spec comes from main-option
+  warn      set acl_m0 =    ok:   hostlist
+      hosts =    net-pgsql;select * from them where id='$local_part'
+      # ... but setting a per-query servers spec fails due to the taint
+  warn      set acl_m0 =    FAIL: hostlist
+      hosts =    <& net-pgsql;servers=SSPEC; select * from them where id='$local_part'
+
+      # The newer server-list-as-option-to-lookup-type is not a solution to tainted data in the lookup, because
+      # string-expansion is done before list-expansion so the taint contaminates the entire list.
+  warn      set acl_m0 =    FAIL: hostlist
+      hosts =    <& net-pgsql,servers=SSPEC; select * from them where id='$local_part'
+
   accept  domains = +local_domains
   accept  hosts = +relay_hosts
   deny    message = relay not permitted
diff --git a/test/stderr/2610 b/test/stderr/2610
index a59624c..951ddec 100644
--- a/test/stderr/2610
+++ b/test/stderr/2610
@@ -9,7 +9,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="select name from them where id='ph10';" opts=NULL
  database lookup required for select name from them where id='ph10';
- MySQL query: select name from them where id='ph10';
+ MySQL query: "select name from them where id='ph10';" opts 'NULL'
  MYSQL new connection: host=127.0.0.1 port=1223 socket=NULL database=test user=root
  lookup yielded: Philip Hazel
  search_open: mysql "NULL"
@@ -29,7 +29,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="select name from them where id='xxxx';" opts=NULL
  database lookup required for select name from them where id='xxxx';
- MySQL query: select name from them where id='xxxx';
+ MySQL query: "select name from them where id='xxxx';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  MYSQL: no data found
  lookup failed
@@ -41,7 +41,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="select name from them where id='nothing';" opts=NULL
  database lookup required for select name from them where id='nothing';
- MySQL query: select name from them where id='nothing';
+ MySQL query: "select name from them where id='nothing';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  lookup yielded: 
  search_open: mysql "NULL"
@@ -52,7 +52,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="select id,name from them where id='nothing';" opts=NULL
  database lookup required for select id,name from them where id='nothing';
- MySQL query: select id,name from them where id='nothing';
+ MySQL query: "select id,name from them where id='nothing';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  lookup yielded: id=nothing name="" 
  search_open: mysql "NULL"
@@ -63,7 +63,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="delete from them where id='nonexist';" opts=NULL
  database lookup required for delete from them where id='nonexist';
- MySQL query: delete from them where id='nonexist';
+ MySQL query: "delete from them where id='nonexist';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  MYSQL: query was not one that returns data
  lookup forced cache cleanup
@@ -76,7 +76,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="select * from them where id='quote';" opts=NULL
  database lookup required for select * from them where id='quote';
- MySQL query: select * from them where id='quote';
+ MySQL query: "select * from them where id='quote';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  MYSQL: no data found
  lookup failed
@@ -88,7 +88,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="select * from them where id='filter';" opts=NULL
  database lookup required for select * from them where id='filter';
- MySQL query: select * from them where id='filter';
+ MySQL query: "select * from them where id='filter';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  MYSQL: no data found
  lookup failed
@@ -100,7 +100,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="select * from them where id='quote2';" opts=NULL
  database lookup required for select * from them where id='quote2';
- MySQL query: select * from them where id='quote2';
+ MySQL query: "select * from them where id='quote2';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  lookup yielded: name="\"stquot" id=quote2 
  search_open: mysql "NULL"
@@ -111,7 +111,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="select * from them where id='nlonly';" opts=NULL
  database lookup required for select * from them where id='nlonly';
- MySQL query: select * from them where id='nlonly';
+ MySQL query: "select * from them where id='nlonly';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  MYSQL: no data found
  lookup failed
@@ -123,7 +123,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="servers=x:127.0.0.1::1223; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=x:127.0.0.1::1223; select name from them where id='ph10';
- MySQL query: servers=x:127.0.0.1::1223; select name from them where id='ph10';
+ MySQL query: "servers=x:127.0.0.1::1223; select name from them where id='ph10';" opts 'NULL'
  lookup deferred: MySQL server "x" not found in mysql_servers
  search_open: mysql "NULL"
    cached open
@@ -133,7 +133,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="servers=127.0.0.1::1223:x; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=127.0.0.1::1223:x; select name from them where id='ph10';
- MySQL query: servers=127.0.0.1::1223:x; select name from them where id='ph10';
+ MySQL query: "servers=127.0.0.1::1223:x; select name from them where id='ph10';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  lookup yielded: Philip Hazel
  search_open: mysql "NULL"
@@ -144,7 +144,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="servers=127.0.0.1::1223/test/root/:x; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=127.0.0.1::1223/test/root/:x; select name from them where id='ph10';
- MySQL query: servers=127.0.0.1::1223/test/root/:x; select name from them where id='ph10';
+ MySQL query: "servers=127.0.0.1::1223/test/root/:x; select name from them where id='ph10';" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  lookup yielded: Philip Hazel
  search_open: mysql "NULL"
@@ -155,7 +155,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="servers=ip4.ip4.ip4.ip4::1223/test/root/:127.0.0.1::1223; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=ip4.ip4.ip4.ip4::1223/test/root/:127.0.0.1::1223; select name from them where id='ph10';
- MySQL query: servers=ip4.ip4.ip4.ip4::1223/test/root/:127.0.0.1::1223; select name from them where id='ph10';
+ MySQL query: "servers=ip4.ip4.ip4.ip4::1223/test/root/:127.0.0.1::1223; select name from them where id='ph10';" opts 'NULL'
  MYSQL new connection: host=ip4.ip4.ip4.ip4 port=1223 socket=NULL database=test user=root
  lookup yielded: Philip Hazel
  search_open: mysql "NULL"
@@ -166,7 +166,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="servers=localhost(TESTSUITE/mysql/sock)/test/root/; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=localhost(TESTSUITE/mysql/sock)/test/root/; select name from them where id='ph10';
- MySQL query: servers=localhost(TESTSUITE/mysql/sock)/test/root/; select name from them where id='ph10';
+ MySQL query: "servers=localhost(TESTSUITE/mysql/sock)/test/root/; select name from them where id='ph10';" opts 'NULL'
  MYSQL new connection: host=localhost port=0 socket=TESTSUITE/mysql/sock database=test user=root
  lookup yielded: Philip Hazel
  search_open: mysql "NULL"
@@ -177,7 +177,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="SELECT name FROM them WHERE id IN ('ph10', 'aaaa');" opts=NULL
  database lookup required for SELECT name FROM them WHERE id IN ('ph10', 'aaaa');
- MySQL query: SELECT name FROM them WHERE id IN ('ph10', 'aaaa');
+ MySQL query: "SELECT name FROM them WHERE id IN ('ph10', 'aaaa');" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  lookup yielded: Philip Hazel
  Aristotle
@@ -189,7 +189,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="SELECT *    FROM them WHERE id IN ('ph10', 'aaaa');" opts=NULL
  database lookup required for SELECT *    FROM them WHERE id IN ('ph10', 'aaaa');
- MySQL query: SELECT *    FROM them WHERE id IN ('ph10', 'aaaa');
+ MySQL query: "SELECT *    FROM them WHERE id IN ('ph10', 'aaaa');" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  lookup yielded: name="Philip Hazel" id=ph10 
  name=Aristotle id=aaaa 
@@ -201,7 +201,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=mysql key="delete from them where id='aaaa'" opts=NULL
  database lookup required for delete from them where id='aaaa'
- MySQL query: delete from them where id='aaaa'
+ MySQL query: "delete from them where id='aaaa'" opts 'NULL'
  MYSQL using cached connection for 127.0.0.1:1223/test/root
  MYSQL: query was not one that returns data
  lookup forced cache cleanup
@@ -243,28 +243,135 @@ log directory space = nnnnnK inodes = nnnnn check_space = 10240K inodes = 100

SMTP>> 250 OK

 SMTP<< rcpt to:<c@d>
 using ACL "check_recipient"
-processing "accept" (TESTSUITE/test-config 22)
+processing "warn" (TESTSUITE/test-config 25)
+ search_open: mysql "NULL"
+ search_find: file="NULL"
+   key="select name from them where id = 'c'" partial=-1 affix=NULL starflags=0 opts=NULL
+ LRU list:
+ internal_search_find: file="NULL"
+   type=mysql key="select name from them where id = 'c'" opts=NULL
+ database lookup required for select name from them where id = 'c'
+ MySQL query: "select name from them where id = 'c'" opts 'NULL'
+ MYSQL new connection: host=127.0.0.1 port=1223 socket=NULL database=test user=root
+ MYSQL: no data found
+ lookup failed
+check set acl_m0 = ok:   ${lookup mysql                    {select name from them where id = '$local_part'}}
+                 = ok:   
+ search_open: mysql "NULL"
+   cached open
+ search_find: file="NULL"
+   key="select name from them where id = 'c'" partial=-1 affix=NULL starflags=0 opts="servers=127.0.0.1::1223/test/root/"
+ LRU list:
+ internal_search_find: file="NULL"
+   type=mysql key="select name from them where id = 'c'" opts="servers=127.0.0.1::1223/test/root/"
+ cached data found but either wrong opts or dated;  database lookup required for select name from them where id = 'c'
+ MySQL query: "select name from them where id = 'c'" opts 'servers=127.0.0.1::1223/test/root/'
+ MYSQL using cached connection for 127.0.0.1:1223/test/root
+ MYSQL: no data found
+ lookup failed
+check set acl_m0 = ok:   ${lookup mysql,servers=127.0.0.1::1223/test/root/      {select name from them where id = '$local_part'}}
+                 = ok:   
+ search_open: mysql "NULL"
+   cached open
+ search_find: file="NULL"
+   key="select name from them where id = 'c'" partial=-1 affix=NULL starflags=0 opts="servers=127.0.0.1::1223"
+ LRU list:
+ internal_search_find: file="NULL"
+   type=mysql key="select name from them where id = 'c'" opts="servers=127.0.0.1::1223"
+ cached data found but either wrong opts or dated;  database lookup required for select name from them where id = 'c'
+ MySQL query: "select name from them where id = 'c'" opts 'servers=127.0.0.1::1223'
+ MYSQL using cached connection for 127.0.0.1:1223/test/root
+ MYSQL: no data found
+ lookup failed
+check set acl_m0 = ok:   ${lookup mysql,servers=127.0.0.1::1223    {select name from them where id = '$local_part'}}
+                 = ok:   
+ search_open: mysql "NULL"
+   cached open
+ search_find: file="NULL"
+   key="servers=127.0.0.1::1223/test/root/; select name from them where id = 'c'" partial=-1 affix=NULL starflags=0 opts=NULL
+ LRU list:
+ internal_search_find: file="NULL"
+   type=mysql key="servers=127.0.0.1::1223/test/root/; select name from them where id = 'c'" opts=NULL
+ database lookup required for servers=127.0.0.1::1223/test/root/; select name from them where id = 'c'
+ MySQL query: "servers=127.0.0.1::1223/test/root/; select name from them where id = 'c'" opts 'NULL'
+ lookup deferred: MySQL server "127.0.0.1:1223/test/root/" is tainted
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: MySQL server "127.0.0.1:1223/test/root/" is tainted
+processing "warn" (TESTSUITE/test-config 36)
+check set acl_m0 = ok:   hostlist
+check hosts = net-mysql;select * from them where id='$local_part'
+search_open: mysql "NULL"
+  cached open
+search_find: file="NULL"
+  key="select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts=NULL
+LRU list:
+internal_search_find: file="NULL"
+  type=mysql key="select * from them where id='c'" opts=NULL
+database lookup required for select * from them where id='c'
+MySQL query: "select * from them where id='c'" opts 'NULL'
+MYSQL using cached connection for 127.0.0.1:1223/test/root
+MYSQL: no data found
+lookup failed
+host in "net-mysql;select * from them where id='c'"? no (end of list)
+warn: condition test failed in ACL "check_recipient"
+processing "warn" (TESTSUITE/test-config 39)
+check set acl_m0 = FAIL: hostlist
+check hosts = <& net-mysql;servers=127.0.0.1::1223/test/root/; select * from them where id='$local_part'
+search_open: mysql "NULL"
+  cached open
+search_find: file="NULL"
+  key="servers=127.0.0.1::1223/test/root/; select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts=NULL
+LRU list:
+internal_search_find: file="NULL"
+  type=mysql key="servers=127.0.0.1::1223/test/root/; select * from them where id='c'" opts=NULL
+database lookup required for servers=127.0.0.1::1223/test/root/; select * from them where id='c'
+MySQL query: "servers=127.0.0.1::1223/test/root/; select * from them where id='c'" opts 'NULL'
+lookup deferred: MySQL server "127.0.0.1:1223/test/root/" is tainted
+host in "<& net-mysql;servers=127.0.0.1::1223/test/root/; select * from them where id='c'"? list match deferred for net-mysql;servers=127.0.0.1::1223/test/root/; select * from them where id='c'
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: MySQL server "127.0.0.1:1223/test/root/" is tainted
+processing "warn" (TESTSUITE/test-config 44)
+check set acl_m0 = FAIL: hostlist
+check hosts = <& net-mysql,servers=127.0.0.1::1223/test/root/; select * from them where id='$local_part'
+search_open: mysql "NULL"
+  cached open
+search_find: file="NULL"
+  key=" select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts="servers=127.0.0.1::1223/test/root/"
+LRU list:
+internal_search_find: file="NULL"
+  type=mysql key=" select * from them where id='c'" opts="servers=127.0.0.1::1223/test/root/"
+database lookup required for  select * from them where id='c'
+MySQL query: " select * from them where id='c'" opts 'servers=127.0.0.1::1223/test/root/'
+lookup deferred: MySQL server "127.0.0.1:1223/test/root/" is tainted
+host in "<& net-mysql,servers=127.0.0.1::1223/test/root/; select * from them where id='c'"? list match deferred for net-mysql,servers=127.0.0.1::1223/test/root/; select * from them where id='c'
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: MySQL server "127.0.0.1:1223/test/root/" is tainted
+processing "accept" (TESTSUITE/test-config 47)
 check domains = +local_domains
 d in "@"? no (end of list)
 d in "+local_domains"? no (end of list)
 accept: condition test failed in ACL "check_recipient"
-processing "accept" (TESTSUITE/test-config 23)
+processing "accept" (TESTSUITE/test-config 48)
 check hosts = +relay_hosts
 search_open: mysql "NULL"
+  cached open
 search_find: file="NULL"
   key="select * from them where id='10.0.0.0'" partial=-1 affix=NULL starflags=0 opts=NULL
 LRU list:
 internal_search_find: file="NULL"
   type=mysql key="select * from them where id='10.0.0.0'" opts=NULL
 database lookup required for select * from them where id='10.0.0.0'
-MySQL query: select * from them where id='10.0.0.0'
-MYSQL new connection: host=127.0.0.1 port=1223 socket=NULL database=test user=root
+MySQL query: "select * from them where id='10.0.0.0'" opts 'NULL'
+MYSQL using cached connection for 127.0.0.1:1223/test/root
 MYSQL: no data found
 lookup failed
 host in "net-mysql;select * from them where id='10.0.0.0'"? no (end of list)
 host in "+relay_hosts"? no (end of list)
 accept: condition test failed in ACL "check_recipient"
-processing "deny" (TESTSUITE/test-config 24)
+processing "deny" (TESTSUITE/test-config 49)
   message: relay not permitted
 deny: condition test succeeded in ACL "check_recipient"
 end of ACL "check_recipient": DENY
@@ -374,7 +481,7 @@ processing address_data
  internal_search_find: file="NULL"
    type=mysql key="select name from them where id='ph10'" opts=NULL
  database lookup required for select name from them where id='ph10'
- MySQL query: select name from them where id='ph10'
+ MySQL query: "select name from them where id='ph10'" opts 'NULL'
  MYSQL new connection: host=127.0.0.1 port=1223 socket=NULL database=test user=root
  lookup yielded: Philip Hazel
 calling r1 router
@@ -418,7 +525,7 @@ appendfile transport entered
  internal_search_find: file="NULL"
    type=mysql key="select id from them where id='ph10'" opts=NULL
  database lookup required for select id from them where id='ph10'
- MySQL query: select id from them where id='ph10'
+ MySQL query: "select id from them where id='ph10'" opts 'NULL'
  MYSQL new connection: host=127.0.0.1 port=1223 socket=NULL database=test user=root
  lookup yielded: ph10
 appendfile: mode=600 notify_comsat=0 quota=0 warning=0
diff --git a/test/stderr/2620 b/test/stderr/2620
index 2eae8ee..337d518 100644
--- a/test/stderr/2620
+++ b/test/stderr/2620
@@ -13,7 +13,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select name from them where id='ph10';" opts=NULL
  database lookup required for select name from them where id='ph10';
- PostgreSQL query: select name from them where id='ph10';
+ PostgreSQL query: "select name from them where id='ph10';" opts 'NULL'
  PGSQL new connection: host=localhost port=1223 database=test user=CALLER
  lookup yielded: Philip Hazel
  search_open: pgsql "NULL"
@@ -33,7 +33,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select name from them where id='xxxx';" opts=NULL
  database lookup required for select name from them where id='xxxx';
- PostgreSQL query: select name from them where id='xxxx';
+ PostgreSQL query: "select name from them where id='xxxx';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  PGSQL: no data found
  lookup failed
@@ -45,7 +45,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select name from them where id='nothing';" opts=NULL
  database lookup required for select name from them where id='nothing';
- PostgreSQL query: select name from them where id='nothing';
+ PostgreSQL query: "select name from them where id='nothing';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: 
  search_open: pgsql "NULL"
@@ -56,7 +56,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select id,name from them where id='nothing';" opts=NULL
  database lookup required for select id,name from them where id='nothing';
- PostgreSQL query: select id,name from them where id='nothing';
+ PostgreSQL query: "select id,name from them where id='nothing';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: id=nothing name="" 
  search_open: pgsql "NULL"
@@ -67,7 +67,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="delete from them where id='nonexist';" opts=NULL
  database lookup required for delete from them where id='nonexist';
- PostgreSQL query: delete from them where id='nonexist';
+ PostgreSQL query: "delete from them where id='nonexist';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  PGSQL: command does not return any data but was successful. Rows affected: 0
  lookup forced cache cleanup
@@ -80,7 +80,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select * from them where id='quote2';" opts=NULL
  database lookup required for select * from them where id='quote2';
- PostgreSQL query: select * from them where id='quote2';
+ PostgreSQL query: "select * from them where id='quote2';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: name="\"stquot" id=quote2 
  search_open: pgsql "NULL"
@@ -91,7 +91,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select * from them where id='newline';" opts=NULL
  database lookup required for select * from them where id='newline';
- PostgreSQL query: select * from them where id='newline';
+ PostgreSQL query: "select * from them where id='newline';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: name="before
  after" id=newline 
@@ -103,7 +103,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select * from them where id='tab';" opts=NULL
  database lookup required for select * from them where id='tab';
- PostgreSQL query: select * from them where id='tab';
+ PostgreSQL query: "select * from them where id='tab';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: name="x    x" id=tab 
  search_open: pgsql "NULL"
@@ -114,7 +114,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select * from them where name='''stquot';" opts=NULL
  database lookup required for select * from them where name='''stquot';
- PostgreSQL query: select * from them where name='''stquot';
+ PostgreSQL query: "select * from them where name='''stquot';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: name='stquot id=quote1 
  search_open: pgsql "NULL"
@@ -125,7 +125,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="servers=x:localhost; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=x:localhost; select name from them where id='ph10';
- PostgreSQL query: servers=x:localhost; select name from them where id='ph10';
+ PostgreSQL query: "servers=x:localhost; select name from them where id='ph10';" opts 'NULL'
  lookup deferred: PostgreSQL server "x" not found in pgsql_servers
  search_open: pgsql "NULL"
    cached open
@@ -135,7 +135,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="servers=localhost::1223:x; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=localhost::1223:x; select name from them where id='ph10';
- PostgreSQL query: servers=localhost::1223:x; select name from them where id='ph10';
+ PostgreSQL query: "servers=localhost::1223:x; select name from them where id='ph10';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: Philip Hazel
  search_open: pgsql "NULL"
@@ -146,7 +146,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="servers=localhost::1223/test/CALLER/:x; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=localhost::1223/test/CALLER/:x; select name from them where id='ph10';
- PostgreSQL query: servers=localhost::1223/test/CALLER/:x; select name from them where id='ph10';
+ PostgreSQL query: "servers=localhost::1223/test/CALLER/:x; select name from them where id='ph10';" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: Philip Hazel
  search_open: pgsql "NULL"
@@ -157,7 +157,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="servers=(TESTSUITE/pgsql/.s.PGSQL.1223)/test/CALLER/:x; select name from them where id='ph10';" opts=NULL
  database lookup required for servers=(TESTSUITE/pgsql/.s.PGSQL.1223)/test/CALLER/:x; select name from them where id='ph10';
- PostgreSQL query: servers=(TESTSUITE/pgsql/.s.PGSQL.1223)/test/CALLER/:x; select name from them where id='ph10';
+ PostgreSQL query: "servers=(TESTSUITE/pgsql/.s.PGSQL.1223)/test/CALLER/:x; select name from them where id='ph10';" opts 'NULL'
  PGSQL new connection: socket=TESTSUITE/pgsql/.s.PGSQL.1223 database=test user=CALLER
  lookup yielded: Philip Hazel
  search_open: pgsql "NULL"
@@ -168,7 +168,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="SELECT name FROM them WHERE id IN ('ph10', 'aaaa');" opts=NULL
  database lookup required for SELECT name FROM them WHERE id IN ('ph10', 'aaaa');
- PostgreSQL query: SELECT name FROM them WHERE id IN ('ph10', 'aaaa');
+ PostgreSQL query: "SELECT name FROM them WHERE id IN ('ph10', 'aaaa');" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: Philip Hazel
  Aristotle
@@ -180,7 +180,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="SELECT *    FROM them WHERE id IN ('ph10', 'aaaa');" opts=NULL
  database lookup required for SELECT *    FROM them WHERE id IN ('ph10', 'aaaa');
- PostgreSQL query: SELECT *    FROM them WHERE id IN ('ph10', 'aaaa');
+ PostgreSQL query: "SELECT *    FROM them WHERE id IN ('ph10', 'aaaa');" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  lookup yielded: name="Philip Hazel" id=ph10 
  name=Aristotle id=aaaa 
@@ -192,7 +192,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="delete from them where id='aaaa'" opts=NULL
  database lookup required for delete from them where id='aaaa'
- PostgreSQL query: delete from them where id='aaaa'
+ PostgreSQL query: "delete from them where id='aaaa'" opts 'NULL'
  PGSQL using cached connection for localhost:1223/test/CALLER
  PGSQL: command does not return any data but was successful. Rows affected: 1
  lookup forced cache cleanup
@@ -233,28 +233,107 @@ log directory space = nnnnnK inodes = nnnnn check_space = 10240K inodes = 100

SMTP>> 250 OK

 SMTP<< rcpt to:<c@d>
 using ACL "check_recipient"
-processing "accept" (TESTSUITE/test-config 25)
+processing "warn" (TESTSUITE/test-config 27)
+ search_open: pgsql "NULL"
+ search_find: file="NULL"
+   key="select name from them where id = 'c'" partial=-1 affix=NULL starflags=0 opts=NULL
+ LRU list:
+ internal_search_find: file="NULL"
+   type=pgsql key="select name from them where id = 'c'" opts=NULL
+ database lookup required for select name from them where id = 'c'
+ PostgreSQL query: "select name from them where id = 'c'" opts 'NULL'
+ PGSQL new connection: host=localhost port=1223 database=test user=CALLER
+ PGSQL: no data found
+ lookup failed
+check set acl_m0 = ok:   ${lookup pgsql                    {select name from them where id = '$local_part'}}
+                 = ok:   
+ search_open: pgsql "NULL"
+   cached open
+ search_find: file="NULL"
+   key="select name from them where id = 'c'" partial=-1 affix=NULL starflags=0 opts="servers=SSPEC"
+ LRU list:
+ internal_search_find: file="NULL"
+   type=pgsql key="select name from them where id = 'c'" opts="servers=SSPEC"
+ cached data found but either wrong opts or dated;  database lookup required for select name from them where id = 'c'
+ PostgreSQL query: "select name from them where id = 'c'" opts 'servers=SSPEC'
+ lookup deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+processing "warn" (TESTSUITE/test-config 38)
+check set acl_m0 = ok:   hostlist
+check hosts = net-pgsql;select * from them where id='$local_part'
+search_open: pgsql "NULL"
+  cached open
+search_find: file="NULL"
+  key="select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts=NULL
+LRU list:
+internal_search_find: file="NULL"
+  type=pgsql key="select * from them where id='c'" opts=NULL
+database lookup required for select * from them where id='c'
+PostgreSQL query: "select * from them where id='c'" opts 'NULL'
+PGSQL using cached connection for localhost:1223/test/CALLER
+PGSQL: no data found
+lookup failed
+host in "net-pgsql;select * from them where id='c'"? no (end of list)
+warn: condition test failed in ACL "check_recipient"
+processing "warn" (TESTSUITE/test-config 41)
+check set acl_m0 = FAIL: hostlist
+check hosts = <& net-pgsql;servers=SSPEC; select * from them where id='$local_part'
+search_open: pgsql "NULL"
+  cached open
+search_find: file="NULL"
+  key="servers=SSPEC; select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts=NULL
+LRU list:
+internal_search_find: file="NULL"
+  type=pgsql key="servers=SSPEC; select * from them where id='c'" opts=NULL
+database lookup required for servers=SSPEC; select * from them where id='c'
+PostgreSQL query: "servers=SSPEC; select * from them where id='c'" opts 'NULL'
+lookup deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+host in "<& net-pgsql;servers=SSPEC; select * from them where id='c'"? list match deferred for net-pgsql;servers=SSPEC; select * from them where id='c'
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+processing "warn" (TESTSUITE/test-config 46)
+check set acl_m0 = FAIL: hostlist
+check hosts = <& net-pgsql,servers=SSPEC; select * from them where id='$local_part'
+search_open: pgsql "NULL"
+  cached open
+search_find: file="NULL"
+  key=" select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts="servers=SSPEC"
+LRU list:
+internal_search_find: file="NULL"
+  type=pgsql key=" select * from them where id='c'" opts="servers=SSPEC"
+database lookup required for  select * from them where id='c'
+PostgreSQL query: " select * from them where id='c'" opts 'servers=SSPEC'
+lookup deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+host in "<& net-pgsql,servers=SSPEC; select * from them where id='c'"? list match deferred for net-pgsql,servers=SSPEC; select * from them where id='c'
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+processing "accept" (TESTSUITE/test-config 49)
 check domains = +local_domains
 d in "@"? no (end of list)
 d in "+local_domains"? no (end of list)
 accept: condition test failed in ACL "check_recipient"
-processing "accept" (TESTSUITE/test-config 26)
+processing "accept" (TESTSUITE/test-config 50)
 check hosts = +relay_hosts
 search_open: pgsql "NULL"
+  cached open
 search_find: file="NULL"
   key="select * from them where id='10.0.0.0'" partial=-1 affix=NULL starflags=0 opts=NULL
 LRU list:
 internal_search_find: file="NULL"
   type=pgsql key="select * from them where id='10.0.0.0'" opts=NULL
 database lookup required for select * from them where id='10.0.0.0'
-PostgreSQL query: select * from them where id='10.0.0.0'
-PGSQL new connection: host=localhost port=1223 database=test user=CALLER
+PostgreSQL query: "select * from them where id='10.0.0.0'" opts 'NULL'
+PGSQL using cached connection for localhost:1223/test/CALLER
 PGSQL: no data found
 lookup failed
 host in "net-pgsql;select * from them where id='10.0.0.0'"? no (end of list)
 host in "+relay_hosts"? no (end of list)
 accept: condition test failed in ACL "check_recipient"
-processing "deny" (TESTSUITE/test-config 27)
+processing "deny" (TESTSUITE/test-config 51)
   message: relay not permitted
 deny: condition test succeeded in ACL "check_recipient"
 end of ACL "check_recipient": DENY
@@ -263,12 +342,85 @@ LOG: MAIN REJECT
   H=[10.0.0.0] F=<a@b> rejected RCPT <c@d>: relay not permitted
 SMTP<< rcpt to:<c@d>
 using ACL "check_recipient"
-processing "accept" (TESTSUITE/test-config 25)
+processing "warn" (TESTSUITE/test-config 27)
+ search_open: pgsql "NULL"
+   cached open
+ search_find: file="NULL"
+   key="select name from them where id = 'c'" partial=-1 affix=NULL starflags=0 opts=NULL
+ LRU list:
+ internal_search_find: file="NULL"
+   type=pgsql key="select name from them where id = 'c'" opts=NULL
+ cached data used for lookup of select name from them where id = 'c'
+ lookup failed
+check set acl_m0 = ok:   ${lookup pgsql                    {select name from them where id = '$local_part'}}
+                 = ok:   
+ search_open: pgsql "NULL"
+   cached open
+ search_find: file="NULL"
+   key="select name from them where id = 'c'" partial=-1 affix=NULL starflags=0 opts="servers=SSPEC"
+ LRU list:
+ internal_search_find: file="NULL"
+   type=pgsql key="select name from them where id = 'c'" opts="servers=SSPEC"
+ cached data found but either wrong opts or dated;  database lookup required for select name from them where id = 'c'
+ PostgreSQL query: "select name from them where id = 'c'" opts 'servers=SSPEC'
+ lookup deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+processing "warn" (TESTSUITE/test-config 38)
+check set acl_m0 = ok:   hostlist
+check hosts = net-pgsql;select * from them where id='$local_part'
+search_open: pgsql "NULL"
+  cached open
+search_find: file="NULL"
+  key="select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts=NULL
+LRU list:
+internal_search_find: file="NULL"
+  type=pgsql key="select * from them where id='c'" opts=NULL
+cached data used for lookup of select * from them where id='c'
+lookup failed
+host in "net-pgsql;select * from them where id='c'"? no (end of list)
+warn: condition test failed in ACL "check_recipient"
+processing "warn" (TESTSUITE/test-config 41)
+check set acl_m0 = FAIL: hostlist
+check hosts = <& net-pgsql;servers=SSPEC; select * from them where id='$local_part'
+search_open: pgsql "NULL"
+  cached open
+search_find: file="NULL"
+  key="servers=SSPEC; select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts=NULL
+LRU list:
+internal_search_find: file="NULL"
+  type=pgsql key="servers=SSPEC; select * from them where id='c'" opts=NULL
+database lookup required for servers=SSPEC; select * from them where id='c'
+PostgreSQL query: "servers=SSPEC; select * from them where id='c'" opts 'NULL'
+lookup deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+host in "<& net-pgsql;servers=SSPEC; select * from them where id='c'"? list match deferred for net-pgsql;servers=SSPEC; select * from them where id='c'
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+processing "warn" (TESTSUITE/test-config 46)
+check set acl_m0 = FAIL: hostlist
+check hosts = <& net-pgsql,servers=SSPEC; select * from them where id='$local_part'
+search_open: pgsql "NULL"
+  cached open
+search_find: file="NULL"
+  key=" select * from them where id='c'" partial=-1 affix=NULL starflags=0 opts="servers=SSPEC"
+LRU list:
+internal_search_find: file="NULL"
+  type=pgsql key=" select * from them where id='c'" opts="servers=SSPEC"
+database lookup required for  select * from them where id='c'
+PostgreSQL query: " select * from them where id='c'" opts 'servers=SSPEC'
+lookup deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+host in "<& net-pgsql,servers=SSPEC; select * from them where id='c'"? list match deferred for net-pgsql,servers=SSPEC; select * from them where id='c'
+warn: condition test deferred in ACL "check_recipient"
+LOG: MAIN
+  H=[10.0.0.0] Warning: ACL "warn" statement skipped: condition test deferred: PostgreSQL server "SSPEC" not found in pgsql_servers
+processing "accept" (TESTSUITE/test-config 49)
 check domains = +local_domains
 d in "@"? no (end of list)
 d in "+local_domains"? no (end of list)
 accept: condition test failed in ACL "check_recipient"
-processing "accept" (TESTSUITE/test-config 26)
+processing "accept" (TESTSUITE/test-config 50)
 check hosts = +relay_hosts
 search_open: pgsql "NULL"
   cached open
@@ -282,7 +434,7 @@ lookup failed
 host in "net-pgsql;select * from them where id='10.0.0.0'"? no (end of list)
 host in "+relay_hosts"? no (end of list)
 accept: condition test failed in ACL "check_recipient"
-processing "deny" (TESTSUITE/test-config 27)
+processing "deny" (TESTSUITE/test-config 51)
   message: relay not permitted
 deny: condition test succeeded in ACL "check_recipient"
 end of ACL "check_recipient": DENY
@@ -392,7 +544,7 @@ processing address_data
  internal_search_find: file="NULL"
    type=pgsql key="select name from them where id='ph10'" opts=NULL
  database lookup required for select name from them where id='ph10'
- PostgreSQL query: select name from them where id='ph10'
+ PostgreSQL query: "select name from them where id='ph10'" opts 'NULL'
  PGSQL new connection: host=localhost port=1223 database=test user=CALLER
  lookup yielded: Philip Hazel
 calling r1 router
@@ -436,7 +588,7 @@ appendfile transport entered
  internal_search_find: file="NULL"
    type=pgsql key="select id from them where id='ph10'" opts=NULL
  database lookup required for select id from them where id='ph10'
- PostgreSQL query: select id from them where id='ph10'
+ PostgreSQL query: "select id from them where id='ph10'" opts 'NULL'
  PGSQL new connection: host=localhost port=1223 database=test user=CALLER
  lookup yielded: ph10
 appendfile: mode=600 notify_comsat=0 quota=0 warning=0
@@ -500,7 +652,7 @@ dropping to exim gid; retaining priv uid
  internal_search_find: file="NULL"
    type=pgsql key="select name from them where id='ph10';" opts=NULL
  database lookup required for select name from them where id='ph10';
- PostgreSQL query: select name from them where id='ph10';
+ PostgreSQL query: "select name from them where id='ph10';" opts 'NULL'
  PGSQL new connection: socket=TESTSUITE/pgsql/.s.PGSQL.1223 database=test user=CALLER
  lookup yielded: Philip Hazel
 search_tidyup called