Avoid processing in ${sg...} when skipping
[exim.git] / src / src / expand.c
index 6478920f859b9482e5a45cae7675ce0fdf1a434c..744ab4d0b934ca4baa4f18a7e1725992a9168148 100644 (file)
@@ -2,8 +2,8 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
-/* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
@@ -444,9 +444,9 @@ enum vtypes {
   vtype_pspace,         /* partition space; value is T/F for spool/log */
   vtype_pinodes,        /* partition inodes; value is T/F for spool/log */
   vtype_cert           /* SSL certificate */
-  #ifndef DISABLE_DKIM
+#ifndef DISABLE_DKIM
   ,vtype_dkim           /* Lookup of value in DKIM signature */
-  #endif
+#endif
 };
 
 /* Type for main variable table */
@@ -583,9 +583,9 @@ static var_entry var_table[] = {
   { "interface_address",   vtype_stringptr,   &interface_address },
   { "interface_port",      vtype_int,         &interface_port },
   { "item",                vtype_stringptr,   &iterate_item },
-  #ifdef LOOKUP_LDAP
+#ifdef LOOKUP_LDAP
   { "ldap_dn",             vtype_stringptr,   &eldap_dn },
-  #endif
+#endif
   { "load_average",        vtype_load_avg,    NULL },
   { "local_part",          vtype_stringptr,   &deliver_localpart },
   { "local_part_data",     vtype_stringptr,   &deliver_localpart_data },
@@ -750,17 +750,8 @@ static var_entry var_table[] = {
   { "spool_directory",     vtype_stringptr,   &spool_directory },
   { "spool_inodes",        vtype_pinodes,     (void *)TRUE },
   { "spool_space",         vtype_pspace,      (void *)TRUE },
-#ifdef EXPERIMENTAL_SRS_ALT
-  { "srs_db_address",      vtype_stringptr,   &srs_db_address },
-  { "srs_db_key",          vtype_stringptr,   &srs_db_key },
-  { "srs_orig_recipient",  vtype_stringptr,   &srs_orig_recipient },
-  { "srs_orig_sender",     vtype_stringptr,   &srs_orig_sender },
-#endif
-#if defined(EXPERIMENTAL_SRS_ALT) || defined(SUPPORT_SRS)
+#ifdef SUPPORT_SRS
   { "srs_recipient",       vtype_stringptr,   &srs_recipient },
-#endif
-#ifdef EXPERIMENTAL_SRS_ALT
-  { "srs_status",          vtype_stringptr,   &srs_status },
 #endif
   { "thisaddress",         vtype_stringptr,   &filter_thisaddress },
 
@@ -1297,7 +1288,7 @@ expand_getlistele(int field, const uschar * list)
 const uschar * tlist = list;
 int sep = 0;
 /* Tainted mem for the throwaway element copies */
-uschar * dummy = store_get(2, TRUE);
+uschar * dummy = store_get(2, GET_TAINTED);
 
 if (field < 0)
   {
@@ -1984,7 +1975,7 @@ switch (vp->type)
       int len = message_body_visible;
 
       if (len > message_size) len = message_size;
-      *ss = body = store_get(len+1, TRUE);
+      *ss = body = store_get(len+1, GET_TAINTED);
       body[0] = 0;
       if (vp->type == vtype_msgbody_end)
        {
@@ -2907,64 +2898,49 @@ switch(cond_type = identify_operator(&s, &opname))
     {
     case ECOND_NUM_E:
     case ECOND_NUM_EE:
-    tempcond = (num[0] == num[1]);
-    break;
+      tempcond = (num[0] == num[1]); break;
 
     case ECOND_NUM_G:
-    tempcond = (num[0] > num[1]);
-    break;
+      tempcond = (num[0] > num[1]); break;
 
     case ECOND_NUM_GE:
-    tempcond = (num[0] >= num[1]);
-    break;
+      tempcond = (num[0] >= num[1]); break;
 
     case ECOND_NUM_L:
-    tempcond = (num[0] < num[1]);
-    break;
+      tempcond = (num[0] < num[1]); break;
 
     case ECOND_NUM_LE:
-    tempcond = (num[0] <= num[1]);
-    break;
+      tempcond = (num[0] <= num[1]); break;
 
     case ECOND_STR_LT:
-    tempcond = (Ustrcmp(sub[0], sub[1]) < 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) < 0); break;
 
     case ECOND_STR_LTI:
-    tempcond = (strcmpic(sub[0], sub[1]) < 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) < 0); break;
 
     case ECOND_STR_LE:
-    tempcond = (Ustrcmp(sub[0], sub[1]) <= 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) <= 0); break;
 
     case ECOND_STR_LEI:
-    tempcond = (strcmpic(sub[0], sub[1]) <= 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) <= 0); break;
 
     case ECOND_STR_EQ:
-    tempcond = (Ustrcmp(sub[0], sub[1]) == 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) == 0); break;
 
     case ECOND_STR_EQI:
