Merge branch 'exim-4.96+security' into master+security
authorHeiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Sun, 15 Oct 2023 17:53:25 +0000 (19:53 +0200)
committerHeiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Sun, 15 Oct 2023 17:53:25 +0000 (19:53 +0200)
* exim-4.96+security:
  docs: Changelog
  Harden dnsdb against crafted DNS responses.  Bug 3033
  SPF: harden against crafted DNS responses
  fix: string_is_ip_address (CVE-2023-42117) Bug 3031
  Testsuite: Add testcases for string_is_ip_address (CVE-2023-42117)

1  2 
doc/doc-txt/ChangeLog
src/src/dns.c
src/src/expand.c
src/src/functions.h
src/src/lookups/dnsdb.c
src/src/string.c
test/scripts/0000-Basic/0002
test/stdout/0002

diff --combined doc/doc-txt/ChangeLog
index a78ec386fc59047195869947c4a0d62d022a0bac,fb70203ed6f7e914cf500233aca4b53a80e52faa..4306cabc0c7693313451ba43bc062c0ae6599e88
@@@ -2,214 -2,41 +2,222 @@@ This document describes *changes* to pr
  affect Exim's operation, with an unchanged configuration file.  For new
  options, and new features, see the NewStuff file next to this ChangeLog.
  
 -Exim version 4.96.2
 --------------------
 -
 -JH/01 Bug 3033: Harden dnsdb lookups against crafted DNS responses.
 -      CVE-2023-42219
 +Exim version 4.97
 +-----------------
  
 -HS/01 Fix string_is_ip_address() CVE-2023-42117 (Bug 3031)
 +JH/01 The hosts_connection_nolog main option now also controls "no MAIL in
 +      SMTP connection" log lines.
  
 +JH/02 Option default value updates:
 +      - queue_fast_ramp (main)        true (was false)
 +      - remote_max_parallel (main)    4 (was 2)
  
 -Exim version 4.96.1
 --------------------
 +JH/03 Cache static regex pattern compilations, for use by ACLs.
  
 -This is a security release.
 +JH/04 Bug 2903: avoid exit on an attempt to rewrite a malformed address.
 +      Make the rewrite never match and keep the logging.  Trust the
 +      admin to be using verify=header-syntax (to actually reject the message).
  
 -JH/01 Bug 2999: Fix a possible OOB write in the external authenticator, which
 +JH/05 Follow symlinks for placing a watch on TLS creds files.  This means
 +      (under Linux) we watch the dir containing the final file; previously
 +      it would be the dir with the first symlink.  We still do not monitor
 +      the entire path.
 +
 +JH/06 Check for bad chars in rDNS for sender_host_name.  The OpenBSD (at least)
 +      dn_expand() is happy to pass them through.
 +
 +JH/07 OpenSSL Fix auto-reload of changed server OCSP proof.  Previously, if
 +      the file with the proof had an unchanged name, the new proof(s) were
 +      loaded on top of the old ones (and nover used; the old ones were stapled).
 +
 +JH/08 Bug 2915: Fix use-after-free for $regex<n> variables. Previously when
 +      more than one message arrived in a single connection a reference from
 +      the earlier message could be re-used.  Often a sigsegv resulted.
 +      These variables were introduced in Exim 4.87.
 +      Debug help from Graeme Fowler.
 +
 +JH/09 Fix ${filter } for conditions that modify $value.  Previously the
 +      modified version would be used in construction the result, and a memory
 +      error would occur.
 +
 +JH/10 GnuTLS: fix for (IOT?) clients offering no TLS extensions at all.
 +      Find and fix by Jasen Betts.
 +
 +JH/11 OpenSSL: fix for ancient clients needing TLS support for versions earlier
 +      than TLSv1,2,  Previously, more-recent versions of OpenSSL were permitting
 +      the systemwide configuration to override the Exim config.
 +
 +HS/01 Bug 2728: Introduce EDITME option "DMARC_API" to work around incompatible
 +      API changes in libopendmarc.
 +
 +JH/12 Bug 2930: Fix daemon startup.  When started from any process apart from
 +      pid 1, in the normal "background daemon" mode, having to drop process-
 +      group leadership also lost track of needing to create listener sockets.
 +
 +JH/13 Bug 2929: Fix using $recipients after ${run...}.  A change made for 4.96
 +      resulted in the variable appearing empty.  Find and fix by Ruben Jenster.
 +
 +JH/14 Bug 2933: Fix regex substring match variables for null matches. Since 4.96
 +      a capture group which obtained no text (eg. "(abc)*" matching zero
 +      occurrences) could cause a segfault if the corresponding $<n> was
 +      expanded.
 +
 +JH/15 Fix argument parsing for ${run } expansion. Previously, when an argument
 +      included a close-brace character (eg. it itself used an expansion) an
 +      error occurred.
 +
 +JH/16 Move running the smtp connect ACL to before, for TLS-on-connect ports,
 +      starting TLS.  Previously it was after, meaning that attackers on such
 +      ports had to be screened using the host_reject_connection main config
 +      option. The new sequence aligns better with the STARTTLS behaviour, and
 +      permits defences against crypto-processing load attacks, even though it
 +      is strictly an incompatible change.
 +      Also, avoid sending any SMTP fail response for either the connect ACL
 +      or host_reject_connection, for TLS-on-connect ports.
 +
 +JH/17 Permit the ACL "encrypted" condition to be used in a HELO/EHLO ACL,
 +      Previously this was not permitted, but it makes reasonable sense.
 +      While there, restore a restriction on using it from a connect ACL; given
 +      the change JH/16 it could only return false (and before 4.91 was not
 +      permitted).
 +
 +JH/18 Fix a fencepost error in logging.  Previously (since 4.92) when a log line
 +      was exactly sized compared to the log buffer, a crash occurred with the
 +      misleading message "bad memory reference; pool not found".
 +      Found and traced by Jasen Betts.
 +
 +JH/19 Bug 2911: Fix a recursion in DNS lookups.  Previously, if the main option
 +      dns_again_means_nonexist included an element causing a DNS lookup which
 +      itself returned DNS_AGAIN, unbounded recursion occurred.  Possible results
 +      included (though probably not limited to) a process crash from stack
 +      memory limit, or from excessive open files.  Replace this with a paniclog
 +      whine (as this is likely a configuration error), and returning
 +      DNS_NOMATCH.
 +
 +JH/20 Bug 2954: (OpenSSL) Fix setting of explicit EC curve/group.  Previously
 +      this always failed, probably leading to the usual downgrade to in-clear
 +      connections.
 +
 +JH/21 Fix TLSA lookups.  Previously dns_again_means_nonexist would affect
 +      SERVFAIL results, which breaks the downgrade resistance of DANE.  Change
 +      to not checking that list for these lookups.
 +
 +JH/22 Bug 2434: Add connection-elapsed "D=" element to more connection
 +      closure log lines.
 +
 +JH/23 Fix crash in string expansions. Previously, if an empty variable was
 +      immediately followed by an expansion operator, a null-indirection read
 +      was done, killing the process.
 +
 +JH/24 Bug 2997: When built with EXPERIMENTAL_DSN_INFO, bounce messages can
 +      include an SMTP response string which is longer than that supported
 +      by the delivering transport.  Alleviate by wrapping such lines before
 +      column 80.
 +
 +JH/25 Bug 2827: Restrict size of References: header in bounce messages to 998
 +      chars (RFC limit).  Previously a limit of 12 items was made, which with
 +      a not-impossible References: in the message being bounced could still
 +      be over-large and get stopped in the transport.
 +
 +JH/26 For a ${readsocket } in TLS mode, send a TLS Close Alert before the TCP
 +      close.  Previously a bare socket close was done.
 +
 +JH/27 Fix ${srs_encode ..}.  Previously it would give a bad result for one day
 +      every 1024 days.
 +
 +JH/28 Bug 2996: Fix a crash in the smtp transport.  When finding that the
 +      message being considered for delivery was already being handled by
 +      another process, and having an SMTP connection already open, the function
 +      to close it tried to use an uninitialized variable.  This would afftect
 +      high-volume sites more, especially when running mailing-list-style loads.
 +      Pollution of logs was the major effect, as the other process delivered
 +      the message.  Found and partly investigated by Graeme Fowler.
 +
 +JH/29 Change format of the internal ID used for message identification. The old
 +      version only supported 31 bits for a PID element; the new 64 (on systems
 +      which can use Base-62 encoding, which is all currently supported ones
 +      but not Darwin (MacOS) or Cygwin, which have case-insensitive filesystems
 +      and must use Base-36).  The new ID is 23 characters rather than 16, and is
 +      visible in various places - notably logs, message headers, and spool file
 +      names.  Various of the ancillary utilities also have to know the format.
 +      As well as the expanded PID portion, the sub-second part of the time
 +      recorded in the ID is expanded to support finer precision.  Theoretically
 +      this permits a receive rate from a single comms channel of better than the
 +      previous 2000/sec.
 +        The major timestamp part of the ID is not changed; at 6 characters it is
 +      usable until about year 3700.
 +        Updating from previously releases is fully supported: old-format spool
 +      files are still usable, and the utilities support both formats.  New
 +      message will use the new format.  The one hints-DB file type which uses
 +      message-IDs (the transport wait- DB) will be discarded if an old-format ID
 +      is seen; new ones will be built with only new-format IDs.
 +      Optionally, a utility can be used to convert spool files from old to new,
 +      but this is only an efficiency measure not a requirement for operation
 +        Downgrading from new to old requires running a provided utility, having
 +      first stopped all operations.  This will convert any spool files from new
 +      back to old (losing time-precision and PID information) and remove any
 +      wait- hints databases.
 +
 +JH/30 Bug 3006: Fix handling of JSON strings having embedded commas. Previously
 +      we treated them as item separators when parsing for a list item, but they
 +      need to be protected by the doublequotes.  While there, add handling for
 +      backslashes.
 +
 +JH/31 Bug 2998: Fix ${utf8clean:...} to disallow UTF-16 surrogate codepoints.
 +      Found and fixed by Jasen Betts. No testcase for this as my usual text
 +      editor insists on emitting only valid UTF-8.
 +
 +JH/32 Fix "tls_dhparam = none" under GnuTLS.  At least with 3.7.9 this gave
 +      a null-indirection SIGSEGV for the receive process.
 +
 +JH/33 Fix free for live variable $value created by a ${run ...} expansion during
 +      -bh use.  Internal checking would spot this and take a panic.
 +
 +JH/34 Bug 3013: Fix use of $recipients within arguments for ${run...}.
 +      In 4.96 this would expand to empty.
 +
 +JH/35 Bug 3014: GnuTLS: fix expiry date for an auto-generated server
 +      certificate.  Find and fix by Andreas Metzler.
 +
 +JH/36 Add ARC info to DMARC hostory records.
 +
 +JH/37 Bug 3016: Avoid sending DSN when message was accepted under fakereject
 +      or fakedefer.  Previously the sender could discover that the message
 +      had in fact been accepted.
 +
 +JH/38 Taint-track intermediate values from the peer in multi-stage authentation
 +      sequences.  Previously the input was not noted as being tainted; notably
 +      this resulted in behaviour of LOGIN vs. PLAIN being inconsistent under
 +      bad coding of authenticators.
 +
 +JH/39 Bug 3023: Fix crash induced by some combinations of zero-length strings
 +      and ${tr...}.  Found and diagnosed by Heiko Schlichting.
 +
 +JH/40 Bug 2999: Fix a possible OOB write in the external authenticator, which
        could be triggered by externally-supplied input.  Found by Trend Micro.
        CVE-2023-42115
  
 -JH/02 Bug 3000: Fix a possible OOB write in the SPA authenticator, which could
 +JH/41 Bug 3000: Fix a possible OOB write in the SPA authenticator, which could
        be triggered by externally-controlled input.  Found by Trend Micro.
        CVE-2023-42116
  
 -JH/03 Bug 3001: Fix a possible OOB read in the SPA authenticator, which could
 +JH/42 Bug 3001: Fix a possible OOB read in the SPA authenticator, which could
        be triggered by externally-controlled input.  Found by Trend Micro.
        CVE-2023-42114
  
 -JH/04 Bug 2903: avoid exit on an attempt to rewrite a malformed address.
