Regex compile cacheing
[exim.git] / src / src / expand.c
index f7c8f05374e03f28c15911f631830dbb60e13200..4d7dc721920d6d88a554dbab8e2e0eb7be6e42ac 100644 (file)
@@ -2,8 +2,8 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
-/* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
@@ -14,7 +14,7 @@
 
 /* Recursively called function */
 
-static uschar *expand_string_internal(const uschar *, BOOL, const uschar **, BOOL, BOOL, BOOL *);
+static uschar *expand_string_internal(const uschar *, BOOL, const uschar **, BOOL, BOOL, BOOL *, BOOL *);
 static int_eximarith_t expanded_string_integer(const uschar *, BOOL);
 
 #ifdef STAND_ALONE
@@ -444,9 +444,9 @@ enum vtypes {
   vtype_pspace,         /* partition space; value is T/F for spool/log */
   vtype_pinodes,        /* partition inodes; value is T/F for spool/log */
   vtype_cert           /* SSL certificate */
-  #ifndef DISABLE_DKIM
+#ifndef DISABLE_DKIM
   ,vtype_dkim           /* Lookup of value in DKIM signature */
-  #endif
+#endif
 };
 
 /* Type for main variable table */
@@ -583,9 +583,9 @@ static var_entry var_table[] = {
   { "interface_address",   vtype_stringptr,   &interface_address },
   { "interface_port",      vtype_int,         &interface_port },
   { "item",                vtype_stringptr,   &iterate_item },
-  #ifdef LOOKUP_LDAP
+#ifdef LOOKUP_LDAP
   { "ldap_dn",             vtype_stringptr,   &eldap_dn },
-  #endif
+#endif
   { "load_average",        vtype_load_avg,    NULL },
   { "local_part",          vtype_stringptr,   &deliver_localpart },
   { "local_part_data",     vtype_stringptr,   &deliver_localpart_data },
@@ -750,17 +750,8 @@ static var_entry var_table[] = {
   { "spool_directory",     vtype_stringptr,   &spool_directory },
   { "spool_inodes",        vtype_pinodes,     (void *)TRUE },
   { "spool_space",         vtype_pspace,      (void *)TRUE },
-#ifdef EXPERIMENTAL_SRS_ALT
-  { "srs_db_address",      vtype_stringptr,   &srs_db_address },
-  { "srs_db_key",          vtype_stringptr,   &srs_db_key },
-  { "srs_orig_recipient",  vtype_stringptr,   &srs_orig_recipient },
-  { "srs_orig_sender",     vtype_stringptr,   &srs_orig_sender },
-#endif
-#if defined(EXPERIMENTAL_SRS_ALT) || defined(SUPPORT_SRS)
+#ifdef SUPPORT_SRS
   { "srs_recipient",       vtype_stringptr,   &srs_recipient },
-#endif
-#ifdef EXPERIMENTAL_SRS_ALT
-  { "srs_status",          vtype_stringptr,   &srs_status },
 #endif
   { "thisaddress",         vtype_stringptr,   &filter_thisaddress },
 
@@ -1297,7 +1288,7 @@ expand_getlistele(int field, const uschar * list)
 const uschar * tlist = list;
 int sep = 0;
 /* Tainted mem for the throwaway element copies */
-uschar * dummy = store_get(2, TRUE);
+uschar * dummy = store_get(2, GET_TAINTED);
 
 if (field < 0)
   {
@@ -1757,9 +1748,7 @@ uschar buf[16];
 int fd;
 ssize_t len;
 const uschar * where;
-#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
 uschar * sname;
-#endif
 
 if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0)
   {
@@ -1767,17 +1756,9 @@ if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0)
   return NULL;
   }
 
-#ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
-sa_un.sun_path[0] = 0; /* Abstract local socket addr - Linux-specific? */
-len = offsetof(struct sockaddr_un, sun_path) + 1
-  + snprintf(sa_un.sun_path+1, sizeof(sa_un.sun_path)-1, "exim_%d", getpid());
-#else
-sname = string_sprintf("%s/p_%d", spool_directory, getpid());
-len = offsetof(struct sockaddr_un, sun_path)
-  + snprintf(sa_un.sun_path, sizeof(sa_un.sun_path), "%s", sname);
-#endif
+len = daemon_client_sockname(&sa_un, &sname);
 
-if (bind(fd, (const struct sockaddr *)&sa_un, len) < 0)
+if (bind(fd, (const struct sockaddr *)&sa_un, (socklen_t)len) < 0)
   { where = US"bind"; goto bad; }
 
 #ifdef notdef
@@ -1786,17 +1767,7 @@ debug_printf("local addr '%s%s'\n",
   sa_un.sun_path + (*sa_un.sun_path ? 0 : 1));
 #endif
 
-#ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
-sa_un.sun_path[0] = 0; /* Abstract local socket addr - Linux-specific? */
-len = offsetof(struct sockaddr_un, sun_path) + 1
-  + snprintf(sa_un.sun_path+1, sizeof(sa_un.sun_path)-1, "%s",
-             expand_string(notifier_socket));
-#else
-len = offsetof(struct sockaddr_un, sun_path)
-  + snprintf(sa_un.sun_path, sizeof(sa_un.sun_path), "%s",
-             expand_string(notifier_socket));
-#endif
-
+len = daemon_notifier_sockname(&sa_un);
 if (connect(fd, (const struct sockaddr *)&sa_un, len) < 0)
   { where = US"connect"; goto bad2; }
 
@@ -1984,7 +1955,7 @@ switch (vp->type)
       int len = message_body_visible;
 
       if (len > message_size) len = message_size;
-      *ss = body = store_get(len+1, TRUE);
+      *ss = body = store_get(len+1, GET_TAINTED);
       body[0] = 0;
       if (vp->type == vtype_msgbody_end)
        {
@@ -2127,7 +2098,9 @@ Arguments:
   check_end  if TRUE, check for final '}'
   name       name of item, for error message
   resetok    if not NULL, pointer to flag - write FALSE if unsafe to reset
-            the store.
+            the store
+  textonly_p if not NULL, pointer to bitmask of which subs were text-only
+            (did not change when expended)
 
 Returns:     0 OK; string pointer updated
              1 curly bracketing error (too few arguments)
@@ -2137,13 +2110,15 @@ Returns:     0 OK; string pointer updated
 
 static int
 read_subs(uschar **sub, int n, int m, const uschar **sptr, BOOL skipping,
-  BOOL check_end, uschar *name, BOOL *resetok)
+  BOOL check_end, uschar *name, BOOL *resetok, unsigned * textonly_p)
 {
-const uschar *s = *sptr;
+const uschar * s = *sptr;
+unsigned textonly_l = 0;
 
 Uskip_whitespace(&s);
 for (int i = 0; i < n; i++)
   {
+  BOOL textonly;
   if (*s != '{')
     {
     if (i < m)
@@ -2155,9 +2130,11 @@ for (int i = 0; i < n; i++)
     sub[i] = NULL;
     break;
     }
-  if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, resetok)))
+  if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, resetok,
+                                               textonly_p ? &textonly : NULL)))
     return 3;
   if (*s++ != '}') return 1;
+  if (textonly_p && textonly) textonly_l |= BIT(i);
   Uskip_whitespace(&s);
   }
 if (check_end && *s++ != '}')
@@ -2172,6 +2149,7 @@ if (check_end && *s++ != '}')
   return 1;
   }
 
+if (textonly_p) *textonly_p = textonly_l;
 *sptr = s;
 return 0;
 }
@@ -2532,11 +2510,11 @@ Returns:   a pointer to the first character after the condition, or
 */
 
 static const uschar *
-eval_condition(const uschar *s, BOOL *resetok, BOOL *yield)
+eval_condition(const uschar * s, BOOL * resetok, BOOL * yield)
 {
 BOOL testfor = TRUE;
 BOOL tempcond, combined_cond;
-BOOL *subcondptr;
+BOOL * subcondptr;
 BOOL sub2_honour_dollar = TRUE;
 BOOL is_forany, is_json, is_jsons;
 int rc, cond_type;
@@ -2544,7 +2522,8 @@ int_eximarith_t num[2];
 struct stat statbuf;
 uschar * opname;
 uschar name[256];
-const uschar *sub[10];
+const uschar * sub[10];
+unsigned sub_textonly = 0;
 
 for (;;)
   if (Uskip_whitespace(&s) == '!') { testfor = !testfor; s++; } else break;
@@ -2638,8 +2617,12 @@ switch(cond_type = identify_operator(&s, &opname))
 
   if (Uskip_whitespace(&s) != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */
 
-  sub[0] = expand_string_internal(s+1, TRUE, &s, yield == NULL, TRUE, resetok);
-  if (!sub[0]) return NULL;
+   {
+    BOOL textonly;
+    sub[0] = expand_string_internal(s+1, TRUE, &s, yield == NULL, TRUE, resetok, &textonly);
+    if (!sub[0]) return NULL;
+    if (textonly) sub_textonly |= BIT(0);
+   }
   /* {-for-text-editors */
   if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
@@ -2737,7 +2720,7 @@ switch(cond_type = identify_operator(&s, &opname))
     if (*s++ != '{') goto COND_FAILED_CURLY_START;     /*}*/
 
     switch(read_subs(sub, nelem(sub), 1,
-      &s, yield == NULL, TRUE, name, resetok))
+      &s, yield == NULL, TRUE, name, resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
         "error for acl";
@@ -2789,7 +2772,7 @@ switch(cond_type = identify_operator(&s, &opname))
     Uskip_whitespace(&s);
     if (*s++ != '{') goto COND_FAILED_CURLY_START;     /* }-for-text-editors */
     switch(read_subs(sub, nelem(sub), 2, &s, yield == NULL, TRUE, name,
-                   resetok))
+                   resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
        "error for saslauthd";
@@ -2857,9 +2840,11 @@ switch(cond_type = identify_operator(&s, &opname))
 
   for (int i = 0; i < 2; i++)
     {
+    BOOL textonly;
     /* Sometimes, we don't expand substrings; too many insecure configurations
     created using match_address{}{} and friends, where the second param
     includes information from untrustworthy sources. */
+    /*XXX is this moot given taint-tracking? */
     BOOL honour_dollar = TRUE;
     if ((i > 0) && !sub2_honour_dollar)
       honour_dollar = FALSE;
@@ -2872,8 +2857,9 @@ switch(cond_type = identify_operator(&s, &opname))
       return NULL;
       }
     if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL,
-        honour_dollar, resetok)))
+        honour_dollar, resetok, &textonly)))
       return NULL;
+    if (textonly) sub_textonly |= BIT(i);
     DEBUG(D_expand) if (i == 1 && !sub2_honour_dollar && Ustrchr(sub[1], '$'))
       debug_printf_indent("WARNING: the second arg is NOT expanded,"
                        " for security reasons\n");