-    tempcond = (strcmpic(sub[0], sub[1]) == 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) == 0); break;
 
     case ECOND_STR_GT:
-    tempcond = (Ustrcmp(sub[0], sub[1]) > 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) > 0); break;
 
     case ECOND_STR_GTI:
-    tempcond = (strcmpic(sub[0], sub[1]) > 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) > 0); break;
 
     case ECOND_STR_GE:
-    tempcond = (Ustrcmp(sub[0], sub[1]) >= 0);
-    break;
+      tempcond = (Ustrcmp(sub[0], sub[1]) >= 0); break;
 
     case ECOND_STR_GEI:
-    tempcond = (strcmpic(sub[0], sub[1]) >= 0);
-    break;
+      tempcond = (strcmpic(sub[0], sub[1]) >= 0); break;
 
     case ECOND_MATCH:   /* Regular expression match */
       {
@@ -2987,72 +2963,68 @@ switch(cond_type = identify_operator(&s, &opname))
       }
 
     case ECOND_MATCH_ADDRESS:  /* Match in an address list */
-    rc = match_address_list(sub[0], TRUE, FALSE, &(sub[1]), NULL, -1, 0, NULL);
-    goto MATCHED_SOMETHING;
+      rc = match_address_list(sub[0], TRUE, FALSE, &(sub[1]), NULL, -1, 0,
+                             CUSS &lookup_value);
+      goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_DOMAIN:   /* Match in a domain list */
-    rc = match_isinlist(sub[0], &(sub[1]), 0, &domainlist_anchor, NULL,
-      MCL_DOMAIN + MCL_NOEXPAND, TRUE, NULL);
-    goto MATCHED_SOMETHING;
+      rc = match_isinlist(sub[0], &(sub[1]), 0, &domainlist_anchor, NULL,
+       MCL_DOMAIN + MCL_NOEXPAND, TRUE, CUSS &lookup_value);
+      goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_IP:       /* Match IP address in a host list */
-    if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) == 0)
-      {
-      expand_string_message = string_sprintf("\"%s\" is not an IP address",
-        sub[0]);
-      return NULL;
-      }
-    else
-      {
-      unsigned int *nullcache = NULL;
-      check_host_block cb;
-
-      cb.host_name = US"";
-      cb.host_address = sub[0];
-
-      /* If the host address starts off ::ffff: it is an IPv6 address in
-      IPv4-compatible mode. Find the IPv4 part for checking against IPv4
-      addresses. */
-
-      cb.host_ipv4 = (Ustrncmp(cb.host_address, "::ffff:", 7) == 0)?
-        cb.host_address + 7 : cb.host_address;
-
-      rc = match_check_list(
-             &sub[1],                   /* the list */
-             0,                         /* separator character */
-             &hostlist_anchor,          /* anchor pointer */
-             &nullcache,                /* cache pointer */
-             check_host,                /* function for testing */
-             &cb,                       /* argument for function */
-             MCL_HOST,                  /* type of check */
-             sub[0],                    /* text for debugging */
-             NULL);                     /* where to pass back data */
-      }
-    goto MATCHED_SOMETHING;
+      if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) == 0)
+       {
+       expand_string_message = string_sprintf("\"%s\" is not an IP address",
+         sub[0]);
+       return NULL;
+       }
+      else
+       {
+       unsigned int *nullcache = NULL;
+       check_host_block cb;
+
+       cb.host_name = US"";
+       cb.host_address = sub[0];
+
+       /* If the host address starts off ::ffff: it is an IPv6 address in
+       IPv4-compatible mode. Find the IPv4 part for checking against IPv4
+       addresses. */
+
+       cb.host_ipv4 = (Ustrncmp(cb.host_address, "::ffff:", 7) == 0)?
+         cb.host_address + 7 : cb.host_address;
+
+       rc = match_check_list(
+              &sub[1],                   /* the list */
+              0,                         /* separator character */
+              &hostlist_anchor,          /* anchor pointer */
+              &nullcache,                /* cache pointer */
+              check_host,                /* function for testing */
+              &cb,                       /* argument for function */
+              MCL_HOST,                  /* type of check */
+              sub[0],                    /* text for debugging */
+              CUSS &lookup_value);       /* where to pass back data */
+       }
+      goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_LOCAL_PART:
-    rc = match_isinlist(sub[0], &(sub[1]), 0, &localpartlist_anchor, NULL,
-      MCL_LOCALPART + MCL_NOEXPAND, TRUE, NULL);
-    /* Fall through */
-    /* VVVVVVVVVVVV */
-    MATCHED_SOMETHING:
-    switch(rc)
-      {
-      case OK:
-      tempcond = TRUE;
-      break;
-
-      case FAIL:
-      tempcond = FALSE;
-      break;
+      rc = match_isinlist(sub[0], &(sub[1]), 0, &localpartlist_anchor, NULL,
+       MCL_LOCALPART + MCL_NOEXPAND, TRUE, CUSS &lookup_value);
+      /* Fall through */
+      /* VVVVVVVVVVVV */
+      MATCHED_SOMETHING:
+      switch(rc)
+       {
+       case OK:   tempcond = TRUE;  break;
+       case FAIL: tempcond = FALSE; break;
 
-      case DEFER:
-      expand_string_message = string_sprintf("unable to complete match "
-        "against \"%s\": %s", sub[1], search_error_message);
-      return NULL;
-      }
+       case DEFER:
+         expand_string_message = string_sprintf("unable to complete match "
+           "against \"%s\": %s", sub[1], search_error_message);
+         return NULL;
+       }
 
