Split out separate fn for delay-warning-message send
[exim.git] / src / src / expand.c
index 12edd195caf222e82d396389d00875e52d40e072..baf7134cdc2b79848cd323d7ae154cf7f310935f 100644 (file)
@@ -2,9 +2,10 @@
 *     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. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Functions for handling string expansion. */
 
 #include "exim.h"
 
-/* Recursively called function */
+#ifdef MACRO_PREDEF
+# include "macro_predef.h"
+#endif
 
-static uschar *expand_string_internal(const uschar *, BOOL, const uschar **, BOOL, BOOL, BOOL *);
-static int_eximarith_t expanded_string_integer(const uschar *, BOOL);
+typedef unsigned esi_flags;
+#define ESI_NOFLAGS            0
+#define ESI_BRACE_ENDS         BIT(0)  /* expansion should stop at } */
+#define ESI_HONOR_DOLLAR       BIT(1)  /* $ is meaningfull */
+#define ESI_SKIPPING           BIT(2)  /* value will not be needed */
 
 #ifdef STAND_ALONE
 # ifndef SUPPORT_CRYPTEQ
 #  define SUPPORT_CRYPTEQ
 # endif
-#endif
+#else
+
+/* Recursively called function */
+
+static uschar *expand_string_internal(const uschar *, esi_flags, const uschar **, BOOL *, BOOL *);
+static int_eximarith_t expanded_string_integer(const uschar *, BOOL);
+
+#endif /*!STAND_ALONE*/
 
 #ifdef LOOKUP_LDAP
 # include "lookups/ldap.h"
@@ -465,8 +478,8 @@ typedef struct {
   int  *length;
 } alblock;
 
-static uschar * fn_recipients(void);
 typedef uschar * stringptr_fn_t(void);
+static uschar * fn_recipients(void);
 static uschar * fn_queue_size(void);
 
 /* This table must be kept in alphabetical order. */
@@ -672,7 +685,7 @@ static var_entry var_table[] = {
   { "qualify_domain",      vtype_stringptr,   &qualify_domain_sender },
   { "qualify_recipient",   vtype_stringptr,   &qualify_domain_recipient },
   { "queue_name",          vtype_stringptr,   &queue_name },
-  { "queue_size",          vtype_string_func, &fn_queue_size },
+  { "queue_size",          vtype_string_func, (void *) &fn_queue_size },
   { "rcpt_count",          vtype_int,         &rcpt_count },
   { "rcpt_defer_count",    vtype_int,         &rcpt_defer_count },
   { "rcpt_fail_count",     vtype_int,         &rcpt_fail_count },
@@ -686,6 +699,7 @@ static var_entry var_table[] = {
   { "recipient_verify_failure",vtype_stringptr,&recipient_verify_failure },
   { "recipients",          vtype_string_func, (void *) &fn_recipients },
   { "recipients_count",    vtype_int,         &recipients_count },
+  { "regex_cachesize",     vtype_int,         &regex_cachesize },/* undocumented; devel observability */
 #ifdef WITH_CONTENT_SCAN
   { "regex_match_string",  vtype_stringptr,   &regex_match_string },
 #endif
@@ -703,6 +717,7 @@ static var_entry var_table[] = {
   { "sender_fullhost",     vtype_stringptr,   &sender_fullhost },
   { "sender_helo_dnssec",  vtype_bool,        &sender_helo_dnssec },
   { "sender_helo_name",    vtype_stringptr,   &sender_helo_name },
+  { "sender_helo_verified",vtype_string_func, (void *) &sender_helo_verified_boolstr },
   { "sender_host_address", vtype_stringptr,   &sender_host_address },
   { "sender_host_authenticated",vtype_stringptr, &sender_host_authenticated },
   { "sender_host_dnssec",  vtype_bool,        &sender_host_dnssec },
@@ -822,7 +837,75 @@ static var_entry var_table[] = {
   { "warnmsg_recipients",  vtype_stringptr,   &warnmsg_recipients }
 };
 
-static int var_table_size = nelem(var_table);
+#ifdef MACRO_PREDEF
+
+/* dummies */
+uschar * fn_arc_domains(void) {return NULL;}
+uschar * fn_hdrs_added(void) {return NULL;}
+uschar * fn_queue_size(void) {return NULL;}
+uschar * fn_recipients(void) {return NULL;}
+uschar * sender_helo_verified_boolstr(void) {return NULL;}
+uschar * smtp_cmd_hist(void) {return NULL;}
+
+
+
+static void
+expansion_items(void)
+{
+uschar buf[64];
+for (int i = 0; i < nelem(item_table); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_ITEM_%T", item_table[i]);
+  builtin_macro_create(buf);
+  }
+}
+static void
+expansion_operators(void)
+{
+uschar buf[64];
+for (int i = 0; i < nelem(op_table_underscore); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_OP_%T", op_table_underscore[i]);
+  builtin_macro_create(buf);
+  }
+for (int i = 0; i < nelem(op_table_main); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_OP_%T", op_table_main[i]);
+  builtin_macro_create(buf);
+  }
+}
+static void
+expansion_conditions(void)
+{
+uschar buf[64];
+for (int i = 0; i < nelem(cond_table); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_COND_%T", cond_table[i]);
+  builtin_macro_create(buf);
+  }
+}
+static void
+expansion_variables(void)
+{
+uschar buf[64];
+for (int i = 0; i < nelem(var_table); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_VAR_%T", var_table[i].name);
+  builtin_macro_create(buf);
+  }
+}
+
+void
+expansions(void)
+{
+expansion_items();
+expansion_operators();
+expansion_conditions();
+expansion_variables();
+}
+
+#else  /*!MACRO_PREDEF*/
+
 static uschar var_buffer[256];
 static BOOL malformed_header;
 
@@ -1195,7 +1278,7 @@ static var_entry *
 find_var_ent(uschar * name)
 {
 int first = 0;
-int last = var_table_size;
+int last = nelem(var_table);
 
 while (last > first)
   {
@@ -1584,12 +1667,13 @@ Returns:        NULL if the header does not exist, else a pointer to a new
 */
 
 static uschar *
-find_header(uschar *name, int *newsize, unsigned flags, const uschar *charset)
+find_header(uschar * name, int * newsize, unsigned flags, const uschar * charset)
 {
 BOOL found = !name;
 int len = name ? Ustrlen(name) : 0;
 BOOL comma = FALSE;
 gstring * g = NULL;
+uschar * rawhdr;
 
 for (header_line * h = header_list; h; h = h->next)
   if (h->type != htype_old && h->text)  /* NULL => Received: placeholder */
@@ -1652,8 +1736,9 @@ if (!g) return US"";
 /* That's all we do for raw header expansion. */
 
 *newsize = g->size;
+rawhdr = string_from_gstring(g);
 if (flags & FH_WANT_RAW)
-  return string_from_gstring(g);
+  return rawhdr;
 
 /* Otherwise do RFC 2047 decoding, translating the charset if requested.
 The rfc2047_decode2() function can return an error with decoded data if the
@@ -1661,12 +1746,12 @@ charset translation fails. If decoding fails, it returns NULL. */
 
 else
   {
-  uschar * error, * decoded = rfc2047_decode2(string_from_gstring(g),
+  uschar * error, * decoded = rfc2047_decode2(rawhdr,
     check_rfc2047_length, charset, '?', NULL, newsize, &error);
   if (error)
     DEBUG(D_any) debug_printf("*** error in RFC 2047 decoding: %s\n"
-      "    input was: %s\n", error, g->s);
-  return decoded ? decoded : string_from_gstring(g);
+      "    input was: %s\n", error, rawhdr);
+  return decoded ? decoded : rawhdr;
   }
 }
 
@@ -1731,7 +1816,7 @@ for (int i = 0; i < recipients_count; i++)
   s = recipients_list[i].address;
   g = string_append2_listele_n(g, US", ", s, Ustrlen(s));
   }
-return g ? g->s : NULL;
+return string_from_gstring(g);
 }
 
 
@@ -1748,9 +1833,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)
   {
@@ -1758,17 +1841,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
@@ -1777,17 +1852,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; }
 
@@ -1873,7 +1938,7 @@ else if (Ustrncmp(name, "r_", 2) == 0)
   return node ? node->data.ptr : strict_acl_vars ? NULL : US"";
   }
 
-/* Handle $auth<n> variables. */
+/* Handle $auth<n>, $regex<n> variables. */
 
 if (Ustrncmp(name, "auth", 4) == 0)
   {
@@ -1882,6 +1947,7 @@ if (Ustrncmp(name, "auth", 4) == 0)
   if (!*endptr && n != 0 && n <= AUTH_VARS)
     return auth_vars[n-1] ? auth_vars[n-1] : US"";
   }
+#ifdef WITH_CONTENT_SCAN
 else if (Ustrncmp(name, "regex", 5) == 0)
   {
   uschar *endptr;
@@ -1889,6 +1955,7 @@ else if (Ustrncmp(name, "regex", 5) == 0)
   if (!*endptr && n != 0 && n <= REGEX_VARS)
     return regex_vars[n-1] ? regex_vars[n-1] : US"";
   }
+#endif
 
 /* For all other variables, search the table */
 
@@ -2114,27 +2181,33 @@ Arguments:
   n          maximum number of substrings
   m          minimum required
   sptr       points to current string pointer
-  skipping   the skipping flag
+  flags
+   skipping   the skipping flag
   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
+Returns:     -1 OK; string pointer updated, but in "skipping" mode
+            0 OK; string pointer updated
              1 curly bracketing error (too few arguments)
              2 too many arguments (only if check_end is set); message set
              3 other error (expansion failure)
 */
 
 static int
-read_subs(uschar **sub, int n, int m, const uschar **sptr, BOOL skipping,
-  BOOL check_end, uschar *name, BOOL *resetok)
+read_subs(uschar ** sub, int n, int m, const uschar ** sptr, esi_flags flags,
+  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)
@@ -2146,11 +2219,14 @@ 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,
+         ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags & ESI_SKIPPING, &s, 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++ != '}')
   {
   if (s[-1] == '{')
@@ -2163,8 +2239,9 @@ if (check_end && *s++ != '}')
   return 1;
   }
 