@@ -2907,152 +2893,125 @@ switch(cond_type = identify_operator(&s, &opname))
     {
     case ECOND_NUM_E:
     case ECOND_NUM_EE:
-    tempcond = (num[0] == num[1]);
-    break;
+      tempcond = (num[0] == num[1]); break;
 
     case ECOND_NUM_G:
-    tempcond = (num[0] > num[1]);
-    break;
+      tempcond = (num[0] > num[1]); break;
 
     case ECOND_NUM_GE:
-    tempcond = (num[0] >= num[1]);
-    break;
+      tempcond = (num[0] >= num[1]); break;
 
     case ECOND_NUM_L:
-    tempcond = (num[0] < num[1]);
-    break;
+      tempcond = (num[0] < num[1]); break;
 
     case ECOND_NUM_LE:
-    tempcond = (num[0] <= num[1]);
-    break;
+      tempcond = (num[0] <= num[1]); break;
 
     case ECOND_STR_LT:
-    tempcond = (Ustrcmp(sub[0], sub[1]) < 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) < 0); break;
 
     case ECOND_STR_LTI:
-    tempcond = (strcmpic(sub[0], sub[1]) < 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) < 0); break;
 
     case ECOND_STR_LE:
-    tempcond = (Ustrcmp(sub[0], sub[1]) <= 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) <= 0); break;
 
     case ECOND_STR_LEI:
-    tempcond = (strcmpic(sub[0], sub[1]) <= 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) <= 0); break;
 
     case ECOND_STR_EQ:
-    tempcond = (Ustrcmp(sub[0], sub[1]) == 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) == 0); break;
 
     case ECOND_STR_EQI:
-    tempcond = (strcmpic(sub[0], sub[1]) == 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) == 0); break;
 
     case ECOND_STR_GT:
-    tempcond = (Ustrcmp(sub[0], sub[1]) > 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) > 0); break;
 
     case ECOND_STR_GTI:
-    tempcond = (strcmpic(sub[0], sub[1]) > 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) > 0); break;
 
     case ECOND_STR_GE:
-    tempcond = (Ustrcmp(sub[0], sub[1]) >= 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) >= 0); break;
 
     case ECOND_STR_GEI:
-    tempcond = (strcmpic(sub[0], sub[1]) >= 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) >= 0); break;
 
     case ECOND_MATCH:   /* Regular expression match */
       {
-      const pcre2_code * re;
-      PCRE2_SIZE offset;
-      int err;
-
-      if (!(re = pcre2_compile((PCRE2_SPTR)sub[1], PCRE2_ZERO_TERMINATED,
-                               PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
-       {
-       uschar errbuf[128];
-       pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-       expand_string_message = string_sprintf("regular expression error in "
-         "\"%s\": %s at offset %ld", sub[1], errbuf, (long)offset);
+      const pcre2_code * re = regex_compile(sub[1],
+                 sub_textonly & BIT(1) ? MCS_CACHEABLE : MCS_NOFLAGS,
+                 &expand_string_message, pcre_gen_cmp_ctx);
+      if (!re)
        return NULL;
-       }
 
       tempcond = regex_match_and_setup(re, sub[0], 0, -1);
       break;
       }
 
     case ECOND_MATCH_ADDRESS:  /* Match in an address list */
-    rc = match_address_list(sub[0], TRUE, FALSE, &(sub[1]), NULL, -1, 0, NULL);
-    goto MATCHED_SOMETHING;
+      rc = match_address_list(sub[0], TRUE, FALSE, &(sub[1]), NULL, -1, 0,
+                             CUSS &lookup_value);
+      goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_DOMAIN:   /* Match in a domain list */
-    rc = match_isinlist(sub[0], &(sub[1]), 0, &domainlist_anchor, NULL,
-      MCL_DOMAIN + MCL_NOEXPAND, TRUE, NULL);
-    goto MATCHED_SOMETHING;
+      rc = match_isinlist(sub[0], &(sub[1]), 0, &domainlist_anchor, NULL,
+       MCL_DOMAIN + MCL_NOEXPAND, TRUE, CUSS &lookup_value);
+      goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_IP:       /* Match IP address in a host list */
-    if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) == 0)
-      {
-      expand_string_message = string_sprintf("\"%s\" is not an IP address",
-        sub[0]);
-      return NULL;
-      }
-    else
-      {
-      unsigned int *nullcache = NULL;
-      check_host_block cb;
-
-      cb.host_name = US"";
-      cb.host_address = sub[0];
-
-      /* If the host address starts off ::ffff: it is an IPv6 address in
-      IPv4-compatible mode. Find the IPv4 part for checking against IPv4
-      addresses. */
-
-      cb.host_ipv4 = (Ustrncmp(cb.host_address, "::ffff:", 7) == 0)?
-        cb.host_address + 7 : cb.host_address;
-
-      rc = match_check_list(
-             &sub[1],                   /* the list */
-             0,                         /* separator character */
-             &hostlist_anchor,          /* anchor pointer */
-             &nullcache,                /* cache pointer */
-             check_host,                /* function for testing */
-             &cb,                       /* argument for function */
-             MCL_HOST,                  /* type of check */
-             sub[0],                    /* text for debugging */
-             NULL);                     /* where to pass back data */
-      }
-    goto MATCHED_SOMETHING;
+      if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) == 0)
+       {
+       expand_string_message = string_sprintf("\"%s\" is not an IP address",
+         sub[0]);
+       return NULL;
+       }
+      else
+       {
+       unsigned int *nullcache = NULL;
+       check_host_block cb;
+
+       cb.host_name = US"";
+       cb.host_address = sub[0];
+
+       /* If the host address starts off ::ffff: it is an IPv6 address in
+       IPv4-compatible mode. Find the IPv4 part for checking against IPv4
+       addresses. */
+
+       cb.host_ipv4 = (Ustrncmp(cb.host_address, "::ffff:", 7) == 0)?
+         cb.host_address + 7 : cb.host_address;
+
+       rc = match_check_list(
+              &sub[1],                   /* the list */
+              0,                         /* separator character */
+              &hostlist_anchor,          /* anchor pointer */
+              &nullcache,                /* cache pointer */
+              check_host,                /* function for testing */
+              &cb,                       /* argument for function */
+              MCL_HOST,                  /* type of check */
+              sub[0],                    /* text for debugging */
+              CUSS &lookup_value);       /* where to pass back data */
+       }
+      goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_LOCAL_PART:
-    rc = match_isinlist(sub[0], &(sub[1]), 0, &localpartlist_anchor, NULL,
-      MCL_LOCALPART + MCL_NOEXPAND, TRUE, NULL);
-    /* Fall through */
-    /* VVVVVVVVVVVV */
-    MATCHED_SOMETHING:
-    switch(rc)
-      {
-      case OK:
-      tempcond = TRUE;
-      break;
-
-      case FAIL:
-      tempcond = FALSE;
-      break;
+      rc = match_isinlist(sub[0], &(sub[1]), 0, &localpartlist_anchor, NULL,
+       MCL_LOCALPART + MCL_NOEXPAND, TRUE, CUSS &lookup_value);
+      /* Fall through */
+      /* VVVVVVVVVVVV */
+      MATCHED_SOMETHING:
+      switch(rc)
+       {
+       case OK:   tempcond = TRUE;  break;
+       case FAIL: tempcond = FALSE; break;
 
-      case DEFER:
-      expand_string_message = string_sprintf("unable to complete match "
-        "against \"%s\": %s", sub[1], search_error_message);
-      return NULL;
-      }
+       case DEFER:
+         expand_string_message = string_sprintf("unable to complete match "
+           "against \"%s\": %s", sub[1], search_error_message);
+         return NULL;
+       }
 
-    break;
+      break;
 
     /* Various "encrypted" comparisons. If the second string starts with
     "{" then an encryption type is given. Default to crypt() or crypt16()
@@ -3061,138 +3020,138 @@ switch(cond_type = identify_operator(&s, &opname))
 
     case ECOND_CRYPTEQ:
     #ifndef SUPPORT_CRYPTEQ
-    goto COND_FAILED_NOT_COMPILED;
+      goto COND_FAILED_NOT_COMPILED;
     #else
-    if (strncmpic(sub[1], US"{md5}", 5) == 0)
-      {
-      int sublen = Ustrlen(sub[1]+5);
-      md5 base;
-      uschar digest[16];
+      if (strncmpic(sub[1], US"{md5}", 5) == 0)
+       {
+       int sublen = Ustrlen(sub[1]+5);
+       md5 base;
+       uschar digest[16];
 
-      md5_start(&base);
-      md5_end(&base, sub[0], Ustrlen(sub[0]), digest);
+       md5_start(&base);
+       md5_end(&base, sub[0], Ustrlen(sub[0]), digest);
 
-      /* If the length that we are comparing against is 24, the MD5 digest
-      is expressed as a base64 string. This is the way LDAP does it. However,
-      some other software uses a straightforward hex representation. We assume
-      this if the length is 32. Other lengths fail. */
+       /* If the length that we are comparing against is 24, the MD5 digest
+       is expressed as a base64 string. This is the way LDAP does it. However,
+       some other software uses a straightforward hex representation. We assume
+       this if the length is 32. Other lengths fail. */
 