++JH/43 Bug 2903: avoid exit on an attempt to rewrite a malformed address.
+       Make the rewrite never match and keep the logging.  Trust the
+       admin to be using verify=header-syntax (to actually reject the message).
++JH/44 Bug 3033: Harden dnsdb lookups against crafted DNS responses.
++      CVE-2023-42219
++
++HS/02 Fix string_is_ip_address() CVE-2023-42117 (Bug 3031)
  
  Exim version 4.96
  -----------------
  
 -JH/01 Move the wait-for-next-tick (needed for unique messmage IDs) from
 +JH/01 Move the wait-for-next-tick (needed for unique message IDs) from
        after reception to before a subsequent reception.  This should
        mean slightly faster delivery, and also confirmation of reception
        to senders.
diff --combined src/src/dns.c
index d39b4b5904e469c2985b0746d39b05835d8b2c85,8dc3695a118782926b1ec994997f2dfc420a5acb..db566f2e86b54a0840d9a229e3aca3a262994beb
@@@ -5,7 -5,6 +5,7 @@@
  /* Copyright (c) The Exim Maintainers 2020 - 2022 */
  /* 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 interfacing with the DNS. */
  
@@@ -305,7 -304,7 +305,7 @@@ Return: TRUE for a bad resul
  static BOOL
  dnss_inc_aptr(const dns_answer * dnsa, dns_scan * dnss, unsigned delta)
  {
- return (dnss->aptr += delta) >= dnsa->answer + dnsa->answerlen;
+ return (dnss->aptr += delta) > dnsa->answer + dnsa->answerlen;
  }
  
  /*************************************************
@@@ -389,7 -388,7 +389,7 @@@ if (reset != RESET_NEXT
        TRACE trace = "A-hdr";
        if (dnss_inc_aptr(dnsa, dnss, namelen+8)) goto null_return;
        GETSHORT(dnss->srr.size, dnss->aptr); /* size of data portion */
-       /* skip over it */
+       /* skip over it, checking for a bogus size */
        TRACE trace = "A-skip";
        if (dnss_inc_aptr(dnsa, dnss, dnss->srr.size)) goto null_return;
        }
@@@ -429,10 -428,9 +429,9 @@@ GETLONG(dnss->srr.ttl, dnss->aptr);               /
  GETSHORT(dnss->srr.size, dnss->aptr);         /* Size of data portion */
  dnss->srr.data = dnss->aptr;                  /* The record's data follows */
  
- /* Unchecked increment ok here since no further access on this iteration;
- will be checked on next at "R-name". */
- dnss->aptr += dnss->srr.size;                 /* Advance to next RR */
+ /* skip over it, checking for a bogus size */
+ if (dnss_inc_aptr(dnsa, dnss, dnss->srr.size))
+   goto null_return;
  
  /* Return a pointer to the dns_record structure within the dns_answer. This is
  for convenience so that the scans can use nice-looking for loops. */
@@@ -802,7 -800,6 +801,7 @@@ dns_basic_lookup(dns_answer * dnsa, con
  int rc;
  #ifndef STAND_ALONE
  const uschar * save_domain;
 +static BOOL try_again_recursion = FALSE;
  #endif
  
  /* DNS lookup failures of any kind are cached in a tree. This is mainly so that
@@@ -907,31 -904,11 +906,31 @@@ if (dnsa->answerlen < 0) switch (h_errn
  
      /* Cut this out for various test programs */
  #ifndef STAND_ALONE
 -    save_domain = deliver_domain;
 -    deliver_domain = string_copy(name);  /* set $domain */
 -    rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0,
 -      &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL);
 -    deliver_domain = save_domain;
 +    /* Permitting dns_again_means nonexist for TLSA lookups breaks the
 +    doewngrade resistance of dane, so avoid for those. */
 +
 +    if (type == T_TLSA)
 +      rc = FAIL;
 +    else
 +      {
 +      if (try_again_recursion)
 +      {
 +      log_write(0, LOG_MAIN|LOG_PANIC,
 +        "dns_again_means_nonexist recursion seen for %s"
 +        " (assuming nonexist)", name);
 +      return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type),
 +                            DNS_NOMATCH);
 +      }
 +
 +      try_again_recursion = TRUE;
 +      save_domain = deliver_domain;
 +      deliver_domain = string_copy(name);  /* set $domain */
 +      rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0,
 +      &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL);
 +      deliver_domain = save_domain;
 +      try_again_recursion = FALSE;
 +      }
 +
      if (rc != OK)
        {
        DEBUG(D_dns) debug_printf("returning DNS_AGAIN\n");
@@@ -1346,7 -1323,7 +1345,7 @@@ dns_pattern_init(void
  {
  if (check_dns_names_pattern[0] != 0 && !regex_check_dns_names)
    regex_check_dns_names =
 -    regex_must_compile(check_dns_names_pattern, FALSE, TRUE);
 +    regex_must_compile(check_dns_names_pattern, MCS_NOFLAGS, TRUE);
  }
  
  /* vi: aw ai sw=2
diff --combined src/src/expand.c
index 1d0ddec2a465ffc2a3a92d1f0df995fea121fb2a,4986e4657ac52ea9927a761174b391a3ee1e4160..40cc8d73a2afb4f1d37b72635e1844a2d846168f
@@@ -2,10 -2,9 +2,10 @@@
  *     Exim - an Internet mail transport agent    *
  *************************************************/
  
 -/* Copyright (c) The Exim Maintainers 2020 - 2022 */
 +/* Copyright (c) The Exim Maintainers 2020 - 2023 */
  /* 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 */
  
  #ifdef STAND_ALONE
  # ifndef SUPPORT_CRYPTEQ
  #  define SUPPORT_CRYPTEQ
  # endif
 -#endif
 +#endif        /*!STAND_ALONE*/
  
  #ifdef LOOKUP_LDAP
  # include "lookups/ldap.h"
@@@ -230,7 -224,6 +230,7 @@@ static uschar *op_table_main[] = 
    US"expand",
    US"h",
    US"hash",
 +  US"headerwrap",
    US"hex2b64",
    US"hexquote",
    US"ipv6denorm",
@@@ -278,7 -271,6 +278,7 @@@ enum 
    EOP_EXPAND,
    EOP_H,
    EOP_HASH,
 +  EOP_HEADERWRAP,
    EOP_HEX2B64,
    EOP_HEXQUOTE,
    EOP_IPV6DENORM,
@@@ -473,9 -465,8 +473,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. */
@@@ -681,7 -672,7 +681,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 },
    { "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
    { "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 },
    { "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;
  
@@@ -938,10 -857,6 +938,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             *
@@@ -1280,7 -1195,7 +1280,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)
    {
@@@ -1669,13 -1584,12 +1669,13 @@@ Returns:        NULL if the header doe
  */
  
  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 */
@@@ -1738,9 -1652,8 +1738,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
@@@ -1748,12 -1661,12 +1748,12 @@@ charset translation fails. If decoding 
  
  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;
    }
  }
  
@@@ -1803,9 -1716,7 +1803,9 @@@ 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 comman in the elements, and uses comma-space as seperator -
 +so cannot be used as an exim list as-is. */
  
  static uschar *
  fn_recipients(void)
@@@ -1820,24 -1731,7 +1820,24 @@@ for (int i = 0; i < recipients_count; i
    s = recipients_list[i].address;
    g = string_append2_listele_n(g, US", ", s, Ustrlen(s));
    }
 -return g ? g->s : NULL;
 +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);
  }
  
  
@@@ -1854,7 -1748,9 +1854,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)
    {
    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
@@@ -1873,7 -1777,17 +1873,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; }
  
@@@ -1959,7 -1873,7 +1959,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)
    {
    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;
    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 */
  
@@@ -2059,8 -1971,7 +2059,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;
        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)
    case vtype_string_func:
      {
      stringptr_fn_t * fn = (stringptr_fn_t *) val;
 -    uschar* s = fn();
 +    uschar * s = fn();
      return s ? s : US"";
      }
  
@@@ -2203,33 -2114,27 +2203,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)
      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] == '{')
    return 1;
    }
  
 +if (textonly_p) *textonly_p = textonly_l;
  *sptr = s;
 -return 0;
 +return flags & ESI_SKIPPING ? -1 : 0;
  }
  
  