+if (textonly_p) *textonly_p = textonly_l;
 *sptr = s;
-return 0;
+return flags & ESI_SKIPPING ? -1 : 0;
 }
 
 
@@ -2523,11 +2600,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;
@@ -2535,7 +2612,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;
@@ -2629,8 +2707,14 @@ 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,
+      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yield ? ESI_NOFLAGS : ESI_SKIPPING),
+      &s, resetok, &textonly);
+    if (!sub[0]) return NULL;
+    if (textonly) sub_textonly |= BIT(0);
+   }
   /* {-for-text-editors */
   if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
@@ -2727,8 +2811,8 @@ switch(cond_type = identify_operator(&s, &opname))
     Uskip_whitespace(&s);
     if (*s++ != '{') goto COND_FAILED_CURLY_START;     /*}*/
 
-    switch(read_subs(sub, nelem(sub), 1,
-      &s, yield == NULL, TRUE, name, resetok))
+    switch(read_subs(sub, nelem(sub), 1, &s,
+       yield ? ESI_NOFLAGS : ESI_SKIPPING, TRUE, name, resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
         "error for acl";
@@ -2779,8 +2863,8 @@ switch(cond_type = identify_operator(&s, &opname))
     uschar *sub[4];
     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))
+    switch(read_subs(sub, nelem(sub), 2, &s,
+       yield ? ESI_NOFLAGS : ESI_SKIPPING, TRUE, name, resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
        "error for saslauthd";
@@ -2848,12 +2932,16 @@ 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. */
-    BOOL honour_dollar = TRUE;
-    if ((i > 0) && !sub2_honour_dollar)
-      honour_dollar = FALSE;
+    /*XXX is this moot given taint-tracking? */
+
+    esi_flags flags = ESI_BRACE_ENDS;
+
+    if (!(i > 0 && !sub2_honour_dollar)) flags |= ESI_HONOR_DOLLAR;
+    if (!yield) flags |= ESI_SKIPPING;
 
     if (Uskip_whitespace(&s) != '{')
       {
@@ -2862,9 +2950,9 @@ switch(cond_type = identify_operator(&s, &opname))
         "after \"%s\"", opname);
       return NULL;
       }
-    if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL,
-        honour_dollar, resetok)))
+    if (!(sub[i] = expand_string_internal(s+1, flags, &s, 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");
@@ -2944,19 +3032,11 @@ switch(cond_type = identify_operator(&s, &opname))
 
     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;
@@ -3274,7 +3354,9 @@ 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,
+      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yield ? ESI_NOFLAGS : ESI_SKIPPING),
+      &s, resetok, NULL)))
       return NULL;
     /* {-for-text-editors */
     if (*s++ != '}') goto COND_FAILED_CURLY_END;
@@ -3362,7 +3444,8 @@ 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 ? ESI_NOFLAGS : ESI_SKIPPING, FALSE, ourname, resetok, NULL))
       {
       case 1: expand_string_message = string_sprintf(
                   "too few arguments or bracketing error for %s",
@@ -3430,7 +3513,8 @@ 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 ? ESI_NOFLAGS : ESI_SKIPPING, FALSE, name, resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
        "error for inbound_srs";
@@ -3441,10 +3525,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;
@@ -3521,6 +3605,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;
     }
@@ -3628,7 +3713,8 @@ expanded, to check their syntax, but "skipping" is set when the result is not
 needed - this avoids unnecessary nested lookups.
 
 Arguments:
-  skipping       TRUE if we were skipping when this item was reached
+  flags
+   skipping       TRUE if we were skipping when this item was reached
   yes            TRUE if the first string is to be used, else use the second
   save_lookup    a value to put back into lookup_value before the 2nd expansion
   sptr           points to the input string pointer
@@ -3644,7 +3730,7 @@ Returns:         0 OK; lookup_value has been reset to save_lookup
 */
 
 static int
-process_yesno(BOOL skipping, BOOL yes, uschar *save_lookup, const uschar **sptr,
+process_yesno(esi_flags flags, BOOL yes, uschar *save_lookup, const uschar **sptr,
   gstring ** yieldptr, uschar *type, BOOL *resetok)
 {
 int rc = 0;
@@ -3652,6 +3738,8 @@ const uschar *s = *sptr;    /* Local value */
 uschar *sub1, *sub2;
 const uschar * errwhere;
 
+flags &= ESI_SKIPPING;         /* Ignore all buf the skipping flag */
+
 /* If there are no following strings, we substitute the contents of $value for
 lookups and for extractions in the success case. For the ${if item, the string
 "true" is substituted. In the fail case, nothing is substituted for all three
@@ -3661,12 +3749,12 @@ if (skip_whitespace(&s) == '}')
   {
   if (type[0] == 'i')
     {
-    if (yes && !skipping)
+    if (yes && !(flags & ESI_SKIPPING))
       *yieldptr = string_catn(*yieldptr, US"true", 4);
     }
   else
     {
-    if (yes && lookup_value && !skipping)
+    if (yes && lookup_value && !(flags & ESI_SKIPPING))
       *yieldptr = string_cat(*yieldptr, lookup_value);
     lookup_value = save_lookup;
     }
@@ -3678,7 +3766,7 @@ if (skip_whitespace(&s) == '}')
 
 if (*s++ != '{')
   {
-  errwhere = US"'yes' part did not start with '{'";
+  errwhere = US"'yes' part did not start with '{'";            /*}}*/
   goto FAILED_CURLY;
   }
 
@@ -3686,9 +3774,12 @@ 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,
+  ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yes ? ESI_NOFLAGS : ESI_SKIPPING),
+  &s, resetok, NULL);
 if (sub1 == NULL && (yes || !f.expand_string_forcedfail)) goto FAILED;
 f.expand_string_forcedfail = FALSE;