-      if (sublen == 24)
-        {
-        uschar *coded = b64encode(CUS digest, 16);
-        DEBUG(D_auth) debug_printf("crypteq: using MD5+B64 hashing\n"
-          "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
-        tempcond = (Ustrcmp(coded, sub[1]+5) == 0);
-        }
-      else if (sublen == 32)
-        {
-        uschar coded[36];
-        for (int i = 0; i < 16; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
-        coded[32] = 0;
-        DEBUG(D_auth) debug_printf("crypteq: using MD5+hex hashing\n"
-          "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
-        tempcond = (strcmpic(coded, sub[1]+5) == 0);
-        }
-      else
-        {
-        DEBUG(D_auth) debug_printf("crypteq: length for MD5 not 24 or 32: "
-          "fail\n  crypted=%s\n", sub[1]+5);
-        tempcond = FALSE;
-        }
-      }
+       if (sublen == 24)
+         {
+         uschar *coded = b64encode(CUS digest, 16);
+         DEBUG(D_auth) debug_printf("crypteq: using MD5+B64 hashing\n"
+           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
+         tempcond = (Ustrcmp(coded, sub[1]+5) == 0);
+         }
+       else if (sublen == 32)
+         {
+         uschar coded[36];
+         for (int i = 0; i < 16; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
+         coded[32] = 0;
+         DEBUG(D_auth) debug_printf("crypteq: using MD5+hex hashing\n"
+           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
+         tempcond = (strcmpic(coded, sub[1]+5) == 0);
+         }
+       else
+         {
+         DEBUG(D_auth) debug_printf("crypteq: length for MD5 not 24 or 32: "
+           "fail\n  crypted=%s\n", sub[1]+5);
+         tempcond = FALSE;
+         }
+       }
 
-    else if (strncmpic(sub[1], US"{sha1}", 6) == 0)
-      {
-      int sublen = Ustrlen(sub[1]+6);
-      hctx h;
-      uschar digest[20];
+      else if (strncmpic(sub[1], US"{sha1}", 6) == 0)
+       {
+       int sublen = Ustrlen(sub[1]+6);
+       hctx h;
+       uschar digest[20];
 
-      sha1_start(&h);
-      sha1_end(&h, sub[0], Ustrlen(sub[0]), digest);
+       sha1_start(&h);
+       sha1_end(&h, sub[0], Ustrlen(sub[0]), digest);
 
-      /* If the length that we are comparing against is 28, assume the SHA1
-      digest is expressed as a base64 string. If the length is 40, assume a
-      straightforward hex representation. Other lengths fail. */
+       /* If the length that we are comparing against is 28, assume the SHA1
+       digest is expressed as a base64 string. If the length is 40, assume a
+       straightforward hex representation. Other lengths fail. */
 
-      if (sublen == 28)
-        {
-        uschar *coded = b64encode(CUS digest, 20);
-        DEBUG(D_auth) debug_printf("crypteq: using SHA1+B64 hashing\n"
-          "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
-        tempcond = (Ustrcmp(coded, sub[1]+6) == 0);
-        }
-      else if (sublen == 40)
-        {
-        uschar coded[44];
-        for (int i = 0; i < 20; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
-        coded[40] = 0;
-        DEBUG(D_auth) debug_printf("crypteq: using SHA1+hex hashing\n"
-          "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
-        tempcond = (strcmpic(coded, sub[1]+6) == 0);
-        }
-      else
-        {
-        DEBUG(D_auth) debug_printf("crypteq: length for SHA-1 not 28 or 40: "
-          "fail\n  crypted=%s\n", sub[1]+6);
-       tempcond = FALSE;
-        }
-      }
+       if (sublen == 28)
+         {
+         uschar *coded = b64encode(CUS digest, 20);
+         DEBUG(D_auth) debug_printf("crypteq: using SHA1+B64 hashing\n"
+           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
+         tempcond = (Ustrcmp(coded, sub[1]+6) == 0);
+         }
+       else if (sublen == 40)
+         {
+         uschar coded[44];
+         for (int i = 0; i < 20; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
+         coded[40] = 0;
+         DEBUG(D_auth) debug_printf("crypteq: using SHA1+hex hashing\n"
+           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
+         tempcond = (strcmpic(coded, sub[1]+6) == 0);
+         }
+       else
+         {
+         DEBUG(D_auth) debug_printf("crypteq: length for SHA-1 not 28 or 40: "
+           "fail\n  crypted=%s\n", sub[1]+6);
+         tempcond = FALSE;
+         }
+       }
 
-    else   /* {crypt} or {crypt16} and non-{ at start */
-           /* }-for-text-editors */
-      {
-      int which = 0;
-      uschar *coded;
+      else   /* {crypt} or {crypt16} and non-{ at start */
+            /* }-for-text-editors */
+       {
+       int which = 0;
+       uschar *coded;
 
-      if (strncmpic(sub[1], US"{crypt}", 7) == 0)
-        {
-        sub[1] += 7;
-        which = 1;
-        }
-      else if (strncmpic(sub[1], US"{crypt16}", 9) == 0)
-        {
-        sub[1] += 9;
-        which = 2;
-        }
-      else if (sub[1][0] == '{')               /* }-for-text-editors */
-        {
-        expand_string_message = string_sprintf("unknown encryption mechanism "
-          "in \"%s\"", sub[1]);
-        return NULL;
-        }
+       if (strncmpic(sub[1], US"{crypt}", 7) == 0)
+         {
+         sub[1] += 7;
+         which = 1;
+         }
+       else if (strncmpic(sub[1], US"{crypt16}", 9) == 0)
+         {
+         sub[1] += 9;
+         which = 2;
+         }
+       else if (sub[1][0] == '{')              /* }-for-text-editors */
+         {
+         expand_string_message = string_sprintf("unknown encryption mechanism "
+           "in \"%s\"", sub[1]);
+         return NULL;
+         }
 
-      switch(which)
-        {
-        case 0:  coded = US DEFAULT_CRYPT(CS sub[0], CS sub[1]); break;
-        case 1:  coded = US crypt(CS sub[0], CS sub[1]); break;
-        default: coded = US crypt16(CS sub[0], CS sub[1]); break;
-        }
+       switch(which)
+         {
+         case 0:  coded = US DEFAULT_CRYPT(CS sub[0], CS sub[1]); break;
+         case 1:  coded = US crypt(CS sub[0], CS sub[1]); break;
+         default: coded = US crypt16(CS sub[0], CS sub[1]); break;
+         }
 
-      #define STR(s) # s
-      #define XSTR(s) STR(s)
-      DEBUG(D_auth) debug_printf("crypteq: using %s()\n"
-        "  subject=%s\n  crypted=%s\n",
-        which == 0 ? XSTR(DEFAULT_CRYPT) : which == 1 ? "crypt" : "crypt16",
-        coded, sub[1]);
-      #undef STR
-      #undef XSTR
-
-      /* If the encrypted string contains fewer than two characters (for the
-      salt), force failure. Otherwise we get false positives: with an empty
-      string the yield of crypt() is an empty string! */
-
-      if (coded)
-       tempcond = Ustrlen(sub[1]) < 2 ? FALSE : Ustrcmp(coded, sub[1]) == 0;
-      else if (errno == EINVAL)
-       tempcond = FALSE;
-      else
-       {
-       expand_string_message = string_sprintf("crypt error: %s\n",
-         US strerror(errno));
-       return NULL;
+       #define STR(s) # s
+       #define XSTR(s) STR(s)
+       DEBUG(D_auth) debug_printf("crypteq: using %s()\n"
+         "  subject=%s\n  crypted=%s\n",
+         which == 0 ? XSTR(DEFAULT_CRYPT) : which == 1 ? "crypt" : "crypt16",
+         coded, sub[1]);
+       #undef STR
+       #undef XSTR
+
+       /* If the encrypted string contains fewer than two characters (for the
+       salt), force failure. Otherwise we get false positives: with an empty
+       string the yield of crypt() is an empty string! */
+
+       if (coded)
+         tempcond = Ustrlen(sub[1]) < 2 ? FALSE : Ustrcmp(coded, sub[1]) == 0;
+       else if (errno == EINVAL)
+         tempcond = FALSE;
+       else
+         {
+         expand_string_message = string_sprintf("crypt error: %s\n",
+           US strerror(errno));
+         return NULL;
+         }
        }
-      }
-    break;
+      break;
     #endif  /* SUPPORT_CRYPTEQ */
 
     case ECOND_INLIST:
@@ -3215,6 +3174,7 @@ switch(cond_type = identify_operator(&s, &opname))
         if (compare(sub[0], iterate_item) == 0)
           {
           tempcond = TRUE;
+         lookup_value = iterate_item;
           break;
           }
        }
@@ -3301,7 +3261,7 @@ switch(cond_type = identify_operator(&s, &opname))
 
     Uskip_whitespace(&s);
     if (*s++ != '{') goto COND_FAILED_CURLY_START;     /* }-for-text-editors */
-    if (!(sub[0] = expand_string_internal(s, TRUE, &s, yield == NULL, TRUE, resetok)))
+    if (!(sub[0] = expand_string_internal(s, TRUE, &s, yield == NULL, TRUE, resetok, NULL)))
       return NULL;
     /* {-for-text-editors */
     if (*s++ != '}') goto COND_FAILED_CURLY_END;
@@ -3389,7 +3349,7 @@ switch(cond_type = identify_operator(&s, &opname))
 
     if (Uskip_whitespace(&s) != '{') goto COND_FAILED_CURLY_START;     /* }-for-text-editors */
     ourname = cond_type == ECOND_BOOL_LAX ? US"bool_lax" : US"bool";
-    switch(read_subs(sub_arg, 1, 1, &s, yield == NULL, FALSE, ourname, resetok))
+    switch(read_subs(sub_arg, 1, 1, &s, yield == NULL, FALSE, ourname, resetok, NULL))
       {
       case 1: expand_string_message = string_sprintf(
                   "too few arguments or bracketing error for %s",
@@ -3457,7 +3417,7 @@ switch(cond_type = identify_operator(&s, &opname))
     uschar cksum[4];
     BOOL boolvalue = FALSE;
 
-    switch(read_subs(sub, 2, 2, CUSS &s, yield == NULL, FALSE, name, resetok))
+    switch(read_subs(sub, 2, 2, CUSS &s, yield == NULL, FALSE, name, resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
        "error for inbound_srs";
@@ -3468,10 +3428,10 @@ switch(cond_type = identify_operator(&s, &opname))
     /* Match the given local_part against the SRS-encoded pattern */
 
     re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]+)=([^=]*)=(.*)$",
-                           TRUE, FALSE);
+                           MCS_CASELESS | MCS_CACHEABLE, FALSE);
     md = pcre2_match_data_create(4+1, pcre_gen_ctx);
     if (pcre2_match(re, sub[0], PCRE2_ZERO_TERMINATED, 0, PCRE_EOPT,
-                   md, pcre_mtc_ctx) < 0)
+                   md, pcre_gen_mtc_ctx) < 0)
       {
       DEBUG(D_expand) debug_printf("no match for SRS'd local-part pattern\n");
       goto srs_result;
@@ -3548,6 +3508,7 @@ switch(cond_type = identify_operator(&s, &opname))
     boolvalue = TRUE;
 
 srs_result:
+    /* pcre2_match_data_free(md);      gen ctx needs no free */
     if (yield) *yield = (boolvalue == testfor);
     return s;
     }
@@ -3713,7 +3674,7 @@ if (*s++ != '{')
 want this string. Set skipping in the call in the fail case (this will always
 be the case if we were already skipping). */
 
-sub1 = expand_string_internal(s, TRUE, &s, !yes, TRUE, resetok);
+sub1 = expand_string_internal(s, TRUE, &s, !yes, TRUE, resetok, NULL);
 if (sub1 == NULL && (yes || !f.expand_string_forcedfail)) goto FAILED;
 f.expand_string_forcedfail = FALSE;
 if (*s++ != '}')
@@ -3742,7 +3703,7 @@ already skipping. */
 
 if (skip_whitespace(&s) == '{')
   {
-  sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping, TRUE, resetok);
+  sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping, TRUE, resetok, NULL);
   if (sub2 == NULL && (!yes || !f.expand_string_forcedfail)) goto FAILED;
   f.expand_string_forcedfail = FALSE;
   if (*s++ != '}')
@@ -3843,8 +3804,8 @@ Returns:  pointer to string containing the last three
 static uschar *
 prvs_daystamp(int day_offset)
 {
-uschar *days = store_get(32, FALSE);         /* Need at least 24 for cases */
-(void)string_format(days, 32, TIME_T_FMT,    /* where TIME_T_FMT is %lld */
+uschar * days = store_get(32, GET_UNTAINTED);      /* Need at least 24 for cases */
+(void)string_format(days, 32, TIME_T_FMT,         /* where TIME_T_FMT is %lld */
   (time(NULL) + day_offset*86400)/86400);
 return (Ustrlen(days) >= 3) ? &days[Ustrlen(days)-3] : US"100";
 }
@@ -3915,7 +3876,7 @@ chash_end(HMAC_SHA1, &h, innerhash, 20, finalhash);
 
 /* Hashing is deemed sufficient to de-taint any input data */
 
-p = finalhash_hex = store_get(40, FALSE);
+p = finalhash_hex = store_get(40, GET_UNTAINTED);
 for (int i = 0; i < 3; i++)
   {
   *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
@@ -3946,7 +3907,7 @@ Returns:       new pointer for expandable string, terminated if non-null
 */
 
 gstring *
-cat_file(FILE *f, gstring *yield, uschar *eol)
+cat_file(FILE * f, gstring * yield, uschar * eol)
 {
 uschar buffer[1024];
 
@@ -3958,8 +3919,6 @@ while (Ufgets(buffer, sizeof(buffer), f))
   if (eol && buffer[len])
     yield = string_cat(yield, eol);
   }
-
-(void) string_from_gstring(yield);
 return yield;
 }
 
@@ -3981,7 +3940,6 @@ while ((rc = tls_read(tls_ctx, buffer, sizeof(buffer))) > 0)
 /* We assume that all errors, and any returns of zero bytes,
 are actually EOF. */
 
-(void) string_from_gstring(yield);
 return yield;
 }
 #endif
@@ -4356,7 +4314,7 @@ list = ((namedlist_block *)(t->data.ptr))->string;
 /* The list could be quite long so we (re)use a buffer for each element
 rather than getting each in new memory */
 
-if (is_tainted(list)) buffer = store_get(LISTNAMED_BUF_SIZE, TRUE);
+if (is_tainted(list)) buffer = store_get(LISTNAMED_BUF_SIZE, GET_TAINTED);
 while ((item = string_nextinlist(&list, &sep, buffer, LISTNAMED_BUF_SIZE)))
   {
   uschar * buf = US" : ";
@@ -4398,6 +4356,36 @@ return yield;
 
 
 
+/************************************************/
+static void
+debug_expansion_interim(const uschar * what, const uschar * value, int nchar,
+  BOOL skipping)
+{
+DEBUG(D_noutf8)
+  debug_printf_indent("|");
+else
+  debug_printf_indent(UTF8_VERT_RIGHT);
+
+for (int fill = 11 - Ustrlen(what); fill > 0; fill--)
+  DEBUG(D_noutf8)
+    debug_printf("-");
+  else
+    debug_printf(UTF8_HORIZ);
+
+debug_printf("%s: %.*s\n", what, nchar, value);
+if (is_tainted(value))
+  {
+  DEBUG(D_noutf8)
+    debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
+  else
+    debug_printf_indent("%s",
+      skipping
+      ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
+  debug_printf("(tainted)\n");
+  }
+}
+
+
 /*************************************************
 *                 Expand string                  *
 *************************************************/
@@ -4454,6 +4442,7 @@ Arguments:
                  FALSE if it's just another character
   resetok_p     if not NULL, pointer to flag - write FALSE if unsafe to reset
                 the store.
+  textonly_p    if not NULL, pointer to flag - write bool for only-met-text
 
 Returns:         NULL if expansion fails:
                    expand_string_forcedfail is set TRUE if failure was forced
@@ -4463,7 +4452,7 @@ Returns:         NULL if expansion fails:
 
 static uschar *
 expand_string_internal(const uschar *string, BOOL ket_ends, const uschar **left,
-  BOOL skipping, BOOL honour_dollar, BOOL *resetok_p)
+  BOOL skipping, BOOL honour_dollar, BOOL *resetok_p, BOOL * textonly_p)
 {
 rmark reset_point = store_mark();
 gstring * yield = string_get(Ustrlen(string) + 64);
@@ -4471,19 +4460,19 @@ int item_type;
 const uschar * s = string;
 const uschar * save_expand_nstring[EXPAND_MAXN+1];
 int save_expand_nlength[EXPAND_MAXN+1];
-BOOL resetok = TRUE, first = TRUE;
+BOOL resetok = TRUE, first = TRUE, textonly = TRUE;
 
 expand_level++;
 f.expand_string_forcedfail = FALSE;
 expand_string_message = US"";
 
-{ uschar *m;
-if ((m = is_tainted2(string, LOG_MAIN|LOG_PANIC, "Tainted string '%s' in expansion", s)))
+if (is_tainted(string))
   {
-  expand_string_message = m;
+  expand_string_message =
+    string_sprintf("attempt to expand tainted string '%s'", s);
+  log_write(0, LOG_MAIN|LOG_PANIC, "%s", expand_string_message);
   goto EXPAND_FAILED;
   }
-}
 
 while (*s)
   {
@@ -4522,14 +4511,11 @@ while (*s)
       {
       const uschar * t = s + 2;
       for (s = t; *s ; s++) if (*s == '\\' && s[1] == 'N') break;
+
       DEBUG(D_expand)
-       DEBUG(D_noutf8)
-         debug_printf_indent("|--protected: %.*s\n", (int)(s - t), t);
-       else
-         debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
-           "protected: %.*s\n", (int)(s - t), t);
+       debug_expansion_interim(US"protected", t, (int)(s - t), skipping);
       yield = string_catn(yield, t, s - t);
-      if (*s != 0) s += 2;
+      if (*s) s += 2;
       }
     else
       {
@@ -4558,18 +4544,13 @@ while (*s)
     for (const uschar * t = s+1;
        *t && *t != '$' && *t != '}' && *t != '\\'; t++) i++;
 
-    DEBUG(D_expand)
-      DEBUG(D_noutf8)
-       debug_printf_indent("|-------text: %.*s\n", i, s);
-      else
-       debug_printf_indent(UTF8_VERT_RIGHT
-         UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-         "text: %.*s\n", i, s);
+    DEBUG(D_expand) debug_expansion_interim(US"text", s, i, skipping);
 
     yield = string_catn(yield, s, i);
     s += i;
     continue;
     }
+  textonly = FALSE;
 
   /* No { after the $ - must be a plain name or a number for string
   match variable. There has to be a fudge for variables that are the
@@ -4592,13 +4573,13 @@ while (*s)
     buffer. */
 
     if (!yield)
-      g = store_get(sizeof(gstring), FALSE);
+      g = store_get(sizeof(gstring), GET_UNTAINTED);
     else if (yield->ptr == 0)
       {
       if (resetok) reset_point = store_reset(reset_point);
       yield = NULL;
       reset_point = store_mark();
-      g = store_get(sizeof(gstring), FALSE);   /* alloc _before_ calling find_variable() */
+      g = store_get(sizeof(gstring), GET_UNTAINTED);   /* alloc _before_ calling find_variable() */
       }
 
     /* Header */
@@ -4708,6 +4689,11 @@ while (*s)
   s = read_name(name, sizeof(name), s, US"_-");
   item_type = chop_match(name, item_table, nelem(item_table));
 
+  /* Switch on item type.  All nondefault choices should "continue* when
+  skipping, but "break" otherwise so we get debug output for the item
+  expansion. */
+  {
+  int start = gstring_length(yield);
   switch(item_type)
     {
     /* Call an ACL from an expansion.  We feed data in via $acl_arg1 - $acl_arg9.
@@ -4722,12 +4708,12 @@ while (*s)
     case EITEM_ACL:
       /* ${acl {name} {arg1}{arg2}...} */
       {
-      uschar *sub[10]; /* name + arg1-arg9 (which must match number of acl_arg[]) */
-      uschar *user_msg;
+      uschar * sub[10];        /* name + arg1-arg9 (which must match number of acl_arg[]) */
+      uschar * user_msg;
       int rc;
 
       switch(read_subs(sub, nelem(sub), 1, &s, skipping, TRUE, name,
-                     &resetok))
+                     &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -4744,7 +4730,7 @@ while (*s)
            debug_printf_indent("acl expansion yield: %s\n", user_msg);
          if (user_msg)
             yield = string_cat(yield, user_msg);
-         continue;
+         break;
 
        case DEFER:
           f.expand_string_forcedfail = TRUE;
@@ -4754,15 +4740,16 @@ while (*s)
            rc_names[rc], sub[0]);
          goto EXPAND_FAILED;
        }
+      break;
       }
 
     case EITEM_AUTHRESULTS:
       /* ${authresults {mysystemname}} */
       {
-      uschar *sub_arg[1];
+      uschar * sub_arg[1];
 
       switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, skipping, TRUE, name,
-                     &resetok))
+                     &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -4788,7 +4775,7 @@ while (*s)
 #ifdef EXPERIMENTAL_ARC
       yield = authres_arc(yield);
 #endif
-      continue;
+      break;
       }
 
     /* Handle conditionals - preserve the values of the numerical expansion
@@ -4802,27 +4789,18 @@ while (*s)
       const uschar *next_s;
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
+      uschar * save_lookup_value = lookup_value;
 
       Uskip_whitespace(&s);
       if (!(next_s = eval_condition(s, &resetok, skipping ? NULL : &cond)))
        goto EXPAND_FAILED;  /* message already set */
 
       DEBUG(D_expand)
-       DEBUG(D_noutf8)
-         {
-         debug_printf_indent("|--condition: %.*s\n", (int)(next_s - s), s);
-         debug_printf_indent("|-----result: %s\n", cond ? "true" : "false");
-         }
-       else
-         {
-         debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
-           "condition: %.*s\n",
-           (int)(next_s - s), s);
-         debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
-           UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-           "result: %s\n",
-           cond ? "true" : "false");
-         }
+       {
+       debug_expansion_interim(US"condition", s, (int)(next_s - s), skipping);
+       debug_expansion_interim(US"result",
+         cond ? US"true" : US"false", cond ? 4 : 5, skipping);
+       }
 
       s = next_s;
 
@@ -4845,9 +4823,10 @@ while (*s)
       /* Restore external setting of expansion variables for continuation
       at this level. */
 
+      lookup_value = save_lookup_value;
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
-      continue;
+      break;
       }
 
 #ifdef SUPPORT_I18N
@@ -4857,7 +4836,7 @@ while (*s)
       uschar *encoded;
 
       switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, skipping, TRUE, name,
-                     &resetok))
+                     &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -4878,14 +4857,13 @@ while (*s)
        goto EXPAND_FAILED;
        }
 
-      if (!skipping)
-       {
-       if (!(encoded = imap_utf7_encode(sub_arg[0], headers_charset,
-                           sub_arg[1][0], sub_arg[2], &expand_string_message)))
-         goto EXPAND_FAILED;
-       yield = string_cat(yield, encoded);
-       }
-      continue;
+      if (skipping) continue;
+
+      if (!(encoded = imap_utf7_encode(sub_arg[0], headers_charset,
+                         sub_arg[1][0], sub_arg[2], &expand_string_message)))
+       goto EXPAND_FAILED;
+      yield = string_cat(yield, encoded);
+      break;
       }
 #endif
 
