transport dynamic modules
[exim.git] / src / src / expand.c
index 36c9f423bedf06e844bb84a2a9aece8344a57472..2ed802fd4f8374a3a0604612fb10cb465a81440d 100644 (file)
@@ -2,9 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) The Exim Maintainers 2020 - 2024 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* 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 */
+#define ESI_EXISTS_ONLY                BIT(3)  /* actual value not needed */
 
 #ifdef STAND_ALONE
 # ifndef SUPPORT_CRYPTEQ
 #  define SUPPORT_CRYPTEQ
 # endif
-#endif
+#endif /*!STAND_ALONE*/
 
 #ifdef LOOKUP_LDAP
 # include "lookups/ldap.h"
@@ -224,6 +231,7 @@ static uschar *op_table_main[] = {
   US"expand",
   US"h",
   US"hash",
+  US"headerwrap",
   US"hex2b64",
   US"hexquote",
   US"ipv6denorm",
@@ -252,7 +260,9 @@ static uschar *op_table_main[] = {
   US"strlen",
   US"substr",
   US"uc",
-  US"utf8clean" };
+  US"utf8clean",
+  US"xtextd",
+  };
 
 enum {
   EOP_ADDRESS =  nelem(op_table_underscore),
@@ -271,6 +281,7 @@ enum {
   EOP_EXPAND,
   EOP_H,
   EOP_HASH,
+  EOP_HEADERWRAP,
   EOP_HEX2B64,
   EOP_HEXQUOTE,
   EOP_IPV6DENORM,
@@ -299,7 +310,9 @@ enum {
   EOP_STRLEN,
   EOP_SUBSTR,
   EOP_UC,
-  EOP_UTF8CLEAN };
+  EOP_UTF8CLEAN,
+  EOP_XTEXTD,
+  };
 
 
 /* Table of condition names, and corresponding switch numbers. The names must
@@ -465,8 +478,9 @@ typedef struct {
   int  *length;
 } alblock;
 
-static uschar * fn_recipients(void);
 typedef uschar * stringptr_fn_t(void);
+static uschar * fn_recipients(void);
+static uschar * fn_recipients_list(void);
 static uschar * fn_queue_size(void);
 
 /* This table must be kept in alphabetical order. */
@@ -518,6 +532,7 @@ static var_entry var_table[] = {
   { "compile_number",      vtype_stringptr,   &version_cnumber },
   { "config_dir",          vtype_stringptr,   &config_main_directory },
   { "config_file",         vtype_stringptr,   &config_main_filename },
+  { "connection_id",       vtype_stringptr,   &connection_id },
   { "csa_status",          vtype_stringptr,   &csa_status },
 #ifdef EXPERIMENTAL_DCC
   { "dcc_header",          vtype_stringptr,   &dcc_header },
@@ -672,7 +687,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 +701,8 @@ 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 },
+  { "recipients_list",     vtype_string_func, (void *) &fn_recipients_list },
+  { "regex_cachesize",     vtype_int,         &regex_cachesize },/* undocumented; devel observability */
 #ifdef WITH_CONTENT_SCAN
   { "regex_match_string",  vtype_stringptr,   &regex_match_string },
 #endif
@@ -703,6 +720,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 +840,76 @@ 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 * fn_recipients_list(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;
 
@@ -857,6 +944,10 @@ static uschar *mtable_sticky[] =
 #define FH_WANT_RAW    BIT(1)
 #define FH_WANT_LIST   BIT(2)
 
+/* 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);
+
 
 /*************************************************
 *           Tables for UTF-8 support             *
@@ -953,9 +1044,10 @@ Returns:        TRUE if condition is met, FALSE if not
 */
 
 BOOL
-expand_check_condition(uschar *condition, uschar *m1, uschar *m2)
+expand_check_condition(const uschar * condition,
+  const uschar * m1, const uschar * m2)
 {
-uschar * ss = expand_string(condition);
+const uschar * ss = expand_cstring(condition);
 if (!ss)
   {
   if (!f.expand_string_forcedfail && !f.search_find_defer)
@@ -1177,7 +1269,8 @@ while (*s)
 
   while (*s && *s != '=' && !isspace(*s)) s++;
   dkeylength = s - dkey;
-  if (Uskip_whitespace(&s) == '=') while (isspace(*++s));
+  if (Uskip_whitespace(&s) == '=')
+    while (isspace(*++s)) ;
 
   data = string_dequote(&s);
   if (length == dkeylength && strncmpic(key, dkey, length) == 0)
@@ -1195,7 +1288,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 +1677,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 +1746,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 +1756,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;
   }
 }
 
@@ -1716,22 +1811,40 @@ return g;
 *************************************************/
 /* A recipients list is available only during system message filtering,
 during ACL processing after DATA, and while expanding pipe commands
-generated from a system filter, but not elsewhere. */
+generated from a system filter, but not elsewhere.  Note that this does
+not check for commas in the elements, and uses comma-space as seperator -
+so cannot be used as an exim list as-is. */
 
 static uschar *
 fn_recipients(void)
 {
-uschar * s;
 gstring * g = NULL;
 
 if (!f.enable_dollar_recipients) return NULL;
 
 for (int i = 0; i < recipients_count; i++)
   {
-  s = recipients_list[i].address;
+  const uschar * s = recipients_list[i].address;
   g = string_append2_listele_n(g, US", ", s, Ustrlen(s));
   }
-return g ? g->s : NULL;
+gstring_release_unused(g);
+return string_from_gstring(g);
+}
+
+/* Similar, but as a properly-quoted exim list */
+
+
+static uschar *
+fn_recipients_list(void)
+{
+gstring * g = NULL;
+
+if (!f.enable_dollar_recipients) return NULL;
+
+for (int i = 0; i < recipients_count; i++)
+  g = string_append_listele(g, ':', recipients_list[i].address);
+gstring_release_unused(g);
+return string_from_gstring(g);
 }
 
 
@@ -1748,9 +1861,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 +1869,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 +1880,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; }
 
@@ -1829,8 +1922,9 @@ chop.
 
 Arguments:
   name          the name of the variable being sought
-  exists_only   TRUE if this is a def: test; passed on to find_header()
-  skipping      TRUE => skip any processing evaluation; this is not the same as
+  flags
+    exists_only  TRUE if this is a def: test; passed on to find_header()
+    skipping     TRUE => skip any processing evaluation; this is not the same as
                   exists_only because def: may test for values that are first
                   evaluated here
   newsize       pointer to an int which is initially zero; if the answer is in
@@ -1842,7 +1936,7 @@ Returns:        NULL if the variable does not exist, or
 */
 
 static const uschar *
-find_variable(uschar *name, BOOL exists_only, BOOL skipping, int *newsize)
+find_variable(uschar * name, esi_flags flags, int * newsize)
 {
 var_entry * vp;
 uschar *s, *domain;
@@ -1873,7 +1967,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 +1976,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 +1984,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 */
 
@@ -1898,7 +1994,7 @@ if (!(vp = find_var_ent(name)))
 /* Found an existing variable. If in skipping state, the value isn't needed,
 and we want to avoid processing (such as looking up the host name). */
 
-if (skipping)
+if (flags & ESI_SKIPPING)
   return US"";
 
 val = vp->value;
@@ -1959,11 +2055,13 @@ switch (vp->type)
     return domain ? domain + 1 : US"";
 
   case vtype_msgheaders:
-    return find_header(NULL, newsize, exists_only ? FH_EXISTS_ONLY : 0, NULL);
+    return find_header(NULL, newsize,
+           flags & ESI_EXISTS_ONLY ? FH_EXISTS_ONLY : 0, NULL);
 
   case vtype_msgheaders_raw:
     return find_header(NULL, newsize,
-               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW, NULL);
+           flags & ESI_EXISTS_ONLY ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
+           NULL);
 
   case vtype_msgbody:                        /* Pointer to msgbody string */
   case vtype_msgbody_end:                    /* Ditto, the end of the msg */
@@ -1971,7 +2069,8 @@ switch (vp->type)
     if (!*ss && deliver_datafile >= 0)  /* Read body when needed */
       {
       uschar * body;
-      off_t start_offset = SPOOL_DATA_START_OFFSET;
+      off_t start_offset_o = spool_data_start_offset(message_id);
+      off_t start_offset = start_offset_o;
       int len = message_body_visible;
 
       if (len > message_size) len = message_size;
@@ -1983,8 +2082,8 @@ switch (vp->type)
        if (fstat(deliver_datafile, &statbuf) == 0)
          {
          start_offset = statbuf.st_size - len;
-         if (start_offset < SPOOL_DATA_START_OFFSET)
-           start_offset = SPOOL_DATA_START_OFFSET;
+         if (start_offset < start_offset_o)
+           start_offset = start_offset_o;
          }
        }
       if (lseek(deliver_datafile, start_offset, SEEK_SET) < 0)
@@ -2029,15 +2128,15 @@ switch (vp->type)
 
   case vtype_reply:                          /* Get reply address */
     s = find_header(US"reply-to:", newsize,
-               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
-               headers_charset);
+           flags & ESI_EXISTS_ONLY ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
+           headers_charset);
     if (s) Uskip_whitespace(&s);
     if (!s || !*s)
       {
       *newsize = 0;                            /* For the *s==0 case */
       s = find_header(US"from:", newsize,
-               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
-               headers_charset);
+           flags & ESI_EXISTS_ONLY ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
+           headers_charset);
       }
     if (s)
       {
@@ -2052,7 +2151,7 @@ switch (vp->type)
   case vtype_string_func:
     {
     stringptr_fn_t * fn = (stringptr_fn_t *) val;
-    uschar* s = fn();
+    uschar * s = fn();
     return s ? s : US"";
     }
 
@@ -2114,27 +2213,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 +2251,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 +2271,9 @@ if (check_end && *s++ != '}')
   return 1;
   }
 
+if (textonly_p) *textonly_p = textonly_l;
 *sptr = s;
-return 0;
+return flags & ESI_SKIPPING ? -1 : 0;
 }
 
 