-    break;
+      break;
 
     /* Various "encrypted" comparisons. If the second string starts with
     "{" then an encryption type is given. Default to crypt() or crypt16()
@@ -3061,138 +3033,138 @@ switch(cond_type = identify_operator(&s, &opname))
 
     case ECOND_CRYPTEQ:
     #ifndef SUPPORT_CRYPTEQ
-    goto COND_FAILED_NOT_COMPILED;
+      goto COND_FAILED_NOT_COMPILED;
     #else
-    if (strncmpic(sub[1], US"{md5}", 5) == 0)
-      {
-      int sublen = Ustrlen(sub[1]+5);
-      md5 base;
-      uschar digest[16];
+      if (strncmpic(sub[1], US"{md5}", 5) == 0)
+       {
+       int sublen = Ustrlen(sub[1]+5);
+       md5 base;
+       uschar digest[16];
 
-      md5_start(&base);
-      md5_end(&base, sub[0], Ustrlen(sub[0]), digest);
+       md5_start(&base);
+       md5_end(&base, sub[0], Ustrlen(sub[0]), digest);
 
-      /* If the length that we are comparing against is 24, the MD5 digest
-      is expressed as a base64 string. This is the way LDAP does it. However,
-      some other software uses a straightforward hex representation. We assume
-      this if the length is 32. Other lengths fail. */
+       /* If the length that we are comparing against is 24, the MD5 digest
+       is expressed as a base64 string. This is the way LDAP does it. However,
+       some other software uses a straightforward hex representation. We assume
+       this if the length is 32. Other lengths fail. */
 