+                                                               /*{{*/
 if (*s++ != '}')
   {
   errwhere = US"'yes' part did not end with '}'";
@@ -3713,14 +3804,16 @@ time, forced failures are noticed only if we want the second string. We must
 set skipping in the nested call if we don't want this string, or if we were
 already skipping. */
 
-if (skip_whitespace(&s) == '{')
+if (skip_whitespace(&s) == '{')                                        /*}*/
   {
-  sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping, TRUE, resetok);
-  if (sub2 == NULL && (!yes || !f.expand_string_forcedfail)) goto FAILED;
-  f.expand_string_forcedfail = FALSE;
+  esi_flags s_flags = ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags;
+  if (yes) s_flags |= ESI_SKIPPING;
+  sub2 = expand_string_internal(s+1, s_flags, &s, resetok, NULL);
+  if (!sub2 && (!yes || !f.expand_string_forcedfail)) goto FAILED;
+  f.expand_string_forcedfail = FALSE;                          /*{*/
   if (*s++ != '}')
     {
-    errwhere = US"'no' part did not start with '{'";
+    errwhere = US"'no' part did not start with '{'";           /*}*/
     goto FAILED_CURLY;
     }
 
@@ -3729,7 +3822,7 @@ if (skip_whitespace(&s) == '{')
   if (!yes)
     *yieldptr = string_cat(*yieldptr, sub2);
   }
-
+                                                               /*{{*/
 /* If there is no second string, but the word "fail" is present when the use of
 the second string is wanted, set a flag indicating it was a forced failure
 rather than a syntactic error. Swallow the terminating } in case this is nested
@@ -3742,9 +3835,9 @@ else if (*s != '}')
   s = US read_name(name, sizeof(name), s, US"_");
   if (Ustrcmp(name, "fail") == 0)
     {
-    if (!yes && !skipping)
+    if (!yes && !(flags & ESI_SKIPPING))
       {
-      Uskip_whitespace(&s);
+      Uskip_whitespace(&s);                                    /*{{*/
       if (*s++ != '}')
         {
        errwhere = US"did not close with '}' after forcedfail";
@@ -3766,7 +3859,7 @@ else if (*s != '}')
 
 /* All we have to do now is to check on the final closing brace. */
 
-skip_whitespace(&s);
+skip_whitespace(&s);                                           /*{{*/
 if (*s++ != '}')
   {
   errwhere = US"did not close with '}'";
@@ -4445,15 +4538,17 @@ string expansion becoming too powerful.
 
 Arguments:
   string         the string to be expanded
-  ket_ends       true if expansion is to stop at }
+  flags
+   brace_ends     expansion is to stop at }
+   honour_dollar  TRUE if $ is to be expanded,
+                  FALSE if it's just another character
+   skipping       TRUE for recursive calls when the value isn't actually going
+                  to be used (to allow for optimisation)
   left           if not NULL, a pointer to the first character after the
-                 expansion is placed here (typically used with ket_ends)
-  skipping       TRUE for recursive calls when the value isn't actually going
-                 to be used (to allow for optimisation)
-  honour_dollar  TRUE if $ is to be expanded,
-                 FALSE if it's just another character
+                 expansion is placed here (typically used with brace_ends)
   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
@@ -4462,8 +4557,8 @@ 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)
+expand_string_internal(const uschar * string, esi_flags flags, const uschar ** left,
+  BOOL *resetok_p, BOOL * textonly_p)
 {
 rmark reset_point = store_mark();
 gstring * yield = string_get(Ustrlen(string) + 64);
@@ -4471,7 +4566,7 @@ 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;
@@ -4494,11 +4589,11 @@ while (*s)
     DEBUG(D_noutf8)
       debug_printf_indent("%c%s: %s\n",
        first ? '/' : '|',
-       skipping ? "---scanning" : "considering", s);
+       flags & ESI_SKIPPING ? "---scanning" : "considering", s);
     else
       debug_printf_indent("%s%s: %s\n",
        first ? UTF8_DOWN_RIGHT : UTF8_VERT_RIGHT,
-       skipping
+       flags & ESI_SKIPPING
        ? UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ "scanning"
        : "considering",
        s);
@@ -4524,7 +4619,7 @@ while (*s)
       for (s = t; *s ; s++) if (*s == '\\' && s[1] == 'N') break;
 
       DEBUG(D_expand)
-       debug_expansion_interim(US"protected", t, (int)(s - t), skipping);
+       debug_expansion_interim(US"protected", t, (int)(s - t), !!(flags & ESI_SKIPPING));
       yield = string_catn(yield, t, s - t);
       if (*s) s += 2;
       }
@@ -4547,20 +4642,21 @@ while (*s)
   /* Anything other than $ is just copied verbatim, unless we are
   looking for a terminating } character. */
 
-  if (ket_ends && *s == '}') break;
+  if (flags & ESI_BRACE_ENDS && *s == '}') break;
 