@@ -2307,19 +2416,26 @@ static uschar *
 json_nextinlist(const uschar ** list)
 {
 unsigned array_depth = 0, object_depth = 0;
+BOOL quoted = FALSE;
 const uschar * s = *list, * item;
 
 skip_whitespace(&s);
 
 for (item = s;
-     *s && (*s != ',' || array_depth != 0 || object_depth != 0);
+     *s && (*s != ',' || array_depth != 0 || object_depth != 0 || quoted);
      s++)
-  switch (*s)
+  if (!quoted) switch (*s)
     {
     case '[': array_depth++; break;
     case ']': array_depth--; break;
     case '{': object_depth++; break;
     case '}': object_depth--; break;
+    case '"': quoted = TRUE;
+    }
+  else switch(*s)
+    {
+    case '\\': s++; break;             /* backslash protects one char */
+    case '"':  quoted = FALSE; break;
     }
 *list = *s ? s+1 : s;
 if (item == s) return NULL;
@@ -2373,6 +2489,7 @@ if (!name[0])
     "but found \"%.16s\"", s);
   return -1;
   }
+DEBUG(D_expand) debug_printf_indent("cond: %s\n", name);
 if (opname)
   *opname = string_copy(name);
 
@@ -2523,20 +2640,20 @@ 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 sub2_honour_dollar = TRUE;
-BOOL is_forany, is_json, is_jsons;
+BOOL testfor = TRUE, tempcond, combined_cond;
+BOOL * subcondptr;
+BOOL sub2_honour_dollar = TRUE, is_forany, is_json, is_jsons;
 int rc, cond_type;
 int_eximarith_t num[2];
 struct stat statbuf;
 uschar * opname;
 uschar name[256];
-const uschar *sub[10];
+const uschar * sub[10], * next;
+unsigned sub_textonly = 0;
 
+expand_level++;
 for (;;)
   if (Uskip_whitespace(&s) == '!') { testfor = !testfor; s++; } else break;
 
@@ -2552,7 +2669,7 @@ switch(cond_type = identify_operator(&s, &opname))
     if (*s != ':')
       {
       expand_string_message = US"\":\" expected after \"def\"";
-      return NULL;
+      goto failout;
       }
 
     s = read_name(name, sizeof(name), s+1, US"_");
@@ -2579,18 +2696,19 @@ switch(cond_type = identify_operator(&s, &opname))
 
     else
       {
-      if (!(t = find_variable(name, TRUE, yield == NULL, NULL)))
+      if (!(t = find_variable(name,
+       yield ? ESI_EXISTS_ONLY : ESI_EXISTS_ONLY | ESI_SKIPPING, NULL)))
        {
        expand_string_message = name[0]
          ? string_sprintf("unknown variable \"%s\" after \"def:\"", name)
          : US"variable name omitted after \"def:\"";
        check_variable_error_message(name);
-       return NULL;
+       goto failout;
        }
       if (yield) *yield = (t[0] != 0) == testfor;
       }
 
-    return s;
+    next = s; goto out;
     }
 
 
@@ -2598,14 +2716,14 @@ switch(cond_type = identify_operator(&s, &opname))
 
   case ECOND_FIRST_DELIVERY:
   if (yield) *yield = f.deliver_firsttime == testfor;
-  return s;
+  next = s; goto out;
 
 
   /* queue_running tests for any process started by a queue runner */
 
   case ECOND_QUEUE_RUNNING:
   if (yield) *yield = (queue_run_pid != (pid_t)0) == testfor;
-  return s;
+  next = s; goto out;
 
 
   /* exists:  tests for file existence
@@ -2629,12 +2747,18 @@ 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]) goto failout;
+    if (textonly) sub_textonly |= BIT(0);
+   }
   /* {-for-text-editors */
   if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
-  if (!yield) return s;   /* No need to run the test if skipping */
+  if (!yield) { next = s; goto out; }  /* No need to run the test if skipping */
 
   switch(cond_type)
     {
@@ -2642,7 +2766,7 @@ switch(cond_type = identify_operator(&s, &opname))
     if ((expand_forbid & RDO_EXISTS) != 0)
       {
       expand_string_message = US"File existence tests are not permitted";
-      return NULL;
+      goto failout;
       }
     *yield = (Ustat(sub[0], &statbuf) == 0) == testfor;
     break;
@@ -2650,9 +2774,17 @@ switch(cond_type = identify_operator(&s, &opname))
     case ECOND_ISIP:
     case ECOND_ISIP4:
     case ECOND_ISIP6:
-    rc = string_is_ip_address(sub[0], NULL);
-    *yield = ((cond_type == ECOND_ISIP)? (rc != 0) :
-             (cond_type == ECOND_ISIP4)? (rc == 4) : (rc == 6)) == testfor;
+    {
+      const uschar *errp;
+      const uschar **errpp;
+      DEBUG(D_expand) errpp = &errp; else errpp = 0;
+      if (0 == (rc = string_is_ip_addressX(sub[0], NULL, errpp)))
+        DEBUG(D_expand) debug_printf("failed: %s\n", errp);
+
+      *yield = ( cond_type == ECOND_ISIP  ? rc != 0 :
+                 cond_type == ECOND_ISIP4 ? rc == 4 : rc == 6) == testfor;
+    }
+
     break;
 
     /* Various authentication tests - all optionally compiled */
@@ -2700,11 +2832,11 @@ switch(cond_type = identify_operator(&s, &opname))
     #if defined(SUPPORT_PAM) || defined(RADIUS_CONFIG_FILE) || \
         defined(LOOKUP_LDAP) || defined(CYRUS_PWCHECK_SOCKET)
     END_AUTH:
-    if (rc == ERROR || rc == DEFER) return NULL;
+    if (rc == ERROR || rc == DEFER) goto failout;
     *yield = (rc == OK) == testfor;
     #endif
     }
-  return s;
+  next = s; goto out;
 
 
   /* call ACL (in a conditional context).  Accept true, deny false.
@@ -2727,13 +2859,13 @@ 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";
       case 2:
-      case 3: return NULL;
+      case 3: goto failout;
       }
 
     if (yield)
@@ -2757,10 +2889,10 @@ switch(cond_type = identify_operator(&s, &opname))
        default:
           expand_string_message = string_sprintf("%s from acl \"%s\"",
            rc_names[rc], sub[0]);
-         return NULL;
+         goto failout;
        }
       }
-    return s;
+    next = s; goto out;
     }
 
 
@@ -2779,23 +2911,23 @@ 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";
       case 2:
-      case 3: return NULL;
+      case 3: goto failout;
       }
     if (!sub[2]) sub[3] = NULL;  /* realm if no service */
     if (yield)
       {
       int rc = auth_call_saslauthd(sub[0], sub[1], sub[2], sub[3],
        &expand_string_message);
-      if (rc == ERROR || rc == DEFER) return NULL;
+      if (rc == ERROR || rc == DEFER) goto failout;
       *yield = (rc == OK) == testfor;
       }
-    return s;
+    next = s; goto out;
     }
 #endif /* CYRUS_SASLAUTHD_SOCKET */
 
@@ -2848,23 +2980,27 @@ 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) != '{')
       {
       if (i == 0) goto COND_FAILED_CURLY_START;
       expand_string_message = string_sprintf("missing 2nd string in {} "
         "after \"%s\"", opname);
-      return NULL;
+      goto failout;
       }
-    if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL,
-        honour_dollar, resetok)))
-      return NULL;
+    if (!(sub[i] = expand_string_internal(s+1, flags, &s, resetok, &textonly)))
+      goto failout;
+    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");
@@ -2884,13 +3020,13 @@ switch(cond_type = identify_operator(&s, &opname))
       else
         {
         num[i] = expanded_string_integer(sub[i], FALSE);
-        if (expand_string_message) return NULL;
+        if (expand_string_message) goto failout;
         }
     }
 
   /* Result not required */
 
-  if (!yield) return s;
+  if (!yield) { next = s; goto out; }
 
   /* Do an appropriate comparison */
 
@@ -2944,19 +3080,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);
-       return NULL;
-       }
+      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)
+       goto failout;
 
       tempcond = regex_match_and_setup(re, sub[0], 0, -1);
       break;
@@ -2977,7 +3105,7 @@ switch(cond_type = identify_operator(&s, &opname))
        {
        expand_string_message = string_sprintf("\"%s\" is not an IP address",
          sub[0]);
-       return NULL;
+       goto failout;
        }
       else
        {
@@ -3021,7 +3149,7 @@ switch(cond_type = identify_operator(&s, &opname))
        case DEFER:
          expand_string_message = string_sprintf("unable to complete match "
            "against \"%s\": %s", sub[1], search_error_message);
-         return NULL;
+         goto failout;
        }
 
       break;
@@ -3130,7 +3258,7 @@ switch(cond_type = identify_operator(&s, &opname))
          {
          expand_string_message = string_sprintf("unknown encryption mechanism "
            "in \"%s\"", sub[1]);
-         return NULL;
+         goto failout;
          }
 
        switch(which)
@@ -3161,7 +3289,7 @@ switch(cond_type = identify_operator(&s, &opname))
          {
          expand_string_message = string_sprintf("crypt error: %s\n",
            US strerror(errno));
-         return NULL;
+         goto failout;
          }
        }
       break;
@@ -3197,7 +3325,7 @@ switch(cond_type = identify_operator(&s, &opname))
     }   /* Switch for comparison conditions */
 
   *yield = tempcond == testfor;
-  return s;    /* End of comparison conditions */
+  next = s; goto out;    /* End of comparison conditions */
 
 
   /* and/or: computes logical and/or of several conditions */
@@ -3218,14 +3346,14 @@ switch(cond_type = identify_operator(&s, &opname))
       {
       expand_string_message = string_sprintf("each subcondition "
         "inside an \"%s{...}\" condition must be in its own {}", opname);
-      return NULL;
+      goto failout;
       }
 
     if (!(s = eval_condition(s+1, resetok, subcondptr)))
       {
       expand_string_message = string_sprintf("%s inside \"%s{...}\" condition",
         expand_string_message, opname);
-      return NULL;
+      goto failout;
       }
     Uskip_whitespace(&s);
 
@@ -3235,7 +3363,7 @@ switch(cond_type = identify_operator(&s, &opname))
       /* {-for-text-editors */
       expand_string_message = string_sprintf("missing } at end of condition "
         "inside \"%s\" group", opname);