@@ -4901,9 +4879,9 @@ while (*s)
       int stype, partial, affixlen, starflags;
       int expand_setup = 0;
       int nameptr = 0;
-      uschar *key, *filename;
+      uschar * key, * filename;
       const uschar * affix, * opts;
-      uschar *save_lookup_value = lookup_value;
+      uschar * save_lookup_value = lookup_value;
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
 
@@ -4918,7 +4896,7 @@ while (*s)
 
       if (Uskip_whitespace(&s) == '{')                                 /*}*/
         {
-        key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+        key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok, NULL);
         if (!key) goto EXPAND_FAILED;                  /*{{*/
         if (*s++ != '}')
          {
@@ -4971,15 +4949,12 @@ while (*s)
           goto EXPAND_FAILED;
           }
         }
-      else
-        {
-        if (key)
-          {
-          expand_string_message = string_sprintf("a single key was given for "
-            "lookup type \"%s\", which is not a single-key lookup type", name);
-          goto EXPAND_FAILED;
-          }
-        }
+      else if (key)
+       {
+       expand_string_message = string_sprintf("a single key was given for "
+         "lookup type \"%s\", which is not a single-key lookup type", name);
+       goto EXPAND_FAILED;
+       }
 
       /* Get the next string in brackets and expand it. It is the file name for
       single-key+file lookups, and the whole query otherwise. In the case of
@@ -4991,7 +4966,7 @@ while (*s)
        expand_string_message = US"missing '{' for lookup file-or-query arg";
        goto EXPAND_FAILED_CURLY;                                               /*}}*/
        }