@@@ -2406,26 -2307,19 +2406,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;
@@@ -2629,11 -2523,11 +2629,11 @@@ Returns:   a pointer to the first chara
  */
  
  static const uschar *
 -eval_condition(const uschar *s, BOOL *resetok, BOOL *yield)
 +eval_condition(const uschar * s, BOOL * resetok, BOOL * yield)
  {
  BOOL testfor = TRUE;
  BOOL tempcond, combined_cond;
 -BOOL *subcondptr;
 +BOOL * subcondptr;
  BOOL sub2_honour_dollar = TRUE;
  BOOL is_forany, is_json, is_jsons;
  int rc, cond_type;
@@@ -2641,8 -2535,7 +2641,8 @@@ int_eximarith_t num[2]
  struct stat statbuf;
  uschar * opname;
  uschar name[256];
 -const uschar *sub[10];
 +const uschar * sub[10];
 +unsigned sub_textonly = 0;
  
  for (;;)
    if (Uskip_whitespace(&s) == '!') { testfor = !testfor; s++; } else break;
@@@ -2736,14 -2629,8 +2736,14 @@@ switch(cond_type = identify_operator(&s
  
    if (Uskip_whitespace(&s) != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */
  
 -  sub[0] = expand_string_internal(s+1, TRUE, &s, yield == NULL, TRUE, resetok);
 -  if (!sub[0]) return NULL;
 +   {
 +    BOOL textonly;
 +    sub[0] = expand_string_internal(s+1,
 +      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yield ? ESI_NOFLAGS : ESI_SKIPPING),
 +      &s, resetok, &textonly);
 +    if (!sub[0]) return NULL;
 +    if (textonly) sub_textonly |= BIT(0);
 +   }
    /* {-for-text-editors */
    if (*s++ != '}') goto COND_FAILED_CURLY_END;
  
      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 */
      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";
      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";
  
    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) != '{')
        {
          "after \"%s\"", opname);
        return NULL;
        }
 -    if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL,
 -        honour_dollar, resetok)))
 +    if (!(sub[i] = expand_string_internal(s+1, flags, &s, resetok, &textonly)))
        return NULL;
 +    if (textonly) sub_textonly |= BIT(i);
      DEBUG(D_expand) if (i == 1 && !sub2_honour_dollar && Ustrchr(sub[1], '$'))
        debug_printf_indent("WARNING: the second arg is NOT expanded,"
                        " for security reasons\n");
  
      case ECOND_MATCH:   /* Regular expression match */
        {
 -      const pcre2_code * re;
 -      PCRE2_SIZE offset;
 -      int err;
 -
 -      if (!(re = pcre2_compile((PCRE2_SPTR)sub[1], PCRE2_ZERO_TERMINATED,
 -                              PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
 -      {
 -      uschar errbuf[128];
 -      pcre2_get_error_message(err, errbuf, sizeof(errbuf));
 -      expand_string_message = string_sprintf("regular expression error in "
 -        "\"%s\": %s at offset %ld", sub[1], errbuf, (long)offset);
 +      const pcre2_code * re = regex_compile(sub[1],
 +                sub_textonly & BIT(1) ? MCS_CACHEABLE : MCS_NOFLAGS,
 +                &expand_string_message, pcre_gen_cmp_ctx);
 +      if (!re)
        return NULL;
 -      }
  
        tempcond = regex_match_and_setup(re, sub[0], 0, -1);
        break;
  
      Uskip_whitespace(&s);
      if (*s++ != '{') goto COND_FAILED_CURLY_START;    /* }-for-text-editors */
 -    if (!(sub[0] = expand_string_internal(s, TRUE, &s, yield == NULL, TRUE, resetok)))
 +    if (!(sub[0] = expand_string_internal(s,
 +      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yield ? ESI_NOFLAGS : ESI_SKIPPING),
 +      &s, resetok, NULL)))
        return NULL;
      /* {-for-text-editors */
      if (*s++ != '}') goto COND_FAILED_CURLY_END;
  
      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",
      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";
  
      /* 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;
      /* 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 */
 -
 -    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 */
 +      /* 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;
      }
@@@ -3739,8 -3636,7 +3747,8 @@@ expanded, to check their syntax, but "s
  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
@@@ -3756,7 -3652,7 +3764,7 @@@ Returns:         0 OK; lookup_value ha
  */
  
  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;
@@@ -3764,8 -3660,6 +3772,8 @@@ const uschar *s = *sptr;    /* Local va
  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
@@@ -3775,12 -3669,12 +3783,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;
      }
  
  if (*s++ != '{')
    {
 -  errwhere = US"'yes' part did not start with '{'";
 +  errwhere = US"'yes' part did not start with '{'";           /*}}*/
    goto FAILED_CURLY;
    }
  
  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 '}'";
@@@ -3830,16 -3721,14 +3838,16 @@@ time, forced failures are noticed only 
  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;
      }
  
    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
@@@ -3861,9 -3750,9 +3869,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";
  
  /* 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 '}'";
@@@ -3983,9 -3872,10 +3991,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);
@@@ -4563,17 -4453,15 +4571,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
  */
  
  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);