-      return NULL;
+      goto failout;
       }
 
     if (yield)
@@ -3252,7 +3380,7 @@ switch(cond_type = identify_operator(&s, &opname))
     }
 
   if (yield) *yield = (combined_cond == testfor);
-  return ++s;
+  next = ++s; goto out;
 
 
   /* forall/forany: iterates a condition with different values */
@@ -3274,8 +3402,10 @@ 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)))
-      return NULL;
+    if (!(sub[0] = expand_string_internal(s,
+      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yield ? ESI_NOFLAGS : ESI_SKIPPING),
+      &s, resetok, NULL)))
+      goto failout;
     /* {-for-text-editors */
     if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
@@ -3292,7 +3422,7 @@ switch(cond_type = identify_operator(&s, &opname))
       {
       expand_string_message = string_sprintf("%s inside \"%s\" condition",
         expand_string_message, opname);
-      return NULL;
+      goto failout;
       }
     Uskip_whitespace(&s);
 
@@ -3302,7 +3432,7 @@ switch(cond_type = identify_operator(&s, &opname))
       /* {-for-text-editors */
       expand_string_message = string_sprintf("missing } at end of condition "
         "inside \"%s\"", opname);
-      return NULL;
+      goto failout;
       }
 
     if (yield) *yield = !testfor;
@@ -3318,7 +3448,7 @@ switch(cond_type = identify_operator(&s, &opname))
            string_sprintf("%s wrapping string result for extract jsons",
              expand_string_message);
          iterate_item = save_iterate_item;
-         return NULL;
+         goto failout;
          }
 
       DEBUG(D_expand) debug_printf_indent("%s: $item = \"%s\"\n", opname, iterate_item);
@@ -3327,7 +3457,7 @@ switch(cond_type = identify_operator(&s, &opname))
         expand_string_message = string_sprintf("%s inside \"%s\" condition",
           expand_string_message, opname);
         iterate_item = save_iterate_item;
-        return NULL;
+        goto failout;
         }
       DEBUG(D_expand) debug_printf_indent("%s: condition evaluated to %s\n", opname,
         tempcond? "true":"false");
@@ -3337,7 +3467,7 @@ switch(cond_type = identify_operator(&s, &opname))
       }
 
     iterate_item = save_iterate_item;
-    return s;
+    next = s; goto out;
     }
 
 
@@ -3362,14 +3492,15 @@ 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",
                   ourname);
       /*FALLTHROUGH*/
       case 2:
-      case 3: return NULL;
+      case 3: goto failout;
       }
     t = sub_arg[0];
     Uskip_whitespace(&t);
@@ -3410,12 +3541,12 @@ switch(cond_type = identify_operator(&s, &opname))
       {
       expand_string_message = string_sprintf("unrecognised boolean "
        "value \"%s\"", t);
-      return NULL;
+      goto failout;
       }
     DEBUG(D_expand) debug_printf_indent("%s: condition evaluated to %s\n", ourname,
         boolvalue? "true":"false");
     if (yield) *yield = (boolvalue == testfor);
-    return s;
+    next = s; goto out;
     }
 
 #ifdef SUPPORT_SRS
@@ -3430,21 +3561,22 @@ 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";
       case 2:
-      case 3: return NULL;
+      case 3: goto failout;
       }
 
     /* Match the given local_part against the SRS-encoded pattern */
 
-    re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]+)=([^=]*)=(.*)$",
-                           TRUE, FALSE);
+    re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]{2})=([^=]*)=(.*)$",
+                           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;
@@ -3470,59 +3602,57 @@ switch(cond_type = identify_operator(&s, &opname))
     /* If a zero-length secret was given, we're done.  Otherwise carry on
     and validate the given SRS local_part againt our secret. */
 
-    if (!*sub[1])
+    if (*sub[1])
       {
-      boolvalue = TRUE;
-      goto srs_result;
-      }
+      /* check the timestamp */
+       {
+       struct timeval now;
+       uschar * ss = sub[0] + ovec[4]; /* substring 2, the timestamp */
+       long d;
+       int n;
 
-    /* check the timestamp */
-      {
-      struct timeval now;
-      uschar * ss = sub[0] + ovec[4];  /* substring 2, the timestamp */
-      long d;
-      int n;
+       gettimeofday(&now, NULL);
+       now.tv_sec /= 86400;                    /* days since epoch */
 
-      gettimeofday(&now, NULL);
-      now.tv_sec /= 86400;             /* days since epoch */
+       /* Decode substring 2 from base32 to a number */
 
-      /* Decode substring 2 from base32 to a number */
+       for (d = 0, n = ovec[5]-ovec[4]; n; n--)
+         {
+         uschar * t = Ustrchr(base32_chars, *ss++);
+         d = d * 32 + (t - base32_chars);
+         }
 
-      for (d = 0, n = ovec[5]-ovec[4]; n; n--)
-       {
-       uschar * t = Ustrchr(base32_chars, *ss++);
-       d = d * 32 + (t - base32_chars);
+       if (((now.tv_sec - d) & 0x3ff) > 10)    /* days since SRS generated */
+         {
+         DEBUG(D_expand) debug_printf("SRS too old\n");
+         goto srs_result;
+         }
        }
 
-      if (((now.tv_sec - d) & 0x3ff) > 10)     /* days since SRS generated */
+      /* check length of substring 1, the offered checksum */
+
+      if (ovec[3]-ovec[2] != 4)
        {
-       DEBUG(D_expand) debug_printf("SRS too old\n");
+       DEBUG(D_expand) debug_printf("SRS checksum wrong size\n");
        goto srs_result;
        }
-      }
 
-    /* check length of substring 1, the offered checksum */
+      /* Hash the address with our secret, and compare that computed checksum
+      with the one extracted from the arg */
 
-    if (ovec[3]-ovec[2] != 4)
-      {
-      DEBUG(D_expand) debug_printf("SRS checksum wrong size\n");
-      goto srs_result;
-      }
-
-    /* Hash the address with our secret, and compare that computed checksum
-    with the one extracted from the arg */
-
-    hmac_md5(sub[1], srs_recipient, cksum, sizeof(cksum));
-    if (Ustrncmp(cksum, sub[0] + ovec[2], 4) != 0)
-      {
-      DEBUG(D_expand) debug_printf("SRS checksum mismatch\n");
-      goto srs_result;
+      hmac_md5(sub[1], srs_recipient, cksum, sizeof(cksum));
+      if (Ustrncmp(cksum, sub[0] + ovec[2], 4) != 0)
+       {
+       DEBUG(D_expand) debug_printf("SRS checksum mismatch\n");
+       goto srs_result;
+       }
       }
     boolvalue = TRUE;
 
 srs_result:
+    /* pcre2_match_data_free(md);      gen ctx needs no free */
     if (yield) *yield = (boolvalue == testfor);
-    return s;
+    next = s; goto out;
     }
 #endif /*SUPPORT_SRS*/
 
@@ -3531,19 +3661,19 @@ srs_result:
   default:
     if (!expand_string_message || !*expand_string_message)
       expand_string_message = string_sprintf("unknown condition \"%s\"", opname);
-    return NULL;
+    goto failout;
   }   /* End switch on condition type */
 
 /* Missing braces at start and end of data */
 
 COND_FAILED_CURLY_START:
 expand_string_message = string_sprintf("missing { after \"%s\"", opname);
-return NULL;
+goto failout;
 
 COND_FAILED_CURLY_END:
 expand_string_message = string_sprintf("missing } at end of \"%s\" condition",
   opname);
-return NULL;
+goto failout;
 
 /* A condition requires code that is not compiled */
 
@@ -3553,8 +3683,14 @@ return NULL;
 COND_FAILED_NOT_COMPILED:
 expand_string_message = string_sprintf("support for \"%s\" not compiled",
   opname);
-return NULL;
+goto failout;
 #endif
+
+failout:
+  next = NULL;
+out:
+  expand_level--;
+  return next;
 }
 
 
@@ -3628,7 +3764,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 +3781,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 +3789,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 +3800,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 +3817,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 +3825,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 +3855,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 +3873,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 +3886,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 +3910,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 '}'";
@@ -3864,10 +4008,9 @@ if (Ustrlen(key) > 64)
 hash_source = string_catn(NULL, key_num, 1);
 hash_source = string_catn(hash_source, daystamp, 3);
 hash_source = string_cat(hash_source, address);
-(void) string_from_gstring(hash_source);
 
 DEBUG(D_expand)
-  debug_printf_indent("prvs: hash source is '%s'\n", hash_source->s);
+  debug_printf_indent("prvs: hash source is '%Y'\n", hash_source);
 
 memset(innerkey, 0x36, 64);
 memset(outerkey, 0x5c, 64);
@@ -3989,7 +4132,7 @@ if (!*error)
     if (*s != ')')
       *error = US"expecting closing parenthesis";
     else
-      while (isspace(*++s));
+      while (isspace(*++s)) ;
   else if (*s)
     *error = US"expecting operator";
 *sptr = s;
@@ -4371,30 +4514,17 @@ return yield;
 /************************************************/
 static void
 debug_expansion_interim(const uschar * what, const uschar * value, int nchar,
-  BOOL skipping)
+  esi_flags flags)
 {
-DEBUG(D_noutf8)
-  debug_printf_indent("|");
-else
-  debug_printf_indent(UTF8_VERT_RIGHT);
+debug_printf_indent("%V", "K");
 
 for (int fill = 11 - Ustrlen(what); fill > 0; fill--)
-  DEBUG(D_noutf8)
-    debug_printf("-");
-  else
-    debug_printf(UTF8_HORIZ);
+  debug_printf("%V", "-");
 
-debug_printf("%s: %.*s\n", what, nchar, value);
+debug_printf("%s: %.*W\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");
-  }
+  debug_printf_indent("%V          %V(tainted)\n",
+    flags & ESI_SKIPPING ? "|" : " ", "\\__");
 }
 
 