-      if (!(filename = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok)))
+      if (!(filename = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok, NULL)))
        goto EXPAND_FAILED;
                                                                                /*{{*/
       if (*s++ != '}')
@@ -5065,7 +5040,9 @@ while (*s)
 
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
-      continue;
+
+      if (skipping) continue;
+      break;
       }
 
     /* If Perl support is configured, handle calling embedded perl subroutines,
@@ -5083,17 +5060,17 @@ while (*s)
 
 #else   /* EXIM_PERL */
       {
-      uschar *sub_arg[EXIM_PERL_MAX_ARGS + 2];
-      gstring *new_yield;
+      uschar * sub_arg[EXIM_PERL_MAX_ARGS + 2];
+      gstring * new_yield;
 
-      if ((expand_forbid & RDO_PERL) != 0)
+      if (expand_forbid & RDO_PERL)
         {
         expand_string_message = US"Perl calls are not permitted";
         goto EXPAND_FAILED;
         }
 
       switch(read_subs(sub_arg, EXIM_PERL_MAX_ARGS + 1, 1, &s, skipping, TRUE,
-           name, &resetok))
+           name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -5108,7 +5085,7 @@ while (*s)
 
       if (!opt_perl_started)
         {
-        uschar *initerror;
+        uschar * initerror;
         if (!opt_perl_startup)
           {
           expand_string_message = US"A setting of perl_startup is needed when "
@@ -5152,7 +5129,7 @@ while (*s)
 
       f.expand_string_forcedfail = FALSE;
       yield = new_yield;
-      continue;
+      break;
       }
 #endif /* EXIM_PERL */
 
@@ -5161,10 +5138,9 @@ while (*s)
 
     case EITEM_PRVS:
       {
-      uschar *sub_arg[3];
-      uschar *p,*domain;
+      uschar * sub_arg[3], * p, * domain;
 
-      switch(read_subs(sub_arg, 3, 2, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub_arg, 3, 2, &s, skipping, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -5211,17 +5187,16 @@ while (*s)
       yield = string_catn(yield, US"@", 1);
       yield = string_cat (yield, domain);
 
-      continue;
+      break;
       }
 
     /* Check a prvs-encoded address for validity */
 
     case EITEM_PRVSCHECK:
       {
-      uschar *sub_arg[3];
+      uschar * sub_arg[3], * p;
       gstring * g;
-      const pcre2_code *re;
-      uschar *p;
+      const pcre2_code * re;
 
       /* TF: Ugliness: We want to expand parameter 1 first, then set
          up expansion variables that are used in the expansion of
@@ -5238,29 +5213,33 @@ while (*s)
       prvscheck_address = NULL;
       prvscheck_keynum = NULL;
 
-      switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, name, &resetok))
+      switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      re = regex_must_compile(US"^prvs\\=([0-9])([0-9]{3})([A-F0-9]{6})\\=(.+)\\@(.+)$",
-                              TRUE,FALSE);
+      re = regex_must_compile(
+       US"^prvs\\=([0-9])([0-9]{3})([A-F0-9]{6})\\=(.+)\\@(.+)$",
+       MCS_CASELESS | MCS_CACHEABLE, FALSE);
 
       if (regex_match_and_setup(re,sub_arg[0],0,-1))
         {
-        uschar *local_part = string_copyn(expand_nstring[4],expand_nlength[4]);
-        uschar *key_num = string_copyn(expand_nstring[1],expand_nlength[1]);
-        uschar *daystamp = string_copyn(expand_nstring[2],expand_nlength[2]);
-        uschar *hash = string_copyn(expand_nstring[3],expand_nlength[3]);
-        uschar *domain = string_copyn(expand_nstring[5],expand_nlength[5]);
-
-        DEBUG(D_expand) debug_printf_indent("prvscheck localpart: %s\n", local_part);
-        DEBUG(D_expand) debug_printf_indent("prvscheck key number: %s\n", key_num);
-        DEBUG(D_expand) debug_printf_indent("prvscheck daystamp: %s\n", daystamp);
-        DEBUG(D_expand) debug_printf_indent("prvscheck hash: %s\n", hash);
-        DEBUG(D_expand) debug_printf_indent("prvscheck domain: %s\n", domain);
+        uschar * local_part = string_copyn(expand_nstring[4],expand_nlength[4]);
+        uschar * key_num = string_copyn(expand_nstring[1],expand_nlength[1]);
+        uschar * daystamp = string_copyn(expand_nstring[2],expand_nlength[2]);
+        uschar * hash = string_copyn(expand_nstring[3],expand_nlength[3]);
+        uschar * domain = string_copyn(expand_nstring[5],expand_nlength[5]);
+
+        DEBUG(D_expand)
+         {
+         debug_printf_indent("prvscheck localpart: %s\n", local_part);
+         debug_printf_indent("prvscheck key number: %s\n", key_num);
+         debug_printf_indent("prvscheck daystamp: %s\n", daystamp);
+         debug_printf_indent("prvscheck hash: %s\n", hash);
+         debug_printf_indent("prvscheck domain: %s\n", domain);
+         }
 
         /* Set up expansion variables */
         g = string_cat (NULL, local_part);
@@ -5270,7 +5249,7 @@ while (*s)
         prvscheck_keynum = string_copy(key_num);
 
         /* Now expand the second argument */
-        switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, name, &resetok))
+        switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
@@ -5281,7 +5260,6 @@ while (*s)
 
         p = prvs_hmac_sha1(prvscheck_address, sub_arg[0], prvscheck_keynum,
           daystamp);
-
         if (!p)
           {
           expand_string_message = US"hmac-sha1 conversion failed";
@@ -5324,7 +5302,7 @@ while (*s)
         /* Now expand the final argument. We leave this till now so that
         it can include $prvscheck_result. */
 
-        switch(read_subs(sub_arg, 1, 0, &s, skipping, TRUE, name, &resetok))
+        switch(read_subs(sub_arg, 1, 0, &s, skipping, TRUE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
@@ -5345,22 +5323,23 @@ while (*s)
            We need to make sure all subs are expanded first, so as to skip over
            the entire item. */
 
-        switch(read_subs(sub_arg, 2, 1, &s, skipping, TRUE, name, &resetok))
+        switch(read_subs(sub_arg, 2, 1, &s, skipping, TRUE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
           case 3: goto EXPAND_FAILED;
           }
 
-      continue;
+      if (skipping) continue;
+      break;
       }
 
     /* Handle "readfile" to insert an entire file */
 
     case EITEM_READFILE:
       {
-      FILE *f;
-      uschar *sub_arg[2];
+      FILE * f;
+      uschar * sub_arg[2];
 
       if ((expand_forbid & RDO_READFILE) != 0)
         {
@@ -5368,7 +5347,7 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      switch(read_subs(sub_arg, 2, 1, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub_arg, 2, 1, &s, skipping, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -5389,7 +5368,7 @@ while (*s)
 
       yield = cat_file(f, yield, sub_arg[1]);
       (void)fclose(f);
-      continue;
+      break;
       }
 
     /* Handle "readsocket" to insert data from a socket, either
@@ -5409,7 +5388,7 @@ while (*s)
       /* Read up to 4 arguments, but don't do the end of item check afterwards,
       because there may be a string for expansion on failure. */
 
-      switch(read_subs(sub_arg, 4, 2, &s, skipping, FALSE, name, &resetok))
+      switch(read_subs(sub_arg, 4, 2, &s, skipping, FALSE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:                             /* Won't occur: no end check */
@@ -5497,7 +5476,7 @@ while (*s)
 
       if (*s == '{')                                                   /*}*/
         {
-        if (!expand_string_internal(s+1, TRUE, &s, TRUE, TRUE, &resetok))
+        if (!expand_string_internal(s+1, TRUE, &s, TRUE, TRUE, &resetok, NULL))
           goto EXPAND_FAILED;                                          /*{*/
         if (*s++ != '}')
          {                                                             /*{*/
@@ -5513,7 +5492,8 @@ while (*s)
        expand_string_message = US"missing '}' closing readsocket";
        goto EXPAND_FAILED_CURLY;
        }
-      continue;
+      if (skipping) continue;
+      break;
 
       /* Come here on failure to create socket, connect socket, write to the
       socket, or timeout on reading. If another substring follows, expand and
@@ -5522,7 +5502,7 @@ while (*s)
     SOCK_FAIL:
       if (*s != '{') goto EXPAND_FAILED;                               /*}*/
       DEBUG(D_any) debug_printf("%s\n", expand_string_message);
-      if (!(arg = expand_string_internal(s+1, TRUE, &s, FALSE, TRUE, &resetok)))
+      if (!(arg = expand_string_internal(s+1, TRUE, &s, FALSE, TRUE, &resetok, NULL)))
         goto EXPAND_FAILED;
       yield = string_cat(yield, arg);                                  /*{*/
       if (*s++ != '}')
@@ -5538,11 +5518,9 @@ while (*s)
 
     case EITEM_RUN:
       {
-      FILE *f;
-      uschar *arg;
-      const uschar **argv;
-      pid_t pid;
-      int fd_in, fd_out;
+      FILE * f;
+      const uschar * arg, ** argv;
+      BOOL late_expand = TRUE;
 
       if ((expand_forbid & RDO_RUN) != 0)
         {
@@ -5550,17 +5528,45 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
+      /* Handle options to the "run" */
+
+      while (*s == ',')
+       {
+       if (Ustrncmp(++s, "preexpand", 9) == 0)
+         { late_expand = FALSE; s += 9; }
+       else
+         {
+         const uschar * t = s;
+         while (isalpha(*++t)) ;
+         expand_string_message = string_sprintf("bad option '%.*s' for run",
+                                                 (int)(t-s), s);
+         goto EXPAND_FAILED;
+         }
+       }
       Uskip_whitespace(&s);
-      if (*s != '{')
+
+      if (*s != '{')                                   /*}*/
         {
        expand_string_message = US"missing '{' for command arg of run";
-       goto EXPAND_FAILED_CURLY;
+       goto EXPAND_FAILED_CURLY;                       /*"}*/
        }
-      if (!(arg = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok)))
-       goto EXPAND_FAILED;
-      Uskip_whitespace(&s);
+      s++;
+
+      if (late_expand)         /* this is the default case */
+       {                                               /*{*/
+       int n = Ustrcspn(s, "}");
+       arg = skipping ? NULL : string_copyn(s, n);
+       s += n;
+       }
+      else
+       {
+       if (!(arg = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok, NULL)))
+         goto EXPAND_FAILED;
+       Uskip_whitespace(&s);
+       }
+                                                       /*{*/
       if (*s++ != '}')
-        {
+        {                                              /*{*/
        expand_string_message = US"missing '}' closing command arg of run";
        goto EXPAND_FAILED_CURLY;
        }
@@ -5572,13 +5578,17 @@ while (*s)
        }
       else
         {
+       int fd_in, fd_out;
+       pid_t pid;
+
         if (!transport_set_up_command(&argv,    /* anchor for arg list */
             arg,                                /* raw command */
-            FALSE,                              /* don't expand the arguments */
-            0,                                  /* not relevant when... */
-            NULL,                               /* no transporting address */
-            US"${run} expansion",               /* for error messages */
-            &expand_string_message))            /* where to put error message */
+           late_expand,                /* expand args if not already done */
+            0,                          /* not relevant when... */
+            NULL,                       /* no transporting address */
+           late_expand,                /* allow tainted args, when expand-after-split */
+            US"${run} expansion",       /* for error messages */
+            &expand_string_message))    /* where to put error message */
           goto EXPAND_FAILED;
 
         /* Create the child process, making it a group leader. */
@@ -5589,7 +5599,7 @@ while (*s)
           expand_string_message =
             string_sprintf("couldn't create child process: %s", strerror(errno));
           goto EXPAND_FAILED;
-          }
+         }
 
         /* Nothing is written to the standard input. */
 
@@ -5647,7 +5657,8 @@ while (*s)
         case 2: goto EXPAND_FAILED_CURLY;    /* returned value is 0 */
         }
 
-      continue;
+      if (skipping) continue;
+      break;
       }
 
     /* Handle character translation for "tr" */
@@ -5656,9 +5667,9 @@ while (*s)
       {
       int oldptr = gstring_length(yield);
       int o2m;
-      uschar *sub[3];
+      uschar * sub[3];
 
-      switch(read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -5678,7 +5689,8 @@ while (*s)
           }
         }
 
-      continue;
+      if (skipping) continue;
+      break;
       }
 
     /* Handle "hash", "length", "nhash", and "substr" when they are given with
@@ -5692,14 +5704,14 @@ while (*s)
       int len;
       uschar *ret;
       int val[2] = { 0, -1 };
-      uschar *sub[3];
+      uschar * sub[3];
 
       /* "length" takes only 2 arguments whereas the others take 2 or 3.
       Ensure that sub[2] is set in the ${length } case. */
 
       sub[2] = NULL;
       switch(read_subs(sub, (item_type == EITEM_LENGTH)? 2:3, 2, &s, skipping,
-             TRUE, name, &resetok))
+             TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -5741,7 +5753,8 @@ while (*s)
       if (!ret)
        goto EXPAND_FAILED;
       yield = string_catn(yield, ret, len);
-      continue;
+      if (skipping) continue;
+      break;
       }
 
     /* Handle HMAC computation: ${hmac{<algorithm>}{<secret>}{<text>}}
@@ -5756,14 +5769,14 @@ while (*s)
 
     case EITEM_HMAC:
       {
-      uschar *sub[3];
+      uschar * sub[3];
       md5 md5_base;
       hctx sha1_ctx;
-      void *use_base;
+      void * use_base;
       int type;
       int hashlen;      /* Number of octets for the hash algorithm's output */
       int hashblocklen; /* Number of octets the hash algorithm processes */
-      uschar *keyptr, *p;
+      uschar * keyptr, * p;
       unsigned int keylen;
 
       uschar keyhash[MAX_HASHLEN];
@@ -5773,86 +5786,85 @@ while (*s)
       uschar innerkey[MAX_HASHBLOCKLEN];
       uschar outerkey[MAX_HASHBLOCKLEN];
 
-      switch (read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok))
+      switch (read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      if (!skipping)
+      if (skipping) continue;
+
+      if (Ustrcmp(sub[0], "md5") == 0)
        {
-       if (Ustrcmp(sub[0], "md5") == 0)
-         {
-         type = HMAC_MD5;
-         use_base = &md5_base;
-         hashlen = 16;
-         hashblocklen = 64;
-         }
-       else if (Ustrcmp(sub[0], "sha1") == 0)
-         {
-         type = HMAC_SHA1;
-         use_base = &sha1_ctx;
-         hashlen = 20;
-         hashblocklen = 64;
-         }
-       else
-         {
-         expand_string_message =
-           string_sprintf("hmac algorithm \"%s\" is not recognised", sub[0]);
-         goto EXPAND_FAILED;
-         }
+       type = HMAC_MD5;
+       use_base = &md5_base;
+       hashlen = 16;
+       hashblocklen = 64;
+       }
+      else if (Ustrcmp(sub[0], "sha1") == 0)
+       {
+       type = HMAC_SHA1;
+       use_base = &sha1_ctx;
+       hashlen = 20;
+       hashblocklen = 64;
+       }
+      else
+       {
+       expand_string_message =
+         string_sprintf("hmac algorithm \"%s\" is not recognised", sub[0]);
+       goto EXPAND_FAILED;
+       }
 
-       keyptr = sub[1];
-       keylen = Ustrlen(keyptr);
+      keyptr = sub[1];
+      keylen = Ustrlen(keyptr);
 
-       /* If the key is longer than the hash block length, then hash the key
-       first */
+      /* If the key is longer than the hash block length, then hash the key
+      first */
 
-       if (keylen > hashblocklen)
-         {
-         chash_start(type, use_base);
-         chash_end(type, use_base, keyptr, keylen, keyhash);
-         keyptr = keyhash;
-         keylen = hashlen;
-         }
+      if (keylen > hashblocklen)
+       {
+       chash_start(type, use_base);
+       chash_end(type, use_base, keyptr, keylen, keyhash);
+       keyptr = keyhash;
+       keylen = hashlen;
+       }
 
-       /* Now make the inner and outer key values */
+      /* Now make the inner and outer key values */
 
-       memset(innerkey, 0x36, hashblocklen);
-       memset(outerkey, 0x5c, hashblocklen);
+      memset(innerkey, 0x36, hashblocklen);
+      memset(outerkey, 0x5c, hashblocklen);
 
-       for (int i = 0; i < keylen; i++)
-         {
-         innerkey[i] ^= keyptr[i];
-         outerkey[i] ^= keyptr[i];
-         }
+      for (int i = 0; i < keylen; i++)
+       {
+       innerkey[i] ^= keyptr[i];
+       outerkey[i] ^= keyptr[i];
+       }
 
-       /* Now do the hashes */
+      /* Now do the hashes */
 
-       chash_start(type, use_base);
-       chash_mid(type, use_base, innerkey);
-       chash_end(type, use_base, sub[2], Ustrlen(sub[2]), innerhash);
+      chash_start(type, use_base);
+      chash_mid(type, use_base, innerkey);
+      chash_end(type, use_base, sub[2], Ustrlen(sub[2]), innerhash);
 
-       chash_start(type, use_base);
-       chash_mid(type, use_base, outerkey);
-       chash_end(type, use_base, innerhash, hashlen, finalhash);
+      chash_start(type, use_base);
+      chash_mid(type, use_base, outerkey);
+      chash_end(type, use_base, innerhash, hashlen, finalhash);
 
-       /* Encode the final hash as a hex string */
+      /* Encode the final hash as a hex string */
 
-       p = finalhash_hex;
-       for (int i = 0; i < hashlen; i++)
-         {
-         *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
-         *p++ = hex_digits[finalhash[i] & 0x0f];
-         }
+      p = finalhash_hex;
+      for (int i = 0; i < hashlen; i++)
+       {
+       *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
+       *p++ = hex_digits[finalhash[i] & 0x0f];
+       }
 
-       DEBUG(D_any) debug_printf("HMAC[%s](%.*s,%s)=%.*s\n",
-         sub[0], (int)keylen, keyptr, sub[2], hashlen*2, finalhash_hex);
+      DEBUG(D_any) debug_printf("HMAC[%s](%.*s,%s)=%.*s\n",
+       sub[0], (int)keylen, keyptr, sub[2], hashlen*2, finalhash_hex);
 
-       yield = string_catn(yield, finalhash_hex, hashlen*2);
-       }
-      continue;
+      yield = string_catn(yield, finalhash_hex, hashlen*2);
+      break;
       }
 
     /* Handle global substitution for "sg" - like Perl's s/xxx/yyy/g operator.
@@ -5862,31 +5874,29 @@ while (*s)
       {
       const pcre2_code * re;
       int moffset, moffsetextra, slen;
-      PCRE2_SIZE roffset;
       pcre2_match_data * md;
-      int err, emptyopt;
+      int emptyopt;
       uschar * subject, * sub[3];
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
+      unsigned sub_textonly = 0;
 
-      switch(read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok, &sub_textonly))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
+      if (skipping) continue;
 
       /* Compile the regular expression */
 
-      if (!(re = pcre2_compile((PCRE2_SPTR)sub[1], PCRE2_ZERO_TERMINATED,
-                 PCRE_COPT, &err, &roffset, pcre_cmp_ctx)))
-        {
-        uschar errbuf[128];
-       pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-        expand_string_message = string_sprintf("regular expression error in "
-          "\"%s\": %s at offset %ld", sub[1], errbuf, (long)roffset);
+      re = regex_compile(sub[1],
+             sub_textonly & BIT(1) ? MCS_CACHEABLE : MCS_NOFLAGS,
+             &expand_string_message, pcre_gen_cmp_ctx);
+      if (!re)
         goto EXPAND_FAILED;
-        }
+
       md = pcre2_match_data_create(EXPAND_MAXN + 1, pcre_gen_ctx);
 
       /* Now run a loop to do the substitutions as often as necessary. It ends
@@ -5902,7 +5912,7 @@ while (*s)
         {
        PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
        int n = pcre2_match(re, (PCRE2_SPTR)subject, slen, moffset + moffsetextra,
-         PCRE_EOPT | emptyopt, md, pcre_mtc_ctx);
+         PCRE_EOPT | emptyopt, md, pcre_gen_mtc_ctx);
         uschar * insert;
 
         /* No match - if we previously set PCRE_NOTEMPTY after a null match, this
@@ -5938,8 +5948,7 @@ while (*s)
 
         /* Copy the characters before the match, plus the expanded insertion. */
 
-       if (ovec[0] > moffset)
-         yield = string_catn(yield, subject + moffset, ovec[0] - moffset);
+       yield = string_catn(yield, subject + moffset, ovec[0] - moffset);
 
         if (!(insert = expand_string(sub[2])))
          goto EXPAND_FAILED;
@@ -5965,9 +5974,10 @@ while (*s)
 
       /* All done - restore numerical variables. */
 
+      /* pcre2_match_data_free(md);    gen ctx needs no free */
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
-      continue;
+      break;
       }
 
     /* Handle keyed and numbered substring extraction. If the first argument
@@ -6005,7 +6015,7 @@ while (*s)
        {
         for (int j = 5; j > 0 && *s == '{'; j--)                       /*'}'*/
          {
-          if (!expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok))
+          if (!expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok, NULL))
            goto EXPAND_FAILED;                                 /*'{'*/
           if (*s++ != '}')
            {
@@ -6032,7 +6042,7 @@ while (*s)
         {
        if (Uskip_whitespace(&s) == '{')                                /*'}'*/
           {
-          if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok)))
+          if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok, NULL)))
            goto EXPAND_FAILED;                                         /*'{'*/
           if (*s++ != '}')
            {
@@ -6202,7 +6212,8 @@ while (*s)
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
 
-      continue;
+      if (skipping) continue;
+      break;
       }
 
     /* return the Nth item from a list */