-      if (sublen == 24)
-        {
-        uschar *coded = b64encode(CUS digest, 16);
-        DEBUG(D_auth) debug_printf("crypteq: using MD5+B64 hashing\n"
-          "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
-        tempcond = (Ustrcmp(coded, sub[1]+5) == 0);
-        }
-      else if (sublen == 32)
-        {
-        uschar coded[36];
-        for (int i = 0; i < 16; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
-        coded[32] = 0;
-        DEBUG(D_auth) debug_printf("crypteq: using MD5+hex hashing\n"
-          "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
-        tempcond = (strcmpic(coded, sub[1]+5) == 0);
-        }
-      else
-        {
-        DEBUG(D_auth) debug_printf("crypteq: length for MD5 not 24 or 32: "
-          "fail\n  crypted=%s\n", sub[1]+5);
-        tempcond = FALSE;
-        }
-      }
+       if (sublen == 24)
+         {
+         uschar *coded = b64encode(CUS digest, 16);
+         DEBUG(D_auth) debug_printf("crypteq: using MD5+B64 hashing\n"
+           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
+         tempcond = (Ustrcmp(coded, sub[1]+5) == 0);
+         }
+       else if (sublen == 32)
+         {
+         uschar coded[36];
+         for (int i = 0; i < 16; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
+         coded[32] = 0;
+         DEBUG(D_auth) debug_printf("crypteq: using MD5+hex hashing\n"
+           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
+         tempcond = (strcmpic(coded, sub[1]+5) == 0);
+         }
+       else
+         {
+         DEBUG(D_auth) debug_printf("crypteq: length for MD5 not 24 or 32: "
+           "fail\n  crypted=%s\n", sub[1]+5);
+         tempcond = FALSE;
+         }
+       }
 
-    else if (strncmpic(sub[1], US"{sha1}", 6) == 0)
-      {
-      int sublen = Ustrlen(sub[1]+6);
-      hctx h;
-      uschar digest[20];
+      else if (strncmpic(sub[1], US"{sha1}", 6) == 0)
+       {
+       int sublen = Ustrlen(sub[1]+6);
+       hctx h;
+       uschar digest[20];
 
-      sha1_start(&h);
-      sha1_end(&h, sub[0], Ustrlen(sub[0]), digest);
+       sha1_start(&h);
+       sha1_end(&h, sub[0], Ustrlen(sub[0]), digest);
 
-      /* If the length that we are comparing against is 28, assume the SHA1
-      digest is expressed as a base64 string. If the length is 40, assume a
-      straightforward hex representation. Other lengths fail. */
+       /* If the length that we are comparing against is 28, assume the SHA1
+       digest is expressed as a base64 string. If the length is 40, assume a
+       straightforward hex representation. Other lengths fail. */
 
-      if (sublen == 28)
-        {
-        uschar *coded = b64encode(CUS digest, 20);
-        DEBUG(D_auth) debug_printf("crypteq: using SHA1+B64 hashing\n"
-          "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
-        tempcond = (Ustrcmp(coded, sub[1]+6) == 0);
-        }
-      else if (sublen == 40)
-        {
-        uschar coded[44];
-        for (int i = 0; i < 20; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
-        coded[40] = 0;
-        DEBUG(D_auth) debug_printf("crypteq: using SHA1+hex hashing\n"
-          "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
-        tempcond = (strcmpic(coded, sub[1]+6) == 0);
-        }
-      else
-        {
-        DEBUG(D_auth) debug_printf("crypteq: length for SHA-1 not 28 or 40: "
-          "fail\n  crypted=%s\n", sub[1]+6);
-       tempcond = FALSE;
-        }
-      }
+       if (sublen == 28)
+         {
+         uschar *coded = b64encode(CUS digest, 20);
+         DEBUG(D_auth) debug_printf("crypteq: using SHA1+B64 hashing\n"
+           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
+         tempcond = (Ustrcmp(coded, sub[1]+6) == 0);
+         }
+       else if (sublen == 40)
+         {
+         uschar coded[44];
+         for (int i = 0; i < 20; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
+         coded[40] = 0;
+         DEBUG(D_auth) debug_printf("crypteq: using SHA1+hex hashing\n"
+           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
+         tempcond = (strcmpic(coded, sub[1]+6) == 0);
+         }
+       else
+         {
+         DEBUG(D_auth) debug_printf("crypteq: length for SHA-1 not 28 or 40: "
+           "fail\n  crypted=%s\n", sub[1]+6);
+         tempcond = FALSE;
+         }
+       }
 
-    else   /* {crypt} or {crypt16} and non-{ at start */
-           /* }-for-text-editors */
-      {
-      int which = 0;
-      uschar *coded;
+      else   /* {crypt} or {crypt16} and non-{ at start */
+            /* }-for-text-editors */
+       {
+       int which = 0;
+       uschar *coded;
 
-      if (strncmpic(sub[1], US"{crypt}", 7) == 0)
-        {
-        sub[1] += 7;
-        which = 1;
-        }
-      else if (strncmpic(sub[1], US"{crypt16}", 9) == 0)
-        {
-        sub[1] += 9;
-        which = 2;
-        }
-      else if (sub[1][0] == '{')               /* }-for-text-editors */
-        {
-        expand_string_message = string_sprintf("unknown encryption mechanism "
-          "in \"%s\"", sub[1]);
-        return NULL;
-        }
+       if (strncmpic(sub[1], US"{crypt}", 7) == 0)
+         {
+         sub[1] += 7;
+         which = 1;
+         }
+       else if (strncmpic(sub[1], US"{crypt16}", 9) == 0)
+         {
+         sub[1] += 9;
+         which = 2;
+         }
+       else if (sub[1][0] == '{')              /* }-for-text-editors */
+         {
+         expand_string_message = string_sprintf("unknown encryption mechanism "
+           "in \"%s\"", sub[1]);
+         return NULL;
+         }
 
-      switch(which)
-        {
-        case 0:  coded = US DEFAULT_CRYPT(CS sub[0], CS sub[1]); break;
-        case 1:  coded = US crypt(CS sub[0], CS sub[1]); break;
-        default: coded = US crypt16(CS sub[0], CS sub[1]); break;
-        }
+       switch(which)
+         {
+         case 0:  coded = US DEFAULT_CRYPT(CS sub[0], CS sub[1]); break;
+         case 1:  coded = US crypt(CS sub[0], CS sub[1]); break;
+         default: coded = US crypt16(CS sub[0], CS sub[1]); break;
+         }
 
-      #define STR(s) # s
-      #define XSTR(s) STR(s)
-      DEBUG(D_auth) debug_printf("crypteq: using %s()\n"
-        "  subject=%s\n  crypted=%s\n",
-        which == 0 ? XSTR(DEFAULT_CRYPT) : which == 1 ? "crypt" : "crypt16",
-        coded, sub[1]);
-      #undef STR
-      #undef XSTR
-
-      /* If the encrypted string contains fewer than two characters (for the
-      salt), force failure. Otherwise we get false positives: with an empty
-      string the yield of crypt() is an empty string! */
-
-      if (coded)
-       tempcond = Ustrlen(sub[1]) < 2 ? FALSE : Ustrcmp(coded, sub[1]) == 0;
-      else if (errno == EINVAL)
-       tempcond = FALSE;
-      else
-       {
-       expand_string_message = string_sprintf("crypt error: %s\n",
-         US strerror(errno));
-       return NULL;
+       #define STR(s) # s
+       #define XSTR(s) STR(s)
+       DEBUG(D_auth) debug_printf("crypteq: using %s()\n"
+         "  subject=%s\n  crypted=%s\n",
+         which == 0 ? XSTR(DEFAULT_CRYPT) : which == 1 ? "crypt" : "crypt16",
+         coded, sub[1]);
+       #undef STR
+       #undef XSTR
+
+       /* If the encrypted string contains fewer than two characters (for the
+       salt), force failure. Otherwise we get false positives: with an empty
+       string the yield of crypt() is an empty string! */
+
+       if (coded)
+         tempcond = Ustrlen(sub[1]) < 2 ? FALSE : Ustrcmp(coded, sub[1]) == 0;
+       else if (errno == EINVAL)
+         tempcond = FALSE;
+       else
+         {
+         expand_string_message = string_sprintf("crypt error: %s\n",
+           US strerror(errno));
+         return NULL;
+         }
        }
-      }
-    break;
+      break;
     #endif  /* SUPPORT_CRYPTEQ */
 
     case ECOND_INLIST:
@@ -3215,6 +3187,7 @@ switch(cond_type = identify_operator(&s, &opname))
         if (compare(sub[0], iterate_item) == 0)
           {
           tempcond = TRUE;
+         lookup_value = iterate_item;
           break;
           }
        }
@@ -3843,8 +3816,8 @@ Returns:  pointer to string containing the last three
 static uschar *
 prvs_daystamp(int day_offset)
 {
-uschar *days = store_get(32, FALSE);         /* Need at least 24 for cases */
-(void)string_format(days, 32, TIME_T_FMT,    /* where TIME_T_FMT is %lld */
+uschar * days = store_get(32, GET_UNTAINTED);      /* Need at least 24 for cases */
+(void)string_format(days, 32, TIME_T_FMT,         /* where TIME_T_FMT is %lld */
   (time(NULL) + day_offset*86400)/86400);
 return (Ustrlen(days) >= 3) ? &days[Ustrlen(days)-3] : US"100";
 }
@@ -3915,7 +3888,7 @@ chash_end(HMAC_SHA1, &h, innerhash, 20, finalhash);
 
 /* Hashing is deemed sufficient to de-taint any input data */
 
-p = finalhash_hex = store_get(40, FALSE);
+p = finalhash_hex = store_get(40, GET_UNTAINTED);
 for (int i = 0; i < 3; i++)
   {
   *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
@@ -3946,7 +3919,7 @@ Returns:       new pointer for expandable string, terminated if non-null
 */
 
 gstring *
-cat_file(FILE *f, gstring *yield, uschar *eol)
+cat_file(FILE * f, gstring * yield, uschar * eol)
 {
 uschar buffer[1024];
 
@@ -3958,8 +3931,6 @@ while (Ufgets(buffer, sizeof(buffer), f))
   if (eol && buffer[len])
     yield = string_cat(yield, eol);
   }
-
-(void) string_from_gstring(yield);
 return yield;
 }
 
@@ -3981,7 +3952,6 @@ while ((rc = tls_read(tls_ctx, buffer, sizeof(buffer))) > 0)
 /* We assume that all errors, and any returns of zero bytes,
 are actually EOF. */
 
-(void) string_from_gstring(yield);
 return yield;
 }
 #endif
@@ -4356,7 +4326,7 @@ list = ((namedlist_block *)(t->data.ptr))->string;
 /* The list could be quite long so we (re)use a buffer for each element
 rather than getting each in new memory */
 
-if (is_tainted(list)) buffer = store_get(LISTNAMED_BUF_SIZE, TRUE);
+if (is_tainted(list)) buffer = store_get(LISTNAMED_BUF_SIZE, GET_TAINTED);
 while ((item = string_nextinlist(&list, &sep, buffer, LISTNAMED_BUF_SIZE)))
   {
   uschar * buf = US" : ";
@@ -4507,13 +4477,13 @@ expand_level++;
 f.expand_string_forcedfail = FALSE;
 expand_string_message = US"";
 
-{ uschar *m;
-if ((m = is_tainted2(string, LOG_MAIN|LOG_PANIC, "Tainted string '%s' in expansion", s)))
+if (is_tainted(string))
   {
-  expand_string_message = m;
+  expand_string_message =
+    string_sprintf("attempt to expand tainted string '%s'", s);
+  log_write(0, LOG_MAIN|LOG_PANIC, "%s", expand_string_message);
   goto EXPAND_FAILED;
   }
-}
 
 while (*s)
   {
@@ -4613,13 +4583,13 @@ while (*s)
     buffer. */
 
     if (!yield)
-      g = store_get(sizeof(gstring), FALSE);
+      g = store_get(sizeof(gstring), GET_UNTAINTED);
     else if (yield->ptr == 0)
       {
       if (resetok) reset_point = store_reset(reset_point);
       yield = NULL;
       reset_point = store_mark();
-      g = store_get(sizeof(gstring), FALSE);   /* alloc _before_ calling find_variable() */
+      g = store_get(sizeof(gstring), GET_UNTAINTED);   /* alloc _before_ calling find_variable() */
       }
 
     /* Header */
@@ -4829,6 +4799,7 @@ while (*s)
       const uschar *next_s;
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
+      uschar * save_lookup_value = lookup_value;
 
       Uskip_whitespace(&s);
       if (!(next_s = eval_condition(s, &resetok, skipping ? NULL : &cond)))
@@ -4862,6 +4833,7 @@ while (*s)
       /* Restore external setting of expansion variables for continuation
       at this level. */
 
+      lookup_value = save_lookup_value;
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
       break;
@@ -5554,10 +5526,8 @@ while (*s)
     case EITEM_RUN:
       {
       FILE * f;
-      uschar * arg;
-      const uschar ** argv;
-      pid_t pid;
-      int fd_in, fd_out;
+      const uschar * arg, ** argv;
+      BOOL late_expand = TRUE;
 
       if ((expand_forbid & RDO_RUN) != 0)
         {
@@ -5565,17 +5535,45 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
+      /* Handle options to the "run" */
+
+      while (*s == ',')
+       {
+       if (Ustrncmp(++s, "preexpand", 9) == 0)
+         { late_expand = FALSE; s += 9; }
+       else
+         {
+         const uschar * t = s;
+         while (isalpha(*++t)) ;
+         expand_string_message = string_sprintf("bad option '%.*s' for run",
+                                                 (int)(t-s), s);
+         goto EXPAND_FAILED;
+         }
+       }
       Uskip_whitespace(&s);
-      if (*s != '{')
+
+      if (*s != '{')                                   /*}*/
         {
        expand_string_message = US"missing '{' for command arg of run";
-       goto EXPAND_FAILED_CURLY;
+       goto EXPAND_FAILED_CURLY;                       /*"}*/
        }
-      if (!(arg = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok)))
-       goto EXPAND_FAILED;
-      Uskip_whitespace(&s);
+      s++;
+
+      if (late_expand)         /* this is the default case */
+       {
+       int n = Ustrcspn(s, "}");
+       arg = skipping ? NULL : string_copyn(s, n);
+       s += n;
+       }
+      else
+       {
+       if (!(arg = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok)))
+         goto EXPAND_FAILED;
+       Uskip_whitespace(&s);
+       }
+                                                       /*{*/
       if (*s++ != '}')
-        {
+        {                                              /*{*/
        expand_string_message = US"missing '}' closing command arg of run";
        goto EXPAND_FAILED_CURLY;
        }
@@ -5587,13 +5585,17 @@ while (*s)
        }
       else
         {
+       int fd_in, fd_out;
+       pid_t pid;
+
         if (!transport_set_up_command(&argv,    /* anchor for arg list */
             arg,                                /* raw command */
-            FALSE,                              /* don't expand the arguments */
-            0,                                  /* not relevant when... */
-            NULL,                               /* no transporting address */
-            US"${run} expansion",               /* for error messages */
-            &expand_string_message))            /* where to put error message */
+           late_expand,                /* expand args if not already done */
+            0,                          /* not relevant when... */
+            NULL,                       /* no transporting address */
+           late_expand,                /* allow tainted args, when expand-after-split */
+            US"${run} expansion",       /* for error messages */
+            &expand_string_message))    /* where to put error message */
           goto EXPAND_FAILED;
 
         /* Create the child process, making it a group leader. */
@@ -5604,7 +5606,7 @@ while (*s)
           expand_string_message =
             string_sprintf("couldn't create child process: %s", strerror(errno));
           goto EXPAND_FAILED;
-          }
+         }
 
         /* Nothing is written to the standard input. */
 
@@ -5892,8 +5894,8 @@ while (*s)
         case 2:
         case 3: goto EXPAND_FAILED;
         }
+      if (skipping) continue;
 
-      /*XXX no handling of skipping? */
       /* Compile the regular expression */
 
       if (!(re = pcre2_compile((PCRE2_SPTR)sub[1], PCRE2_ZERO_TERMINATED,
@@ -5984,7 +5986,6 @@ while (*s)
 
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
-      if (skipping) continue;
       break;
       }
 
@@ -6571,6 +6572,9 @@ while (*s)
         item of the output list, add in a space if the new item begins with the
         separator character, or is an empty string. */
 
+/*XXX is there not a standard support function for this, appending to a list? */
+/* yes, string_append_listele(), but it depends on lack of text before the list */
+
         if (  yield && yield->ptr != save_ptr
           && (temp[0] == *outsep || temp[0] == 0))
           yield = string_catn(yield, US" ", 1);
@@ -6844,7 +6848,7 @@ while (*s)
           log_write(0, LOG_MAIN|LOG_PANIC, "%s", expand_string_message);
           goto EXPAND_FAILED;
           }
-        t = store_get_perm(sizeof(tree_node) + Ustrlen(argv[0]), is_tainted(argv[0]));
+        t = store_get_perm(sizeof(tree_node) + Ustrlen(argv[0]), argv[0]);
         Ustrcpy(t->name, argv[0]);
         t->data.ptr = handle;
         (void)tree_insertnode(&dlobj_anchor, t);
@@ -7207,14 +7211,14 @@ NOT_ITEM: ;
        }
 
       case EOP_MD5:
-  #ifndef DISABLE_TLS
+#ifndef DISABLE_TLS
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_md5(*(void **)vp->value);
          yield = string_cat(yield, cp);
          }
        else
-  #endif
+#endif
          {
          md5 base;
          uschar digest[16];
@@ -7226,14 +7230,14 @@ NOT_ITEM: ;
        break;
 
       case EOP_SHA1:
-  #ifndef DISABLE_TLS
+#ifndef DISABLE_TLS
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_sha1(*(void **)vp->value);
          yield = string_cat(yield, cp);
          }
        else
-  #endif
+#endif
          {
          hctx h;
          uschar digest[20];
@@ -7246,7 +7250,7 @@ NOT_ITEM: ;
 
       case EOP_SHA2:
       case EOP_SHA256:
-  #ifdef EXIM_HAVE_SHA2
+#ifdef EXIM_HAVE_SHA2
        if (vp && *(void **)vp->value)
          if (c == EOP_SHA256)
            yield = string_cat(yield, tls_cert_fprt_sha256(*(void **)vp->value));
@@ -7268,18 +7272,18 @@ NOT_ITEM: ;
            goto EXPAND_FAILED;
            }
 
-         exim_sha_update(&h, sub, Ustrlen(sub));
+         exim_sha_update_string(&h, sub);
          exim_sha_finish(&h, &b);
          while (b.len-- > 0)
            yield = string_fmt_append(yield, "%02X", *b.data++);
          }
-  #else
+#else
          expand_string_message = US"sha256 only supported with TLS";
-  #endif
+#endif
        break;
 
       case EOP_SHA3:
-  #ifdef EXIM_HAVE_SHA3
+#ifdef EXIM_HAVE_SHA3
        {
        hctx h;
        blob b;
@@ -7296,16 +7300,16 @@ NOT_ITEM: ;
          goto EXPAND_FAILED;
          }
 
-       exim_sha_update(&h, sub, Ustrlen(sub));
+       exim_sha_update_string(&h, sub);
        exim_sha_finish(&h, &b);
        while (b.len-- > 0)
          yield = string_fmt_append(yield, "%02X", *b.data++);
        }
        break;
-  #else
+#else
        expand_string_message = US"sha3 only supported with GnuTLS 3.5.0 + or OpenSSL 1.1.1 +";
        goto EXPAND_FAILED;
-  #endif
+#endif
 
       /* Convert hex encoding to base64 encoding */
 
@@ -7373,7 +7377,7 @@ NOT_ITEM: ;
       case EOP_LISTCOUNT:
        {
        int cnt = 0, sep = 0;
-       uschar * buf = store_get(2, is_tainted(sub));
+       uschar * buf = store_get(2, sub);
 
        while (string_nextinlist(CUSS &sub, &sep, buf, 1)) cnt++;
        yield = string_fmt_append(yield, "%d", cnt);
@@ -7633,10 +7637,10 @@ NOT_ITEM: ;
            goto EXPAND_FAILED;
            }
 
-        if (lookup_list[n]->quote)
-          sub = (lookup_list[n]->quote)(sub, opt);
-        else if (opt)
-         sub = NULL;
+         if (lookup_list[n]->quote)
+           sub = (lookup_list[n]->quote)(sub, opt, (unsigned)n);
+         else if (opt)
+           sub = NULL;
 
          if (!sub)
            {
@@ -7696,7 +7700,7 @@ NOT_ITEM: ;
 
        case EOP_FROM_UTF8:
          {
-         uschar * buff = store_get(4, is_tainted(sub));
+         uschar * buff = store_get(4, sub);
          while (*sub)
            {
            int c;
@@ -7722,13 +7726,10 @@ NOT_ITEM: ;
 
          /* Manually track tainting, as we deal in individual chars below */
 
-         if (is_tainted(sub))
-           {
-           if (yield->s && yield->ptr)
-             gstring_rebuffer(yield);
-           else
-             yield->s = store_get(yield->size = Ustrlen(sub), is_tainted(sub));
-           }
+         if (!yield->s || !yield->ptr)
+           yield->s = store_get(yield->size = Ustrlen(sub), sub);
+         else if (is_incompatible(yield->s, sub))
+           gstring_rebuffer(yield, sub);
 
          /* Check the UTF-8, byte-by-byte */
 
@@ -7809,7 +7810,7 @@ NOT_ITEM: ;
          break;
          }
 
-  #ifdef SUPPORT_I18N
+#ifdef SUPPORT_I18N
        case EOP_UTF8_DOMAIN_TO_ALABEL:
          {
          uschar * error = NULL;
@@ -7870,7 +7871,7 @@ NOT_ITEM: ;
          yield = string_cat(yield, s);
          break;
          }
-  #endif       /* EXPERIMENTAL_INTERNATIONAL */
+#endif /* EXPERIMENTAL_INTERNATIONAL */
 
        /* escape turns all non-printing characters into escape sequences. */
 
@@ -7946,13 +7947,13 @@ NOT_ITEM: ;
        case EOP_STR2B64:
        case EOP_BASE64:
          {
-  #ifndef DISABLE_TLS
+#ifndef DISABLE_TLS
          uschar * s = vp && *(void **)vp->value
            ? tls_cert_der_b64(*(void **)vp->value)
            : b64encode(CUS sub, Ustrlen(sub));
-  #else
+#else
          uschar * s = b64encode(CUS sub, Ustrlen(sub));
-  #endif
+#endif
          yield = string_cat(yield, s);
          break;
          }
@@ -8160,9 +8161,34 @@ NOT_ITEM: ;
        }       /* EOP_* switch */
 
        DEBUG(D_expand)
-       if (start > 0 || *s)            /* only if not the sole expansion of the line */
-         debug_expansion_interim(US"op-res",
-                                 yield->s + start, yield->ptr - start, skipping);
+       {
+       const uschar * s = yield->s + start;
+       int i = yield->ptr - start;
+       BOOL tainted = is_tainted(s);
+
+       DEBUG(D_noutf8)
+         {
+         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);
+           }
+         }
+       }
        continue;
        }
     }
@@ -8183,13 +8209,13 @@ NOT_ITEM: ;
     gstring * g = NULL;
 
     if (!yield)
-      g = store_get(sizeof(gstring), FALSE);
+      g = store_get(sizeof(gstring), GET_UNTAINTED);
     else if (yield->ptr == 0)
       {
       if (resetok) reset_point = store_reset(reset_point);
       yield = NULL;
       reset_point = store_mark();
-      g = store_get(sizeof(gstring), FALSE);   /* alloc _before_ calling find_variable() */
+      g = store_get(sizeof(gstring), GET_UNTAINTED);   /* alloc _before_ calling find_variable() */
       }
     if (!(value = find_variable(name, FALSE, skipping, &newsize)))
       {
@@ -8255,8 +8281,10 @@ DEBUG(D_expand)
     debug_printf_indent("%sresult: %s\n",
       skipping ? "|-----" : "\\_____", yield->s);
     if (tainted)
-      debug_printf_indent("%s     \\__(tainted)\n",
-       skipping ? "|     " : "      ");
+      {
+      debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
+      debug_print_taint(yield->s);
+      }
     if (skipping)
       debug_printf_indent("\\___skipping: result is not used\n");
     }
@@ -8270,9 +8298,12 @@ DEBUG(D_expand)
       skipping ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
       yield->s);
     if (tainted)
-      debug_printf_indent("%s(tainted)\n",
+      {
+      debug_printf_indent("%s",
        skipping
        ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
+      debug_print_taint(yield->s);
+      }
     if (skipping)
       debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
        "skipping: result is not used\n");