@@ -4445,15 +4575,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 +4594,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 +4603,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;
@@ -4491,17 +4623,10 @@ while (*s)
 
   DEBUG(D_expand)
     {
-    DEBUG(D_noutf8)
-      debug_printf_indent("%c%s: %s\n",
-       first ? '/' : '|',
-       skipping ? "---scanning" : "considering", s);
-    else
-      debug_printf_indent("%s%s: %s\n",
-       first ? UTF8_DOWN_RIGHT : UTF8_VERT_RIGHT,
-       skipping
-       ? UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ "scanning"
-       : "considering",
-       s);
+    debug_printf_indent("%V%V%s: %W\n",
+      first ? "/" : "K",
+      flags & ESI_SKIPPING ? "---" : "",
+      flags & ESI_SKIPPING ? "scanning" : "considering", s);
     first = FALSE;
     }
 
@@ -4524,21 +4649,20 @@ 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);
-      yield = string_catn(yield, t, s - t);
+       debug_expansion_interim(US"protected", t, (int)(s - t), flags);
+      if (!(flags & ESI_SKIPPING))
+       yield = string_catn(yield, t, s - t);
       if (*s) s += 2;
       }
     else
       {
       uschar ch[1];
       DEBUG(D_expand)
-       DEBUG(D_noutf8)
-         debug_printf_indent("|backslashed: '\\%c'\n", s[1]);
-       else
-         debug_printf_indent(UTF8_VERT_RIGHT "backslashed: '\\%c'\n", s[1]);
+       debug_printf_indent("%Vbackslashed: '\\%c'\n", "K", s[1]);
       ch[0] = string_interpret_escape(&s);
+      if (!(flags & ESI_SKIPPING))
+       yield = string_catn(yield, ch, 1);
       s++;
-      yield = string_catn(yield, ch, 1);
       }
     continue;
     }
@@ -4547,20 +4671,22 @@ 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);
 
-    yield = string_catn(yield, s, i);
+    if (!(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
@@ -4582,15 +4708,16 @@ while (*s)
     /* If this is the first thing to be expanded, release the pre-allocated
     buffer. */
 
-    if (!yield)
-      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), GET_UNTAINTED);   /* alloc _before_ calling find_variable() */
-      }
+    if (!(flags & ESI_SKIPPING))
+      if (!yield)
+       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), GET_UNTAINTED);  /* alloc _before_ calling find_variable() */
+       }
 
     /* Header */
 
@@ -4622,7 +4749,7 @@ while (*s)
 
     /* Variable */
 
-    else if (!(value = find_variable(name, FALSE, skipping, &newsize)))
+    else if (!(value = find_variable(name, flags, &newsize)))
       {
       expand_string_message =
        string_sprintf("unknown variable name \"%s\"", name);
@@ -4639,25 +4766,31 @@ while (*s)
     reset in the middle of the buffer will make it inaccessible. */
 
     len = Ustrlen(value);
-    if (!yield && newsize != 0)
-      {
-      yield = g;
-      yield->size = newsize;
-      yield->ptr = len;
-      yield->s = US value; /* known to be in new store i.e. a copy, so deconst safe */
-      }
-    else
-      yield = string_catn(yield, value, len);
+    DEBUG(D_expand) debug_expansion_interim(US"value", value, len, flags);
+    if (!(flags & ESI_SKIPPING))
+      if (!yield && newsize != 0)
+       {
+       yield = g;
+       yield->size = newsize;
+       yield->ptr = len;
+       yield->s = US value; /* known to be in new store i.e. a copy, so deconst safe */
+       }
+      else
+       yield = string_catn(yield, value, len);
 
     continue;
     }
 
-  if (isdigit(*s))
+  if (isdigit(*s))             /* A $<n> variable */
     {
     int n;
     s = read_cnumber(&n, s);
     if (n >= 0 && n <= expand_nmax)
-      yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+      {
+      DEBUG(D_expand) debug_expansion_interim(US"value", expand_nstring[n], expand_nlength[n], flags);
+      if (!(flags & ESI_SKIPPING))
+       yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+      }
     continue;
     }
 
@@ -4682,7 +4815,11 @@ while (*s)
       goto EXPAND_FAILED;
       }
     if (n >= 0 && n <= expand_nmax)
-      yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+      {
+      DEBUG(D_expand) debug_expansion_interim(US"value", expand_nstring[n], expand_nlength[n], flags);
+      if (!(flags & ESI_SKIPPING))
+       yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+      }
     continue;
     }
 
@@ -4703,7 +4840,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 +4859,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 +4894,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 +4904,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 +4938,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);
        debug_expansion_interim(US"result",
-         cond ? US"true" : US"false", cond ? 4 : 5, skipping);
+         cond ? US"true" : US"false", cond ? 4 : 5, flags);
        }
 
       s = next_s;
@@ -4818,12 +4954,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 +4981,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 +5002,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 +5041,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 +5112,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 +5144,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 +5170,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 +5188,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 +5216,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 +5284,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 +5340,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 +5364,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 +5381,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 +5392,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";
@@ -5292,24 +5417,24 @@ while (*s)
           if (iexpire >= inow)
             {
             prvscheck_result = US"1";
-            DEBUG(D_expand) debug_printf_indent("prvscheck: success, $pvrs_result set to 1\n");
+            DEBUG(D_expand) debug_printf_indent("prvscheck: success, $prvscheck_result set to 1\n");
             }
          else
             {
             prvscheck_result = NULL;
-            DEBUG(D_expand) debug_printf_indent("prvscheck: signature expired, $pvrs_result unset\n");
+            DEBUG(D_expand) debug_printf_indent("prvscheck: signature expired, $prvscheck_result unset\n");
             }
           }
         else
           {
           prvscheck_result = NULL;
-          DEBUG(D_expand) debug_printf_indent("prvscheck: hash failure, $pvrs_result unset\n");
+          DEBUG(D_expand) debug_printf_indent("prvscheck: hash failure, $prvscheck_result unset\n");
           }
 
         /* 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 +5455,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 +5479,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 +5517,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 +5527,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 +5605,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 +5622,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 +5632,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++ != '}')
@@ -5527,9 +5651,9 @@ while (*s)
       {
       FILE * f;
       const uschar * arg, ** argv;
-      BOOL late_expand = TRUE;
+      unsigned late_expand = TSUC_EXPAND_ARGS | TSUC_ALLOW_TAINTED_ARGS | TSUC_ALLOW_RECIPIENTS;
 
-      if ((expand_forbid & RDO_RUN) != 0)
+      if (expand_forbid & RDO_RUN)
         {
         expand_string_message = US"running a command is not permitted";
         goto EXPAND_FAILED;
@@ -5538,9 +5662,8 @@ while (*s)
       /* Handle options to the "run" */
 
       while (*s == ',')
-       {
        if (Ustrncmp(++s, "preexpand", 9) == 0)
-         { late_expand = FALSE; s += 9; }
+         { late_expand = 0; s += 9; }
        else
          {
          const uschar * t = s;
@@ -5549,7 +5672,6 @@ while (*s)
                                                  (int)(t-s), s);
          goto EXPAND_FAILED;
          }
-       }
       Uskip_whitespace(&s);
 
       if (*s != '{')                                   /*}*/
@@ -5561,13 +5683,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 +5708,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;
@@ -5593,7 +5723,6 @@ while (*s)
            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;
@@ -5652,19 +5781,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,27 +5805,26 @@ 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;
         }
 
-      yield = string_cat(yield, sub[0]);
-      o2m = Ustrlen(sub[2]) - 1;
-
-      if (o2m >= 0) for (; oldptr < yield->ptr; oldptr++)
+      if (  (yield = string_cat(yield, sub[0]))
+         && (o2m = Ustrlen(sub[2]) - 1) >= 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];
-          yield->s[oldptr] = sub[2][(o < o2m)? o : o2m];
+          yield->s[oldptr] = sub[2][o < o2m ? o : o2m];
           }
         }
 
-      if (skipping) continue;
       break;
       }
 
@@ -5717,9 +5845,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 +5889,6 @@ while (*s)
       if (!ret)
        goto EXPAND_FAILED;
       yield = string_catn(yield, ret, len);
-      if (skipping) continue;
       break;
       }
 
@@ -5793,15 +5921,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 +6008,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 +6046,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 +6108,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 +6145,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 +6177,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 +6237,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 +6331,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 +6348,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 +6362,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 +6373,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 +6399,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 +6425,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 +6448,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 +6468,6 @@ while (*s)
        yield = string_catn(yield, sub[1], 1);
        }
       else yield = string_catn(yield, US" ", 1);
-      if (skipping) continue;
       break;
       }
 
@@ -6356,7 +6484,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 +6517,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 +6526,7 @@ while (*s)
        goto EXPAND_FAILED_CURLY;
        }
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
        lookup_value = NULL;
       else
        {
@@ -6404,12 +6534,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 +6548,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 +6573,9 @@ while (*s)
        goto EXPAND_FAILED_CURLY;                                       /*}*/
        }
 
-      if (!(list = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok)))
+      DEBUG(D_expand) debug_printf_indent("%s: evaluate input list list\n", name);
+      if (!(list = expand_string_internal(s,
+             ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
        goto EXPAND_FAILED;                                             /*{{*/
       if (*s++ != '}')
         {
@@ -6461,7 +6593,9 @@ 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);
+       DEBUG(D_expand) debug_printf_indent("reduce: initial result list\n");
+        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++ != '}')
@@ -6487,8 +6621,10 @@ while (*s)
       condition for real. For EITEM_MAP and EITEM_REDUCE, do the same, using
       the normal internal expansion function. */
 
+      DEBUG(D_expand) debug_printf_indent("%s: find end of conditionn\n", name);
       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 +6655,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 +6666,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 +6677,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 +6686,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 +6761,7 @@ while (*s)
       /* Restore preserved $item */
 
       iterate_item = save_iterate_item;
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -6641,7 +6780,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 +6796,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 +6832,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 +6849,7 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
 
       while ((srcitem = string_nextinlist(&srclist, &sep, NULL, 0)))
        {
@@ -6718,8 +6860,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 +6966,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 +7040,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 +7052,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,13 +7076,14 @@ 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;
         }
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
 
       if (sub[1] && *(sub[1]))
        {
@@ -6957,13 +7098,11 @@ while (*s)
          {
          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);
+         i = (now.tv_sec / 86400) & 0x3ff;
+         g = string_catn(g, &base32_chars[i >> 5], 1);
+         g = string_catn(g, &base32_chars[i & 0x1f], 1);
          }
        g = string_catn(g, US"=", 1);
 
@@ -6995,14 +7134,14 @@ while (*s)
            gstring_release_unused(h);
            s = string_from_gstring(h);
            }
-         g = string_cat(g, s);
+         if (s) g = string_cat(g, s);
          }
 
        /* 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);
+       yield = gstring_append(yield, g);
        if (quoted) yield = string_catn(yield, US"\"", 1);
 
        /* @$original_domain */