@@ -6225,7 +6236,7 @@ while (*s)
          goto EXPAND_FAILED_CURLY;
          }
 
-       sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+       sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok, NULL);
        if (!sub[i])     goto EXPAND_FAILED;                            /*{{*/
        if (*s++ != '}')
          {
@@ -6299,13 +6310,14 @@ while (*s)
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
 
-      continue;
+      if (skipping) continue;
+      break;
       }
 
     case EITEM_LISTQUOTE:
       {
       uschar * sub[2];
-      switch(read_subs(sub, 2, 2, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub, 2, 2, &s, skipping, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -6317,7 +6329,8 @@ while (*s)
        yield = string_catn(yield, sub[1], 1);
        }
       else yield = string_catn(yield, US" ", 1);
-      continue;
+      if (skipping) continue;
+      break;
       }
 
 #ifndef DISABLE_TLS
@@ -6333,7 +6346,7 @@ while (*s)
        expand_string_message = US"missing '{' for field arg of certextract";
        goto EXPAND_FAILED_CURLY;                                       /*}*/
        }
-      sub[0] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+      sub[0] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok, NULL);
       if (!sub[0])     goto EXPAND_FAILED;                             /*{{*/
       if (*s++ != '}')
         {
@@ -6365,7 +6378,7 @@ while (*s)
          "be a certificate variable";
        goto EXPAND_FAILED;
        }