-  if (*s != '$' || !honour_dollar)
+  if (*s != '$' || !(flags & ESI_HONOR_DOLLAR))
     {
     int i = 1;                                                         /*{*/
     for (const uschar * t = s+1;
        *t && *t != '$' && *t != '}' && *t != '\\'; t++) i++;
 
-    DEBUG(D_expand) debug_expansion_interim(US"text", s, i, skipping);
+    DEBUG(D_expand) debug_expansion_interim(US"text", s, i, !!(flags & ESI_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
@@ -4622,7 +4718,7 @@ while (*s)
 
     /* Variable */
 
-    else if (!(value = find_variable(name, FALSE, skipping, &newsize)))
+    else if (!(value = find_variable(name, FALSE, !!(flags & ESI_SKIPPING), &newsize)))
       {
       expand_string_message =
        string_sprintf("unknown variable name \"%s\"", name);
@@ -4639,6 +4735,7 @@ while (*s)
     reset in the middle of the buffer will make it inaccessible. */
 
     len = Ustrlen(value);
+    DEBUG(D_expand) debug_expansion_interim(US"value", value, len, !!(flags & ESI_SKIPPING));
     if (!yield && newsize != 0)
       {
       yield = g;
@@ -4652,12 +4749,15 @@ while (*s)
     continue;
     }
 
-  if (isdigit(*s))
+  if (isdigit(*s))             /* A $<n> variable */
     {
     int n;
     s = read_cnumber(&n, s);
     if (n >= 0 && n <= expand_nmax)
+      {
+      DEBUG(D_expand) debug_expansion_interim(US"value", expand_nstring[n], expand_nlength[n], !!(flags & ESI_SKIPPING));
       yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+      }
     continue;
     }
 
@@ -4682,7 +4782,10 @@ while (*s)
       goto EXPAND_FAILED;
       }
     if (n >= 0 && n <= expand_nmax)
+      {
+      DEBUG(D_expand) debug_expansion_interim(US"value", expand_nstring[n], expand_nlength[n], !!(flags & ESI_SKIPPING));
       yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+      }
     continue;
     }
 
@@ -4703,7 +4806,7 @@ while (*s)
   skipping, but "break" otherwise so we get debug output for the item
   expansion. */
   {
-  int start = gstring_length(yield);
+  int expansion_start = gstring_length(yield);
   switch(item_type)
     {
     /* Call an ACL from an expansion.  We feed data in via $acl_arg1 - $acl_arg9.
@@ -4722,14 +4825,13 @@ while (*s)
       uschar * user_msg;
       int rc;
 
-      switch(read_subs(sub, nelem(sub), 1, &s, skipping, TRUE, name,
-                     &resetok))
+      switch(read_subs(sub, nelem(sub), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
+       case -1: continue;              /* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
-      if (skipping) continue;
 
       resetok = FALSE;
       switch(rc = eval_acl(sub, nelem(sub), &user_msg))
@@ -4758,9 +4860,9 @@ while (*s)
       {
       uschar * sub_arg[1];
 
-      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, skipping, TRUE, name,
-                     &resetok))
+      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* If skipping, we don't actually do anything */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
@@ -4768,7 +4870,7 @@ while (*s)
 
       yield = string_append(yield, 3,
                        US"Authentication-Results: ", sub_arg[0], US"; none");
-      yield->ptr -= 6;
+      yield->ptr -= 6;                 /* ignore tha ": none" for now */
 
       yield = authres_local(yield, sub_arg[0]);
       yield = authres_iprev(yield);
@@ -4802,14 +4904,14 @@ while (*s)
       uschar * save_lookup_value = lookup_value;
 
       Uskip_whitespace(&s);
-      if (!(next_s = eval_condition(s, &resetok, skipping ? NULL : &cond)))
+      if (!(next_s = eval_condition(s, &resetok, flags & ESI_SKIPPING ? NULL : &cond)))
        goto EXPAND_FAILED;  /* message already set */
 
       DEBUG(D_expand)
        {
-       debug_expansion_interim(US"condition", s, (int)(next_s - s), skipping);
+       debug_expansion_interim(US"condition", s, (int)(next_s - s), !!(flags & ESI_SKIPPING));
        debug_expansion_interim(US"result",
-         cond ? US"true" : US"false", cond ? 4 : 5, skipping);
+         cond ? US"true" : US"false", cond ? 4 : 5, !!(flags & ESI_SKIPPING));
        }
 
       s = next_s;
@@ -4818,12 +4920,12 @@ while (*s)
       function that is also used by ${lookup} and ${extract} and ${run}. */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               cond,                         /* success/failure indicator */
-               lookup_value,                 /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"if",                       /* condition type */
+               flags,                  /* were previously skipping */
+               cond,                   /* success/failure indicator */
+               lookup_value,                   /* value to reset for string2 */
+               &s,                     /* input pointer */
+               &yield,                 /* output pointer */
+               US"if",                 /* condition type */
               &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -4845,8 +4947,7 @@ while (*s)
       uschar *sub_arg[3];
       uschar *encoded;
 
-      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, skipping, TRUE, name,
-                     &resetok))
+      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -4867,7 +4968,7 @@ while (*s)
        goto EXPAND_FAILED;
        }
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
 
       if (!(encoded = imap_utf7_encode(sub_arg[0], headers_charset,
                          sub_arg[1][0], sub_arg[2], &expand_string_message)))