@@ -7021,10 +7160,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);
   continue;
 
 NOT_ITEM: ;
@@ -7060,6 +7200,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
@@ -7070,8 +7211,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 != '}')
            {                                           /*{*/
@@ -7089,7 +7230,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;
@@ -7101,22 +7243,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);
@@ -7152,13 +7294,13 @@ NOT_ITEM: ;
        {
        uschar *t;
        unsigned long int n = Ustrtoul(sub, &t, 10);
-       if (*t != 0)
+       if (*t)
          {
          expand_string_message = string_sprintf("argument for base62 "
            "operator is \"%s\", which is not a decimal number", sub);
          goto EXPAND_FAILED;
          }
-       yield = string_cat(yield, string_base62(n));
+       yield = string_cat(yield, string_base62_32(n));         /*XXX only handles 32b input range.  Need variants? */
        break;
        }
 
@@ -7168,7 +7310,7 @@ NOT_ITEM: ;
        {
        uschar *tt = sub;
        unsigned long int n = 0;
-       while (*tt != 0)
+       while (*tt)
          {
          uschar *t = Ustrchr(base62_chars, *tt++);
          if (!t)
@@ -7186,7 +7328,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 =
@@ -7200,19 +7343,17 @@ NOT_ITEM: ;
 
       case EOP_LC:
        {
-       int count = 0;
-       uschar *t = sub - 1;
-       while (*(++t) != 0) { *t = tolower(*t); count++; }
-       yield = string_catn(yield, sub, count);
+       uschar * t = sub - 1;
+       while (*++t) *t = tolower(*t);
+       yield = string_catn(yield, sub, t-sub);
        break;
        }
 
       case EOP_UC:
        {
-       int count = 0;
-       uschar *t = sub - 1;
-       while (*(++t) != 0) { *t = toupper(*t); count++; }
-       yield = string_catn(yield, sub, count);
+       uschar * t = sub - 1;
+       while (*++t) *t = toupper(*t);
+       yield = string_catn(yield, sub, t-sub);
        break;
        }
 
@@ -7317,6 +7458,29 @@ NOT_ITEM: ;
        goto EXPAND_FAILED;
 #endif
 
+      /* Line-wrap a string as if it is a header line */
+
+      case EOP_HEADERWRAP:
+       {
+       unsigned col = 80, lim = 998;
+       uschar * s;
+
+       if (arg)
+         {
+         const uschar * list = arg;
+         int sep = '_';
+         if ((s = string_nextinlist(&list, &sep, NULL, 0)))
+           {
+           col = atoi(CS s);
+           if ((s = string_nextinlist(&list, &sep, NULL, 0)))
+             lim = atoi(CS s);
+           }
+         }
+         if ((s =  wrap_header(sub, col, lim, US"\t", 8)))
+           yield = string_cat(yield, s);
+       }
+       break;
+
       /* Convert hex encoding to base64 encoding */
 
       case EOP_HEX2B64:
@@ -7625,7 +7789,6 @@ NOT_ITEM: ;
            }
          else
            yield = string_cat(yield, sub);
-         break;
          }
 
        /* quote_lookuptype does lookup-specific quoting */
@@ -7657,542 +7820,546 @@ NOT_ITEM: ;
            }
 
          yield = string_cat(yield, sub);
-         break;
          }
+       break;
 
-       /* rx quote sticks in \ before any non-alphameric character so that
-       the insertion works in a regular expression. */
+      /* rx quote sticks in \ before any non-alphameric character so that
+      the insertion works in a regular expression. */
 
-       case EOP_RXQUOTE:
+      case EOP_RXQUOTE:
+       {
+       uschar *t = sub - 1;
+       while (*(++t) != 0)
          {
-         uschar *t = sub - 1;
-         while (*(++t) != 0)
-           {
-           if (!isalnum(*t))
-             yield = string_catn(yield, US"\\", 1);
-           yield = string_catn(yield, t, 1);
-           }
-         break;
+         if (!isalnum(*t))
+           yield = string_catn(yield, US"\\", 1);
+         yield = string_catn(yield, t, 1);
          }
+       break;
+       }
 
-       /* RFC 2047 encodes, assuming headers_charset (default ISO 8859-1) as
-       prescribed by the RFC, if there are characters that need to be encoded */
+      /* RFC 2047 encodes, assuming headers_charset (default ISO 8859-1) as
+      prescribed by the RFC, if there are characters that need to be encoded */
 
-       case EOP_RFC2047:
-         yield = string_cat(yield,
-                             parse_quote_2047(sub, Ustrlen(sub), headers_charset,
-                               FALSE));
-         break;
+      case EOP_RFC2047:
+       yield = string_cat(yield,
+                           parse_quote_2047(sub, Ustrlen(sub), headers_charset,
+                             FALSE));
+       break;
 
-       /* RFC 2047 decode */
+      /* RFC 2047 decode */
 
-       case EOP_RFC2047D:
+      case EOP_RFC2047D:
+       {
+       int len;
+       uschar *error;
+       uschar *decoded = rfc2047_decode(sub, check_rfc2047_length,
+         headers_charset, '?', &len, &error);
+       if (error)
          {
-         int len;
-         uschar *error;
-         uschar *decoded = rfc2047_decode(sub, check_rfc2047_length,
-           headers_charset, '?', &len, &error);
-         if (error)
-           {
-           expand_string_message = error;
-           goto EXPAND_FAILED;
-           }
-         yield = string_catn(yield, decoded, len);
-         break;
+         expand_string_message = error;
+         goto EXPAND_FAILED;
          }
+       yield = string_catn(yield, decoded, len);
+       break;
+       }
 
-       /* from_utf8 converts UTF-8 to 8859-1, turning non-existent chars into
-       underscores */
+      /* from_utf8 converts UTF-8 to 8859-1, turning non-existent chars into
+      underscores */
 
-       case EOP_FROM_UTF8:
+      case EOP_FROM_UTF8:
+       {
+       uschar * buff = store_get(4, sub);
+       while (*sub)
          {
-         uschar * buff = store_get(4, sub);
-         while (*sub)
-           {
-           int c;
-           GETUTF8INC(c, sub);
-           if (c > 255) c = '_';
-           buff[0] = c;
-           yield = string_catn(yield, buff, 1);
-           }
-         break;
+         int c;
+         GETUTF8INC(c, sub);
+         if (c > 255) c = '_';
+         buff[0] = c;
+         yield = string_catn(yield, buff, 1);
          }
+       break;
+       }
 
-       /* replace illegal UTF-8 sequences by replacement character  */
+      /* replace illegal UTF-8 sequences by replacement character  */
 
-       #define UTF8_REPLACEMENT_CHAR US"?"
+      #define UTF8_REPLACEMENT_CHAR US"?"
 
-       case EOP_UTF8CLEAN:
-         {
-         int seq_len = 0, index = 0;
-         int bytes_left = 0;
-         long codepoint = -1;
-         int complete;
-         uschar seq_buff[4];                   /* accumulate utf-8 here */
+      case EOP_UTF8CLEAN:
+       {
+       int seq_len = 0, index = 0, bytes_left = 0, complete;
+       u_long codepoint = (u_long)-1;
+       uschar seq_buff[4];                     /* accumulate utf-8 here */
 
-         /* Manually track tainting, as we deal in individual chars below */
+       /* Manually track tainting, as we deal in individual chars below */
 
-         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);
+       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);
 
-         /* Check the UTF-8, byte-by-byte */
+       /* Check the UTF-8, byte-by-byte */
 