@@@ -4591,7 -4479,7 +4599,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;
@@@ -4614,11 -4502,11 +4622,11 @@@ while (*s
      DEBUG(D_noutf8)
        debug_printf_indent("%c%s: %s\n",
        first ? '/' : '|',
 -      skipping ? "---scanning" : "considering", s);
 +      flags & ESI_SKIPPING ? "---scanning" : "considering", s);
      else
        debug_printf_indent("%s%s: %s\n",
        first ? UTF8_DOWN_RIGHT : UTF8_VERT_RIGHT,
 -      skipping
 +      flags & ESI_SKIPPING
        ? UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ "scanning"
        : "considering",
        s);
        for (s = t; *s ; s++) if (*s == '\\' && s[1] == 'N') break;
  
        DEBUG(D_expand)
 -      debug_expansion_interim(US"protected", t, (int)(s - t), skipping);
 +      debug_expansion_interim(US"protected", t, (int)(s - t), !!(flags & ESI_SKIPPING));
        yield = string_catn(yield, t, s - t);
        if (*s) s += 2;
        }
    /* Anything other than $ is just copied verbatim, unless we are
    looking for a terminating } character. */
  
 -  if (ket_ends && *s == '}') break;
 +  if (flags & ESI_BRACE_ENDS && *s == '}') break;
  
 -  if (*s != '$' || !honour_dollar)
 +  if (*s != '$' || !(flags & ESI_HONOR_DOLLAR))
      {
      int i = 1;                                                                /*{*/
      for (const uschar * t = s+1;
        *t && *t != '$' && *t != '}' && *t != '\\'; t++) i++;
  
 -    DEBUG(D_expand) debug_expansion_interim(US"text", s, i, skipping);
 +    DEBUG(D_expand) debug_expansion_interim(US"text", s, i, !!(flags & ESI_SKIPPING));
  
      yield = string_catn(yield, s, i);
      s += i;
      continue;
      }
 +  textonly = FALSE;
  
    /* No { after the $ - must be a plain name or a number for string
    match variable. There has to be a fudge for variables that are the
  
      /* Variable */
  
 -    else if (!(value = find_variable(name, FALSE, skipping, &newsize)))
 +    else if (!(value = find_variable(name, FALSE, !!(flags & ESI_SKIPPING), &newsize)))
        {
        expand_string_message =
        string_sprintf("unknown variable name \"%s\"", name);
      reset in the middle of the buffer will make it inaccessible. */
  
      len = Ustrlen(value);
 +    DEBUG(D_expand) debug_expansion_interim(US"value", value, len, !!(flags & ESI_SKIPPING));
      if (!yield && newsize != 0)
        {
        yield = g;
      continue;
      }
  
 -  if (isdigit(*s))
 +  if (isdigit(*s))            /* A $<n> variable */
      {
      int n;
      s = read_cnumber(&n, s);
      if (n >= 0 && n <= expand_nmax)
 +      {
 +      DEBUG(D_expand) debug_expansion_interim(US"value", expand_nstring[n], expand_nlength[n], !!(flags & ESI_SKIPPING));
        yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
 +      }
      continue;
      }
  
        goto EXPAND_FAILED;
        }
      if (n >= 0 && n <= expand_nmax)
 +      {
 +      DEBUG(D_expand) debug_expansion_interim(US"value", expand_nstring[n], expand_nlength[n], !!(flags & ESI_SKIPPING));
        yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
 +      }
      continue;
      }
  
    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.
        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))
        {
        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;
  
        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);
        uschar * save_lookup_value = lookup_value;
  
        Uskip_whitespace(&s);
 -      if (!(next_s = eval_condition(s, &resetok, skipping ? NULL : &cond)))
 +      if (!(next_s = eval_condition(s, &resetok, flags & ESI_SKIPPING ? NULL : &cond)))
        goto EXPAND_FAILED;  /* message already set */
  
        DEBUG(D_expand)
        {
 -      debug_expansion_interim(US"condition", s, (int)(next_s - s), skipping);
 +      debug_expansion_interim(US"condition", s, (int)(next_s - s), !!(flags & ESI_SKIPPING));
        debug_expansion_interim(US"result",
 -        cond ? US"true" : US"false", cond ? 4 : 5, skipping);
 +        cond ? US"true" : US"false", cond ? 4 : 5, !!(flags & ESI_SKIPPING));
        }
  
        s = next_s;
        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 */
        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:
        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)))
  
        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++ != '}')
          {
        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++ != '}')
        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
          {
        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 */
        restore_expand_strings(save_expand_nmax, save_expand_nstring,
          save_expand_nlength);
  
 -      if (skipping) continue;
 +      if (flags & ESI_SKIPPING) continue;
        break;
        }
  
          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)
        {
        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)
        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))
          {
          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);
          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:
  
          p = prvs_hmac_sha1(prvscheck_address, sub_arg[0], prvscheck_keynum,
            daystamp);
 -
          if (!p)
            {
            expand_string_message = US"hmac-sha1 conversion failed";
            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:
             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;
        }
  
          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")))
        /* 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 */
        /* 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;
  
        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++ != '}')
          {                                                             /*{*/
        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
      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++ != '}')
        {
        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;
        /* 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;
                                                  (int)(t-s), s);
          goto EXPAND_FAILED;
          }
 -      }
        Uskip_whitespace(&s);
  
        if (*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);
        }
        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;
            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;
        /* 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;
        }
  
        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;
        }
  
        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;
        if (!ret)
        goto EXPAND_FAILED;
        yield = string_catn(yield, ret, len);
 -      if (skipping) continue;
        break;
        }
  
        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;
        {
        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
          {
        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
  
        /* 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;
        }
  
        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++ != '}')
            {
          {
        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++ != '}')
            {
        /* 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)
        {
        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 */
        restore_expand_strings(save_expand_nmax, save_expand_nstring,
          save_expand_nlength);
  
 -      if (skipping) continue;
 +      if (flags & ESI_SKIPPING) continue;
        break;
        }
  
          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++)
          {
          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++ != '}')
          {
          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";
        /* 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 */
        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;
        yield = string_catn(yield, sub[1], 1);
        }
        else yield = string_catn(yield, US" ", 1);
 -      if (skipping) continue;
        break;
        }
  
        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++ != '}')
          {
          "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++ != '}')
          {
        goto EXPAND_FAILED_CURLY;
        }
  
 -      if (skipping)
 +      if (flags & ESI_SKIPPING)
        lookup_value = NULL;
        else
        {
        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 */
  
        restore_expand_strings(save_expand_nmax, save_expand_nstring,
          save_expand_nlength);
 -      if (skipping) continue;
 +      if (flags & ESI_SKIPPING) continue;
        break;
        }
  #endif        /*DISABLE_TLS*/
        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++ != '}')
          {
          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++ != '}')
        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;
  
        /* 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 */
          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;
                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)
              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",
        /* Restore preserved $item */
  
        iterate_item = save_iterate_item;
 -      if (skipping) continue;
 +      if (flags & ESI_SKIPPING) continue;
        break;
        }
  
        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++ != '}')
          {
        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++ != '}')
          {
        }
  
        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);
                                                                        /*{{*/
          goto EXPAND_FAILED;
          }
  
 -      if (skipping) continue;
 +      if (flags & ESI_SKIPPING) continue;
  
        while ((srcitem = string_nextinlist(&srclist, &sep, NULL, 0)))
        {
  
        /* 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(
          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. */
  
        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++ != '}')
          {
        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;
        }
  
        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]))
        {
          {
          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);
  
        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 */
      } /* EITEM_* switch */
      /*NOTREACHED*/
  
 -  DEBUG(D_expand)
 -    if (yield && (start > 0 || *s))   /* only if not the sole expansion of the line */
 +  DEBUG(D_expand)             /* only if not the sole expansion of the line */
 +    if (yield && (expansion_start > 0 || *s))
        debug_expansion_interim(US"item-res",
 -                            yield->s + start, yield->ptr - start, skipping);
 +        yield->s + expansion_start, yield->ptr - expansion_start,
 +        !!(flags & ESI_SKIPPING));
    continue;
  
  NOT_ITEM: ;
  
      /* 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
        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 != '}')
            {                                           /*{*/
          /*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;
      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);
        {
        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;
        }
  
        {
        uschar *tt = sub;
        unsigned long int n = 0;
 -      while (*tt != 0)
 +      while (*tt)
          {
          uschar *t = Ustrchr(base62_chars, *tt++);
          if (!t)
  
        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 =
        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:
  
        case EOP_UTF8CLEAN:
          {
 -        int seq_len = 0, index = 0;
 -        int bytes_left = 0;
 -        long codepoint = -1;
 -        int complete;
 +        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 */
  
 -        if (!yield->s || !yield->ptr)
 +        if (!yield)
 +          yield = string_get_tainted(Ustrlen(sub), sub);
 +        else if (!yield->s || !yield->ptr)
 +          {
            yield->s = store_get(yield->size = Ustrlen(sub), sub);
 +          gstring_reset(yield);
 +          }
          else if (is_incompatible(yield->s, sub))
            gstring_rebuffer(yield, sub);
  
                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
                    {           /* finished; output utf-8 sequence */
                    yield = string_catn(yield, seq_buff, seq_len);
              }
            else        /* no bytes left: new sequence */
              {
 -            if(!(c & 0x80))   /* 1-byte sequence, US-ASCII, keep it */
 +            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 */
 +            if ((c & 0xe0) == 0xc0)           /* 2-byte sequence */
 +              if (c == 0xc0 || c == 0xc1)     /* 0xc0 and 0xc1 are illegal */
                  complete = -1;
                else
                  {
 -                  bytes_left = 1;
 -                  codepoint = c & 0x1f;
 +                bytes_left = 1;
 +                codepoint = c & 0x1f;
                  }
 -              }
 -            else if((c & 0xf0) == 0xe0)               /* 3-byte sequence */
 +            else if ((c & 0xf0) == 0xe0)              /* 3-byte sequence */
                {
                bytes_left = 2;
                codepoint = c & 0x0f;
                }
 -            else if((c & 0xf8) == 0xf0)               /* 4-byte sequence */
 +            else if ((c & 0xf8) == 0xf0)              /* 4-byte sequence */
                {
                bytes_left = 3;
                codepoint = c & 0x07;
            goto EXPAND_FAILED;
            }
          yield = string_cat(yield, s);
 -        DEBUG(D_expand) debug_printf_indent("yield: '%s'\n", yield->s);
 +        DEBUG(D_expand) debug_printf_indent("yield: '%Y'\n", yield);
          break;
          }
  
        case EOP_BASE64D:
          {
          uschar * s;
 -        int len = b64decode(sub, &s);
 +        int len = b64decode(sub, &s, sub);
          if (len < 0)
            {
            expand_string_message = string_sprintf("string \"%s\" is not "
  
         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("|-----op-res: %.*s\n", i, s);
          if (tainted)
            {
 -          debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
 -          debug_print_taint(yield->s);
 +          debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
 +          debug_print_taint(res);
            }
          }
        else
          if (tainted)
            {
            debug_printf_indent("%s",
 -            skipping
 +            flags & ESI_SKIPPING
              ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
 -          debug_print_taint(yield->s);
 +          debug_print_taint(res);
            }
          }
        }
        reset_point = store_mark();
        g = store_get(sizeof(gstring), GET_UNTAINTED);  /* alloc _before_ calling find_variable() */
        }
 -    if (!(value = find_variable(name, FALSE, skipping, &newsize)))
 +    if (!(value = find_variable(name, FALSE, !!(flags & ESI_SKIPPING), &newsize)))
        {
        expand_string_message =
          string_sprintf("unknown variable in \"${%s}\"", name);
    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";
  added to the string. If so, set up an empty string. Add a terminating zero. If
  left != NULL, return a pointer to the terminator. */
  
 -if (!yield)
 -  yield = string_get(1);
 -(void) string_from_gstring(yield);
 -if (left) *left = s;
 + {
 +  uschar * res;
  
 -/* Any stacking store that was used above the final string is no longer needed.
 -In many cases the final string will be the first one that was got and so there
 -will be optimal store usage. */
 +  if (!yield)
 +    yield = string_get(1);
 +  res = string_from_gstring(yield);
 +  if (left) *left = s;
  
 -if (resetok) gstring_release_unused(yield);
 -else if (resetok_p) *resetok_p = FALSE;
 +  /* Any stacking store that was used above the final string is no longer needed.
 +  In many cases the final string will be the first one that was got and so there
 +  will be optimal store usage. */
  
 -DEBUG(D_expand)
 -  {
 -  BOOL tainted = is_tainted(yield->s);
 -  DEBUG(D_noutf8)
 +  if (resetok) gstring_release_unused(yield);
 +  else if (resetok_p) *resetok_p = FALSE;
 +
 +  DEBUG(D_expand)
      {
 -    debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
 -    debug_printf_indent("%sresult: %s\n",
 -      skipping ? "|-----" : "\\_____", yield->s);
 -    if (tainted)
 +    BOOL tainted = is_tainted(res);
 +    DEBUG(D_noutf8)
        {
 -      debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
 -      debug_print_taint(yield->s);
 +      debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
 +      debug_printf_indent("%sresult: %s\n",
 +      flags & ESI_SKIPPING ? "|-----" : "\\_____", res);
 +      if (tainted)
 +      {
 +      debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
 +      debug_print_taint(res);
 +      }
 +      if (flags & ESI_SKIPPING)
 +      debug_printf_indent("\\___skipping: result is not used\n");
        }
 -    if (skipping)
 -      debug_printf_indent("\\___skipping: result is not used\n");
 -    }
 -  else
 -    {
 -    debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
 -      "expanding: %.*s\n",
 -      (int)(s - string), string);
 -    debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
 -      "result: %s\n",
 -      skipping ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
 -      yield->s);
 -    if (tainted)
 +    else
        {
 -      debug_printf_indent("%s",
 -      skipping
 -      ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
 -      debug_print_taint(yield->s);
 +      debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
 +      "expanding: %.*s\n",
 +      (int)(s - string), string);
 +      debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
 +      "result: %s\n",
 +      flags & ESI_SKIPPING ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
 +      res);
 +      if (tainted)
 +      {
 +      debug_printf_indent("%s",
 +        flags & ESI_SKIPPING
 +        ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
 +      debug_print_taint(res);
 +      }
 +      if (flags & ESI_SKIPPING)
 +      debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
 +        "skipping: result is not used\n");
        }
 -    if (skipping)
 -      debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
 -      "skipping: result is not used\n");
      }
 -  }
 -expand_level--;
 -return yield->s;
 +  if (textonly_p) *textonly_p = textonly;
 +  expand_level--;
 +  return res;
 + }
  
  /* This is the failure exit: easiest to program with a goto. We still need
  to update the pointer to the terminator, for cases of nested calls with "fail".
@@@ -8536,20 -8371,16 +8544,20 @@@ return NULL
  }
  
  
 +
  /* This is the external function call. Do a quick check for any expansion
  metacharacters, and if there are none, just return the input string.
  
 -Argument: the string to be expanded
 +Arguments
 +      the string to be expanded
 +      optional pointer for return boolean indicating no-dynamic-expansions
 +
  Returns:  the expanded string, or NULL if expansion failed; if failure was
            due to a lookup deferring, search_find_defer will be TRUE
  */
  
  const uschar *
 -expand_cstring(const uschar * string)
 +expand_string_2(const uschar * string, BOOL * textonly_p)
  {
  if (Ustrpbrk(string, "$\\") != NULL)
    {
    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); }
 +
  
  
  
@@@ -8871,14 -8699,12 +8879,14 @@@ assert_variable_notin() treats as const
  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);
  
@@@ -9012,9 -8838,8 +9020,9 @@@ search_tidyup()
  return 0;
  }
  
 -#endif
 +#endif        /*STAND_ALONE*/
  
 +#endif        /*!MACRO_PREDEF*/
  /* vi: aw ai sw=2
  */
  /* End of expand.c */
diff --combined src/src/functions.h
index 4222c623a3f77e537fd21f2e2141390ec87780eb,3c8104d25795d65bdbb41cf9c2d3950b72be6959..8f85165e73bc5eb16b508c485bd1e1b16a27fbe2
@@@ -2,10 -2,9 +2,10 @@@
  *     Exim - an Internet mail transport agent    *
  *************************************************/
  
 -/* Copyright (c) The Exim Maintainers 2020 - 2022 */
 +/* Copyright (c) The Exim Maintainers 2020 - 2023 */
  /* 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 */
  
  
  /* Prototypes for functions that appear in various modules. Gathered together
@@@ -54,8 -53,6 +54,8 @@@ extern uschar * tls_cert_fprt_sha256(vo
  extern void    tls_clean_env(void);
  extern BOOL    tls_client_start(client_conn_ctx *, smtp_connect_args *,
                  void *, tls_support *, uschar **);
 +extern BOOL    tls_client_adjunct_start(host_item *, client_conn_ctx *,
 +                const uschar *, uschar **);
  extern void    tls_client_creds_reload(BOOL);
  
  extern void    tls_close(void *, int);
@@@ -103,15 -100,11 +103,15 @@@ extern acl_block *acl_read(uschar *(*)(
  extern int     acl_check(int, uschar *, uschar *, uschar **, uschar **);
  extern uschar *acl_current_verb(void);
  extern int     acl_eval(int, uschar *, uschar **, uschar **);
 +extern uschar *acl_standalone_setvar(const uschar *);
  
  extern tree_node *acl_var_create(uschar *);
  extern void    acl_var_write(uschar *, uschar *, void *);
  
  #ifdef EXPERIMENTAL_ARC
 +# ifdef SUPPORT_DMARC
 +extern gstring *arc_dmarc_hist_append(gstring *);
 +# endif
  extern void   *arc_ams_setup_sign_bodyhash(void);
  extern const uschar *arc_header_feed(gstring *, BOOL);
  extern gstring *arc_sign(const uschar *, gstring *, uschar **);
@@@ -157,7 -150,7 +157,7 @@@ extern gstring *authres_spf(gstring *)
  
  extern uschar *b64encode(const uschar *, int);
  extern uschar *b64encode_taint(const uschar *, int, const void *);
 -extern int     b64decode(const uschar *, uschar **);
 +extern int     b64decode(const uschar *, uschar **, const void *);
  extern int     bdat_getc(unsigned);
  extern uschar *bdat_getbuf(unsigned *);
  extern BOOL    bdat_hasc(void);
@@@ -189,10 -182,6 +189,10 @@@ extern BOOL    cutthrough_predata(void)
  extern void    release_cutthrough_connection(const uschar *);
  
  extern void    daemon_go(void);
 +#ifndef COMPILE_UTILITY
 +extern ssize_t daemon_client_sockname(struct sockaddr_un *, uschar **);
 +extern ssize_t daemon_notifier_sockname(struct sockaddr_un *);
 +#endif
  
  #ifdef EXPERIMENTAL_DCC
  extern int     dcc_process(uschar **);
@@@ -271,7 -260,6 +271,7 @@@ extern int     exp_bool(address_item *a
  extern BOOL    expand_check_condition(uschar *, uschar *, uschar *);
  extern uschar *expand_file_big_buffer(const uschar *);
  extern uschar *expand_string(uschar *);       /* public, cannot make const */
 +extern const uschar *expand_string_2(const uschar *, BOOL *);
  extern const uschar *expand_cstring(const uschar *); /* ... so use this one */
  extern uschar *expand_getkeyed(const uschar *, const uschar *);
  