@@ -4906,7 +5007,8 @@ while (*s)
 
       if (Uskip_whitespace(&s) == '{')                                 /*}*/
         {
-        key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+        key = expand_string_internal(s+1,
+               ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
         if (!key) goto EXPAND_FAILED;                  /*{{*/
         if (*s++ != '}')
          {
@@ -4976,7 +5078,8 @@ 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,
+               ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
        goto EXPAND_FAILED;
                                                                                /*{{*/
       if (*s++ != '}')
@@ -5007,7 +5110,7 @@ while (*s)
       since new variables will have been set. Note that at the end of this
       "lookup" section, the old numeric variables are restored. */
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
         lookup_value = NULL;
       else
         {
@@ -5033,12 +5136,12 @@ while (*s)
       function that is also used by ${if} and ${extract}. */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"lookup",                   /* condition type */
+               flags,                  /* were previously skipping */
+               lookup_value != NULL,   /* success/failure indicator */
+               save_lookup_value,      /* value to reset for string2 */
+               &s,                     /* input pointer */
+               &yield,                 /* output pointer */
+               US"lookup",             /* condition type */
               &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -5051,7 +5154,7 @@ while (*s)
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -5079,18 +5182,15 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      switch(read_subs(sub_arg, EXIM_PERL_MAX_ARGS + 1, 1, &s, skipping, TRUE,
-           name, &resetok))
+      switch(read_subs(sub_arg, EXIM_PERL_MAX_ARGS + 1, 1, &s, flags, TRUE,
+           name, &resetok, NULL))
         {
+       case -1: continue;      /* If skipping, we don't actually do anything */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /* If skipping, we don't actually do anything */
-
-      if (skipping) continue;
-
       /* Start the interpreter if necessary */
 
       if (!opt_perl_started)
@@ -5150,16 +5250,14 @@ while (*s)
       {
       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, flags, TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* If skipping, we don't actually do anything */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /* If skipping, we don't actually do anything */
-      if (skipping) continue;
-
       /* sub_arg[0] is the address */
       if (  !(domain = Ustrrchr(sub_arg[0],'@'))
         || domain == sub_arg[0] || Ustrlen(domain) == 1)
@@ -5208,30 +5306,21 @@ while (*s)
       gstring * g;
       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
-         parameter 2. So we clone the string for the first
-         expansion, where we only expand parameter 1.
-
-         PH: Actually, that isn't necessary. The read_subs() function is
-         designed to work this way for the ${if and ${lookup expansions. I've
-         tidied the code.
-      */                                                               /*}}*/
-
       /* Reset expansion variables */
       prvscheck_result = NULL;
       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, flags, 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))
         {
@@ -5241,11 +5330,14 @@ while (*s)
         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);
+        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);
@@ -5255,7 +5347,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, flags, FALSE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
@@ -5266,7 +5358,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";
@@ -5309,7 +5400,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, flags, TRUE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
@@ -5330,14 +5421,14 @@ 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, flags, TRUE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
           case 3: goto EXPAND_FAILED;
           }
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -5354,17 +5445,14 @@ 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, flags, TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* If skipping, we don't actually do anything */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /* If skipping, we don't actually do anything */
-
-      if (skipping) continue;
-
       /* Open the file and read it */
 
       if (!(f = Ufopen(sub_arg[0], "rb")))
@@ -5395,7 +5483,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, flags, FALSE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:                             /* Won't occur: no end check */
@@ -5405,7 +5493,7 @@ while (*s)
       /* If skipping, we don't actually do anything. Otherwise, arrange to
       connect to either an IP or a Unix socket. */
 
-      if (!skipping)
+      if (!(flags & ESI_SKIPPING))
         {
        int stype = search_findtype(US"readsock", 8);
        gstring * g = NULL;
@@ -5483,7 +5571,8 @@ while (*s)
 
       if (*s == '{')                                                   /*}*/
         {
-        if (!expand_string_internal(s+1, TRUE, &s, TRUE, TRUE, &resetok))
+        if (!expand_string_internal(s+1,
+         ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | ESI_SKIPPING, &s, &resetok, NULL))
           goto EXPAND_FAILED;                                          /*{*/
         if (*s++ != '}')
          {                                                             /*{*/
@@ -5499,7 +5588,7 @@ while (*s)
        expand_string_message = US"missing '}' closing readsocket";
        goto EXPAND_FAILED_CURLY;
        }
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
 
       /* Come here on failure to create socket, connect socket, write to the
@@ -5509,7 +5598,8 @@ 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,
+                   ESI_BRACE_ENDS | ESI_HONOR_DOLLAR, &s, &resetok, NULL)))
         goto EXPAND_FAILED;
       yield = string_cat(yield, arg);                                  /*{*/
       if (*s++ != '}')
@@ -5529,7 +5619,7 @@ while (*s)
       const uschar * arg, ** argv;
       BOOL late_expand = TRUE;
 
-      if ((expand_forbid & RDO_RUN) != 0)
+      if (expand_forbid & RDO_RUN)
         {
         expand_string_message = US"running a command is not permitted";
         goto EXPAND_FAILED;
@@ -5561,13 +5651,21 @@ while (*s)
 
       if (late_expand)         /* this is the default case */
        {
-       int n = Ustrcspn(s, "}");
-       arg = skipping ? NULL : string_copyn(s, n);
+       int n;
+       const uschar * t;
+       /* Locate the end of the args */
+       (void) expand_string_internal(s,
+         ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | ESI_SKIPPING, &t, NULL, NULL);
+       n = t - s;
+       arg = flags & ESI_SKIPPING ? NULL : string_copyn(s, n);
        s += n;
        }
       else
        {
-       if (!(arg = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok)))
+       DEBUG(D_expand)
+         debug_printf_indent("args string for ${run} expand before split\n");
+       if (!(arg = expand_string_internal(s,
+               ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
          goto EXPAND_FAILED;
        Uskip_whitespace(&s);
        }
@@ -5578,7 +5676,7 @@ while (*s)
        goto EXPAND_FAILED_CURLY;
        }
 
-      if (skipping)   /* Just pretend it worked when we're skipping */
+      if (flags & ESI_SKIPPING)   /* Just pretend it worked when we're skipping */
        {
         runrc = 0;
        lookup_value = NULL;
@@ -5652,19 +5750,19 @@ while (*s)
       /* Process the yes/no strings; $value may be useful in both cases */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               runrc == 0,                   /* success/failure indicator */
-               lookup_value,                 /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"run",                      /* condition type */
+               flags,                  /* were previously skipping */
+               runrc == 0,             /* success/failure indicator */
+               lookup_value,           /* value to reset for string2 */
+               &s,                     /* input pointer */
+               &yield,                 /* output pointer */
+               US"run",                        /* condition type */
               &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
         case 2: goto EXPAND_FAILED_CURLY;    /* returned value is 0 */
         }
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -5676,8 +5774,9 @@ while (*s)
       int o2m;
       uschar * sub[3];
 
-      switch(read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub, 3, 3, &s, flags, TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
@@ -5688,7 +5787,7 @@ while (*s)
 
       if (o2m >= 0) for (; oldptr < yield->ptr; oldptr++)
         {
-        uschar *m = Ustrrchr(sub[1], yield->s[oldptr]);
+        uschar * m = Ustrrchr(sub[1], yield->s[oldptr]);
         if (m)
           {
           int o = m - sub[1];
@@ -5696,7 +5795,6 @@ while (*s)
           }
         }
 
-      if (skipping) continue;
       break;
       }
 
@@ -5717,9 +5815,10 @@ while (*s)
       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))
+      switch(read_subs(sub, item_type == EITEM_LENGTH ? 2:3, 2, &s, flags,
+             TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
@@ -5760,7 +5859,6 @@ while (*s)
       if (!ret)
        goto EXPAND_FAILED;
       yield = string_catn(yield, ret, len);
-      if (skipping) continue;
       break;
       }
 
@@ -5793,15 +5891,14 @@ 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, flags, TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      if (skipping) continue;
-
       if (Ustrcmp(sub[0], "md5") == 0)
        {
        type = HMAC_MD5;
@@ -5881,32 +5978,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, flags, TRUE, name, &resetok, &sub_textonly))
         {
+       case -1: continue;      /* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /*XXX no handling of skipping? */
       /* 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
@@ -5922,7 +6016,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
@@ -5984,9 +6078,9 @@ 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);
-      if (skipping) continue;
       break;
       }
 
@@ -6021,11 +6115,12 @@ while (*s)
       available (eg. $item) hence cannot decide on numeric vs. keyed.
       Read a maximum of 5 arguments (including the yes/no) */
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
        {
         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,
+               ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL))
            goto EXPAND_FAILED;                                 /*'{'*/
           if (*s++ != '}')
            {
@@ -6052,7 +6147,8 @@ 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,
+               ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
            goto EXPAND_FAILED;                                         /*'{'*/
           if (*s++ != '}')
            {
@@ -6111,7 +6207,7 @@ while (*s)
       /* Extract either the numbered or the keyed substring into $value. If
       skipping, just pretend the extraction failed. */
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
        lookup_value = NULL;
       else switch (fmt)
        {
@@ -6205,12 +6301,12 @@ while (*s)
       be yes/no strings, as for lookup or if. */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"extract",                  /* condition type */
+               flags,                  /* were previously skipping */
+               lookup_value != NULL,   /* success/failure indicator */
+               save_lookup_value,      /* value to reset for string2 */
+               &s,                     /* input pointer */
+               &yield,                 /* output pointer */
+               US"extract",            /* condition type */
               &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -6222,7 +6318,7 @@ while (*s)
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -6236,6 +6332,7 @@ while (*s)
         save_expand_strings(save_expand_nstring, save_expand_nlength);
 
       /* Read the field & list arguments */
+      /*XXX Could we use read_subs here (and get better efficiency for skipping)? */
 
       for (int i = 0; i < 2; i++)
         {
@@ -6246,7 +6343,8 @@ 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,
+             ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
        if (!sub[i])     goto EXPAND_FAILED;                            /*{{*/
        if (*s++ != '}')
          {
@@ -6271,7 +6369,7 @@ while (*s)
          while (len > 0 && isspace(p[len-1])) len--;
          p[len] = 0;
 
-         if (!*p && !skipping)
+         if (!*p && !(flags & ESI_SKIPPING))
            {
            expand_string_message = US"first argument of \"listextract\" must "
              "not be empty";
@@ -6297,18 +6395,18 @@ while (*s)
       /* Extract the numbered element into $value. If
       skipping, just pretend the extraction failed. */
 
-      lookup_value = skipping ? NULL : expand_getlistele(field_number, sub[1]);
+      lookup_value = flags & ESI_SKIPPING ? NULL : expand_getlistele(field_number, sub[1]);
 
       /* If no string follows, $value gets substituted; otherwise there can
       be yes/no strings, as for lookup or if. */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"listextract",              /* condition type */
+               flags,                          /* were previously skipping */
+               lookup_value != NULL,           /* success/failure indicator */
+               save_lookup_value,              /* value to reset for string2 */
+               &s,                             /* input pointer */
+               &yield,                         /* output pointer */
+               US"listextract",                        /* condition type */
               &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -6320,15 +6418,16 @@ while (*s)
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
 
-      if (skipping) continue;
+      if (flags & ESI_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, flags, TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
@@ -6339,7 +6438,6 @@ while (*s)
        yield = string_catn(yield, sub[1], 1);
        }
       else yield = string_catn(yield, US" ", 1);
-      if (skipping) continue;
       break;
       }
 
@@ -6356,7 +6454,8 @@ 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,
+               ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
       if (!sub[0])     goto EXPAND_FAILED;                             /*{{*/
       if (*s++ != '}')
         {
@@ -6388,7 +6487,8 @@ 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,
+               ESI_BRACE_ENDS | flags & ESI_SKIPPING, &s, &resetok, NULL);
       if (!sub[1])     goto EXPAND_FAILED;                             /*{{*/
       if (*s++ != '}')
         {
@@ -6396,7 +6496,7 @@ while (*s)
        goto EXPAND_FAILED_CURLY;
        }
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
        lookup_value = NULL;
       else
        {
@@ -6404,12 +6504,12 @@ while (*s)
        if (*expand_string_message) goto EXPAND_FAILED;
        }
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"certextract",              /* condition type */
+               flags,                          /* were previously skipping */
+               lookup_value != NULL,           /* success/failure indicator */
+               save_lookup_value,              /* value to reset for string2 */
+               &s,                             /* input pointer */
+               &yield,                         /* output pointer */
+               US"certextract",                        /* condition type */
               &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -6418,7 +6518,7 @@ while (*s)
 
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 #endif /*DISABLE_TLS*/
@@ -6443,7 +6543,8 @@ while (*s)
        goto EXPAND_FAILED_CURLY;                                       /*}*/
        }
 
-      if (!(list = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok)))
+      if (!(list = expand_string_internal(s,
+             ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
        goto EXPAND_FAILED;                                             /*{{*/
       if (*s++ != '}')
         {
@@ -6461,7 +6562,8 @@ 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,
+             ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
         if (!t) goto EXPAND_FAILED;
         lookup_value = t;                                              /*{{*/
         if (*s++ != '}')
@@ -6488,7 +6590,8 @@ 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,
+         ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | ESI_SKIPPING, &s, &resetok, NULL);
       else
         if ((temp = eval_condition(expr, &resetok, NULL))) s = temp;
 
@@ -6519,7 +6622,7 @@ while (*s)
       /* If we are skipping, we can now just move on to the next item. When
       processing for real, we perform the iteration. */
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       while ((iterate_item = string_nextinlist(&list, &sep, NULL, 0)))
         {
         *outsep = (uschar)sep;      /* Separator as a string */
@@ -6530,6 +6633,9 @@ while (*s)
         if (item_type == EITEM_FILTER)
           {
           BOOL condresult;
+         /* the condition could modify $value, as a side-effect */
+         uschar * save_value = lookup_value;
+
           if (!eval_condition(expr, &resetok, &condresult))
             {
             iterate_item = save_iterate_item;
@@ -6538,6 +6644,7 @@ while (*s)
               expand_string_message, name);
             goto EXPAND_FAILED;
             }
+         lookup_value = save_value;
           DEBUG(D_expand) debug_printf_indent("%s: condition is %s\n", name,
             condresult? "true":"false");
           if (condresult)
@@ -6546,13 +6653,12 @@ while (*s)
             continue;               /* FALSE => skip this item */
           }
 
-        /* EITEM_MAP and EITEM_REDUCE */
-
-        else
+        else                   /* EITEM_MAP and EITEM_REDUCE */
           {
-         uschar * t = expand_string_internal(expr, TRUE, NULL, skipping, TRUE, &resetok);
-          temp = t;
-          if (!temp)
+         /* the expansion could modify $value, as a side-effect */
+         uschar * t = expand_string_internal(expr,
+           ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, NULL, &resetok, NULL);
+          if (!(temp = t))
             {
             iterate_item = save_iterate_item;
             expand_string_message = string_sprintf("%s inside \"%s\" item",
@@ -6622,7 +6728,7 @@ while (*s)
       /* Restore preserved $item */
 
       iterate_item = save_iterate_item;
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -6641,7 +6747,8 @@ while (*s)
        goto EXPAND_FAILED_CURLY;                                       /*}*/
        }
 
-      srclist = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
+      srclist = expand_string_internal(s,
+             ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
       if (!srclist) goto EXPAND_FAILED;                                        /*{{*/
       if (*s++ != '}')
         {
@@ -6656,7 +6763,8 @@ while (*s)
        goto EXPAND_FAILED_CURLY;                                       /*}*/
        }
 
-      cmp = expand_string_internal(s, TRUE, &s, skipping, FALSE, &resetok);
+      cmp = expand_string_internal(s,
+             ESI_BRACE_ENDS | flags & ESI_SKIPPING, &s, &resetok, NULL);
       if (!cmp) goto EXPAND_FAILED;                                    /*{{*/
       if (*s++ != '}')
         {
@@ -6691,7 +6799,8 @@ while (*s)
        }
 
       xtract = s;
-      if (!(tmp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok)))
+      if (!(tmp = expand_string_internal(s,
+       ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | ESI_SKIPPING, &s, &resetok, NULL)))
        goto EXPAND_FAILED;
       xtract = string_copyn(xtract, s - xtract);
                                                                        /*{{*/
@@ -6707,7 +6816,7 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
 
       while ((srcitem = string_nextinlist(&srclist, &sep, NULL, 0)))
        {
@@ -6718,8 +6827,8 @@ while (*s)
 
        /* extract field for comparisons */
        iterate_item = srcitem;
-       if (  !(srcfield = expand_string_internal(xtract, FALSE, NULL, FALSE,
-                                         TRUE, &resetok))
+       if (  !(srcfield = expand_string_internal(xtract,
+                                 ESI_HONOR_DOLLAR, NULL, &resetok, NULL))
           || !*srcfield)
          {
          expand_string_message = string_sprintf(
@@ -6824,18 +6933,15 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      switch(read_subs(argv, EXPAND_DLFUNC_MAX_ARGS + 2, 2, &s, skipping,
-           TRUE, name, &resetok))
+      switch(read_subs(argv, EXPAND_DLFUNC_MAX_ARGS + 2, 2, &s, flags,
+           TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /* If skipping, we don't actually do anything */
-
-      if (skipping) continue;
-
       /* Look up the dynamically loaded object handle in the tree. If it isn't
       found, dlopen() the file and put the handle in the tree for next time. */
 
@@ -6901,7 +7007,8 @@ 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,
+             ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
       if (!key) goto EXPAND_FAILED;                                    /*{{*/
       if (*s++ != '}')
         {
@@ -6912,18 +7019,18 @@ while (*s)
       lookup_value = US getenv(CS key);
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"env",                      /* condition type */
+               flags,                          /* were previously skipping */
+               lookup_value != NULL,           /* success/failure indicator */
+               save_lookup_value,              /* value to reset for string2 */
+               &s,                             /* input pointer */
+               &yield,                         /* output pointer */
+               US"env",                                /* condition type */
               &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
         case 2: goto EXPAND_FAILED_CURLY;    /* returned value is 0 */
         }
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -6936,77 +7043,82 @@ 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, flags, TRUE, name, &resetok, NULL))
         {
+       case -1: continue;      /* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      g = string_catn(g, US"SRS0=", 5);
-
-      /* ${l_4:${hmac{md5}{SRS_SECRET}{${lc:$return_path}}}}= */
-      hmac_md5(sub[0], string_copylc(sub[1]), cksum, sizeof(cksum));
-      g = string_catn(g, cksum, sizeof(cksum));
-      g = string_catn(g, US"=", 1);
-
-      /* ${base32:${eval:$tod_epoch/86400&0x3ff}}= */
+      if (sub[1] && *(sub[1]))
        {
-       struct timeval now;
-       unsigned long i;
-       gstring * h = NULL;
-
-       gettimeofday(&now, NULL);
-       for (unsigned long i = (now.tv_sec / 86400) & 0x3ff; i; i >>= 5)
-         h = string_catn(h, &base32_chars[i & 0x1f], 1);
-       if (h) while (h->ptr > 0)
-         g = string_catn(g, &h->s[--h->ptr], 1);
-       }
-      g = string_catn(g, US"=", 1);
+       g = string_catn(g, US"SRS0=", 5);
 
-      /* ${domain:$return_path}=${local_part:$return_path} */
-       {
-        int start, end, domain;
-        uschar * t = parse_extract_address(sub[1], &expand_string_message,
-                                         &start, &end, &domain, FALSE);
-       uschar * s;
+       /* ${l_4:${hmac{md5}{SRS_SECRET}{${lc:$return_path}}}}= */
+       hmac_md5(sub[0], string_copylc(sub[1]), cksum, sizeof(cksum));
+       g = string_catn(g, cksum, sizeof(cksum));
+       g = string_catn(g, US"=", 1);
 
-        if (!t)
-         goto EXPAND_FAILED;
+       /* ${base32:${eval:$tod_epoch/86400&0x3ff}}= */
+         {
+         struct timeval now;
+         unsigned long i;
+         gstring * h = NULL;
 
-       if (domain > 0) g = string_cat(g, t + domain);
+         gettimeofday(&now, NULL);
+         for (unsigned long i = (now.tv_sec / 86400) & 0x3ff; i; i >>= 5)
+           h = string_catn(h, &base32_chars[i & 0x1f], 1);
+         if (h) while (h->ptr > 0)
+           g = string_catn(g, &h->s[--h->ptr], 1);
+         }
        g = string_catn(g, US"=", 1);
 
-       s = domain > 0 ? string_copyn(t, domain - 1) : t;
-       if ((quoted = Ustrchr(s, '"') != NULL))
+       /* ${domain:$return_path}=${local_part:$return_path} */
          {
-         gstring * h = NULL;
-         DEBUG(D_expand) debug_printf_indent("auto-quoting local part\n");
-         while (*s)            /* de-quote */
+         int start, end, domain;
+         uschar * t = parse_extract_address(sub[1], &expand_string_message,
+                                           &start, &end, &domain, FALSE);
+         uschar * s;
+
+         if (!t)
+           goto EXPAND_FAILED;
+
+         if (domain > 0) g = string_cat(g, t + domain);
+         g = string_catn(g, US"=", 1);
+
+         s = domain > 0 ? string_copyn(t, domain - 1) : t;
+         if ((quoted = Ustrchr(s, '"') != NULL))
            {
-           while (*s && *s != '"') h = string_catn(h, s++, 1);
-           if (*s) s++;
-           while (*s && *s != '"') h = string_catn(h, s++, 1);
-           if (*s) s++;
+           gstring * h = NULL;
+           DEBUG(D_expand) debug_printf_indent("auto-quoting local part\n");
+           while (*s)          /* de-quote */
+             {
+             while (*s && *s != '"') h = string_catn(h, s++, 1);
+             if (*s) s++;
+             while (*s && *s != '"') h = string_catn(h, s++, 1);
+             if (*s) s++;
+             }
+           gstring_release_unused(h);
+           s = string_from_gstring(h);
            }
-         gstring_release_unused(h);
-         s = string_from_gstring(h);
+         g = string_cat(g, s);
          }
-       g = string_cat(g, s);
-        }
 
-      /* Assume that if the original local_part had quotes
-      it was for good reason */
+       /* Assume that if the original local_part had quotes
+       it was for good reason */
 
-      if (quoted) yield = string_catn(yield, US"\"", 1);
-      yield = string_catn(yield, g->s, g->ptr);
-      if (quoted) yield = string_catn(yield, US"\"", 1);
+       if (quoted) yield = string_catn(yield, US"\"", 1);
+       yield = gstring_append(yield, g);
+       if (quoted) yield = string_catn(yield, US"\"", 1);
 
-      /* @$original_domain */
-      yield = string_catn(yield, US"@", 1);
-      yield = string_cat(yield, sub[2]);
+       /* @$original_domain */
+       yield = string_catn(yield, US"@", 1);
+       yield = string_cat(yield, sub[2]);
+       }
+      else
+       DEBUG(D_expand) debug_printf_indent("null return_path for srs-encode\n");
 
-      if (skipping) continue;
       break;
       }
 #endif /*SUPPORT_SRS*/
@@ -7016,10 +7128,11 @@ while (*s)
     }  /* EITEM_* switch */
     /*NOTREACHED*/
 
-  DEBUG(D_expand)
-    if (yield && (start > 0 || *s))    /* only if not the sole expansion of the line */
+  DEBUG(D_expand)              /* only if not the sole expansion of the line */
+    if (yield && (expansion_start > 0 || *s))
       debug_expansion_interim(US"item-res",
-                             yield->s + start, yield->ptr - start, skipping);
+         yield->s + expansion_start, yield->ptr - expansion_start,
+         !!(flags & ESI_SKIPPING));
   continue;
 
 NOT_ITEM: ;
@@ -7055,6 +7168,7 @@ NOT_ITEM: ;
 
     /* Deal specially with operators that might take a certificate variable
     as we do not want to do the usual expansion. For most, expand the string.*/
+
     switch(c)
       {
 #ifndef DISABLE_TLS
@@ -7065,8 +7179,8 @@ NOT_ITEM: ;
        if (s[1] == '$')
          {
          const uschar * s1 = s;
-         sub = expand_string_internal(s+2, TRUE, &s1, skipping,
-                 FALSE, &resetok);
+         sub = expand_string_internal(s+2,
+             ESI_BRACE_ENDS | flags & ESI_SKIPPING, &s1, &resetok, NULL);
          if (!sub)       goto EXPAND_FAILED;           /*{*/
          if (*s1 != '}')
            {                                           /*{*/
@@ -7084,7 +7198,8 @@ NOT_ITEM: ;
         /*FALLTHROUGH*/
 #endif
       default:
-       sub = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+       sub = expand_string_internal(s+1,
+               ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
        if (!sub) goto EXPAND_FAILED;
        s++;
        break;
@@ -7096,22 +7211,22 @@ NOT_ITEM: ;
     for the existence of $sender_host_address before trying to mask it. For
     other operations, doing them may not fail, but it is a waste of time. */
 
-    if (skipping && c >= 0) continue;
+    if (flags & ESI_SKIPPING && c >= 0) continue;
 
     /* Otherwise, switch on the operator type.  After handling go back
     to the main loop top. */
 
      {
-     int start = yield->ptr;
+     unsigned expansion_start = gstring_length(yield);
      switch(c)
       {
       case EOP_BASE32:
        {
-       uschar *t;
+       uschar * t;
        unsigned long int n = Ustrtoul(sub, &t, 10);
        gstring * g = NULL;
 
-       if (*t != 0)
+       if (*t)
          {
          expand_string_message = string_sprintf("argument for base32 "
            "operator is \"%s\", which is not a decimal number", sub);
@@ -7181,7 +7296,8 @@ NOT_ITEM: ;
 
       case EOP_EXPAND:
        {
-       uschar *expanded = expand_string_internal(sub, FALSE, NULL, skipping, TRUE, &resetok);
+       uschar *expanded = expand_string_internal(sub,
+               ESI_HONOR_DOLLAR | flags & ESI_SKIPPING, NULL, &resetok, NULL);
        if (!expanded)
          {
          expand_string_message =
@@ -7273,7 +7389,7 @@ NOT_ITEM: ;
            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++);
@@ -7301,7 +7417,7 @@ NOT_ITEM: ;
          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++);
@@ -7719,16 +7835,19 @@ NOT_ITEM: ;
 
        case EOP_UTF8CLEAN:
          {
-         int seq_len = 0, index = 0;
-         int bytes_left = 0;
+         int seq_len = 0, index = 0, bytes_left = 0, complete;
          long codepoint = -1;
-         int complete;
          uschar seq_buff[4];                   /* accumulate utf-8 here */
 
          /* Manually track tainting, as we deal in individual chars below */
 
-         if (!yield->s || !yield->ptr)
+         if (!yield)
+           yield = string_get_tainted(Ustrlen(sub), sub);
+         else if (!yield->s || !yield->ptr)
+           {
            yield->s = store_get(yield->size = Ustrlen(sub), sub);
+           gstring_reset(yield);
+           }
          else if (is_incompatible(yield->s, sub))
            gstring_rebuffer(yield, sub);
 
@@ -7854,7 +7973,7 @@ NOT_ITEM: ;
            goto EXPAND_FAILED;
            }
          yield = string_cat(yield, s);
-         DEBUG(D_expand) debug_printf_indent("yield: '%s'\n", yield->s);
+         DEBUG(D_expand) debug_printf_indent("yield: '%s'\n", string_from_gstring(yield));
          break;
          }
 
@@ -8163,8 +8282,9 @@ NOT_ITEM: ;
 
        DEBUG(D_expand)
        {
-       const uschar * s = yield->s + start;
-       int i = yield->ptr - start;
+       const uschar * res = string_from_gstring(yield);
+       const uschar * s = res + expansion_start;
+       int i = gstring_length(yield) - expansion_start;
        BOOL tainted = is_tainted(s);
 
        DEBUG(D_noutf8)
@@ -8172,8 +8292,8 @@ NOT_ITEM: ;
          debug_printf_indent("|-----op-res: %.*s\n", i, s);
          if (tainted)
            {
-           debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
-           debug_print_taint(yield->s);
+           debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
+           debug_print_taint(res);
            }
          }
        else
@@ -8184,9 +8304,9 @@ NOT_ITEM: ;
          if (tainted)
            {
            debug_printf_indent("%s",
-             skipping
+             flags & ESI_SKIPPING
              ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
-           debug_print_taint(yield->s);
+           debug_print_taint(res);
            }
          }
        }
@@ -8218,7 +8338,7 @@ NOT_ITEM: ;
       reset_point = store_mark();
       g = store_get(sizeof(gstring), GET_UNTAINTED);   /* alloc _before_ calling find_variable() */
       }
-    if (!(value = find_variable(name, FALSE, skipping, &newsize)))
+    if (!(value = find_variable(name, FALSE, !!(flags & ESI_SKIPPING), &newsize)))
       {
       expand_string_message =
         string_sprintf("unknown variable in \"${%s}\"", name);
@@ -8246,11 +8366,11 @@ NOT_ITEM: ;
   goto EXPAND_FAILED;
   }
 
-/* If we hit the end of the string when ket_ends is set, there is a missing
+/* If we hit the end of the string when brace_ends is set, there is a missing
 terminating brace. */
 
-if (ket_ends && !*s)
-  {
+if (flags & ESI_BRACE_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";
@@ -8261,57 +8381,62 @@ if (ket_ends && !*s)
 added to the string. If so, set up an empty string. Add a terminating zero. If
 left != NULL, return a pointer to the terminator. */
 
-if (!yield)
-  yield = string_get(1);
-(void) string_from_gstring(yield);
-if (left) *left = s;
+ {
+  uschar * res;
 
-/* Any stacking store that was used above the final string is no longer needed.
-In many cases the final string will be the first one that was got and so there
-will be optimal store usage. */
+  if (!yield)
+    yield = string_get(1);
+  res = string_from_gstring(yield);
+  if (left) *left = s;
 
-if (resetok) gstring_release_unused(yield);
-else if (resetok_p) *resetok_p = FALSE;
+  /* Any stacking store that was used above the final string is no longer needed.
+  In many cases the final string will be the first one that was got and so there
+  will be optimal store usage. */
 
-DEBUG(D_expand)
-  {
-  BOOL tainted = is_tainted(yield->s);
-  DEBUG(D_noutf8)
+  if (resetok) gstring_release_unused(yield);
+  else if (resetok_p) *resetok_p = FALSE;
+
+  DEBUG(D_expand)
     {
-    debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
-    debug_printf_indent("%sresult: %s\n",
-      skipping ? "|-----" : "\\_____", yield->s);
-    if (tainted)
+    BOOL tainted = is_tainted(res);
+    DEBUG(D_noutf8)
       {
-      debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
-      debug_print_taint(yield->s);
+      debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
+      debug_printf_indent("%sresult: %s\n",
+       flags & ESI_SKIPPING ? "|-----" : "\\_____", res);
+      if (tainted)
+       {
+       debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
+       debug_print_taint(res);
+       }
+      if (flags & ESI_SKIPPING)
+       debug_printf_indent("\\___skipping: result is not used\n");
       }
-    if (skipping)
-      debug_printf_indent("\\___skipping: result is not used\n");
-    }
-  else
-    {
-    debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
-      "expanding: %.*s\n",
-      (int)(s - string), string);
-    debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-      "result: %s\n",
-      skipping ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
-      yield->s);
-    if (tainted)
+    else
       {
-      debug_printf_indent("%s",
-       skipping
-       ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
-      debug_print_taint(yield->s);
+      debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
+       "expanding: %.*s\n",
+       (int)(s - string), string);
+      debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
+       "result: %s\n",
+       flags & ESI_SKIPPING ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
+       res);
+      if (tainted)
+       {
+       debug_printf_indent("%s",
+         flags & ESI_SKIPPING
+         ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
+       debug_print_taint(res);
+       }
+      if (flags & ESI_SKIPPING)
+       debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
+         "skipping: result is not used\n");
       }
-    if (skipping)
-      debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-       "skipping: result is not used\n");
     }
-  }
-expand_level--;
-return yield->s;
+  if (textonly_p) *textonly_p = textonly;
+  expand_level--;
+  return res;
+ }
 
 /* This is the failure exit: easiest to program with a goto. We still need
 to update the pointer to the terminator, for cases of nested calls with "fail".
@@ -8358,16 +8483,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)
   {
@@ -8377,19 +8506,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, ESI_HONOR_DOLLAR, NULL, 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); }
+
 
 
 
@@ -8686,12 +8818,14 @@ assert_variable_notin() treats as const, so deconst is safe. */
 for (int i = 0; i < AUTH_VARS; i++) if (auth_vars[i])
   assert_variable_notin(US"auth<n>", US auth_vars[i], &e);
 
+#ifdef WITH_CONTENT_SCAN
 /* check regex<n> variables. assert_variable_notin() treats as const. */
 for (int i = 0; i < REGEX_VARS; i++) if (regex_vars[i])
   assert_variable_notin(US"regex<n>", US regex_vars[i], &e);
+#endif
 
 /* check known-name variables */
-for (var_entry * v = var_table; v < var_table + var_table_size; v++)
+for (var_entry * v = var_table; v < var_table + nelem(var_table); v++)
   if (v->type == vtype_stringptr)
     assert_variable_notin(US v->name, *(USS v->value), &e);
 
@@ -8825,8 +8959,9 @@ search_tidyup();
 return 0;
 }
 
-#endif
+#endif /*STAND_ALONE*/
 
+#endif /*!MACRO_PREDEF*/
 /* vi: aw ai sw=2
 */
 /* End of expand.c */