-         while (*sub)
-           {
-           complete = 0;
-           uschar c = *sub++;
+       while (*sub)
+         {
+         complete = 0;
+         uschar c = *sub++;
 
-           if (bytes_left)
-             {
-             if ((c & 0xc0) != 0x80)
-                     /* wrong continuation byte; invalidate all bytes */
-               complete = 1; /* error */
-             else
-               {
-               codepoint = (codepoint << 6) | (c & 0x3f);
-               seq_buff[index++] = c;
-               if (--bytes_left == 0)          /* codepoint complete */
-                 if(codepoint > 0x10FFFF)      /* is it too large? */
-                   complete = -1;      /* error (RFC3629 limit) */
-                 else
-                   {           /* finished; output utf-8 sequence */
-                   yield = string_catn(yield, seq_buff, seq_len);
-                   index = 0;
-                   }
-               }
-             }
-           else        /* no bytes left: new sequence */
+         if (bytes_left)
+           {
+           if ((c & 0xc0) != 0x80)
+                   /* wrong continuation byte; invalidate all bytes */
+             complete = 1; /* error */
+           else
              {
-             if(!(c & 0x80))   /* 1-byte sequence, US-ASCII, keep it */
-               {
-               yield = string_catn(yield, &c, 1);
-               continue;
-               }
-             if((c & 0xe0) == 0xc0)            /* 2-byte sequence */
-               {
-               if(c == 0xc0 || c == 0xc1)      /* 0xc0 and 0xc1 are illegal */
+             codepoint = (codepoint << 6) | (c & 0x3f);
+             seq_buff[index++] = c;
+             if (--bytes_left == 0)            /* codepoint complete */
+               if(codepoint > 0x10FFFF)        /* is it too large? */
+                 complete = -1;        /* error (RFC3629 limit) */
+               else if ( (codepoint & 0x1FF800 ) == 0xD800 ) /* surrogate */
+                 /* A UTF-16 surrogate (which should be one of a pair that
+                 encode a Unicode codepoint that is outside the Basic
+                 Multilingual Plane).  Error, not UTF8.
+                 RFC2279.2 is slightly unclear on this, but 
+                 https://unicodebook.readthedocs.io/issues.html#strict-utf8-decoder
+                 says "Surrogates characters are also invalid in UTF-8:
+                 characters in U+D800—U+DFFF have to be rejected." */
                  complete = -1;
                else
-                 {
-                   bytes_left = 1;
-                   codepoint = c & 0x1f;
+                 {             /* finished; output utf-8 sequence */
+                 yield = string_catn(yield, seq_buff, seq_len);
+                 index = 0;
                  }
-               }
-             else if((c & 0xf0) == 0xe0)               /* 3-byte sequence */
-               {
-               bytes_left = 2;
-               codepoint = c & 0x0f;
-               }
-             else if((c & 0xf8) == 0xf0)               /* 4-byte sequence */
+             }
+           }
+         else  /* no bytes left: new sequence */
+           {
+           if (!(c & 0x80))    /* 1-byte sequence, US-ASCII, keep it */
+             {
+             yield = string_catn(yield, &c, 1);
+             continue;
+             }
+           if ((c & 0xe0) == 0xc0)             /* 2-byte sequence */
+             if (c == 0xc0 || c == 0xc1)       /* 0xc0 and 0xc1 are illegal */
+               complete = -1;
+             else
                {
-               bytes_left = 3;
-               codepoint = c & 0x07;
+               bytes_left = 1;
+               codepoint = c & 0x1f;
                }
-             else      /* invalid or too long (RFC3629 allows only 4 bytes) */
-               complete = -1;
-
-             seq_buff[index++] = c;
-             seq_len = bytes_left + 1;
-             }         /* if(bytes_left) */
-
-           if (complete != 0)
+           else if ((c & 0xf0) == 0xe0)                /* 3-byte sequence */
              {
-             bytes_left = index = 0;
-             yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1);
+             bytes_left = 2;
+             codepoint = c & 0x0f;
              }
-           if ((complete == 1) && ((c & 0x80) == 0))
-                         /* ASCII character follows incomplete sequence */
-               yield = string_catn(yield, &c, 1);
-           }
-         /* If given a sequence truncated mid-character, we also want to report ?
-         Eg, ${length_1:フィル} is one byte, not one character, so we expect
-         ${utf8clean:${length_1:フィル}} to yield '?' */
+           else if ((c & 0xf8) == 0xf0)                /* 4-byte sequence */
+             {
+             bytes_left = 3;
+             codepoint = c & 0x07;
+             }
+           else        /* invalid or too long (RFC3629 allows only 4 bytes) */
+             complete = -1;
 
-         if (bytes_left != 0)
-           yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1);
+           seq_buff[index++] = c;
+           seq_len = bytes_left + 1;
+           }           /* if(bytes_left) */
 
-         break;
+         if (complete != 0)
+           {
+           bytes_left = index = 0;
+           yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1);
+           }
+         if ((complete == 1) && ((c & 0x80) == 0))
+                       /* ASCII character follows incomplete sequence */
+             yield = string_catn(yield, &c, 1);
          }
+       /* If given a sequence truncated mid-character, we also want to report ?
+       Eg, ${length_1:フィル} is one byte, not one character, so we expect
+       ${utf8clean:${length_1:フィル}} to yield '?' */
+
+       if (bytes_left != 0)
+         yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1);
+
+       break;
+       }
 
 #ifdef SUPPORT_I18N
-       case EOP_UTF8_DOMAIN_TO_ALABEL:
+      case EOP_UTF8_DOMAIN_TO_ALABEL:
+       {
+       uschar * error = NULL;
+       uschar * s = string_domain_utf8_to_alabel(sub, &error);
+       if (error)
          {
-         uschar * error = NULL;
-         uschar * s = string_domain_utf8_to_alabel(sub, &error);
-         if (error)
-           {
-           expand_string_message = string_sprintf(
-             "error converting utf8 (%s) to alabel: %s",
-             string_printing(sub), error);
-           goto EXPAND_FAILED;
-           }
-         yield = string_cat(yield, s);
-         break;
+         expand_string_message = string_sprintf(
+           "error converting utf8 (%s) to alabel: %s",
+           string_printing(sub), error);
+         goto EXPAND_FAILED;
          }
+       yield = string_cat(yield, s);
+       break;
+       }
 
-       case EOP_UTF8_DOMAIN_FROM_ALABEL:
+      case EOP_UTF8_DOMAIN_FROM_ALABEL:
+       {
+       uschar * error = NULL;
+       uschar * s = string_domain_alabel_to_utf8(sub, &error);
+       if (error)
          {
-         uschar * error = NULL;
-         uschar * s = string_domain_alabel_to_utf8(sub, &error);
-         if (error)
-           {
-           expand_string_message = string_sprintf(
-             "error converting alabel (%s) to utf8: %s",
-             string_printing(sub), error);
-           goto EXPAND_FAILED;
-           }
-         yield = string_cat(yield, s);
-         break;
+         expand_string_message = string_sprintf(
+           "error converting alabel (%s) to utf8: %s",
+           string_printing(sub), error);
+         goto EXPAND_FAILED;
          }
+       yield = string_cat(yield, s);
+       break;
+       }
 
-       case EOP_UTF8_LOCALPART_TO_ALABEL:
+      case EOP_UTF8_LOCALPART_TO_ALABEL:
+       {
+       uschar * error = NULL;
+       uschar * s = string_localpart_utf8_to_alabel(sub, &error);
+       if (error)
          {
-         uschar * error = NULL;
-         uschar * s = string_localpart_utf8_to_alabel(sub, &error);
-         if (error)
-           {
-           expand_string_message = string_sprintf(
-             "error converting utf8 (%s) to alabel: %s",
-             string_printing(sub), error);
-           goto EXPAND_FAILED;
-           }
-         yield = string_cat(yield, s);
-         DEBUG(D_expand) debug_printf_indent("yield: '%s'\n", yield->s);
-         break;
+         expand_string_message = string_sprintf(
+           "error converting utf8 (%s) to alabel: %s",
+           string_printing(sub), error);
+         goto EXPAND_FAILED;
          }
+       yield = string_cat(yield, s);
+       DEBUG(D_expand) debug_printf_indent("yield: '%Y'\n", yield);
+       break;
+       }
 
-       case EOP_UTF8_LOCALPART_FROM_ALABEL:
+      case EOP_UTF8_LOCALPART_FROM_ALABEL:
+       {
+       uschar * error = NULL;
+       uschar * s = string_localpart_alabel_to_utf8(sub, &error);
+       if (error)
          {
-         uschar * error = NULL;
-         uschar * s = string_localpart_alabel_to_utf8(sub, &error);
-         if (error)
-           {
-           expand_string_message = string_sprintf(
-             "error converting alabel (%s) to utf8: %s",
-             string_printing(sub), error);
-           goto EXPAND_FAILED;
-           }
-         yield = string_cat(yield, s);
-         break;
+         expand_string_message = string_sprintf(
+           "error converting alabel (%s) to utf8: %s",
+           string_printing(sub), error);
+         goto EXPAND_FAILED;
          }
+       yield = string_cat(yield, s);
+       break;
+       }
 #endif /* EXPERIMENTAL_INTERNATIONAL */
 
-       /* escape turns all non-printing characters into escape sequences. */
+      /* escape turns all non-printing characters into escape sequences. */
 
-       case EOP_ESCAPE:
-         {
-         const uschar * t = string_printing(sub);
-         yield = string_cat(yield, t);
-         break;
-         }
+      case EOP_ESCAPE:
+       {
+       const uschar * t = string_printing(sub);
+       yield = string_cat(yield, t);
+       break;
+       }
 
-       case EOP_ESCAPE8BIT:
-         {
-         uschar c;
+      case EOP_ESCAPE8BIT:
+       {
+       uschar c;
 
-         for (const uschar * s = sub; (c = *s); s++)
-           yield = c < 127 && c != '\\'
-             ? string_catn(yield, s, 1)
-             : string_fmt_append(yield, "\\%03o", c);
-         break;
-         }
+       for (const uschar * s = sub; (c = *s); s++)
+         yield = c < 127 && c != '\\'
+           ? string_catn(yield, s, 1)
+           : string_fmt_append(yield, "\\%03o", c);
+       break;
+       }
 
-       /* Handle numeric expression evaluation */
+      /* Handle numeric expression evaluation */
 
-       case EOP_EVAL:
-       case EOP_EVAL10:
+      case EOP_EVAL:
+      case EOP_EVAL10:
+       {
+       uschar *save_sub = sub;
+       uschar *error = NULL;
+       int_eximarith_t n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE);
+       if (error)
          {
-         uschar *save_sub = sub;
-         uschar *error = NULL;
-         int_eximarith_t n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE);
-         if (error)
-           {
-           expand_string_message = string_sprintf("error in expression "
-             "evaluation: %s (after processing \"%.*s\")", error,
-             (int)(sub-save_sub), save_sub);
-           goto EXPAND_FAILED;
-           }
-         yield = string_fmt_append(yield, PR_EXIM_ARITH, n);
-         break;
+         expand_string_message = string_sprintf("error in expression "
+           "evaluation: %s (after processing \"%.*s\")", error,
+           (int)(sub-save_sub), save_sub);
+         goto EXPAND_FAILED;
          }
+       yield = string_fmt_append(yield, PR_EXIM_ARITH, n);
+       break;
+       }
 
-       /* Handle time period formatting */
+      /* Handle time period formatting */
 
-       case EOP_TIME_EVAL:
+      case EOP_TIME_EVAL:
+       {
+       int n = readconf_readtime(sub, 0, FALSE);
+       if (n < 0)
          {
-         int n = readconf_readtime(sub, 0, FALSE);
-         if (n < 0)
-           {
-           expand_string_message = string_sprintf("string \"%s\" is not an "
-             "Exim time interval in \"%s\" operator", sub, name);
-           goto EXPAND_FAILED;
-           }
-         yield = string_fmt_append(yield, "%d", n);
-         break;
+         expand_string_message = string_sprintf("string \"%s\" is not an "
+           "Exim time interval in \"%s\" operator", sub, name);
+         goto EXPAND_FAILED;
          }
+       yield = string_fmt_append(yield, "%d", n);
+       break;
+       }
 