-      sub[1] = expand_string_internal(s+1, TRUE, &s, skipping, FALSE, &resetok);
+      sub[1] = expand_string_internal(s+1, TRUE, &s, skipping, FALSE, &resetok, NULL);
       if (!sub[1])     goto EXPAND_FAILED;                             /*{{*/
       if (*s++ != '}')
         {
@@ -6395,7 +6408,8 @@ while (*s)
 
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
-      continue;
+      if (skipping) continue;
+      break;
       }
 #endif /*DISABLE_TLS*/
 
@@ -6419,7 +6433,7 @@ while (*s)
        goto EXPAND_FAILED_CURLY;                                       /*}*/
        }
 
-      if (!(list = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok)))
+      if (!(list = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok, NULL)))
        goto EXPAND_FAILED;                                             /*{{*/
       if (*s++ != '}')
         {
@@ -6437,7 +6451,7 @@ while (*s)
          expand_string_message = US"missing '{' for second arg of reduce";
          goto EXPAND_FAILED_CURLY;                                     /*}*/
          }
-        t = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
+        t = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok, NULL);
         if (!t) goto EXPAND_FAILED;
         lookup_value = t;                                              /*{{*/
         if (*s++ != '}')
@@ -6464,7 +6478,7 @@ while (*s)
       the normal internal expansion function. */
 
       if (item_type != EITEM_FILTER)
-        temp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok);
+        temp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok, NULL);
       else
         if ((temp = eval_condition(expr, &resetok, NULL))) s = temp;
 
@@ -6526,7 +6540,7 @@ while (*s)
 
         else
           {
-         uschar * t = expand_string_internal(expr, TRUE, NULL, skipping, TRUE, &resetok);
+         uschar * t = expand_string_internal(expr, TRUE, NULL, skipping, TRUE, &resetok, NULL);
           temp = t;
           if (!temp)
             {
@@ -6549,6 +6563,9 @@ while (*s)
         item of the output list, add in a space if the new item begins with the
         separator character, or is an empty string. */
 
+/*XXX is there not a standard support function for this, appending to a list? */
+/* yes, string_append_listele(), but it depends on lack of text before the list */
+
         if (  yield && yield->ptr != save_ptr
           && (temp[0] == *outsep || temp[0] == 0))
           yield = string_catn(yield, US" ", 1);
@@ -6595,7 +6612,8 @@ while (*s)
       /* Restore preserved $item */
 
       iterate_item = save_iterate_item;
-      continue;
+      if (skipping) continue;
+      break;
       }
 
     case EITEM_SORT:
@@ -6613,7 +6631,7 @@ while (*s)
        goto EXPAND_FAILED_CURLY;                                       /*}*/
        }
 
-      srclist = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
+      srclist = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok, NULL);
       if (!srclist) goto EXPAND_FAILED;                                        /*{{*/
       if (*s++ != '}')
         {
@@ -6628,7 +6646,7 @@ while (*s)
        goto EXPAND_FAILED_CURLY;                                       /*}*/
        }
 
-      cmp = expand_string_internal(s, TRUE, &s, skipping, FALSE, &resetok);
+      cmp = expand_string_internal(s, TRUE, &s, skipping, FALSE, &resetok, NULL);
       if (!cmp) goto EXPAND_FAILED;                                    /*{{*/
       if (*s++ != '}')
         {
@@ -6663,7 +6681,7 @@ while (*s)
        }
 
       xtract = s;
-      if (!(tmp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok)))
+      if (!(tmp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok, NULL)))
        goto EXPAND_FAILED;
       xtract = string_copyn(xtract, s - xtract);
                                                                        /*{{*/
@@ -6691,7 +6709,7 @@ while (*s)
        /* extract field for comparisons */
        iterate_item = srcitem;
        if (  !(srcfield = expand_string_internal(xtract, FALSE, NULL, FALSE,
-                                         TRUE, &resetok))
+                                         TRUE, &resetok, NULL))
           || !*srcfield)
          {
          expand_string_message = string_sprintf(
@@ -6708,7 +6726,7 @@ while (*s)
 
          /* field for comparison */
          if (!(dstfield = string_nextinlist(&dstkeylist, &sep, NULL, 0)))
-           goto sort_mismatch;
+           goto SORT_MISMATCH;
 
          /* String-comparator names start with a letter; numeric names do not */
 
@@ -6729,7 +6747,7 @@ while (*s)
            while ((dstitem = string_nextinlist(&dstlist, &sep, NULL, 0)))
              {
              if (!(dstfield = string_nextinlist(&dstkeylist, &sep, NULL, 0)))
-               goto sort_mismatch;
+               goto SORT_MISMATCH;
              newlist = string_append_listele(newlist, sep, dstitem);
              newkeylist = string_append_listele(newkeylist, sep, dstfield);
              }
@@ -6760,9 +6778,9 @@ while (*s)
 
       /* Restore preserved $item */
       iterate_item = save_iterate_item;
-      continue;
+      break;
 
-      sort_mismatch:
+      SORT_MISMATCH:
        expand_string_message = US"Internal error in sort (list mismatch)";
        goto EXPAND_FAILED;
       }
@@ -6797,7 +6815,7 @@ while (*s)
         }
 
       switch(read_subs(argv, EXPAND_DLFUNC_MAX_ARGS + 2, 2, &s, skipping,
-           TRUE, name, &resetok))
+           TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -6821,7 +6839,7 @@ while (*s)
           log_write(0, LOG_MAIN|LOG_PANIC, "%s", expand_string_message);
           goto EXPAND_FAILED;
           }
-        t = store_get_perm(sizeof(tree_node) + Ustrlen(argv[0]), is_tainted(argv[0]));
+        t = store_get_perm(sizeof(tree_node) + Ustrlen(argv[0]), argv[0]);
         Ustrcpy(t->name, argv[0]);
         t->data.ptr = handle;
         (void)tree_insertnode(&dlobj_anchor, t);
@@ -6861,7 +6879,7 @@ while (*s)
         }
 
       if (result) yield = string_cat(yield, result);
-      continue;
+      break;
       }
 #endif /* EXPAND_DLFUNC */
 
@@ -6873,7 +6891,7 @@ while (*s)
       if (Uskip_whitespace(&s) != '{')                                 /*}*/
        goto EXPAND_FAILED;
 
-      key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+      key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok, NULL);
       if (!key) goto EXPAND_FAILED;                                    /*{{*/
       if (*s++ != '}')
         {
@@ -6895,7 +6913,8 @@ while (*s)
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
         case 2: goto EXPAND_FAILED_CURLY;    /* returned value is 0 */
         }
-      continue;
+      if (skipping) continue;
+      break;
       }
 
 #ifdef SUPPORT_SRS
@@ -6907,7 +6926,7 @@ while (*s)
       gstring * g = NULL;
       BOOL quoted = FALSE;
 
-      switch (read_subs(sub, 3, 3, CUSS &s, skipping, TRUE, name, &resetok))
+      switch (read_subs(sub, 3, 3, CUSS &s, skipping, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -6976,10 +6995,25 @@ while (*s)
       /* @$original_domain */
       yield = string_catn(yield, US"@", 1);
       yield = string_cat(yield, sub[2]);
-      continue;
+
+      if (skipping) continue;
+      break;
       }
 #endif /*SUPPORT_SRS*/
+
+    default:
+      goto NOT_ITEM;
     }  /* EITEM_* switch */