@@@ -344,7 -332,7 +344,7 @@@ extern BOOL    macro_read_assignment(us
  extern uschar *macros_expand(int, int *, BOOL *);
  extern void    mainlog_close(void);
  #ifdef WITH_CONTENT_SCAN
 -extern int     malware(const uschar *, int);
 +extern int     malware(const uschar *, BOOL, int);
  extern int     malware_in_file(uschar *);
  extern void    malware_init(void);
  extern gstring * malware_show_supported(gstring *);
@@@ -357,7 -345,7 +357,7 @@@ extern int     match_check_list(const u
                   const uschar *, const uschar **);
  extern int     match_isinlist(const uschar *, const uschar **, int, tree_node **,
                   unsigned int *, int, BOOL, const uschar **);
 -extern int     match_check_string(const uschar *, const uschar *, int, BOOL, BOOL, BOOL,
 +extern int     match_check_string(const uschar *, const uschar *, int, mcs_flags,
                   const uschar **);
  
  extern void    message_start(void);
@@@ -372,7 -360,7 +372,7 @@@ extern int     mime_acl_check(uschar *a
                   struct mime_boundary_context *, uschar **, uschar **);
  extern int     mime_decode(const uschar **);
  extern ssize_t mime_decode_base64(FILE *, FILE *, uschar *);
 -extern int     mime_regex(const uschar **);
 +extern int     mime_regex(const uschar **, BOOL);
  extern void    mime_set_anomaly(int);
  #endif
  extern uschar *moan_check_errorcopy(uschar *);
@@@ -403,9 -391,9 +403,9 @@@ extern const uschar *parse_quote_2047(c
  extern const uschar *parse_date_time(const uschar *str, time_t *t);
  extern void priv_drop_temp(const uid_t, const gid_t);
  extern void priv_restore(void);
 -extern int     vaguely_random_number(int);
 -#ifndef DISABLE_TLS
 -extern int     vaguely_random_number_fallback(int);
 +#ifdef SUPPORT_PROXY
 +extern BOOL   proxy_protocol_host(void);
 +extern void   proxy_protocol_setup(void);
  #endif
  
  extern BOOL    queue_action(uschar *, int, uschar **, int, int);
@@@ -416,7 -404,7 +416,7 @@@ extern void    queue_list(int, uschar *
  #ifndef DISABLE_QUEUE_RAMP
  extern void    queue_notify_daemon(const uschar * hostname);
  #endif
 -extern void    queue_run(uschar *, uschar *, BOOL);
 +extern void    queue_run(qrunner *, uschar *, uschar *, BOOL);
  
  extern int     random_number(int);
  extern const uschar *rc_to_string(int);
@@@ -432,7 -420,7 +432,7 @@@ extern void    readconf_main(BOOL)
  extern void    readconf_options_from_list(optionlist *, unsigned, const uschar *, uschar *);
  extern BOOL    readconf_print(const uschar *, uschar *, BOOL);
  extern uschar *readconf_printtime(int);
 -extern uschar *readconf_readname(uschar *, int, uschar *);
 +extern const uschar *readconf_readname(uschar *, int, const uschar *);
  extern int     readconf_readtime(const uschar *, int, BOOL);
  extern void    readconf_rest(void);
  extern uschar *readconf_retry_error(const uschar *, const uschar *, int *, int *);
@@@ -445,15 -433,11 +445,15 @@@ extern BOOL    receive_msg(BOOL)
  extern int_eximarith_t receive_statvfs(BOOL, int *);
  extern void    receive_swallow_smtp(void);
  #ifdef WITH_CONTENT_SCAN
 -extern int     regex(const uschar **);
 +extern int     regex(const uschar **, BOOL);
 +extern void    regex_vars_clear(void);
  #endif
 +extern void    regex_at_daemon(const uschar *);
  extern BOOL    regex_match(const pcre2_code *, const uschar *, int, uschar **);
  extern BOOL    regex_match_and_setup(const pcre2_code *, const uschar *, int, int);
 -extern const pcre2_code *regex_must_compile(const uschar *, BOOL, BOOL);
 +extern const pcre2_code *regex_compile(const uschar *, mcs_flags, uschar **,
 +              pcre2_compile_context *);
 +extern const pcre2_code *regex_must_compile(const uschar *, mcs_flags, BOOL);
  extern void    retry_add_item(address_item *, uschar *, int);
  extern BOOL    retry_check_address(const uschar *, host_item *, uschar *, BOOL,
                   uschar **, uschar **);
@@@ -494,7 -478,6 +494,7 @@@ extern int     search_findtype_partial(
                   int *, const uschar **);
  extern void   *search_open(const uschar *, int, int, uid_t *, gid_t *);
  extern void    search_tidyup(void);
 +extern uschar *sender_helo_verified_boolstr(void);
  extern void    set_process_info(const char *, ...) PRINTF_FUNCTION(1,2);
  extern void    sha1_end(hctx *, const uschar *, int, uschar *);
  extern void    sha1_mid(hctx *, const uschar *);
@@@ -503,7 -486,6 +503,7 @@@ extern int     sieve_interpret(const us
                 const uschar *, const uschar *, const uschar *,
                 address_item **, uschar **);
  extern void    sigalrm_handler(int);
 +extern void    single_queue_run(qrunner *, uschar *, uschar *);
  extern int     smtp_boundsock(smtp_connect_args *);
  extern void    smtp_closedown(uschar *);
  extern void    smtp_command_timeout_exit(void) NORETURN;
@@@ -556,7 -538,6 +556,7 @@@ extern int     stdin_ferror(void)
  extern BOOL    stdin_hasc(void);
  extern int     stdin_ungetc(int);
  
 +extern void    stackdump(void);
  extern void    store_exit(void);
  extern void    store_init(void);
  extern void    store_writeprotect(int);
@@@ -565,8 -546,7 +565,8 @@@ extern gstring *string_append(gstring *
  extern gstring *string_append_listele(gstring *, uschar, const uschar *) WARN_UNUSED_RESULT;
  extern gstring *string_append_listele_n(gstring *, uschar, const uschar *, unsigned) WARN_UNUSED_RESULT;
  extern gstring *string_append2_listele_n(gstring *, const uschar *, const uschar *, unsigned) WARN_UNUSED_RESULT;
 -extern uschar *string_base62(unsigned long int);
 +extern uschar *string_base62_32(unsigned long int);
 +extern uschar *string_base62_64(unsigned long int);
  extern gstring *string_cat (gstring *, const uschar *     ) WARN_UNUSED_RESULT;
  extern gstring *string_catn(gstring *, const uschar *, int) WARN_UNUSED_RESULT;
  extern int     string_compare_by_pointer(const void *, const void *);
@@@ -576,6 -556,7 +576,7 @@@ extern uschar *string_dequote(const usc
  extern uschar *string_format_size(int, uschar *);
  extern int     string_interpret_escape(const uschar **);
  extern int     string_is_ip_address(const uschar *, int *);
+ extern int     string_is_ip_addressX(const uschar *, int *, const uschar **);
  #ifdef SUPPORT_I18N
  extern BOOL    string_is_utf8(const uschar *);
  #endif
@@@ -637,7 -618,7 +638,7 @@@ extern BOOL    transport_pass_socket(co
                        );
  extern uschar *transport_rcpt_address(address_item *, BOOL);
  extern BOOL    transport_set_up_command(const uschar ***, const uschar *,
 -               BOOL, int, address_item *, BOOL, const uschar *, uschar **);
 +               unsigned, int, address_item *, const uschar *, uschar **);
  extern void    transport_update_waiting(host_item *, uschar *);
  extern BOOL    transport_write_block(transport_ctx *, uschar *, int, BOOL);
  extern void    transport_write_reset(int);
@@@ -662,10 -643,6 +663,10 @@@ extern void    unspool_mbox(void)
  extern gstring *utf8_version_report(gstring *);
  #endif
  
 +extern int     vaguely_random_number(int);
 +#ifndef DISABLE_TLS
 +extern int     vaguely_random_number_fallback(int);
 +#endif
  extern int     verify_address(address_item *, FILE *, int, int, int, int,
                   uschar *, uschar *, BOOL *);
  extern int     verify_check_dnsbl(int, const uschar **, uschar **);
@@@ -688,12 -665,6 +689,12 @@@ extern void    version_init(void)
  
  extern BOOL    write_chunk(transport_ctx *, uschar *, int);
  extern ssize_t write_to_fd_buf(int, const uschar *, size_t);
 +extern uschar *wrap_header(const uschar *, unsigned, unsigned, const uschar *, unsigned);
 +
 +#ifdef EXPERIMENTAL_XCLIENT
 +extern uschar * xclient_smtp_command(uschar *, int *, BOOL *);
 +extern gstring * xclient_smtp_advertise_str(gstring *);
 +#endif
  
  
  /******************************************************************************/
@@@ -980,58 -951,12 +981,58 @@@ g->s[g->ptr] = '\0'
  return g->s;
  }
  
 +static inline int
 +len_string_from_gstring(gstring * g, uschar ** sp)
 +{
 +if (g)
 +  {
 +  *sp = g->s;
 +  g->s[g->ptr] = '\0';
 +  return g->ptr;
 +  }
 +else
 +  {
 +  *sp = NULL;
 +  return 0;
 +  }
 +}
 +
 +static inline uschar *
 +string_copy_from_gstring(gstring * g)
 +{
 +return g ? string_copyn(g->s, g->ptr) : NULL;
 +}
 +
  static inline unsigned
  gstring_length(const gstring * g)
  {
  return g ? (unsigned)g->ptr : 0;
  }
  
 +static inline uschar
 +gstring_last_char(gstring * g)
 +{
 +return g->s[g->ptr-1];
 +}
 +
 +static inline void
 +gstring_trim(gstring * g, unsigned amount)
 +{
 +g->ptr -= amount;
 +}
 +
 +static inline void
 +gstring_trim_trailing(gstring * g, uschar c)
 +{
 +if (gstring_last_char(g) == c) gstring_trim(g, 1);
 +}
 +
 +static inline void
 +gstring_reset(gstring * g)
 +{
 +g->ptr = 0;
 +}
 +
  
  #define gstring_release_unused(g) \
        gstring_release_unused_trc(g, __FUNCTION__, __LINE__)
@@@ -1077,13 -1002,6 +1078,13 @@@ memcpy(s, g->s, g->ptr)
  g->s = s;
  }
  
 +/* Append one gstring to another */
 +static inline gstring *
 +gstring_append(gstring * dest, gstring * item)
 +{
 +return string_catn(dest, item->s, item->ptr);
 +}
 +
  
  # ifndef COMPILE_UTILITY
  /******************************************************************************/
@@@ -1170,32 -1088,10 +1171,32 @@@ set_subdir_str(uschar * subdir_str, con
        int search_sequence)
  {
  subdir_str[0] = split_spool_directory == (search_sequence == 0)
 -       ? name[5] : '\0';
 +       ? name[MESSAGE_ID_TIME_LEN-1] : '\0';
  subdir_str[1] = '\0';
  }
  
 +/******************************************************************************/
 +/* Message-ID format transition knowlege */
 +
 +static inline BOOL
 +is_new_message_id(const uschar * id)
 +{
 +return id[MESSAGE_ID_TIME_LEN + 1 + MESSAGE_ID_PID_LEN] == '-';
 +}
 +
 +static inline BOOL
 +is_old_message_id(const uschar * id)
 +{
 +return id[MESSAGE_ID_TIME_LEN + 1 + MESSAGE_ID_PID_LEN_OLD] == '-';
 +}
 +
 +static inline unsigned
 +spool_data_start_offset(const uschar * id)
 +{
 +if (is_old_message_id(id)) return SPOOL_DATA_START_OFFSET_OLD;
 +return SPOOL_DATA_START_OFFSET;
 +}
 +
  /******************************************************************************/
  /* Time calculations */
  
@@@ -1326,7 -1222,6 +1327,7 @@@ pid_t pid
  DEBUG(D_any) debug_printf("%s forking for %s\n", process_purpose, purpose);
  if ((pid = fork()) == 0)
    {
 +  f.daemon_listen = FALSE;
    process_purpose = purpose;
    DEBUG(D_any) debug_printf("postfork: %s\n", purpose);
    }
@@@ -1399,30 -1294,6 +1400,30 @@@ debug_printf("cmdlog: '%s'\n", client_c
  
  
  
 +static inline int
 +expand_max_rcpt(const uschar * str_max_rcpt)
 +{
 +const uschar * s = expand_cstring(str_max_rcpt);
 +int res;
 +return !s || !*s || (res = Uatoi(s)) == 0 ? UNLIMITED_ADDRS : res;
 +}
 +
 +/******************************************************************************/
 +/* Queue-runner operations */
 +
 +static inline BOOL
 +is_onetime_qrun(void)
 +{
 +return qrunners && !qrunners->next && qrunners->interval == 0;
 +}
 +
 +static inline BOOL
 +is_multiple_qrun(void)
 +{
 +return qrunners && (qrunners->interval > 0 || qrunners->next);
 +}
 +
 +
  # endif       /* !COMPILE_UTILITY */
  
  /******************************************************************************/
  
  #endif  /* _FUNCTIONS_H_ */
  
 -/* vi: aw
 +/* vi: aw ai sw=2
  */
  /* End of functions.h */
diff --combined src/src/lookups/dnsdb.c
index 1563eda56d0a73b2172f8a4d8cd6b22ccdb845ee,020dc9a526ad87c68049402dbc9ad70e45ae2a36..35a9464470e983a7e71de5a4e87c4a63cc45b0ae
@@@ -5,7 -5,6 +5,7 @@@
  /* Copyright (c) The Exim Maintainers 2020 - 2022 */
  /* 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 */
  
  #include "../exim.h"
  #include "lf_functions.h"
@@@ -136,12 -135,15 +136,12 @@@ dnsdb_find(void * handle, const uschar 
  {
  int rc;
  int sep = 0;
 -int defer_mode = PASS;
 -int dnssec_mode = PASS;
 -int save_retrans = dns_retrans;
 -int save_retry =   dns_retry;
 +int defer_mode = PASS, dnssec_mode = PASS;
 +int save_retrans = dns_retrans, save_retry =   dns_retry;
  int type;
  int failrc = FAIL;
 -const uschar *outsep = CUS"\n";
 -const uschar *outsep2 = NULL;
 -uschar *equals, *domain, *found;
 +const uschar * outsep = CUS"\n", * outsep2 = NULL;
 +uschar * equals, * domain, * found;
  
  dns_answer * dnsa = store_get_dns_answer();
  dns_scan dnss;
@@@ -382,7 -384,10 +382,7 @@@ while ((domain = string_nextinlist(&key
        if (type == T_A || type == T_AAAA || type == T_ADDRESSES)
          {
          for (dns_address * da = dns_address_from_rr(dnsa, rr); da; da = da->next)
 -          {
 -          if (yield->ptr) yield = string_catn(yield, outsep, 1);
 -          yield = string_cat(yield, da->address);
 -          }
 +        yield = string_append_listele(yield, *outsep, da->address);
          continue;
          }
  
        if (type == T_TXT || type == T_SPF)
          {
          if (!outsep2)                 /* output only the first item of data */
-           yield = string_catn(yield, US (rr->data+1), (rr->data)[0]);
+         {
+         uschar n = (rr->data)[0];
+         /* size byte + data bytes must not excced the RRs length */
+         if (n + 1 <= rr->size)
+           yield = string_catn(yield, US (rr->data+1), n);
+         }
          else
 -          {
 -          /* output all items */
 -          int data_offset = 0;
 -          while (data_offset < rr->size)
 +          for (unsigned data_offset = 0; data_offset < rr->size; )
              {
              uschar chunk_len = (rr->data)[data_offset];
+           int remain = rr->size - data_offset;
+           /* Apparently there are resolvers that do not check RRs before passing
+           them on, and glibc fails to do so.  So every application must...
+           Check for chunk len exceeding RR */
+           if (chunk_len > remain)
+             chunk_len = remain;
              if (*outsep2  && data_offset != 0)
                yield = string_catn(yield, outsep2, 1);
-             yield = string_catn(yield, US ((rr->data) + ++data_offset), chunk_len);
+             yield = string_catn(yield, US ((rr->data) + ++data_offset), --chunk_len);
              data_offset += chunk_len;
              }
 -          }
          }
        else if (type == T_TLSA)
-         {
-         uint8_t usage, selector, matching_type;
-         uint16_t payload_length;
-         uschar s[MAX_TLSA_EXPANDED_SIZE];
-       uschar * sp = s;
-         uschar * p = US rr->data;
+       if (rr->size < 3)
+         continue;
+       else
+         {
+         uint8_t usage, selector, matching_type;
+         uint16_t payload_length;
+         uschar s[MAX_TLSA_EXPANDED_SIZE];
+         uschar * sp = s;
+         uschar * p = US rr->data;
+         usage = *p++;
+         selector = *p++;
+         matching_type = *p++;
+         /* What's left after removing the first 3 bytes above */
+         payload_length = rr->size - 3;
+         sp += sprintf(CS s, "%d%c%d%c%d%c", usage, *outsep2,
+                 selector, *outsep2, matching_type, *outsep2);
+         /* Now append the cert/identifier, one hex char at a time */
+         while (payload_length-- > 0 && sp-s < (MAX_TLSA_EXPANDED_SIZE - 4))
+           sp += sprintf(CS sp, "%02x", *p++);
  
-         usage = *p++;
-         selector = *p++;
-         matching_type = *p++;
-         /* What's left after removing the first 3 bytes above */
-         payload_length = rr->size - 3;
-         sp += sprintf(CS s, "%d%c%d%c%d%c", usage, *outsep2,
-               selector, *outsep2, matching_type, *outsep2);
-         /* Now append the cert/identifier, one hex char at a time */
-       while (payload_length-- > 0 && sp-s < (MAX_TLSA_EXPANDED_SIZE - 4))
-           sp += sprintf(CS sp, "%02x", *p++);
-         yield = string_cat(yield, s);
-         }
+         yield = string_cat(yield, s);
+         }
        else   /* T_CNAME, T_CSA, T_MX, T_MXH, T_NS, T_PTR, T_SOA, T_SRV */
          {
          int priority, weight, port;
        switch (type)
          {
          case T_MXH:
+           if (rr->size < sizeof(u_int16_t)) continue;
            /* mxh ignores the priority number and includes only the hostnames */
            GETSHORT(priority, p);
            break;
  
          case T_MX:
+           if (rr->size < sizeof(u_int16_t)) continue;
            GETSHORT(priority, p);
            sprintf(CS s, "%d%c", priority, *outsep2);
            yield = string_cat(yield, s);
            break;
  
          case T_SRV:
+           if (rr->size < 3*sizeof(u_int16_t)) continue;
            GETSHORT(priority, p);
            GETSHORT(weight, p);
            GETSHORT(port, p);
            break;
  
          case T_CSA:
+           if (rr->size < 3*sizeof(u_int16_t)) continue;
            /* See acl_verify_csa() for more comments about CSA. */
            GETSHORT(priority, p);
            GETSHORT(weight, p);
  
        if (type == T_SOA && outsep2 != NULL)
          {
-         unsigned long serial, refresh, retry, expire, minimum;
+         unsigned long serial = 0, refresh = 0, retry = 0, expire = 0, minimum = 0;
  
          p += rc;
          yield = string_catn(yield, outsep2, 1);
          else yield = string_cat(yield, s);
  
          p += rc;
-         GETLONG(serial, p); GETLONG(refresh, p);
-         GETLONG(retry,  p); GETLONG(expire,  p); GETLONG(minimum, p);
+         if (rr->size >= p - rr->data - 5*sizeof(u_int32_t))
+           {
+           GETLONG(serial, p); GETLONG(refresh, p);
+           GETLONG(retry,  p); GETLONG(expire,  p); GETLONG(minimum, p);
+           }
          sprintf(CS s, "%c%lu%c%lu%c%lu%c%lu%c%lu",
            *outsep2, serial, *outsep2, refresh,
            *outsep2, retry,  *outsep2, expire,  *outsep2, minimum);
diff --combined src/src/string.c
index 52b1d2fb5895ccda12006d34d0b38d71049bbd34,9aefc2b581114be5db73a06a0d029ca2e3f27512..055a37fd66f366af68c480e90db66ec99f9c80ab
@@@ -5,7 -5,6 +5,7 @@@
  /* Copyright (c) The Exim Maintainers 2020 - 2022 */
  /* 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 */
  
  /* Miscellaneous string-handling functions. Some are not required for
  utilities and tests, and are cut out by the COMPILE_UTILITY macro. */
@@@ -30,123 -29,133 +30,133 @@@ Arguments
    maskptr   NULL if no mask is permitted to follow
              otherwise, points to an int where the offset of '/' is placed
              if there is no / followed by trailing digits, *maskptr is set 0
+   errp      NULL if no diagnostic information is required, and if the netmask
+             length should not be checked. Otherwise it is set pointing to a short
+             descriptive text.
  
  Returns:    0 if the string is not a textual representation of an IP address
              4 if it is an IPv4 address
              6 if it is an IPv6 address
- */
  
+ The legacy string_is_ip_address() function follows below.
+ */
  int
- string_is_ip_address(const uschar *s, int *maskptr)
- {
- int yield = 4;
+ string_is_ip_addressX(const uschar *ip_addr, int *maskptr, const uschar **errp) {
+   struct addrinfo hints;
+   struct addrinfo *res;
+   uschar *slash, *percent;
  
- /* If an optional mask is permitted, check for it. If found, pass back the
- offset. */
+   uschar *endp = 0;
+   long int mask = 0;
+   const uschar *addr = 0;
  
- if (maskptr)
+   /* If there is a slash, but we didn't request a (optional) netmask,
+   we return failure, as we do if the mask isn't a pure numerical value,
+   or if it is negative. The actual length is checked later, once we know
+   the address family. */
+   if (slash = Ustrchr(ip_addr, '/'))
    {
-   const uschar *ss = s + Ustrlen(s);
-   *maskptr = 0;
-   if (s != ss && isdigit(*(--ss)))
+     if (!maskptr)
      {
-     while (ss > s && isdigit(ss[-1])) ss--;
-     if (ss > s && *(--ss) == '/') *maskptr = ss - s;
+       if (errp) *errp = "netmask found, but not requested";
+       return 0;
      }
-   }
- /* A colon anywhere in the string => IPv6 address */
- if (Ustrchr(s, ':') != NULL)
-   {
-   BOOL had_double_colon = FALSE;
-   BOOL v4end = FALSE;
-   yield = 6;
-   /* An IPv6 address must start with hex digit or double colon. A single
-   colon is invalid. */
-   if (*s == ':' && *(++s) != ':') return 0;
-   /* Now read up to 8 components consisting of up to 4 hex digits each. There
-   may be one and only one appearance of double colon, which implies any number
-   of binary zero bits. The number of preceding components is held in count. */
  
-   for (int count = 0; count < 8; count++)
+     uschar *rest;
+     mask = Ustrtol(slash+1, &rest, 10);
+     if (*rest || mask < 0)
      {
-     /* If the end of the string is reached before reading 8 components, the
-     address is valid provided a double colon has been read. This also applies
-     if we hit the / that introduces a mask or the % that introduces the
-     interface specifier (scope id) of a link-local address. */
-     if (*s == 0 || *s == '%' || *s == '/') return had_double_colon ? yield : 0;
-     /* If a component starts with an additional colon, we have hit a double
-     colon. This is permitted to appear once only, and counts as at least
-     one component. The final component may be of this form. */
-     if (*s == ':')
-       {
-       if (had_double_colon) return 0;
-       had_double_colon = TRUE;
-       s++;
-       continue;
-       }
-     /* If the remainder of the string contains a dot but no colons, we
-     can expect a trailing IPv4 address. This is valid if either there has
-     been no double-colon and this is the 7th component (with the IPv4 address
-     being the 7th & 8th components), OR if there has been a double-colon
-     and fewer than 6 components. */
-     if (Ustrchr(s, ':') == NULL && Ustrchr(s, '.') != NULL)
-       {
-       if ((!had_double_colon && count != 6) ||
-           (had_double_colon && count > 6)) return 0;
-       v4end = TRUE;
-       yield = 6;
-       break;
-       }
-     /* Check for at least one and not more than 4 hex digits for this
-     component. */
-     if (!isxdigit(*s++)) return 0;
-     if (isxdigit(*s) && isxdigit(*(++s)) && isxdigit(*(++s))) s++;
-     /* If the component is terminated by colon and there is more to
-     follow, skip over the colon. If there is no more to follow the address is
-     invalid. */
-     if (*s == ':' && *(++s) == 0) return 0;
+       if (errp) *errp = "netmask not numeric or <0";
+       return 0;
      }
  
-   /* If about to handle a trailing IPv4 address, drop through. Otherwise
-   all is well if we are at the end of the string or at the mask or at a percent
-   sign, which introduces the interface specifier (scope id) of a link local
-   address. */
+     *maskptr = slash - ip_addr;     /* offset of the slash */
+     endp = slash;
+   } else if (maskptr) *maskptr = 0; /* no slash found */
  
-   if (!v4end)
-     return (*s == 0 || *s == '%' ||
-            (*s == '/' && maskptr != NULL && *maskptr != 0))? yield : 0;
+   /* The interface-ID suffix (%<id>) is optional (for IPv6). If it
+   exists, we check it syntactically. Later, if we know the address
+   family is IPv4, we might reject it.
+   The interface-ID is mutually exclusive with the netmask, to the
+   best of my knowledge. */
+   if (percent = Ustrchr(ip_addr, '%'))
+   {
+     if (slash)
+     {
+       if (errp) *errp = "interface-ID and netmask are mutually exclusive";
+       return 0;
+     }
+     for (uschar *p = percent+1; *p; p++)
+         if (!isalnum(*p) && !ispunct(*p))
+         {
+           if (errp) *errp = "interface-ID must match [[:alnum:][:punct:]]";
+           return 0;
+         }
+     endp = percent;
    }
  
- /* Test for IPv4 address, which may be the tail-end of an IPv6 address. */
- for (int i = 0; i < 4; i++)
+   /* inet_pton() can't parse netmasks and interface IDs, so work on a shortened copy
+   allocated on the current stack */
+   if (endp) {
+     ptrdiff_t l = endp - ip_addr;
+     if (l > 255)
+     {
+       if (errp) *errp = "rudiculous long ip address string";
+       return 0;
+     }
+     addr = alloca(l+1); /* *BSD does not have strndupa() */
+     Ustrncpy((uschar *)addr, ip_addr, l);
+     ((uschar*)addr)[l] = '\0';
+   } else addr = ip_addr;
+   int af;
+   union { /* we do not need this, but inet_pton() needs a place for storage */
+     struct in_addr sa4;
+     struct in6_addr sa6;
+   } sa;
+   af = Ustrchr(addr, ':') ? AF_INET6 : AF_INET;
+   if (!inet_pton(af, addr, &sa))
    {
-   long n;
-   uschar * end;
-   if (i != 0 && *s++ != '.') return 0;
-   n = strtol(CCS s, CSS &end, 10);
-   if (n > 255 || n < 0 || end <= s || end > s+3) return 0;
-   s = end;
+     if (errp) *errp = af == AF_INET6 ? "IP address string not parsable as IPv6"
+                                      : "IP address string not parsable IPv4";
+     return 0;
    }
+   /* we do not check the values of the mask here, as
+   this is done on the callers side (but I don't understand why), so
+   actually I'd like to do it here, but it breaks at least 0002 */
+   switch (af)
+   {
+     case AF_INET6:
+         if (errp && mask > 128)
+         {
+           *errp = "IPv6 netmask value must not be >128";
+           return 0;
+         }
+         return 6;
+     case AF_INET:
+         if (percent)
+         {
+           if (errp) *errp = "IPv4 address string must not have an interface-ID";
+           return 0;
+         }
+         if (errp && mask > 32) {
+           *errp = "IPv4 netmask value must not be >32";
+           return 0;
+         }
+         return 4;
+     default:
+         if (errp) *errp = "unknown address family (should not happen)";
+         return 0;
+  }
+ }
  
- return !*s || (*s == '/' && maskptr && *maskptr != 0) ? yield : 0;
+ int
+ string_is_ip_address(const uschar *ip_addr, int *maskptr) {
+   return string_is_ip_addressX(ip_addr, maskptr, 0);
  }
  #endif  /* COMPILE_UTILITY */
  
  
@@@ -190,44 -199,26 +200,44 @@@ return buffer
  *************************************************/
  
  /* Convert a long integer into an ASCII base 62 string. For Cygwin the value of
 -BASE_62 is actually 36. Always return exactly 6 characters plus zero, in a
 -static area.
 +BASE_62 is actually 36. Always return exactly 6 characters plus a NUL, in a
 +static area.  This is enough for a 32b input, for 62  (for 64b we would want 11+nul);
 +but with 36 we lose half the input range of a 32b input.
  
  Argument: a long integer
  Returns:  pointer to base 62 string
  */
  
  uschar *
 -string_base62(unsigned long int value)
 +string_base62_32(unsigned long int value)
  {
  static uschar yield[7];
 -uschar *p = yield + sizeof(yield) - 1;
 +uschar * p = yield + sizeof(yield) - 1;
  *p = 0;
  while (p > yield)
    {
 -  *(--p) = base62_chars[value % BASE_62];
 +  *--p = base62_chars[value % BASE_62];
    value /= BASE_62;
    }
  return yield;
  }
 +
 +uschar *
 +string_base62_64(unsigned long int value)
 +{
 +static uschar yield[12];
 +uschar * p = yield + sizeof(yield) - 1;
 +*p = '\0';
 +while (p > yield)
 +  if (value)
 +    {
 +    *--p = base62_chars[value % BASE_62];
 +    value /= BASE_62;
 +    }
 +  else
 +    *--p = '0';
 +return yield;
 +}
  #endif  /* COMPILE_UTILITY */
  
  
@@@ -1327,11 -1318,6 +1337,11 @@@ If the "extend" flag is false, the stri
  will not be grown, and is usable in the original place after return.
  The return value can be NULL to signify overflow.
  
 +Field width:          decimal digits, or *
 +Precision:            dot, followed by decimal digits or *
 +Length modifiers:     h  L  l  ll  z
 +Conversion specifiers:        n d o u x X p f e E g G % c s S T Y D M
 +
  Returns the possibly-new (if copy for growth or taint-handling was needed)
  string, not nul-terminated.
  */
@@@ -1576,14 -1562,6 +1586,14 @@@ while (*fp
        slen = string_datestamp_length;
        goto INSERT_STRING;
  
 +    case 'Y':                 /* gstring pointer */
 +      {
 +      gstring * zg = va_arg(ap, gstring *);
 +      if (zg) { s = CS zg->s; slen = zg->ptr;    }
 +      else    { s = null;     slen = Ustrlen(s); }
 +      goto INSERT_GSTRING;
 +      }
 +
      case 's':
      case 'S':                   /* Forces *lower* case */
      case 'T':                   /* Forces *upper* case */
        if (!s) s = null;
        slen = Ustrlen(s);
  
 +    INSERT_GSTRING:           /* Coome to from %Y above */
 +
        if (!(flags & SVFMT_TAINT_NOCHK) && is_incompatible(g->s, s))
        if (flags & SVFMT_REBUFFER)
          {
@@@ -1816,7 -1792,7 +1826,7 @@@ while (fgets(CS buffer, sizeof(buffer)
    int llflag = 0;
    int n = 0;
    int count;
 -  int countset = 0;
 +  BOOL countset = FASE;
    uschar format[256];
    uschar outbuf[256];
    uschar *s;
      else if (Ustrcmp(ss, "*") == 0)
        {
        args[n++] = (void *)(&count);
 -      countset = 1;
 +      countset = TRUE;
        }
  
      else
index b4f2341bb5e21283a10acf092b3da98dca40d9ea,df4f91b4b881cf801d9c4a1e256ae605618bc3b7..c1fa1bdb514a48ad185d3485edae79f03bb6a8a3
@@@ -90,8 -90,6 +90,8 @@@ filter: ${filter{a:b:c}{!eq{$item}{b}}
  filter: ${filter{<' a'b'c}{!eq{$item}{b}}}
  filter: ${filter{<' ''a'b' ''c}{!eq{$item}{b}}}
  filter: "${filter{}{!eq{$item}{b}}}"
 +# check operation when the condition modifies the 'value' variable
 +${filter {E} {inlisti{$item}{ e }}}
  
  map: "${map{}{$item}}"
  map: ${map{a:b:c}{$item}}
@@@ -206,20 -204,6 +206,20 @@@ hex2b64:${hex2b64:1a2b3c4d5e6g
  hex2b64:${hex2b64:${md5:the quick brown fox}}
  hex2b64:${hex2b64:${sha1:the quick brown fox}}
  
 +headerwrap:${headerwrap:}
 +headerwrap:${headerwrap:a}
 +headerwrap:${headerwrap:ab}
 +headerwrap:${headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz}
 +headerwrap_79:${headerwrap_79:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz}
 +headerwrap:${headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab}
 +headerwrap:${headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab}
 +headerwrap:${headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz  Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab}
 +headerwrap:${headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbz}
 +headerwrap:${headerwrap:123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(100).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(200).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(300).678901234567890123456789012345678901234567890123456789012345678901234567890123456789(400).67890123456789012345678901234567890123456789012345678901234567890123456789012345\
 +67890123456789(500).678901234567890123456789012345678901234567890123456789012345678901234567890123456789(600).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(700).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(800).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(900).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(1000).789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(100).67890123456789}
 +headerwrap_81_100:${headerwrap_81_100:123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(100).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(200).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(300).678901234567890123456789012345678901234567890123456789012345678901234567890123456789(400).67890123456789012345678901234567890123456789012345678901234567890123456789012345\
 +67890123456789(500).678901234567890123456789012345678901234567890123456789012345678901234567890123456789(600).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(700).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(800).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(900).6789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(1000).789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789(100).67890123456789}
 +
  base32: 0  <${base32:0}>
  base32: 1  <${base32:1}>
  base32: 31 <${base32:31}>
@@@ -458,6 -442,7 +458,7 @@@ ge:     ${if ge{ABC}{abc}{y}{n}
  gei:    ${if gei{ABC}{abc}{y}{n}}
  
  isip:   ${if isip {1.2.3.4}{y}{n}}  1.2.3.4
+ isip:   ${if isip {1.2.3}{y}{n}}  1.2.3
  isip4:  ${if isip4{1.2.3.4}{y}{n}}  1.2.3.4
  isip6:  ${if isip6{1.2.3.4}{y}{n}}  1.2.3.4
  isip:   ${if isip {::1.2.3.256}{y}{n}}  ::1.2.3.256
@@@ -475,14 -460,15 +476,17 @@@ isip:   ${if isip {fe80::1.2.3.4}{y}{n}
  isip:   ${if isip {rhubarb}{y}{n}}  rhubarb
  isip4:  ${if isip4{rhubarb}{y}{n}}  rhubarb
  isip6:  ${if isip6{rhubarb}{y}{n}}  rhubarb
+ isip6:  ${if isip6{::/100}{y}{n}}  ::/100
+ isip6:  ${if isip6{::/foo}{y}{n}}  ::/foo
+ isip6:  ${if isip6{::/f o}{y}{n}}  ::/f o
  
  match:  ${if match{abcd}{\N^([ab]+)(\w+)$\N}{$2$1}fail}
  match:  ${if match{abcd}{^\N([ab]+)(\w+)$\N}{$2$1}fail}
  match:  ${if match{abcd}{^([ab]+)(\\w+)\$}{$2$1}fail}
  match:  ${if match{wxyz}{^([ab]+)(\\w+)\$}{$2$1}fail}
  match:  ${if match{abcd}{^([ab]+)(\\w+)\$}{$2[${if match{xyz}{(.*)}{$1}fail}]$1}fail}
 +# check for empty capture group
 +match:  ${if match{abc}{\N^(\S+)\s*(\S.+)*$\N}{<$2>}{}}
  
  match_domain:    ${if match_domain{a.b.c}{x.y.z:a.b.c:p.q.r}{yes}{no}}
  match_domain:    ${if match_domain{a.b.c}{x.y.z:p.q.r}{yes}{no}}
@@@ -714,7 -700,6 +718,7 @@@ abcdea aaa xyz ${tr{abcdea}{aaa}{xyz}
  abcdea a   z   ${tr{abcdea}{a}{z}}
  abcdea a       ${tr{abcdea}{a}{}}
  abcdea abc z   ${tr{abcdea}{abc}{z}}
 +(null)         '${sg{$header_foobar:${tr{}{}{foobar}}}{}{}}'
  
  # Boolean
  "TrUe"                ${if bool{TrUe}{true}{false}}      EXPECT: true
@@@ -823,8 -808,6 +827,8 @@@ ${if eq{1}{2}{${run{/non/exist}}}{1!=2}
  rc=$runrc
  ${run,preexpand {DIR/aux-fixed/0002.runfile 0}}
  rc=$runrc
 +${run{DIR/aux-fixed/0002.runfile ${quote:1}}{$value}{2}}
 +rc=$runrc
  
  # PRVS
  
@@@ -983,13 -966,6 +987,13 @@@ expect: <
  <${extract jsons{nonexistent}{ \{"id": \{"a":101, "b":102\}, "IDs": \{"1":116, "2":943, "3":234\}\} }}>
  expect: <>
  
 +# string value with embedded comma
 +<${extract jsons{name}{ \{ "id":"1","name":"Doe, John","age":"unknown" \}}}>
 +expect <Doe, John>
 +# string value with embedded doublequote
 +<${extract jsons{name}{ \{ "id":"1","name":"word1 \\\" word2","age":"unknown" \}}}>
 +expect <word1 \\\" word2>
 +
  ${if forany_json {[1, 2, 3]}{={$item}{1}}{yes}{no}}
  ${if forany_jsons{["A", "B", "C"]}{eq{$item}{B}}{yes}{no}}
  
diff --combined test/stdout/0002
index 2d7c828381f81a7c86380e6890989430c6fe51ae,a0677dc5a51b988c59ffc3542fc6556364675036..d5bb0605c5fac58628816137f1b82047fb21a368
@@@ -79,8 -79,6 +79,8 @@@
  > filter: a'c
  > filter: ''a' ''c
  > filter: ""
 +> # check operation when the condition modifies the 'value' variable
 +> E
  > 
  > map: ""
  > map: a:b:c
@@@ -197,38 -195,6 +197,38 @@@ newline  tab\134backslash ~tilde\177DEL\
  > hex2b64:MPPJPkZDbetYunCBao7BJA==
  > hex2b64:ztcfpyNSMb7Tg/rP3EHE3cwi7PE=
  > 
 +> headerwrap:
 +> headerwrap:a
 +> headerwrap:ab
 +> headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz
 +> headerwrap_79:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 +      z
 +> headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 +      b
 +> headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz
 +      Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab
 +> headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz
 +      Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab
 +> headerwrap:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz
 +      Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
 +      bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbz
 +> headerwrap:12345678901234567890123456789012345678901234567890123456789012345678901234567890
 +      1234567890123456789(100).67890123456789012345678901234567890123456789012
 +      34567890123456789012345678901234567890123456789(200).6789012345678901234
 +      567890123456789012345678901234567890123456789012345678901234567890123456
 +      789(300).678901234567890123456789012345678901234567890123456789012345678
 +      901234567890123456789(400).678901234567890123456789012345678901234567890
 +      1234567890123456789012345678901234567890123456789(500).67890123456789012
 +      3456789012345678901234567890123456789012345678901234567890123456789(600)
 +      .67890123456789012345678901234567890123456789012345678901234567890123456
 +      78901234567890123456789(700).6789012345678901234567890123456789012345678
 +      901234567890123456789012345678901234567890123456789(800).678901234567890
 +      123456789012345678901234567890123456789012345678901234567890123456789012
 +      3456789(900).67890123456789012345678901234567890123456789012345678901234
 +      5678901234567890123456789012
 +> headerwrap_81_100:123456789012345678901234567890123456789012345678901234567890123456789012345678901
 +      23456789012345678
 +> 
  > base32: 0  <>
  > base32: 1  <b>
  > base32: 31 <7>
  > gei:    y
  > 
  > isip:   y  1.2.3.4
+ > isip:   n  1.2.3
  > isip4:  y  1.2.3.4
  > isip6:  n  1.2.3.4
  > isip:   n  ::1.2.3.256
  > isip:   n  rhubarb
  > isip4:  n  rhubarb
  > isip6:  n  rhubarb
+ > isip6:  n  ::/100
+ > isip6:  n  ::/foo
+ > isip6:  n  ::/f o
  > 
  > match:  cdab
  > match:  cdab
  > match:  cdab
  > Failed: "if" failed and "fail" requested
  > match:  cd[xyz]ab
 +> # check for empty capture group
 +> match:  <>
  > 
  > match_domain:    yes
  > match_domain:    no
  > abcdea a   z   zbcdez
  > abcdea a       abcdea
  > abcdea abc z   zzzdez
 +> (null)         ''
  > 
  > # Boolean
  > "TrUe"                true      EXPECT: true
@@@ -816,8 -783,6 +820,8 @@@ xy
  1234
  
  > rc=0
 +> 2
 +> rc=1
  > 
  > # PRVS
  > 
  > <>
  > expect: <>
  > 
 +> # string value with embedded comma
 +> <Doe, John>
 +> expect <Doe, John>
 +> # string value with embedded doublequote
 +> <word1 \" word2>
 +> expect <word1 \" word2>
 +> 
  > yes
  > yes
  >