-       case EOP_TIME_INTERVAL:
+      case EOP_TIME_INTERVAL:
+       {
+       int n;
+       uschar *t = read_number(&n, sub);
+       if (*t != 0) /* Not A Number*/
          {
-         int n;
-         uschar *t = read_number(&n, sub);
-         if (*t != 0) /* Not A Number*/
-           {
-           expand_string_message = string_sprintf("string \"%s\" is not a "
-             "positive number in \"%s\" operator", sub, name);
-           goto EXPAND_FAILED;
-           }
-         t = readconf_printtime(n);
-         yield = string_cat(yield, t);
-         break;
+         expand_string_message = string_sprintf("string \"%s\" is not a "
+           "positive number in \"%s\" operator", sub, name);
+         goto EXPAND_FAILED;
          }
+       t = readconf_printtime(n);
+       yield = string_cat(yield, t);
+       break;
+       }
 
-       /* Convert string to base64 encoding */
+      /* Convert string to base64 encoding */
 
-       case EOP_STR2B64:
-       case EOP_BASE64:
-         {
+      case EOP_STR2B64:
+      case EOP_BASE64:
+       {
 #ifndef DISABLE_TLS
-         uschar * s = vp && *(void **)vp->value
-           ? tls_cert_der_b64(*(void **)vp->value)
-           : b64encode(CUS sub, Ustrlen(sub));
+       uschar * s = vp && *(void **)vp->value
+         ? tls_cert_der_b64(*(void **)vp->value)
+         : b64encode(CUS sub, Ustrlen(sub));
 #else
-         uschar * s = b64encode(CUS sub, Ustrlen(sub));
+       uschar * s = b64encode(CUS sub, Ustrlen(sub));
 #endif
-         yield = string_cat(yield, s);
-         break;
-         }
+       yield = string_cat(yield, s);
+       break;
+       }
 
-       case EOP_BASE64D:
+      case EOP_BASE64D:
+       {
+       uschar * s;
+       int len = b64decode(sub, &s, sub);
+       if (len < 0)
          {
-         uschar * s;
-         int len = b64decode(sub, &s);
-         if (len < 0)
-           {
-           expand_string_message = string_sprintf("string \"%s\" is not "
-             "well-formed for \"%s\" operator", sub, name);
-           goto EXPAND_FAILED;
-           }
-         yield = string_cat(yield, s);
-         break;
+         expand_string_message = string_sprintf("string \"%s\" is not "
+           "well-formed for \"%s\" operator", sub, name);
+         goto EXPAND_FAILED;
          }
+       yield = string_cat(yield, s);
+       break;
+       }
 
-       /* strlen returns the length of the string */
+      /* strlen returns the length of the string */
 
-       case EOP_STRLEN:
-         yield = string_fmt_append(yield, "%d", Ustrlen(sub));
-         break;
+      case EOP_STRLEN:
+       yield = string_fmt_append(yield, "%d", Ustrlen(sub));
+       break;
+
+      /* length_n or l_n takes just the first n characters or the whole string,
+      whichever is the shorter;
+
+      substr_m_n, and s_m_n take n characters from offset m; negative m take
+      from the end; l_n is synonymous with s_0_n. If n is omitted in substr it
+      takes the rest, either to the right or to the left.
+
+      hash_n or h_n makes a hash of length n from the string, yielding n
+      characters from the set a-z; hash_n_m makes a hash of length n, but
+      uses m characters from the set a-zA-Z0-9.
+
+      nhash_n returns a single number between 0 and n-1 (in text form), while
+      nhash_n_m returns a div/mod hash as two numbers "a/b". The first lies
+      between 0 and n-1 and the second between 0 and m-1. */
+
+      case EOP_LENGTH:
+      case EOP_L:
+      case EOP_SUBSTR:
+      case EOP_S:
+      case EOP_HASH:
+      case EOP_H:
+      case EOP_NHASH:
+      case EOP_NH:
+       {
+       int sign = 1;
+       int value1 = 0;
+       int value2 = -1;
+       int *pn;
+       int len;
+       uschar *ret;
 
-       /* length_n or l_n takes just the first n characters or the whole string,
-       whichever is the shorter;
-
-       substr_m_n, and s_m_n take n characters from offset m; negative m take
-       from the end; l_n is synonymous with s_0_n. If n is omitted in substr it
-       takes the rest, either to the right or to the left.
-
-       hash_n or h_n makes a hash of length n from the string, yielding n
-       characters from the set a-z; hash_n_m makes a hash of length n, but
-       uses m characters from the set a-zA-Z0-9.
-
-       nhash_n returns a single number between 0 and n-1 (in text form), while
-       nhash_n_m returns a div/mod hash as two numbers "a/b". The first lies
-       between 0 and n-1 and the second between 0 and m-1. */
-
-       case EOP_LENGTH:
-       case EOP_L:
-       case EOP_SUBSTR:
-       case EOP_S:
-       case EOP_HASH:
-       case EOP_H:
-       case EOP_NHASH:
-       case EOP_NH:
+       if (!arg)
          {
-         int sign = 1;
-         int value1 = 0;
-         int value2 = -1;
-         int *pn;
-         int len;
-         uschar *ret;
+         expand_string_message = string_sprintf("missing values after %s",
+           name);
+         goto EXPAND_FAILED;
+         }
 
-         if (!arg)
-           {
-           expand_string_message = string_sprintf("missing values after %s",
-             name);
-           goto EXPAND_FAILED;
-           }
+       /* "length" has only one argument, effectively being synonymous with
+       substr_0_n. */
 
-         /* "length" has only one argument, effectively being synonymous with
-         substr_0_n. */
+       if (c == EOP_LENGTH || c == EOP_L)
+         {
+         pn = &value2;
+         value2 = 0;
+         }
+
+       /* The others have one or two arguments; for "substr" the first may be
+       negative. The second being negative means "not supplied". */
+
+       else
+         {
+         pn = &value1;
+         if (name[0] == 's' && *arg == '-') { sign = -1; arg++; }
+         }
+
+       /* Read up to two numbers, separated by underscores */
 
-         if (c == EOP_LENGTH || c == EOP_L)
+       ret = arg;
+       while (*arg != 0)
+         {
+         if (arg != ret && *arg == '_' && pn == &value1)
            {
            pn = &value2;
            value2 = 0;
+           if (arg[1] != 0) arg++;
            }
-
-         /* The others have one or two arguments; for "substr" the first may be
-         negative. The second being negative means "not supplied". */
-
-         else
+         else if (!isdigit(*arg))
            {
-           pn = &value1;
-           if (name[0] == 's' && *arg == '-') { sign = -1; arg++; }
+           expand_string_message =
+             string_sprintf("non-digit after underscore in \"%s\"", name);
+           goto EXPAND_FAILED;
            }
+         else *pn = (*pn)*10 + *arg++ - '0';
+         }
+       value1 *= sign;
 
-         /* Read up to two numbers, separated by underscores */
+       /* Perform the required operation */
 
-         ret = arg;
-         while (*arg != 0)
-           {
-           if (arg != ret && *arg == '_' && pn == &value1)
-             {
-             pn = &value2;
-             value2 = 0;
-             if (arg[1] != 0) arg++;
-             }
-           else if (!isdigit(*arg))
-             {
-             expand_string_message =
-               string_sprintf("non-digit after underscore in \"%s\"", name);
-             goto EXPAND_FAILED;
-             }
-           else *pn = (*pn)*10 + *arg++ - '0';
-           }
-         value1 *= sign;
+       ret = c == EOP_HASH || c == EOP_H
+         ? compute_hash(sub, value1, value2, &len)
+         : c == EOP_NHASH || c == EOP_NH
+         ? compute_nhash(sub, value1, value2, &len)
+         : extract_substr(sub, value1, value2, &len);
+       if (!ret) goto EXPAND_FAILED;
 
-         /* Perform the required operation */
-
-         ret = c == EOP_HASH || c == EOP_H
-           ? compute_hash(sub, value1, value2, &len)
-           : c == EOP_NHASH || c == EOP_NH
-           ? compute_nhash(sub, value1, value2, &len)
-           : extract_substr(sub, value1, value2, &len);
-         if (!ret) goto EXPAND_FAILED;
+       yield = string_catn(yield, ret, len);
+       break;
+       }
 
-         yield = string_catn(yield, ret, len);
-         break;
-         }
+      /* Stat a path */
 