+    /*NOTREACHED*/
+
+  DEBUG(D_expand)
+    if (yield && (start > 0 || *s))    /* only if not the sole expansion of the line */
+      debug_expansion_interim(US"item-res",
+                             yield->s + start, yield->ptr - start, skipping);
+  continue;
+
+NOT_ITEM: ;
+  }
 
   /* Control reaches here if the name is not recognized as one of the more
   complicated expansion items. Check for the "operator" syntax (name terminated
@@ -6989,10 +7023,9 @@ while (*s)
   if (*s == ':')
     {
     int c;
-    uschar *arg = NULL;
-    uschar *sub;
+    uschar * arg = NULL, * sub;
 #ifndef DISABLE_TLS
-    var_entry *vp = NULL;
+    var_entry * vp = NULL;
 #endif
 
     /* Owing to an historical mis-design, an underscore may be part of the
@@ -7023,7 +7056,7 @@ while (*s)
          {
          const uschar * s1 = s;
          sub = expand_string_internal(s+2, TRUE, &s1, skipping,
-                 FALSE, &resetok);
+                 FALSE, &resetok, NULL);
          if (!sub)       goto EXPAND_FAILED;           /*{*/
          if (*s1 != '}')
            {                                           /*{*/
@@ -7041,7 +7074,7 @@ while (*s)
         /*FALLTHROUGH*/
 #endif
       default:
-       sub = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+       sub = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok, NULL);
        if (!sub) goto EXPAND_FAILED;
        s++;
        break;
@@ -7138,7 +7171,7 @@ while (*s)
 
       case EOP_EXPAND:
        {
-       uschar *expanded = expand_string_internal(sub, FALSE, NULL, skipping, TRUE, &resetok);
+       uschar *expanded = expand_string_internal(sub, FALSE, NULL, skipping, TRUE, &resetok, NULL);
        if (!expanded)
          {
          expand_string_message =
@@ -7169,14 +7202,14 @@ while (*s)
        }
 
       case EOP_MD5:
-  #ifndef DISABLE_TLS
+#ifndef DISABLE_TLS
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_md5(*(void **)vp->value);
          yield = string_cat(yield, cp);
          }
        else
-  #endif
+#endif
          {
          md5 base;
          uschar digest[16];
@@ -7188,14 +7221,14 @@ while (*s)
        break;
 
       case EOP_SHA1:
-  #ifndef DISABLE_TLS
+#ifndef DISABLE_TLS
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_sha1(*(void **)vp->value);
          yield = string_cat(yield, cp);
          }
        else
-  #endif
+#endif
          {
          hctx h;
          uschar digest[20];
@@ -7208,7 +7241,7 @@ while (*s)
 
       case EOP_SHA2:
       case EOP_SHA256:
-  #ifdef EXIM_HAVE_SHA2
+#ifdef EXIM_HAVE_SHA2
        if (vp && *(void **)vp->value)
          if (c == EOP_SHA256)
            yield = string_cat(yield, tls_cert_fprt_sha256(*(void **)vp->value));
@@ -7230,18 +7263,18 @@ while (*s)
            goto EXPAND_FAILED;
            }
 
-         exim_sha_update(&h, sub, Ustrlen(sub));
+         exim_sha_update_string(&h, sub);
          exim_sha_finish(&h, &b);
          while (b.len-- > 0)
            yield = string_fmt_append(yield, "%02X", *b.data++);
          }
-  #else
+#else
          expand_string_message = US"sha256 only supported with TLS";
-  #endif
+#endif
        break;
 
       case EOP_SHA3:
-  #ifdef EXIM_HAVE_SHA3
+#ifdef EXIM_HAVE_SHA3
        {
        hctx h;
        blob b;
@@ -7258,16 +7291,16 @@ while (*s)
          goto EXPAND_FAILED;
          }
 
-       exim_sha_update(&h, sub, Ustrlen(sub));
+       exim_sha_update_string(&h, sub);
        exim_sha_finish(&h, &b);
        while (b.len-- > 0)
          yield = string_fmt_append(yield, "%02X", *b.data++);
        }
        break;
-  #else
+#else
        expand_string_message = US"sha3 only supported with GnuTLS 3.5.0 + or OpenSSL 1.1.1 +";
        goto EXPAND_FAILED;
-  #endif
+#endif
 
       /* Convert hex encoding to base64 encoding */
 
@@ -7335,7 +7368,7 @@ while (*s)
       case EOP_LISTCOUNT:
        {
        int cnt = 0, sep = 0;
-       uschar * buf = store_get(2, is_tainted(sub));
+       uschar * buf = store_get(2, sub);
 
        while (string_nextinlist(CUSS &sub, &sep, buf, 1)) cnt++;
        yield = string_fmt_append(yield, "%d", cnt);
@@ -7595,10 +7628,10 @@ while (*s)
            goto EXPAND_FAILED;
            }
 
-        if (lookup_list[n]->quote)
-          sub = (lookup_list[n]->quote)(sub, opt);
-        else if (opt)
-         sub = NULL;
+         if (lookup_list[n]->quote)
+           sub = (lookup_list[n]->quote)(sub, opt, (unsigned)n);
+         else if (opt)
+           sub = NULL;
 
          if (!sub)
            {
@@ -7658,7 +7691,7 @@ while (*s)
 
        case EOP_FROM_UTF8:
          {
-         uschar * buff = store_get(4, is_tainted(sub));
+         uschar * buff = store_get(4, sub);
          while (*sub)
            {
            int c;
@@ -7684,13 +7717,10 @@ while (*s)
 
          /* Manually track tainting, as we deal in individual chars below */
 
-         if (is_tainted(sub))
-           {
-           if (yield->s && yield->ptr)
-             gstring_rebuffer(yield);
-           else
-             yield->s = store_get(yield->size = Ustrlen(sub), is_tainted(sub));
-           }
+         if (!yield->s || !yield->ptr)
+           yield->s = store_get(yield->size = Ustrlen(sub), sub);
+         else if (is_incompatible(yield->s, sub))
+           gstring_rebuffer(yield, sub);
 
          /* Check the UTF-8, byte-by-byte */
 
@@ -7771,7 +7801,7 @@ while (*s)
          break;
          }
 
-  #ifdef SUPPORT_I18N
+#ifdef SUPPORT_I18N
        case EOP_UTF8_DOMAIN_TO_ALABEL:
          {
          uschar * error = NULL;
@@ -7832,7 +7862,7 @@ while (*s)
          yield = string_cat(yield, s);
          break;
          }
-  #endif       /* EXPERIMENTAL_INTERNATIONAL */
+#endif /* EXPERIMENTAL_INTERNATIONAL */
 
        /* escape turns all non-printing characters into escape sequences. */
 
@@ -7908,13 +7938,13 @@ while (*s)
        case EOP_STR2B64:
        case EOP_BASE64:
          {
-  #ifndef DISABLE_TLS
+#ifndef DISABLE_TLS
          uschar * s = vp && *(void **)vp->value
            ? tls_cert_der_b64(*(void **)vp->value)
            : b64encode(CUS sub, Ustrlen(sub));
-  #else
+#else
          uschar * s = b64encode(CUS sub, Ustrlen(sub));
-  #endif
+#endif
          yield = string_cat(yield, s);
          break;
          }
@@ -8133,7 +8163,7 @@ while (*s)
          if (tainted)
            {
            debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
-           debug_printf("(tainted)\n");
+           debug_print_taint(yield->s);
            }
          }
        else
@@ -8146,7 +8176,7 @@ while (*s)
            debug_printf_indent("%s",
              skipping
              ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
-           debug_printf("(tainted)\n");
+           debug_print_taint(yield->s);
            }
          }
        }
@@ -8170,13 +8200,13 @@ while (*s)
     gstring * g = NULL;
 
     if (!yield)
-      g = store_get(sizeof(gstring), FALSE);
+      g = store_get(sizeof(gstring), GET_UNTAINTED);
     else if (yield->ptr == 0)
       {
       if (resetok) reset_point = store_reset(reset_point);
       yield = NULL;
       reset_point = store_mark();
-      g = store_get(sizeof(gstring), FALSE);   /* alloc _before_ calling find_variable() */
+      g = store_get(sizeof(gstring), GET_UNTAINTED);   /* alloc _before_ calling find_variable() */
       }
     if (!(value = find_variable(name, FALSE, skipping, &newsize)))
       {
@@ -8210,7 +8240,7 @@ while (*s)
 terminating brace. */
 
 if (ket_ends && !*s)
-  {
+  {                                                    /*{{*/
   expand_string_message = malformed_header
     ? US"missing } at end of string - could be header name not terminated by colon"
     : US"missing } at end of string";
@@ -8242,8 +8272,10 @@ DEBUG(D_expand)
     debug_printf_indent("%sresult: %s\n",
       skipping ? "|-----" : "\\_____", yield->s);
     if (tainted)
-      debug_printf_indent("%s     \\__(tainted)\n",
-       skipping ? "|     " : "      ");
+      {
+      debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
+      debug_print_taint(yield->s);
+      }
     if (skipping)
       debug_printf_indent("\\___skipping: result is not used\n");
     }
@@ -8257,14 +8289,18 @@ DEBUG(D_expand)
       skipping ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
       yield->s);
     if (tainted)
-      debug_printf_indent("%s(tainted)\n",
+      {
+      debug_printf_indent("%s",
        skipping
        ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
+      debug_print_taint(yield->s);
+      }
     if (skipping)
       debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
        "skipping: result is not used\n");
     }
   }
+if (textonly_p) *textonly_p = textonly;
 expand_level--;
 return yield->s;
 
@@ -8313,16 +8349,20 @@ return NULL;
 }
 
 
+
 /* This is the external function call. Do a quick check for any expansion
 metacharacters, and if there are none, just return the input string.
 
-Argument: the string to be expanded
+Arguments
+       the string to be expanded
+       optional pointer for return boolean indicating no-dynamic-expansions
+
 Returns:  the expanded string, or NULL if expansion failed; if failure was
           due to a lookup deferring, search_find_defer will be TRUE
 */
 
 const uschar *
-expand_cstring(const uschar * string)
+expand_string_2(const uschar * string, BOOL * textonly_p)
 {
 if (Ustrpbrk(string, "$\\") != NULL)
   {
@@ -8332,19 +8372,22 @@ if (Ustrpbrk(string, "$\\") != NULL)
   f.search_find_defer = FALSE;
   malformed_header = FALSE;
   store_pool = POOL_MAIN;
-    s = expand_string_internal(string, FALSE, NULL, FALSE, TRUE, NULL);
+    s = expand_string_internal(string, FALSE, NULL, FALSE, TRUE, NULL, textonly_p);
   store_pool = old_pool;
   return s;
   }
+if (textonly_p) *textonly_p = TRUE;
 return string;
 }
 
+const uschar *
+expand_cstring(const uschar * string)
+{ return expand_string_2(string, NULL); }
 
 uschar *
 expand_string(uschar * string)
-{
-return US expand_cstring(CUS string);
-}
+{ return US expand_string_2(CUS string, NULL); }
+