-       /* Stat a path */
+      case EOP_STAT:
+       {
+       uschar smode[12];
+       uschar **modetable[3];
+       mode_t mode;
+       struct stat st;
 
-       case EOP_STAT:
+       if (expand_forbid & RDO_EXISTS)
          {
-         uschar smode[12];
-         uschar **modetable[3];
-         mode_t mode;
-         struct stat st;
+         expand_string_message = US"Use of the stat() expansion is not permitted";
+         goto EXPAND_FAILED;
+         }
 
-         if (expand_forbid & RDO_EXISTS)
-           {
-           expand_string_message = US"Use of the stat() expansion is not permitted";
-           goto EXPAND_FAILED;
-           }
+       if (stat(CS sub, &st) < 0)
+         {
+         expand_string_message = string_sprintf("stat(%s) failed: %s",
+           sub, strerror(errno));
+         goto EXPAND_FAILED;
+         }
+       mode = st.st_mode;
+       switch (mode & S_IFMT)
+         {
+         case S_IFIFO: smode[0] = 'p'; break;
+         case S_IFCHR: smode[0] = 'c'; break;
+         case S_IFDIR: smode[0] = 'd'; break;
+         case S_IFBLK: smode[0] = 'b'; break;
+         case S_IFREG: smode[0] = '-'; break;
+         default: smode[0] = '?'; break;
+         }
 
-         if (stat(CS sub, &st) < 0)
-           {
-           expand_string_message = string_sprintf("stat(%s) failed: %s",
-             sub, strerror(errno));
-           goto EXPAND_FAILED;
-           }
-         mode = st.st_mode;
-         switch (mode & S_IFMT)
-           {
-           case S_IFIFO: smode[0] = 'p'; break;
-           case S_IFCHR: smode[0] = 'c'; break;
-           case S_IFDIR: smode[0] = 'd'; break;
-           case S_IFBLK: smode[0] = 'b'; break;
-           case S_IFREG: smode[0] = '-'; break;
-           default: smode[0] = '?'; break;
-           }
+       modetable[0] = ((mode & 01000) == 0)? mtable_normal : mtable_sticky;
+       modetable[1] = ((mode & 02000) == 0)? mtable_normal : mtable_setid;
+       modetable[2] = ((mode & 04000) == 0)? mtable_normal : mtable_setid;
 
-         modetable[0] = ((mode & 01000) == 0)? mtable_normal : mtable_sticky;
-         modetable[1] = ((mode & 02000) == 0)? mtable_normal : mtable_setid;
-         modetable[2] = ((mode & 04000) == 0)? mtable_normal : mtable_setid;
+       for (int i = 0; i < 3; i++)
+         {
+         memcpy(CS(smode + 7 - i*3), CS(modetable[i][mode & 7]), 3);
+         mode >>= 3;
+         }
 
-         for (int i = 0; i < 3; i++)
-           {
-           memcpy(CS(smode + 7 - i*3), CS(modetable[i][mode & 7]), 3);
-           mode >>= 3;
-           }
+       smode[10] = 0;
+       yield = string_fmt_append(yield,
+         "mode=%04lo smode=%s inode=%ld device=%ld links=%ld "
+         "uid=%ld gid=%ld size=" OFF_T_FMT " atime=%ld mtime=%ld ctime=%ld",
+         (long)(st.st_mode & 077777), smode, (long)st.st_ino,
+         (long)st.st_dev, (long)st.st_nlink, (long)st.st_uid,
+         (long)st.st_gid, st.st_size, (long)st.st_atime,
+         (long)st.st_mtime, (long)st.st_ctime);
+       break;
+       }
 
-         smode[10] = 0;
-         yield = string_fmt_append(yield,
-           "mode=%04lo smode=%s inode=%ld device=%ld links=%ld "
-           "uid=%ld gid=%ld size=" OFF_T_FMT " atime=%ld mtime=%ld ctime=%ld",
-           (long)(st.st_mode & 077777), smode, (long)st.st_ino,
-           (long)st.st_dev, (long)st.st_nlink, (long)st.st_uid,
-           (long)st.st_gid, st.st_size, (long)st.st_atime,
-           (long)st.st_mtime, (long)st.st_ctime);
-         break;
-         }
+      /* vaguely random number less than N */
 
-       /* vaguely random number less than N */
+      case EOP_RANDINT:
+       {
+       int_eximarith_t max = expanded_string_integer(sub, TRUE);
 
-       case EOP_RANDINT:
-         {
-         int_eximarith_t max = expanded_string_integer(sub, TRUE);
+       if (expand_string_message)
+         goto EXPAND_FAILED;
+       yield = string_fmt_append(yield, "%d", vaguely_random_number((int)max));
+       break;
+       }
 
-         if (expand_string_message)
-           goto EXPAND_FAILED;
-         yield = string_fmt_append(yield, "%d", vaguely_random_number((int)max));
-         break;
-         }
+      /* Reverse IP, including IPv6 to dotted-nibble */
 
-       /* Reverse IP, including IPv6 to dotted-nibble */
+      case EOP_REVERSE_IP:
+       {
+       int family, maskptr;
+       uschar reversed[128];
 
-       case EOP_REVERSE_IP:
+       family = string_is_ip_address(sub, &maskptr);
+       if (family == 0)
          {
-         int family, maskptr;
-         uschar reversed[128];
-
-         family = string_is_ip_address(sub, &maskptr);
-         if (family == 0)
-           {
-           expand_string_message = string_sprintf(
-               "reverse_ip() not given an IP address [%s]", sub);
-           goto EXPAND_FAILED;
-           }
-         invert_address(reversed, sub);
-         yield = string_cat(yield, reversed);
-         break;
+         expand_string_message = string_sprintf(
+             "reverse_ip() not given an IP address [%s]", sub);
+         goto EXPAND_FAILED;
          }
+       invert_address(reversed, sub);
+       yield = string_cat(yield, reversed);
+       break;
+       }
 
-       /* Unknown operator */
+      case EOP_XTEXTD:
+       {
+       uschar * s;
+       int len = xtextdecode(sub, &s);
+       yield = string_catn(yield, s, len);
+       break;
+       }
 
-       default:
-         expand_string_message =
-           string_sprintf("unknown expansion operator \"%s\"", name);
-         goto EXPAND_FAILED;
-       }       /* EOP_* switch */
+      /* Unknown operator */
+      default:
+       expand_string_message =
+         string_sprintf("unknown expansion operator \"%s\"", name);
+       goto EXPAND_FAILED;
+      }        /* EOP_* switch */
 
-       DEBUG(D_expand)
+      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)
+       debug_printf_indent("%Vop-res: %.*s\n", "K-----", i, s);
+       if (tainted)
          {
-         debug_printf_indent("|-----op-res: %.*s\n", i, s);
-         if (tainted)
-           {
-           debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
-           debug_print_taint(yield->s);
-           }
-         }
-       else
-         {
-         debug_printf_indent(UTF8_VERT_RIGHT
-           UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-           "op-res: %.*s\n", i, s);
-         if (tainted)
-           {
-           debug_printf_indent("%s",
-             skipping
-             ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
-           debug_print_taint(yield->s);
-           }
+         debug_printf_indent("%V          %V",
+           flags & ESI_SKIPPING ? "|" : " ",
+           "\\__");
+         debug_print_taint(res);
          }
        }
        continue;
@@ -8223,7 +8390,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, flags, &newsize)))
       {
       expand_string_message =
         string_sprintf("unknown variable in \"${%s}\"", name);
@@ -8251,11 +8418,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";
@@ -8266,57 +8433,48 @@ 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)
-    {
-    debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
-    debug_printf_indent("%sresult: %s\n",
-      skipping ? "|-----" : "\\_____", yield->s);
-    if (tainted)
-      {
-      debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
-      debug_print_taint(yield->s);
-      }
-    if (skipping)
-      debug_printf_indent("\\___skipping: result is not used\n");
-    }
-  else
+  if (resetok) gstring_release_unused(yield);
+  else if (resetok_p) *resetok_p = FALSE;
+
+  DEBUG(D_expand)
     {
-    debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
-      "expanding: %.*s\n",
+    BOOL tainted = is_tainted(res);
+    debug_printf_indent("%Vexpanded: %.*W\n",
+      "K---",
       (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);
+    debug_printf_indent("%Vresult: ",
+      flags & ESI_SKIPPING ? "K-----" : "\\_____");
+    if (*res || !(flags & ESI_SKIPPING))
+      debug_printf("%W\n", res);
+    else
+      debug_printf(" %Vskipped%V\n", "<", ">");
     if (tainted)
       {
-      debug_printf_indent("%s",
-       skipping
-       ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
-      debug_print_taint(yield->s);
+      debug_printf_indent("%V          %V",
+       flags & ESI_SKIPPING ? "|" : " ",
+       "\\__"
+       );
+      debug_print_taint(res);
       }
-    if (skipping)
-      debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-       "skipping: result is not used\n");
+    if (flags & ESI_SKIPPING)
+      debug_printf_indent("%Vskipping: 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".
@@ -8337,25 +8495,11 @@ EXPAND_FAILED:
 if (left) *left = s;
 DEBUG(D_expand)
   {
-  DEBUG(D_noutf8)
-    {
-    debug_printf_indent("|failed to expand: %s\n", string);
-    debug_printf_indent("%serror message: %s\n",
-      f.expand_string_forcedfail ? "|---" : "\\___", expand_string_message);
-    if (f.expand_string_forcedfail)
-      debug_printf_indent("\\failure was forced\n");
-    }
-  else
-    {
-    debug_printf_indent(UTF8_VERT_RIGHT "failed to expand: %s\n",
-      string);
-    debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-      "error message: %s\n",
-      f.expand_string_forcedfail ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
-      expand_string_message);
-    if (f.expand_string_forcedfail)
-      debug_printf_indent(UTF8_UP_RIGHT "failure was forced\n");
-    }
+  debug_printf_indent("%Vfailed to expand: %s\n", "K", string);
+  debug_printf_indent("%Verror message: %s\n",
+    f.expand_string_forcedfail ? "K---" : "\\___", expand_string_message);
+  if (f.expand_string_forcedfail)
+    debug_printf_indent("%Vfailure was forced\n", "\\");
   }
 if (resetok_p && !resetok) *resetok_p = FALSE;
 expand_level--;
@@ -8363,38 +8507,44 @@ 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)
 {
+f.expand_string_forcedfail = f.search_find_defer = malformed_header = FALSE;
 if (Ustrpbrk(string, "$\\") != NULL)
   {
   int old_pool = store_pool;
   uschar * s;
 
-  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); }
+
 
 
 
@@ -8551,15 +8701,17 @@ Returns:     OK     value placed in rvalue
 */
 
 int
-exp_bool(address_item *addr,
-  uschar *mtype, uschar *mname, unsigned dbg_opt,
-  uschar *oname, BOOL bvalue,
-  uschar *svalue, BOOL *rvalue)
+exp_bool(address_item * addr,
+  const uschar * mtype, const uschar * mname, unsigned dbg_opt,
+  uschar * oname, BOOL bvalue,
+  const uschar * svalue, BOOL * rvalue)
 {
-uschar *expanded;
+const uschar * expanded;
+
+DEBUG(D_expand) debug_printf("try option %s\n", oname);
 if (!svalue) { *rvalue = bvalue; return OK; }
 
-if (!(expanded = expand_string(svalue)))
+if (!(expanded = expand_cstring(svalue)))
   {
   if (f.expand_string_forcedfail)
     {
@@ -8691,12 +8843,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);
 
@@ -8830,8 +8984,9 @@ search_tidyup();
 return 0;
 }
 
-#endif
+#endif /*STAND_ALONE*/
 
+#endif /*!MACRO_PREDEF*/
 /* vi: aw ai sw=2
 */
 /* End of expand.c */