Expansions: acl expansion error detail
[exim.git] / src / src / expand.c
index f878e7b7f6f91753e43ab480586ce61f1f2257c2..b3be83a46789dad0bb25e7d9f7aa0c8c83222613 100644 (file)
@@ -235,6 +235,7 @@ static uschar *op_table_main[] = {
   US"rxquote",
   US"s",
   US"sha1",
+  US"sha2",
   US"sha256",
   US"sha3",
   US"stat",
@@ -281,6 +282,7 @@ enum {
   EOP_RXQUOTE,
   EOP_S,
   EOP_SHA1,
+  EOP_SHA2,
   EOP_SHA256,
   EOP_SHA3,
   EOP_STAT,
@@ -312,7 +314,11 @@ static uschar *cond_table[] = {
   US"exists",
   US"first_delivery",
   US"forall",
+  US"forall_json",
+  US"forall_jsons",
   US"forany",
+  US"forany_json",
+  US"forany_jsons",
   US"ge",
   US"gei",
   US"gt",
@@ -358,7 +364,11 @@ enum {
   ECOND_EXISTS,
   ECOND_FIRST_DELIVERY,
   ECOND_FORALL,
+  ECOND_FORALL_JSON,
+  ECOND_FORALL_JSONS,
   ECOND_FORANY,
+  ECOND_FORANY_JSON,
+  ECOND_FORANY_JSONS,
   ECOND_STR_GE,
   ECOND_STR_GEI,
   ECOND_STR_GT,
@@ -462,6 +472,8 @@ static var_entry var_table[] = {
   { "address_file",        vtype_stringptr,   &address_file },
   { "address_pipe",        vtype_stringptr,   &address_pipe },
 #ifdef EXPERIMENTAL_ARC
+  { "arc_domains",         vtype_string_func, &fn_arc_domains },
+  { "arc_oldest_pass",     vtype_int,         &arc_oldest_pass },
   { "arc_state",           vtype_stringptr,   &arc_state },
   { "arc_state_reason",    vtype_stringptr,   &arc_state_reason },
 #endif
@@ -562,7 +574,9 @@ static var_entry var_table[] = {
   { "local_part_data",     vtype_stringptr,   &deliver_localpart_data },
   { "local_part_prefix",   vtype_stringptr,   &deliver_localpart_prefix },
   { "local_part_suffix",   vtype_stringptr,   &deliver_localpart_suffix },
+#ifdef HAVE_LOCAL_SCAN
   { "local_scan_data",     vtype_stringptr,   &local_scan_data },
+#endif
   { "local_user_gid",      vtype_gid,         &local_user_gid },
   { "local_user_uid",      vtype_uid,         &local_user_uid },
   { "localhost_number",    vtype_int,         &host_number },
@@ -734,16 +748,21 @@ static var_entry var_table[] = {
   { "tls_in_bits",         vtype_int,         &tls_in.bits },
   { "tls_in_certificate_verified", vtype_int, &tls_in.certificate_verified },
   { "tls_in_cipher",       vtype_stringptr,   &tls_in.cipher },
+  { "tls_in_cipher_std",   vtype_stringptr,   &tls_in.cipher_stdname },
   { "tls_in_ocsp",         vtype_int,         &tls_in.ocsp },
   { "tls_in_ourcert",      vtype_cert,        &tls_in.ourcert },
   { "tls_in_peercert",     vtype_cert,        &tls_in.peercert },
   { "tls_in_peerdn",       vtype_stringptr,   &tls_in.peerdn },
-#if defined(SUPPORT_TLS)
+#ifdef EXPERIMENTAL_TLS_RESUME
+  { "tls_in_resumption",   vtype_int,         &tls_in.resumption },
+#endif
+#ifndef DISABLE_TLS
   { "tls_in_sni",          vtype_stringptr,   &tls_in.sni },
 #endif
   { "tls_out_bits",        vtype_int,         &tls_out.bits },
   { "tls_out_certificate_verified", vtype_int,&tls_out.certificate_verified },
   { "tls_out_cipher",      vtype_stringptr,   &tls_out.cipher },
+  { "tls_out_cipher_std",  vtype_stringptr,   &tls_out.cipher_stdname },
 #ifdef SUPPORT_DANE
   { "tls_out_dane",        vtype_bool,        &tls_out.dane_verified },
 #endif
@@ -751,7 +770,10 @@ static var_entry var_table[] = {
   { "tls_out_ourcert",     vtype_cert,        &tls_out.ourcert },
   { "tls_out_peercert",    vtype_cert,        &tls_out.peercert },
   { "tls_out_peerdn",      vtype_stringptr,   &tls_out.peerdn },
-#if defined(SUPPORT_TLS)
+#ifdef EXPERIMENTAL_TLS_RESUME
+  { "tls_out_resumption",  vtype_int,         &tls_out.resumption },
+#endif
+#ifndef DISABLE_TLS
   { "tls_out_sni",         vtype_stringptr,   &tls_out.sni },
 #endif
 #ifdef SUPPORT_DANE
@@ -759,7 +781,7 @@ static var_entry var_table[] = {
 #endif
 
   { "tls_peerdn",          vtype_stringptr,   &tls_in.peerdn },        /* mind the alphabetical order! */
-#if defined(SUPPORT_TLS)
+#ifndef DISABLE_TLS
   { "tls_sni",             vtype_stringptr,   &tls_in.sni },   /* mind the alphabetical order! */
 #endif
 
@@ -813,6 +835,10 @@ static uschar *mtable_setid[] =
 static uschar *mtable_sticky[] =
   { US"--T", US"--t", US"-wT", US"-wt", US"r-T", US"r-t", US"rwT", US"rwt" };
 
+/* flags for find_header() */
+#define FH_EXISTS_ONLY BIT(0)
+#define FH_WANT_RAW    BIT(1)
+#define FH_WANT_LIST   BIT(2)
 
 
 /*************************************************
@@ -916,7 +942,7 @@ int rc;
 uschar *ss = expand_string(condition);
 if (ss == NULL)
   {
-  if (!expand_string_forcedfail && !search_find_defer)
+  if (!f.expand_string_forcedfail && !f.search_find_defer)
     log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand condition \"%s\" "
       "for %s %s: %s", condition, m1, m2, expand_string_message);
   return FALSE;
@@ -941,7 +967,7 @@ weirdness they'll twist this into.  The result should ideally handle fork().
 However, if we're stuck unable to provide this, then we'll fall back to
 appallingly bad randomness.
 
-If SUPPORT_TLS is defined then this will not be used except as an emergency
+If DISABLE_TLS is not defined then this will not be used except as an emergency
 fallback.
 
 Arguments:
@@ -949,56 +975,55 @@ Arguments:
 Returns     a random number in range [0, max-1]
 */
 
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
 # define vaguely_random_number vaguely_random_number_fallback
 #endif
 int
 vaguely_random_number(int max)
 {
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
 # undef vaguely_random_number
 #endif
-  static pid_t pid = 0;
-  pid_t p2;
-#if defined(HAVE_SRANDOM) && !defined(HAVE_SRANDOMDEV)
-  struct timeval tv;
-#endif
+static pid_t pid = 0;
+pid_t p2;
 
-  p2 = getpid();
-  if (p2 != pid)
+if ((p2 = getpid()) != pid)
+  {
+  if (pid != 0)
     {
-    if (pid != 0)
-      {
 
 #ifdef HAVE_ARC4RANDOM
-      /* cryptographically strong randomness, common on *BSD platforms, not
-      so much elsewhere.  Alas. */
-#ifndef NOT_HAVE_ARC4RANDOM_STIR
-      arc4random_stir();
-#endif
+    /* cryptographically strong randomness, common on *BSD platforms, not
+    so much elsewhere.  Alas. */
+# ifndef NOT_HAVE_ARC4RANDOM_STIR
+    arc4random_stir();
+# endif
 #elif defined(HAVE_SRANDOM) || defined(HAVE_SRANDOMDEV)
-#ifdef HAVE_SRANDOMDEV
-      /* uses random(4) for seeding */
-      srandomdev();
-#else
-      gettimeofday(&tv, NULL);
-      srandom(tv.tv_sec | tv.tv_usec | getpid());
-#endif
+# ifdef HAVE_SRANDOMDEV
+    /* uses random(4) for seeding */
+    srandomdev();
+# else
+    {
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+    srandom(tv.tv_sec | tv.tv_usec | getpid());
+    }
+# endif
 #else
-      /* Poor randomness and no seeding here */
+    /* Poor randomness and no seeding here */
 #endif
 
-      }
-    pid = p2;
     }
+  pid = p2;
+  }
 
 #ifdef HAVE_ARC4RANDOM
-  return arc4random() % max;
+return arc4random() % max;
 #elif defined(HAVE_SRANDOM) || defined(HAVE_SRANDOMDEV)
-  return random() % max;
+return random() % max;
 #else
-  /* This one returns a 16-bit number, definitely not crypto-strong */
-  return random_number(max);
+/* This one returns a 16-bit number, definitely not crypto-strong */
+return random_number(max);
 #endif
 }
 
@@ -1122,20 +1147,20 @@ Returns:    NULL if the subfield was not found, or
 */
 
 static uschar *
-expand_getkeyed(uschar *key, const uschar *s)
+expand_getkeyed(uschar * key, const uschar * s)
 {
 int length = Ustrlen(key);
 while (isspace(*s)) s++;
 
 /* Loop to search for the key */
 
-while (*s != 0)
+while (*s)
   {
   int dkeylength;
-  uschar *data;
-  const uschar *dkey = s;
+  uschar * data;
+  const uschar * dkey = s;
 
-  while (*s != 0 && *s != '=' && !isspace(*s)) s++;
+  while (*s && *s != '=' && !isspace(*s)) s++;
   dkeylength = s - dkey;
   while (isspace(*s)) s++;
   if (*s == '=') while (isspace((*(++s))));
@@ -1246,17 +1271,17 @@ return fieldtext;
 static uschar *
 expand_getlistele(int field, const uschar * list)
 {
-const uschar * tlist= list;
-int sep= 0;
+const uschar * tlist = list;
+int sep = 0;
 uschar dummy;
 
-if(field<0)
+if (field < 0)
   {
-  for(field++; string_nextinlist(&tlist, &sep, &dummy, 1); ) field++;
-  sep= 0;
+  for (field++; string_nextinlist(&tlist, &sep, &dummy, 1); ) field++;
+  sep = 0;
   }
-if(field==0) return NULL;
-while(--field>0 && (string_nextinlist(&list, &sep, &dummy, 1))) ;
+if (field == 0) return NULL;
+while (--field > 0 && (string_nextinlist(&list, &sep, &dummy, 1))) ;
 return string_nextinlist(&list, &sep, NULL, 0);
 }
 
@@ -1264,7 +1289,7 @@ return string_nextinlist(&list, &sep, NULL, 0);
 /* Certificate fields, by name.  Worry about by-OID later */
 /* Names are chosen to not have common prefixes */
 
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
 typedef struct
 {
 uschar * name;
@@ -1290,7 +1315,6 @@ static uschar *
 expand_getcertele(uschar * field, uschar * certvar)
 {
 var_entry * vp;
-certfield * cp;
 
 if (!(vp = find_var_ent(certvar)))
   {
@@ -1312,9 +1336,9 @@ if (!*(void **)vp->value)
 if (*field >= '0' && *field <= '9')
   return tls_cert_ext_by_oid(*(void **)vp->value, field, 0);
 
-for(cp = certfields;
-    cp < certfields + nelem(certfields);
-    cp++)
+for (certfield * cp = certfields;
+     cp < certfields + nelem(certfields);
+     cp++)
   if (Ustrncmp(cp->name, field, cp->namelen) == 0)
     {
     uschar * modifier = *(field += cp->namelen) == ','
@@ -1326,7 +1350,7 @@ expand_string_message =
   string_sprintf("bad field selector \"%s\" for certextract", field);
 return NULL;
 }
-#endif /*SUPPORT_TLS*/
+#endif /*DISABLE_TLS*/
 
 /*************************************************
 *        Extract a substring from a string       *
@@ -1517,22 +1541,25 @@ can also return a concatenation of all the header lines. When concatenating
 specific headers that contain lists of addresses, a comma is inserted between
 them. Otherwise we use a straight concatenation. Because some messages can have
 pathologically large number of lines, there is a limit on the length that is
-returned. Also, to avoid massive store use which would result from using
-string_cat() as it copies and extends strings, we do a preliminary pass to find
-out exactly how much store will be needed. On "normal" messages this will be
-pretty trivial.
+returned.
 
 Arguments:
   name          the name of the header, without the leading $header_ or $h_,
                 or NULL if a concatenation of all headers is required
-  exists_only   TRUE if called from a def: test; don't need to build a string;
-                just return a string that is not "" and not "0" if the header
-                exists
   newsize       return the size of memory block that was obtained; may be NULL
                 if exists_only is TRUE
-  want_raw      TRUE if called for $rh_ or $rheader_ variables; no processing,
-                other than concatenating, will be done on the header. Also used
-                for $message_headers_raw.
+  flags                FH_EXISTS_ONLY
+                 set if called from a def: test; don't need to build a string;
+                 just return a string that is not "" and not "0" if the header
+                 exists
+               FH_WANT_RAW
+                 set if called for $rh_ or $rheader_ items; no processing,
+                 other than concatenating, will be done on the header. Also used
+                 for $message_headers_raw.
+               FH_WANT_LIST
+                 Double colon chars in the content, and replace newline with
+                 colon between each element when concatenating; returning a
+                 colon-sep list (elements might contain newlines)
   charset       name of charset to translate MIME words to; used only if
                 want_raw is false; if NULL, no translation is done (this is
                 used for $bh_ and $bheader_)
@@ -1542,134 +1569,112 @@ Returns:        NULL if the header does not exist, else a pointer to a new
 */
 
 static uschar *
-find_header(uschar *name, BOOL exists_only, int *newsize, BOOL want_raw,
-  uschar *charset)
+find_header(uschar *name, int *newsize, unsigned flags, uschar *charset)
 {
-BOOL found = name == NULL;
-int comma = 0;
-int len = found? 0 : Ustrlen(name);
-int i;
-uschar *yield = NULL;
-uschar *ptr = NULL;
-
-/* Loop for two passes - saves code repetition */
-
-for (i = 0; i < 2; i++)
-  {
-  int size = 0;
-  header_line *h;
-
-  for (h = header_list; size < header_insert_maxlen && h; h = h->next)
-    if (h->type != htype_old && h->text)  /* NULL => Received: placeholder */
-      if (!name || (len <= h->slen && strncmpic(name, h->text, len) == 0))
-        {
-        int ilen;
-        uschar *t;
-
-        if (exists_only) return US"1";      /* don't need actual string */
-        found = TRUE;
-        t = h->text + len;                  /* text to insert */
-        if (!want_raw)                      /* unless wanted raw, */
-          while (isspace(*t)) t++;          /* remove leading white space */
-        ilen = h->slen - (t - h->text);     /* length to insert */
-
-        /* Unless wanted raw, remove trailing whitespace, including the
-        newline. */
-
-        if (!want_raw)
-          while (ilen > 0 && isspace(t[ilen-1])) ilen--;
+BOOL found = !name;
+int len = name ? Ustrlen(name) : 0;
+BOOL comma = FALSE;
+gstring * g = NULL;
 
-        /* Set comma = 1 if handling a single header and it's one of those
-        that contains an address list, except when asked for raw headers. Only
-        need to do this once. */
+for (header_line * h = header_list; h; h = h->next)
+  if (h->type != htype_old && h->text)  /* NULL => Received: placeholder */
+    if (!name || (len <= h->slen && strncmpic(name, h->text, len) == 0))
+      {
+      uschar * s, * t;
+      size_t inc;
 
-        if (!want_raw && name && comma == 0 &&
-            Ustrchr("BCFRST", h->type) != NULL)
-          comma = 1;
+      if (flags & FH_EXISTS_ONLY)
+       return US"1";  /* don't need actual string */
 
-        /* First pass - compute total store needed; second pass - compute
-        total store used, including this header. */
+      found = TRUE;
+      s = h->text + len;               /* text to insert */
+      if (!(flags & FH_WANT_RAW))      /* unless wanted raw, */
+       while (isspace(*s)) s++;        /* remove leading white space */
+      t = h->text + h->slen;           /* end-point */
 
-        size += ilen + comma + 1;  /* +1 for the newline */
+      /* Unless wanted raw, remove trailing whitespace, including the
+      newline. */
 
-        /* Second pass - concatenate the data, up to a maximum. Note that
-        the loop stops when size hits the limit. */
+      if (flags & FH_WANT_LIST)
+       while (t > s && t[-1] == '\n') t--;
+      else if (!(flags & FH_WANT_RAW))
+       {
+       while (t > s && isspace(t[-1])) t--;
 
-        if (i != 0)
-          {
-          if (size > header_insert_maxlen)
-            {
-            ilen -= size - header_insert_maxlen - 1;
-            comma = 0;
-            }
-          Ustrncpy(ptr, t, ilen);
-          ptr += ilen;
+       /* Set comma if handling a single header and it's one of those
+       that contains an address list, except when asked for raw headers. Only
+       need to do this once. */
 
-          /* For a non-raw header, put in the comma if needed, then add
-          back the newline we removed above, provided there was some text in
-          the header. */
+       if (name && !comma && Ustrchr("BCFRST", h->type)) comma = TRUE;
+       }
 
-          if (!want_raw && ilen > 0)
-            {
-            if (comma != 0) *ptr++ = ',';
-            *ptr++ = '\n';
-            }
-          }
-        }
+      /* Trim the header roughly if we're approaching limits */
+      inc = t - s;
+      if ((g ? g->ptr : 0) + inc > header_insert_maxlen)
+       inc = header_insert_maxlen - (g ? g->ptr : 0);
+
+      /* For raw just copy the data; for a list, add the data as a colon-sep
+      list-element; for comma-list add as an unchecked comma,newline sep
+      list-elemment; for other nonraw add as an unchecked newline-sep list (we
+      stripped trailing WS above including the newline). We ignore the potential
+      expansion due to colon-doubling, just leaving the loop if the limit is met
+      or exceeded. */
+
+      if (flags & FH_WANT_LIST)
+        g = string_append_listele_n(g, ':', s, (unsigned)inc);
+      else if (flags & FH_WANT_RAW)
+       {
+       g = string_catn(g, s, (unsigned)inc);
+       (void) string_from_gstring(g);
+       }
+      else if (inc > 0)
+       if (comma)
+         g = string_append2_listele_n(g, US",\n", s, (unsigned)inc);
+       else
+         g = string_append2_listele_n(g, US"\n", s, (unsigned)inc);
 
-  /* At end of first pass, return NULL if no header found. Then truncate size
-  if necessary, and get the buffer to hold the data, returning the buffer size.
-  */
+      if (g && g->ptr >= header_insert_maxlen) break;
+      }
 
-  if (i == 0)
-    {
-    if (!found) return NULL;
-    if (size > header_insert_maxlen) size = header_insert_maxlen;
-    *newsize = size + 1;
-    ptr = yield = store_get(*newsize);
-    }
-  }
+if (!found) return NULL;       /* No header found */
+if (!g) return US"";
 
 /* That's all we do for raw header expansion. */
 
-if (want_raw)
-  *ptr = 0;
+*newsize = g->size;
+if (flags & FH_WANT_RAW)
+  return g->s;
 
-/* Otherwise, remove a final newline and a redundant added comma. Then we do
-RFC 2047 decoding, translating the charset if requested. The rfc2047_decode2()
-function can return an error with decoded data if the charset translation
-fails. If decoding fails, it returns NULL. */
+/* Otherwise do RFC 2047 decoding, translating the charset if requested.
+The rfc2047_decode2() function can return an error with decoded data if the
+charset translation fails. If decoding fails, it returns NULL. */
 
 else
   {
   uschar *decoded, *error;
-  if (ptr > yield && ptr[-1] == '\n') ptr--;
-  if (ptr > yield && comma != 0 && ptr[-1] == ',') ptr--;
-  *ptr = 0;
-  decoded = rfc2047_decode2(yield, check_rfc2047_length, charset, '?', NULL,
+
+  decoded = rfc2047_decode2(g->s, check_rfc2047_length, charset, '?', NULL,
     newsize, &error);
-  if (error != NULL)
+  if (error)
     {
     DEBUG(D_any) debug_printf("*** error in RFC 2047 decoding: %s\n"
-      "    input was: %s\n", error, yield);
+      "    input was: %s\n", error, g->s);
     }
-  if (decoded != NULL) yield = decoded;
+  return decoded ? decoded : g->s;
   }
-
-return yield;
 }
 
 
 
 
-/* Append a "local" element to an Autherntication-Results: header
+/* Append a "local" element to an Authentication-Results: header
 if this was a non-smtp message.
 */
 
 static gstring *
 authres_local(gstring * g, const uschar * sysname)
 {
-if (!authentication_local)
+if (!f.authentication_local)
   return g;
 g = string_append(g, 3, US";\n\tlocal=pass (non-smtp, ", sysname, US")");
 if (authenticated_id) g = string_append(g, 2, " u=", authenticated_id);
@@ -1677,7 +1682,7 @@ return g;
 }
 
 
-/* Append an "iprev" element to an Autherntication-Results: header
+/* Append an "iprev" element to an Authentication-Results: header
 if we have attempted to get the calling host's name.
 */
 
@@ -1685,11 +1690,16 @@ static gstring *
 authres_iprev(gstring * g)
 {
 if (sender_host_name)
-  return string_append(g, 3, US";\n\tiprev=pass (", sender_host_name, US")");
-if (host_lookup_deferred)
-  return string_catn(g, US";\n\tiprev=temperror", 19);
-if (host_lookup_failed)
-  return string_catn(g, US";\n\tiprev=fail", 13);
+  g = string_append(g, 3, US";\n\tiprev=pass (", sender_host_name, US")");
+else if (host_lookup_deferred)
+  g = string_catn(g, US";\n\tiprev=temperror", 19);
+else if (host_lookup_failed)
+  g = string_catn(g, US";\n\tiprev=fail", 13);
+else
+  return g;
+
+if (sender_host_address)
+  g = string_append(g, 2, US" smtp.remote-ip=", sender_host_address);
 return g;
 }
 
@@ -1705,18 +1715,17 @@ generated from a system filter, but not elsewhere. */
 static uschar *
 fn_recipients(void)
 {
+uschar * s;
 gstring * g = NULL;
-int i;
 
-if (!enable_dollar_recipients) return NULL;
+if (!f.enable_dollar_recipients) return NULL;
 
-for (i = 0; i < recipients_count; i++)
+for (int i = 0; i < recipients_count; i++)
   {
-  /*XXX variant of list_appendele? */
-  if (i != 0) g = string_catn(g, US", ", 2);
-  g = string_cat(g, recipients_list[i].address);
+  s = recipients_list[i].address;
+  g = string_append2_listele_n(g, US", ", s, Ustrlen(s));
   }
-return string_from_gstring(g);
+return g ? g->s : NULL;
 }
 
 
@@ -1764,8 +1773,13 @@ set, in which case give an error. */
 if ((Ustrncmp(name, "acl_c", 5) == 0 || Ustrncmp(name, "acl_m", 5) == 0) &&
      !isalpha(name[5]))
   {
-  tree_node *node =
-    tree_search((name[4] == 'c')? acl_var_c : acl_var_m, name + 4);
+  tree_node * node =
+    tree_search(name[4] == 'c' ? acl_var_c : acl_var_m, name + 4);
+  return node ? node->data.ptr : strict_acl_vars ? NULL : US"";
+  }
+else if (Ustrncmp(name, "r_", 2) == 0)
+  {
+  tree_node * node = tree_search(router_var, name + 2);
   return node ? node->data.ptr : strict_acl_vars ? NULL : US"";
   }
 
@@ -1801,7 +1815,7 @@ val = vp->value;
 switch (vp->type)
   {
   case vtype_filter_int:
-    if (!filter_running) return NULL;
+    if (!f.filter_running) return NULL;
     /* Fall through */
     /* VVVVVVVVVVVV */
   case vtype_int:
@@ -1860,10 +1874,11 @@ switch (vp->type)
     return (domain == NULL)? US"" : domain + 1;
 
   case vtype_msgheaders:
-    return find_header(NULL, exists_only, newsize, FALSE, NULL);
+    return find_header(NULL, newsize, exists_only ? FH_EXISTS_ONLY : 0, NULL);
 
   case vtype_msgheaders_raw:
-    return find_header(NULL, exists_only, newsize, TRUE, NULL);
+    return find_header(NULL, newsize,
+               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW, NULL);
 
   case vtype_msgbody:                        /* Pointer to msgbody string */
   case vtype_msgbody_end:                    /* Ditto, the end of the msg */
@@ -1928,15 +1943,18 @@ switch (vp->type)
     return tod_stamp(tod_log_datestamp_daily);
 
   case vtype_reply:                          /* Get reply address */
-    s = find_header(US"reply-to:", exists_only, newsize, TRUE,
-      headers_charset);
-    if (s != NULL) while (isspace(*s)) s++;
-    if (s == NULL || *s == 0)
+    s = find_header(US"reply-to:", newsize,
+               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
+               headers_charset);
+    if (s) while (isspace(*s)) s++;
+    if (!s || !*s)
       {
       *newsize = 0;                            /* For the *s==0 case */
-      s = find_header(US"from:", exists_only, newsize, TRUE, headers_charset);
+      s = find_header(US"from:", newsize,
+               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
+               headers_charset);
       }
-    if (s != NULL)
+    if (s)
       {
       uschar *t;
       while (isspace(*s)) s++;
@@ -1944,7 +1962,7 @@ switch (vp->type)
       while (t > s && isspace(t[-1])) t--;
       *t = 0;
       }
-    return (s == NULL)? US"" : s;
+    return s ? s : US"";
 
   case vtype_string_func:
     {
@@ -1955,7 +1973,7 @@ switch (vp->type)
   case vtype_pspace:
     {
     int inodes;
-    sprintf(CS var_buffer, "%d",
+    sprintf(CS var_buffer, PR_EXIM_ARITH,
       receive_statvfs(val == (void *)TRUE, &inodes));
     }
   return var_buffer;
@@ -2026,11 +2044,10 @@ static int
 read_subs(uschar **sub, int n, int m, const uschar **sptr, BOOL skipping,
   BOOL check_end, uschar *name, BOOL *resetok)
 {
-int i;
 const uschar *s = *sptr;
 
 while (isspace(*s)) s++;
-for (i = 0; i < n; i++)
+for (int i = 0; i < n; i++)
   {
   if (*s != '{')
     {
@@ -2146,6 +2163,89 @@ return ret;
 
 
 
+/* Return pointer to dewrapped string, with enclosing specified chars removed.
+The given string is modified on return.  Leading whitespace is skipped while
+looking for the opening wrap character, then the rest is scanned for the trailing
+(non-escaped) wrap character.  A backslash in the string will act as an escape.
+
+A nul is written over the trailing wrap, and a pointer to the char after the
+leading wrap is returned.
+
+Arguments:
+  s    String for de-wrapping
+  wrap  Two-char string, the first being the opener, second the closer wrapping
+        character
+Return:
+  Pointer to de-wrapped string, or NULL on error (with expand_string_message set).
+*/
+
+static uschar *
+dewrap(uschar * s, const uschar * wrap)
+{
+uschar * p = s;
+unsigned depth = 0;
+BOOL quotesmode = wrap[0] == wrap[1];
+
+while (isspace(*p)) p++;
+
+if (*p == *wrap)
+  {
+  s = ++p;
+  wrap++;
+  while (*p)
+    {
+    if (*p == '\\') p++;
+    else if (!quotesmode && *p == wrap[-1]) depth++;
+    else if (*p == *wrap)
+      if (depth == 0)
+       {
+       *p = '\0';
+       return s;
+       }
+      else
+       depth--;
+    p++;
+    }
+  }
+expand_string_message = string_sprintf("missing '%c'", *wrap);
+return NULL;
+}
+
+
+/* Pull off the leading array or object element, returning
+a copy in an allocated string.  Update the list pointer.
+
+The element may itself be an abject or array.
+Return NULL when the list is empty.
+*/
+
+static uschar *
+json_nextinlist(const uschar ** list)
+{
+unsigned array_depth = 0, object_depth = 0;
+const uschar * s = *list, * item;
+
+while (isspace(*s)) s++;
+
+for (item = s;
+     *s && (*s != ',' || array_depth != 0 || object_depth != 0);
+     s++)
+  switch (*s)
+    {
+    case '[': array_depth++; break;
+    case ']': array_depth--; break;
+    case '{': object_depth++; break;
+    case '}': object_depth--; break;
+    }
+*list = *s ? s+1 : s;
+if (item == s) return NULL;
+item = string_copyn(item, s - item);
+DEBUG(D_expand) debug_printf_indent("  json ele: '%s'\n", item);
+return US item;
+}
+
+
+
 /*************************************************
 *        Read and evaluate a condition           *
 *************************************************/
@@ -2172,7 +2272,8 @@ BOOL testfor = TRUE;
 BOOL tempcond, combined_cond;
 BOOL *subcondptr;
 BOOL sub2_honour_dollar = TRUE;
-int i, rc, cond_type, roffset;
+BOOL is_forany, is_json, is_jsons;
+int rc, cond_type, roffset;
 int_eximarith_t num[2];
 struct stat statbuf;
 uschar name[256];
@@ -2223,56 +2324,58 @@ switch(cond_type)
   yield == NULL we are in a skipping state, and don't care about the answer. */
 
   case ECOND_DEF:
-  if (*s != ':')
     {
-    expand_string_message = US"\":\" expected after \"def\"";
-    return NULL;
-    }
+    uschar * t;
 
-  s = read_name(name, 256, s+1, US"_");
+    if (*s != ':')
+      {
+      expand_string_message = US"\":\" expected after \"def\"";
+      return NULL;
+      }
 
-  /* Test for a header's existence. If the name contains a closing brace
-  character, this may be a user error where the terminating colon has been
-  omitted. Set a flag to adjust a subsequent error message in this case. */
+    s = read_name(name, 256, s+1, US"_");
 
-  if (Ustrncmp(name, "h_", 2) == 0 ||
-      Ustrncmp(name, "rh_", 3) == 0 ||
-      Ustrncmp(name, "bh_", 3) == 0 ||
-      Ustrncmp(name, "header_", 7) == 0 ||
-      Ustrncmp(name, "rheader_", 8) == 0 ||
-      Ustrncmp(name, "bheader_", 8) == 0)
-    {
-    s = read_header_name(name, 256, s);
-    /* {-for-text-editors */
-    if (Ustrchr(name, '}') != NULL) malformed_header = TRUE;
-    if (yield != NULL) *yield =
-      (find_header(name, TRUE, NULL, FALSE, NULL) != NULL) == testfor;
-    }
+    /* Test for a header's existence. If the name contains a closing brace
+    character, this may be a user error where the terminating colon has been
+    omitted. Set a flag to adjust a subsequent error message in this case. */
 
-  /* Test for a variable's having a non-empty value. A non-existent variable
-  causes an expansion failure. */
+    if (  ( *(t = name) == 'h'
+         || (*t == 'r' || *t == 'l' || *t == 'b') && *++t == 'h'
+         )
+       && (*++t == '_' || Ustrncmp(t, "eader_", 6) == 0)
+       )
+      {
+      s = read_header_name(name, 256, s);
+      /* {-for-text-editors */
+      if (Ustrchr(name, '}') != NULL) malformed_header = TRUE;
+      if (yield) *yield =
+       (find_header(name, NULL, FH_EXISTS_ONLY, NULL) != NULL) == testfor;
+      }
 
-  else
-    {
-    uschar *value = find_variable(name, TRUE, yield == NULL, NULL);
-    if (value == NULL)
+    /* Test for a variable's having a non-empty value. A non-existent variable
+    causes an expansion failure. */
+
+    else
       {
-      expand_string_message = (name[0] == 0)?
-        string_sprintf("variable name omitted after \"def:\"") :
-        string_sprintf("unknown variable \"%s\" after \"def:\"", name);
-      check_variable_error_message(name);
-      return NULL;
+      if (!(t = find_variable(name, TRUE, yield == NULL, NULL)))
+       {
+       expand_string_message = (name[0] == 0)?
+         string_sprintf("variable name omitted after \"def:\"") :
+         string_sprintf("unknown variable \"%s\" after \"def:\"", name);
+       check_variable_error_message(name);
+       return NULL;
+       }
+      if (yield) *yield = (t[0] != 0) == testfor;
       }
-    if (yield != NULL) *yield = (value[0] != 0) == testfor;
-    }
 
-  return s;
+    return s;
+    }
 
 
   /* first_delivery tests for first delivery attempt */
 
   case ECOND_FIRST_DELIVERY:
-  if (yield != NULL) *yield = deliver_firsttime == testfor;
+  if (yield != NULL) *yield = f.deliver_firsttime == testfor;
   return s;
 
 
@@ -2414,8 +2517,9 @@ switch(cond_type)
 
     if (yield != NULL)
       {
+      int rc;
       *resetok = FALSE;        /* eval_acl() might allocate; do not reclaim */
-      switch(eval_acl(sub, nelem(sub), &user_msg))
+      switch(rc = eval_acl(sub, nelem(sub), &user_msg))
        {
        case OK:
          cond = TRUE;
@@ -2427,10 +2531,11 @@ switch(cond_type)
          break;
 
        case DEFER:
-          expand_string_forcedfail = TRUE;
+          f.expand_string_forcedfail = TRUE;
          /*FALLTHROUGH*/
        default:
-          expand_string_message = string_sprintf("error from acl \"%s\"", sub[0]);
+          expand_string_message = string_sprintf("%s from acl \"%s\"",
+           rc_names[rc], sub[0]);
          return NULL;
        }
       }
@@ -2520,7 +2625,7 @@ switch(cond_type)
   case ECOND_STR_GE:
   case ECOND_STR_GEI:
 
-  for (i = 0; i < 2; i++)
+  for (int i = 0; i < 2; i++)
     {
     /* Sometimes, we don't expand substrings; too many insecure configurations
     created using match_address{}{} and friends, where the second param
@@ -2737,16 +2842,15 @@ switch(cond_type)
 
       if (sublen == 24)
         {
-        uschar *coded = b64encode(digest, 16);
+        uschar *coded = b64encode(CUS digest, 16);
         DEBUG(D_auth) debug_printf("crypteq: using MD5+B64 hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
         tempcond = (Ustrcmp(coded, sub[1]+5) == 0);
         }
       else if (sublen == 32)
         {
-        int i;
         uschar coded[36];
-        for (i = 0; i < 16; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
+        for (int i = 0; i < 16; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
         coded[32] = 0;
         DEBUG(D_auth) debug_printf("crypteq: using MD5+hex hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
@@ -2775,16 +2879,15 @@ switch(cond_type)
 
       if (sublen == 28)
         {
-        uschar *coded = b64encode(digest, 20);
+        uschar *coded = b64encode(CUS digest, 20);
         DEBUG(D_auth) debug_printf("crypteq: using SHA1+B64 hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
         tempcond = (Ustrcmp(coded, sub[1]+6) == 0);
         }
       else if (sublen == 40)
         {
-        int i;
         uschar coded[44];
-        for (i = 0; i < 20; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
+        for (int i = 0; i < 20; i++) sprintf(CS (coded+2*i), "%02X", digest[i]);
         coded[40] = 0;
         DEBUG(D_auth) debug_printf("crypteq: using SHA1+hex hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
@@ -2947,8 +3050,14 @@ switch(cond_type)
 
   /* forall/forany: iterates a condition with different values */
 
-  case ECOND_FORALL:
-  case ECOND_FORANY:
+  case ECOND_FORALL:     is_forany = FALSE;  is_json = FALSE; is_jsons = FALSE; goto FORMANY;
+  case ECOND_FORANY:     is_forany = TRUE;   is_json = FALSE; is_jsons = FALSE; goto FORMANY;
+  case ECOND_FORALL_JSON: is_forany = FALSE;  is_json = TRUE;  is_jsons = FALSE; goto FORMANY;
+  case ECOND_FORANY_JSON: is_forany = TRUE;   is_json = TRUE;  is_jsons = FALSE; goto FORMANY;
+  case ECOND_FORALL_JSONS: is_forany = FALSE; is_json = TRUE;  is_jsons = TRUE;  goto FORMANY;
+  case ECOND_FORANY_JSONS: is_forany = TRUE;  is_json = TRUE;  is_jsons = TRUE;  goto FORMANY;
+
+  FORMANY:
     {
     const uschar * list;
     int sep = 0;
@@ -2989,10 +3098,22 @@ switch(cond_type)
       return NULL;
       }
 
-    if (yield != NULL) *yield = !testfor;
+    if (yield) *yield = !testfor;
     list = sub[0];
-    while ((iterate_item = string_nextinlist(&list, &sep, NULL, 0)) != NULL)
+    if (is_json) list = dewrap(string_copy(list), US"[]");
+    while ((iterate_item = is_json
+      ? json_nextinlist(&list) : string_nextinlist(&list, &sep, NULL, 0)))
       {
+      if (is_jsons)
+       if (!(iterate_item = dewrap(iterate_item, US"\"\"")))
+         {
+         expand_string_message =
+           string_sprintf("%s wrapping string result for extract jsons",
+             expand_string_message);
+         iterate_item = save_iterate_item;
+         return NULL;
+         }
+
       DEBUG(D_expand) debug_printf_indent("%s: $item = \"%s\"\n", name, iterate_item);
       if (!eval_condition(sub[1], resetok, &tempcond))
         {
@@ -3004,8 +3125,8 @@ switch(cond_type)
       DEBUG(D_expand) debug_printf_indent("%s: condition evaluated to %s\n", name,
         tempcond? "true":"false");
 
-      if (yield != NULL) *yield = (tempcond == testfor);
-      if (tempcond == (cond_type == ECOND_FORANY)) break;
+      if (yield) *yield = (tempcond == testfor);
+      if (tempcond == is_forany) break;
       }
 
     iterate_item = save_iterate_item;
@@ -3141,8 +3262,7 @@ Returns:                the value of expand max to save
 static int
 save_expand_strings(uschar **save_expand_nstring, int *save_expand_nlength)
 {
-int i;
-for (i = 0; i <= expand_nmax; i++)
+for (int i = 0; i <= expand_nmax; i++)
   {
   save_expand_nstring[i] = expand_nstring[i];
   save_expand_nlength[i] = expand_nlength[i];
@@ -3170,9 +3290,8 @@ static void
 restore_expand_strings(int save_expand_nmax, uschar **save_expand_nstring,
   int *save_expand_nlength)
 {
-int i;
 expand_nmax = save_expand_nmax;
-for (i = 0; i <= expand_nmax; i++)
+for (int i = 0; i <= expand_nmax; i++)
   {
   expand_nstring[i] = save_expand_nstring[i];
   expand_nlength[i] = save_expand_nlength[i];
@@ -3254,8 +3373,8 @@ 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);
-if (sub1 == NULL && (yes || !expand_string_forcedfail)) goto FAILED;
-expand_string_forcedfail = FALSE;
+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 '}'";
@@ -3284,8 +3403,8 @@ while (isspace(*s)) s++;
 if (*s == '{')
   {
   sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping, TRUE, resetok);
-  if (sub2 == NULL && (!yes || !expand_string_forcedfail)) goto FAILED;
-  expand_string_forcedfail = FALSE;
+  if (sub2 == NULL && (!yes || !f.expand_string_forcedfail)) goto FAILED;
+  f.expand_string_forcedfail = FALSE;
   if (*s++ != '}')
     {
     errwhere = US"'no' part did not start with '{'";
@@ -3320,7 +3439,7 @@ else if (*s != '}')
        }
       expand_string_message =
         string_sprintf("\"%s\" failed and \"fail\" requested", type);
-      expand_string_forcedfail = TRUE;
+      f.expand_string_forcedfail = TRUE;
       goto FAILED;
       }
     }
@@ -3461,7 +3580,6 @@ prvs_hmac_sha1(uschar *address, uschar *key, uschar *key_num, uschar *daystamp)
 {
 gstring * hash_source;
 uschar * p;
-int i;
 hctx h;
 uschar innerhash[20];
 uschar finalhash[20];
@@ -3486,7 +3604,7 @@ DEBUG(D_expand)
 memset(innerkey, 0x36, 64);
 memset(outerkey, 0x5c, 64);
 
-for (i = 0; i < Ustrlen(key); i++)
+for (int i = 0; i < Ustrlen(key); i++)
   {
   innerkey[i] ^= key[i];
   outerkey[i] ^= key[i];
@@ -3501,7 +3619,7 @@ chash_mid(HMAC_SHA1, &h, outerkey);
 chash_end(HMAC_SHA1, &h, innerhash, 20, finalhash);
 
 p = finalhash_hex;
-for (i = 0; i < 3; i++)
+for (int i = 0; i < 3; i++)
   {
   *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
   *p++ = hex_digits[finalhash[i] & 0x0f];
@@ -3549,6 +3667,27 @@ return yield;
 }
 
 
+#ifndef DISABLE_TLS
+static gstring *
+cat_file_tls(void * tls_ctx, gstring * yield, uschar * eol)
+{
+int rc;
+uschar buffer[1024];
+
+/*XXX could we read direct into a pre-grown string? */
+
+while ((rc = tls_read(tls_ctx, buffer, sizeof(buffer))) > 0)
+  for (uschar * s = buffer; rc--; s++)
+    yield = eol && *s == '\n'
+      ? string_cat(yield, eol) : string_catn(yield, s, 1);
+
+/* We assume that all errors, and any returns of zero bytes,
+are actually EOF. */
+
+(void) string_from_gstring(yield);
+return yield;
+}
+#endif
 
 
 /*************************************************
@@ -3577,17 +3716,15 @@ eval_expr(uschar **sptr, BOOL decimal, uschar **error, BOOL endket)
 {
 uschar *s = *sptr;
 int_eximarith_t x = eval_op_or(&s, decimal, error);
-if (*error == NULL)
-  {
+
+if (!*error)
   if (endket)
-    {
     if (*s != ')')
       *error = US"expecting closing parenthesis";
     else
       while (isspace(*(++s)));
-    }
-  else if (*s != 0) *error = US"expecting operator";
-  }
+  else if (*s)
+    *error = US"expecting operator";
 *sptr = s;
 return x;
 }
@@ -3596,12 +3733,12 @@ return x;
 static int_eximarith_t
 eval_number(uschar **sptr, BOOL decimal, uschar **error)
 {
-register int c;
+int c;
 int_eximarith_t n;
 uschar *s = *sptr;
+
 while (isspace(*s)) s++;
-c = *s;
-if (isdigit(c))
+if (isdigit((c = *s)))
   {
   int count;
   (void)sscanf(CS s, (decimal? SC_EXIM_DEC "%n" : SC_EXIM_ARITH "%n"), &n, &count);
@@ -3644,9 +3781,8 @@ if (*s == '+' || *s == '-' || *s == '~')
     else if (op == '~') x = ~x;
   }
 else
-  {
   x = eval_number(&s, decimal, error);
-  }
+
 *sptr = s;
 return x;
 }
@@ -3901,13 +4037,17 @@ BOOL resetok = TRUE;
 
 expand_level++;
 DEBUG(D_expand)
-  debug_printf_indent(UTF8_DOWN_RIGHT "%s: %s\n",
-    skipping
-    ? UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ "scanning"
-    : "considering",
-    string);
+  DEBUG(D_noutf8)
+    debug_printf_indent("/%s: %s\n",
+      skipping ? "---scanning" : "considering", string);
+  else
+    debug_printf_indent(UTF8_DOWN_RIGHT "%s: %s\n",
+      skipping
+      ? UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ "scanning"
+      : "considering",
+      string);
 
-expand_string_forcedfail = FALSE;
+f.expand_string_forcedfail = FALSE;
 expand_string_message = US"";
 
 while (*s != 0)
@@ -3973,6 +4113,7 @@ while (*s != 0)
     int len;
     int newsize = 0;
     gstring * g = NULL;
+    uschar * t;
 
     s = read_name(name, sizeof(name), s, US"_");
 
@@ -3990,17 +4131,19 @@ while (*s != 0)
 
     /* Header */
 
-    if (Ustrncmp(name, "h_", 2) == 0 ||
-        Ustrncmp(name, "rh_", 3) == 0 ||
-        Ustrncmp(name, "bh_", 3) == 0 ||
-        Ustrncmp(name, "header_", 7) == 0 ||
-        Ustrncmp(name, "rheader_", 8) == 0 ||
-        Ustrncmp(name, "bheader_", 8) == 0)
+    if (  ( *(t = name) == 'h'
+          || (*t == 'r' || *t == 'l' || *t == 'b') && *++t == 'h'
+         )
+       && (*++t == '_' || Ustrncmp(t, "eader_", 6) == 0)
+       )
       {
-      BOOL want_raw = (name[0] == 'r')? TRUE : FALSE;
-      uschar *charset = (name[0] == 'b')? NULL : headers_charset;
+      unsigned flags = *name == 'r' ? FH_WANT_RAW
+                     : *name == 'l' ? FH_WANT_RAW|FH_WANT_LIST
+                     : 0;
+      uschar * charset = *name == 'b' ? NULL : headers_charset;
+
       s = read_header_name(name, sizeof(name), s);
-      value = find_header(name, FALSE, &newsize, want_raw, charset);
+      value = find_header(name, &newsize, flags, charset);
 
       /* If we didn't find the header, and the header contains a closing brace
       character, this may be a user error where the terminating colon
@@ -4109,6 +4252,7 @@ while (*s != 0)
       {
       uschar *sub[10]; /* name + arg1-arg9 (which must match number of acl_arg[]) */
       uschar *user_msg;
+      int rc;
 
       switch(read_subs(sub, nelem(sub), 1, &s, skipping, TRUE, US"acl",
                      &resetok))
@@ -4120,7 +4264,7 @@ while (*s != 0)
       if (skipping) continue;
 
       resetok = FALSE;
-      switch(eval_acl(sub, nelem(sub), &user_msg))
+      switch(rc = eval_acl(sub, nelem(sub), &user_msg))
        {
        case OK:
        case FAIL:
@@ -4131,10 +4275,11 @@ while (*s != 0)
          continue;
 
        case DEFER:
-          expand_string_forcedfail = TRUE;
+          f.expand_string_forcedfail = TRUE;
          /*FALLTHROUGH*/
        default:
-          expand_string_message = string_sprintf("error from acl \"%s\"", sub[0]);
+          expand_string_message = string_sprintf("%s from acl \"%s\"",
+           rc_names[rc], sub[0]);
          goto EXPAND_FAILED;
        }
       }
@@ -4191,15 +4336,21 @@ while (*s != 0)
       if (next_s == NULL) goto EXPAND_FAILED;  /* message already set */
 
       DEBUG(D_expand)
-       {
-        debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
-         "condition: %.*s\n",
-         (int)(next_s - s), s);
-        debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
-         UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-         "result: %s\n",
-         cond ? "true" : "false");
-       }
+       DEBUG(D_noutf8)
+         {
+         debug_printf_indent("|--condition: %.*s\n", (int)(next_s - s), s);
+         debug_printf_indent("|-----result: %s\n", cond ? "true" : "false");
+         }
+       else
+         {
+         debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
+           "condition: %.*s\n",
+           (int)(next_s - s), s);
+         debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
+           UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
+           "result: %s\n",
+           cond ? "true" : "false");
+         }
 
       s = next_s;
 
@@ -4429,7 +4580,7 @@ while (*s != 0)
           }
         lookup_value = search_find(handle, filename, key, partial, affix,
           affixlen, starflags, &expand_setup);
-        if (search_find_defer)
+        if (f.search_find_defer)
           {
           expand_string_message =
             string_sprintf("lookup of \"%s\" gave DEFER: %s",
@@ -4538,7 +4689,7 @@ while (*s != 0)
           expand_string_message =
             string_sprintf("Perl subroutine \"%s\" returned undef to force "
               "failure", sub_arg[0]);
-          expand_string_forcedfail = TRUE;
+          f.expand_string_forcedfail = TRUE;
           }
         goto EXPAND_FAILED;
         }
@@ -4546,7 +4697,7 @@ while (*s != 0)
       /* Yield succeeded. Ensure forcedfail is unset, just in case it got
       set during a callback from Perl. */
 
-      expand_string_forcedfail = FALSE;
+      f.expand_string_forcedfail = FALSE;
       yield = new_yield;
       continue;
       }
@@ -4696,7 +4847,7 @@ while (*s != 0)
           (void)sscanf(CS now,"%u",&inow);
           (void)sscanf(CS daystamp,"%u",&iexpire);
 
-          /* When "iexpire" is < 7, a "flip" has occured.
+          /* When "iexpire" is < 7, a "flip" has occurred.
              Adjust "inow" accordingly. */
           if ( (iexpire < 7) && (inow >= 993) ) inow = 0;
 
@@ -4793,13 +4944,16 @@ while (*s != 0)
 
     case EITEM_READSOCK:
       {
-      int fd;
+      client_conn_ctx cctx;
       int timeout = 5;
       int save_ptr = yield->ptr;
-      FILE *f;
-      uschar *arg;
-      uschar *sub_arg[4];
+      FILE * fp = NULL;
+      uschar * arg;
+      uschar * sub_arg[4];
+      uschar * server_name = NULL;
+      host_item host;
       BOOL do_shutdown = TRUE;
+      BOOL do_tls = FALSE;     /* Only set under ! DISABLE_TLS */
       blob reqstr;
 
       if (expand_forbid & RDO_READSOCK)
@@ -4842,10 +4996,14 @@ while (*s != 0)
 
        while ((item = string_nextinlist(&list, &sep, NULL, 0)))
          if (Ustrncmp(item, US"shutdown=", 9) == 0)
-           if (Ustrcmp(item + 9, US"no") == 0)
-             do_shutdown = FALSE;
+           { if (Ustrcmp(item + 9, US"no") == 0) do_shutdown = FALSE; }
+#ifndef DISABLE_TLS
+         else if (Ustrncmp(item, US"tls=", 4) == 0)
+           { if (Ustrcmp(item + 9, US"no") != 0) do_tls = TRUE; }
+#endif
         }
-      else sub_arg[3] = NULL;                     /* No eol if no timeout */
+      else
+       sub_arg[3] = NULL;                     /* No eol if no timeout */
 
       /* If skipping, we don't actually do anything. Otherwise, arrange to
       connect to either an IP or a Unix socket. */
@@ -4857,8 +5015,10 @@ while (*s != 0)
         if (Ustrncmp(sub_arg[0], "inet:", 5) == 0)
           {
           int port;
-          uschar * server_name = sub_arg[0] + 5;
-          uschar * port_name = Ustrrchr(server_name, ':');
+          uschar * port_name;
+
+          server_name = sub_arg[0] + 5;
+          port_name = Ustrrchr(server_name, ':');
 
           /* Sort out the port */
 
@@ -4893,12 +5053,15 @@ while (*s != 0)
             port = ntohs(service_info->s_port);
             }
 
-         fd = ip_connectedsocket(SOCK_STREAM, server_name, port, port,
-                 timeout, NULL, &expand_string_message, &reqstr);
+         /*XXX we trust that the request is idempotent for TFO.  Hmm. */
+         cctx.sock = ip_connectedsocket(SOCK_STREAM, server_name, port, port,
+                 timeout, &host, &expand_string_message,
+                 do_tls ? NULL : &reqstr);
          callout_address = NULL;
-         if (fd < 0)
-              goto SOCK_FAIL;
-         reqstr.len = 0;
+         if (cctx.sock < 0)
+           goto SOCK_FAIL;
+         if (!do_tls)
+           reqstr.len = 0;
           }
 
         /* Handle a Unix domain socket */
@@ -4908,7 +5071,7 @@ while (*s != 0)
          struct sockaddr_un sockun;         /* don't call this "sun" ! */
           int rc;
 
-          if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
+          if ((cctx.sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
             {
             expand_string_message = string_sprintf("failed to create socket: %s",
               strerror(errno));
@@ -4918,11 +5081,12 @@ while (*s != 0)
           sockun.sun_family = AF_UNIX;
           sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1),
             sub_arg[0]);
+         server_name = US sockun.sun_path;
 
           sigalrm_seen = FALSE;
-          alarm(timeout);
-          rc = connect(fd, (struct sockaddr *)(&sockun), sizeof(sockun));
-          alarm(0);
+          ALARM(timeout);
+          rc = connect(cctx.sock, (struct sockaddr *)(&sockun), sizeof(sockun));
+          ALARM_CLR(0);
           if (sigalrm_seen)
             {
             expand_string_message = US "socket connect timed out";
@@ -4934,12 +5098,29 @@ while (*s != 0)
               "%s: %s", sub_arg[0], strerror(errno));
             goto SOCK_FAIL;
             }
+         host.name = server_name;
+         host.address = US"";
           }
 
         DEBUG(D_expand) debug_printf_indent("connected to socket %s\n", sub_arg[0]);
 
+#ifndef DISABLE_TLS
+       if (do_tls)
+         {
+         smtp_connect_args conn_args = {.host = &host };
+         tls_support tls_dummy = {.sni=NULL};
+         uschar * errstr;
+
+         if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
+           {
+           expand_string_message = string_sprintf("TLS connect failed: %s", errstr);
+           goto SOCK_FAIL;
+           }
+         }
+#endif
+
        /* Allow sequencing of test actions */
-       if (running_in_test_harness) millisleep(100);
+       if (f.running_in_test_harness) millisleep(100);
 
         /* Write the request string, if not empty or already done */
 
@@ -4947,7 +5128,11 @@ while (*s != 0)
           {
           DEBUG(D_expand) debug_printf_indent("writing \"%s\" to socket\n",
             reqstr.data);
-          if (write(fd, reqstr.data, reqstr.len) != reqstr.len)
+          if ( (
+#ifndef DISABLE_TLS
+             do_tls ? tls_write(cctx.tls_ctx, reqstr.data, reqstr.len, FALSE) :
+#endif
+                       write(cctx.sock, reqstr.data, reqstr.len)) != reqstr.len)
             {
             expand_string_message = string_sprintf("request write to socket "
               "failed: %s", strerror(errno));
@@ -4960,20 +5145,34 @@ while (*s != 0)
         system doesn't have this function, make it conditional. */
 
 #ifdef SHUT_WR
-       if (do_shutdown) shutdown(fd, SHUT_WR);
+       if (!do_tls && do_shutdown) shutdown(cctx.sock, SHUT_WR);
 #endif
 
-       if (running_in_test_harness) millisleep(100);
+       if (f.running_in_test_harness) millisleep(100);
 
         /* Now we need to read from the socket, under a timeout. The function
         that reads a file can be used. */
 
-        f = fdopen(fd, "rb");
+       if (!do_tls)
+         fp = fdopen(cctx.sock, "rb");
         sigalrm_seen = FALSE;
-        alarm(timeout);
-        yield = cat_file(f, yield, sub_arg[3]);
-        alarm(0);
-        (void)fclose(f);
+        ALARM(timeout);
+        yield =
+#ifndef DISABLE_TLS
+         do_tls ? cat_file_tls(cctx.tls_ctx, yield, sub_arg[3]) :
+#endif
+                   cat_file(fp, yield, sub_arg[3]);
+        ALARM_CLR(0);
+
+#ifndef DISABLE_TLS
+       if (do_tls)
+         {
+         tls_close(cctx.tls_ctx, TRUE);
+         close(cctx.sock);
+         }
+       else
+#endif
+         (void)fclose(fp);
 
         /* After a timeout, we restore the pointer in the result, that is,
         make sure we add nothing from the socket. */
@@ -5096,9 +5295,9 @@ while (*s != 0)
        resetok = FALSE;
         f = fdopen(fd_out, "rb");
         sigalrm_seen = FALSE;
-        alarm(60);
+        ALARM(60);
        lookup_value = string_from_gstring(cat_file(f, NULL, NULL));
-        alarm(0);
+        ALARM_CLR(0);
         (void)fclose(f);
 
         /* Wait for the process to finish, applying the timeout, and inspect its
@@ -5182,7 +5381,6 @@ while (*s != 0)
     case EITEM_NHASH:
     case EITEM_SUBSTR:
       {
-      int i;
       int len;
       uschar *ret;
       int val[2] = { 0, -1 };
@@ -5215,9 +5413,8 @@ while (*s != 0)
           }
         }
 
-      for (i = 0; i < 2; i++)
+      for (int i = 0; i < 2; i++) if (sub[i])
         {
-        if (sub[i] == NULL) continue;
         val[i] = (int)Ustrtol(sub[i], &ret, 10);
         if (*ret != 0 || (i != 0 && val[i] < 0))
           {
@@ -5255,7 +5452,7 @@ while (*s != 0)
       md5 md5_base;
       hctx sha1_ctx;
       void *use_base;
-      int type, i;
+      int type;
       int hashlen;      /* Number of octets for the hash algorithm's output */
       int hashblocklen; /* Number of octets the hash algorithm processes */
       uschar *keyptr, *p;
@@ -5317,7 +5514,7 @@ while (*s != 0)
        memset(innerkey, 0x36, hashblocklen);
        memset(outerkey, 0x5c, hashblocklen);
 
-       for (i = 0; i < keylen; i++)
+       for (int i = 0; i < keylen; i++)
          {
          innerkey[i] ^= keyptr[i];
          outerkey[i] ^= keyptr[i];
@@ -5336,7 +5533,7 @@ while (*s != 0)
        /* Encode the final hash as a hex string */
 
        p = finalhash_hex;
-       for (i = 0; i < hashlen; i++)
+       for (int i = 0; i < hashlen; i++)
          {
          *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
          *p++ = hex_digits[finalhash[i] & 0x0f];
@@ -5398,7 +5595,6 @@ while (*s != 0)
         int ovector[3*(EXPAND_MAXN+1)];
         int n = pcre_exec(re, NULL, CS subject, slen, moffset + moffsetextra,
           PCRE_EOPT | emptyopt, ovector, nelem(ovector));
-        int nn;
         uschar *insert;
 
         /* No match - if we previously set PCRE_NOTEMPTY after a null match, this
@@ -5424,7 +5620,7 @@ while (*s != 0)
 
         if (n == 0) n = EXPAND_MAXN + 1;
         expand_nmax = 0;
-        for (nn = 0; nn < n*2; nn += 2)
+        for (int nn = 0; nn < n*2; nn += 2)
           {
           expand_nstring[expand_nmax] = subject + ovector[nn];
           expand_nlength[expand_nmax++] = ovector[nn+1] - ovector[nn];
@@ -5468,8 +5664,6 @@ while (*s != 0)
 
     case EITEM_EXTRACT:
       {
-      int i;
-      int j;
       int field_number = 1;
       BOOL field_number_set = FALSE;
       uschar *save_lookup_value = lookup_value;
@@ -5477,17 +5671,34 @@ while (*s != 0)
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
 
+      /* On reflection the original behaviour of extract-json for a string
+      result, leaving it quoted, was a mistake.  But it was already published,
+      hence the addition of jsons.  In a future major version, make json
+      work like josons, and withdraw jsons. */
+
+      enum {extract_basic, extract_json, extract_jsons} fmt = extract_basic;
+
+      while (isspace(*s)) s++;
+
+      /* Check for a format-variant specifier */
+
+      if (*s != '{')                                   /*}*/
+       if (Ustrncmp(s, "json", 4) == 0)
+         if (*(s += 4) == 's')
+           {fmt = extract_jsons; s++;}
+         else
+           fmt = extract_json;
+
       /* While skipping we cannot rely on the data for expansions being
       available (eg. $item) hence cannot decide on numeric vs. keyed.
       Read a maximum of 5 arguments (including the yes/no) */
 
       if (skipping)
        {
-        while (isspace(*s)) s++;
-        for (j = 5; j > 0 && *s == '{'; j--)
+        for (int j = 5; j > 0 && *s == '{'; j--)               /*'}'*/
          {
           if (!expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok))
-           goto EXPAND_FAILED;                                 /*{*/
+           goto EXPAND_FAILED;                                 /*'{'*/
           if (*s++ != '}')
            {
            expand_string_message = US"missing '{' for arg of extract";
@@ -5495,13 +5706,13 @@ while (*s != 0)
            }
          while (isspace(*s)) s++;
          }
-       if (  Ustrncmp(s, "fail", 4) == 0
+       if (  Ustrncmp(s, "fail", 4) == 0                       /*'{'*/
           && (s[4] == '}' || s[4] == ' ' || s[4] == '\t' || !s[4])
           )
          {
          s += 4;
          while (isspace(*s)) s++;
-         }
+         }                                                     /*'{'*/
        if (*s != '}')
          {
          expand_string_message = US"missing '}' closing extract";
@@ -5509,13 +5720,13 @@ while (*s != 0)
          }
        }
 
-      else for (i = 0, j = 2; i < j; i++) /* Read the proper number of arguments */
+      else for (int i = 0, j = 2; i < j; i++) /* Read the proper number of arguments */
         {
-        while (isspace(*s)) s++;
-        if (*s == '{')                                                 /*}*/
+       while (isspace(*s)) s++;
+        if (*s == '{')                                                 /*'}'*/
           {
           sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
-          if (sub[i] == NULL) goto EXPAND_FAILED;              /*{*/
+          if (sub[i] == NULL) goto EXPAND_FAILED;              /*'{'*/
           if (*s++ != '}')
            {
            expand_string_message = string_sprintf(
@@ -5526,7 +5737,7 @@ while (*s != 0)
           /* After removal of leading and trailing white space, the first
           argument must not be empty; if it consists entirely of digits
           (optionally preceded by a minus sign), this is a numerical
-          extraction, and we expect 3 arguments. */
+          extraction, and we expect 3 arguments (normal) or 2 (json). */
 
           if (i == 0)
             {
@@ -5557,7 +5768,7 @@ while (*s != 0)
            if (*p == 0)
              {
              field_number *= x;
-             j = 3;               /* Need 3 args */
+             if (fmt == extract_basic) j = 3;               /* Need 3 args */
              field_number_set = TRUE;
              }
             }
@@ -5573,9 +5784,96 @@ while (*s != 0)
       /* Extract either the numbered or the keyed substring into $value. If
       skipping, just pretend the extraction failed. */
 
-      lookup_value = skipping? NULL : field_number_set?
-        expand_gettokened(field_number, sub[1], sub[2]) :
-        expand_getkeyed(sub[0], sub[1]);
+      if (skipping)
+       lookup_value = NULL;
+      else switch (fmt)
+       {
+       case extract_basic:
+         lookup_value = field_number_set
+           ? expand_gettokened(field_number, sub[1], sub[2])
+           : expand_getkeyed(sub[0], sub[1]);
+         break;
+
+       case extract_json:
+       case extract_jsons:
+         {
+         uschar * s, * item;
+         const uschar * list;
+
+         /* Array: Bracket-enclosed and comma-separated.
+         Object: Brace-enclosed, comma-sep list of name:value pairs */
+
+         if (!(s = dewrap(sub[1], field_number_set ? US"[]" : US"{}")))
+           {
+           expand_string_message =
+             string_sprintf("%s wrapping %s for extract json",
+               expand_string_message,
+               field_number_set ? "array" : "object");
+           goto EXPAND_FAILED_CURLY;
+           }
+
+         list = s;
+         if (field_number_set)
+           {
+           if (field_number <= 0)
+             {
+             expand_string_message = US"first argument of \"extract\" must "
+               "be greater than zero";
+             goto EXPAND_FAILED;
+             }
+           while (field_number > 0 && (item = json_nextinlist(&list)))
+             field_number--;
+           if ((lookup_value = s = item))
+             {
+             while (*s) s++;
+             while (--s >= lookup_value && isspace(*s)) *s = '\0';
+             }
+           }
+         else
+           {
+           lookup_value = NULL;
+           while ((item = json_nextinlist(&list)))
+             {
+             /* Item is:  string name-sep value.  string is quoted.
+             Dequote the string and compare with the search key. */
+
+             if (!(item = dewrap(item, US"\"\"")))
+               {
+               expand_string_message =
+                 string_sprintf("%s wrapping string key for extract json",
+                   expand_string_message);
+               goto EXPAND_FAILED_CURLY;
+               }
+             if (Ustrcmp(item, sub[0]) == 0)   /*XXX should be a UTF8-compare */
+               {
+               s = item + Ustrlen(item) + 1;
+               while (isspace(*s)) s++;
+               if (*s != ':')
+                 {
+                 expand_string_message = string_sprintf(
+                   "missing object value-separator for extract json");
+                 goto EXPAND_FAILED_CURLY;
+                 }
+               s++;
+               while (isspace(*s)) s++;
+               lookup_value = s;
+               break;
+               }
+             }
+           }
+         }
+
+         if (  fmt == extract_jsons
+            && lookup_value
+            && !(lookup_value = dewrap(lookup_value, US"\"\"")))
+           {
+           expand_string_message =
+             string_sprintf("%s wrapping string result for extract jsons",
+               expand_string_message);
+           goto EXPAND_FAILED_CURLY;
+           }
+         break;        /* json/s */
+       }
 
       /* If no string follows, $value gets substituted; otherwise there can
       be yes/no strings, as for lookup or if. */
@@ -5605,7 +5903,6 @@ while (*s != 0)
 
     case EITEM_LISTEXTRACT:
       {
-      int i;
       int field_number = 1;
       uschar *save_lookup_value = lookup_value;
       uschar *sub[2];
@@ -5614,10 +5911,10 @@ while (*s != 0)
 
       /* Read the field & list arguments */
 
-      for (i = 0; i < 2; i++)
+      for (int i = 0; i < 2; i++)
         {
         while (isspace(*s)) s++;
-        if (*s != '{')                                 /*}*/
+        if (*s != '{')                                 /*'}'*/
          {
          expand_string_message = string_sprintf(
            "missing '{' for arg %d of listextract", i+1);
@@ -5675,7 +5972,7 @@ while (*s != 0)
       /* 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 = 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. */
@@ -5701,7 +5998,7 @@ while (*s != 0)
       continue;
       }
 
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
     case EITEM_CERTEXTRACT:
       {
       uschar *save_lookup_value = lookup_value;
@@ -5781,7 +6078,7 @@ while (*s != 0)
         save_expand_nlength);
       continue;
       }
-#endif /*SUPPORT_TLS*/
+#endif /*DISABLE_TLS*/
 
     /* Handle list operations */
 
@@ -6191,8 +6488,7 @@ while (*s != 0)
       /* 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. */
 
-      t = tree_search(dlobj_anchor, argv[0]);
-      if (t == NULL)
+      if (!(t = tree_search(dlobj_anchor, argv[0])))
         {
         void *handle = dlopen(CS argv[0], RTLD_LAZY);
         if (handle == NULL)
@@ -6240,7 +6536,7 @@ while (*s != 0)
       else
         {
         expand_string_message = result == NULL ? US"(no message)" : result;
-        if(status == FAIL_FORCED) expand_string_forcedfail = TRUE;
+        if(status == FAIL_FORCED) f.expand_string_forcedfail = TRUE;
           else if(status != FAIL)
             log_write(0, LOG_MAIN|LOG_PANIC, "dlfunc{%s}{%s} failed (%d): %s",
               argv[0], argv[1], status, expand_string_message);
@@ -6294,7 +6590,9 @@ while (*s != 0)
     int c;
     uschar *arg = NULL;
     uschar *sub;
+#ifndef DISABLE_TLS
     var_entry *vp = NULL;
+#endif
 
     /* Owing to an historical mis-design, an underscore may be part of the
     operator name, or it may introduce arguments.  We therefore first scan the
@@ -6315,7 +6613,7 @@ while (*s != 0)
     as we do not want to do the usual expansion. For most, expand the string.*/
     switch(c)
       {
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
       case EOP_MD5:
       case EOP_SHA1:
       case EOP_SHA256:
@@ -6419,7 +6717,6 @@ while (*s != 0)
 
       case EOP_BASE62D:
         {
-        uschar buf[16];
         uschar *tt = sub;
         unsigned long int n = 0;
         while (*tt != 0)
@@ -6434,8 +6731,7 @@ while (*s != 0)
             }
           n = n * BASE_62 + (t - base62_chars);
           }
-        (void)sprintf(CS buf, "%ld", n);
-        yield = string_cat(yield, buf);
+        yield = string_fmt_append(yield, "%ld", n);
         continue;
         }
 
@@ -6472,7 +6768,7 @@ while (*s != 0)
         }
 
       case EOP_MD5:
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_md5(*(void **)vp->value);
@@ -6483,17 +6779,15 @@ while (*s != 0)
          {
          md5 base;
          uschar digest[16];
-         int j;
-         char st[33];
          md5_start(&base);
          md5_end(&base, sub, Ustrlen(sub), digest);
-         for(j = 0; j < 16; j++) sprintf(st+2*j, "%02x", digest[j]);
-         yield = string_cat(yield, US st);
+         for (int j = 0; j < 16; j++)
+           yield = string_fmt_append(yield, "%02x", digest[j]);
          }
         continue;
 
       case EOP_SHA1:
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_sha1(*(void **)vp->value);
@@ -6504,40 +6798,41 @@ while (*s != 0)
          {
          hctx h;
          uschar digest[20];
-         int j;
-         char st[41];
          sha1_start(&h);
          sha1_end(&h, sub, Ustrlen(sub), digest);
-         for(j = 0; j < 20; j++) sprintf(st+2*j, "%02X", digest[j]);
-         yield = string_catn(yield, US st, 40);
+         for (int j = 0; j < 20; j++)
+           yield = string_fmt_append(yield, "%02X", digest[j]);
          }
         continue;
 
+      case EOP_SHA2:
       case EOP_SHA256:
 #ifdef EXIM_HAVE_SHA2
        if (vp && *(void **)vp->value)
-         {
-         uschar * cp = tls_cert_fprt_sha256(*(void **)vp->value);
-         yield = string_cat(yield, cp);
-         }
+         if (c == EOP_SHA256)
+           yield = string_cat(yield, tls_cert_fprt_sha256(*(void **)vp->value));
+         else
+           expand_string_message = US"sha2_N not supported with certificates";
        else
          {
          hctx h;
          blob b;
-         char st[3];
+         hashmethod m = !arg ? HASH_SHA2_256
+           : Ustrcmp(arg, "256") == 0 ? HASH_SHA2_256
+           : Ustrcmp(arg, "384") == 0 ? HASH_SHA2_384
+           : Ustrcmp(arg, "512") == 0 ? HASH_SHA2_512
+           : HASH_BADTYPE;
 
-         if (!exim_sha_init(&h, HASH_SHA2_256))
+         if (m == HASH_BADTYPE || !exim_sha_init(&h, m))
            {
-           expand_string_message = US"unrecognised sha256 variant";
+           expand_string_message = US"unrecognised sha2 variant";
            goto EXPAND_FAILED;
            }
+
          exim_sha_update(&h, sub, Ustrlen(sub));
          exim_sha_finish(&h, &b);
          while (b.len-- > 0)
-           {
-           sprintf(st, "%02X", *b.data++);
-           yield = string_catn(yield, US st, 2);
-           }
+           yield = string_fmt_append(yield, "%02X", *b.data++);
          }
 #else
          expand_string_message = US"sha256 only supported with TLS";
@@ -6549,7 +6844,6 @@ while (*s != 0)
        {
        hctx h;
        blob b;
-       char st[3];
        hashmethod m = !arg ? HASH_SHA3_256
          : Ustrcmp(arg, "224") == 0 ? HASH_SHA3_224
          : Ustrcmp(arg, "256") == 0 ? HASH_SHA3_256
@@ -6566,10 +6860,7 @@ while (*s != 0)
        exim_sha_update(&h, sub, Ustrlen(sub));
        exim_sha_finish(&h, &b);
        while (b.len-- > 0)
-         {
-         sprintf(st, "%02X", *b.data++);
-         yield = string_catn(yield, US st, 2);
-         }
+         yield = string_fmt_append(yield, "%02X", *b.data++);
        }
         continue;
 #else
@@ -6587,7 +6878,7 @@ while (*s != 0)
         uschar *out = sub;
         uschar *enc;
 
-        for (enc = sub; *enc != 0; enc++)
+        for (enc = sub; *enc; enc++)
           {
           if (!isxdigit(*enc))
             {
@@ -6610,9 +6901,7 @@ while (*s != 0)
           if (isdigit(c)) c -= '0';
           else c = toupper(c) - 'A' + 10;
           if (b == -1)
-            {
             b = c << 4;
-            }
           else
             {
             *out++ = b | c;
@@ -6620,7 +6909,7 @@ while (*s != 0)
             }
           }
 
-        enc = b64encode(sub, out - sub);
+        enc = b64encode(CUS sub, out - sub);
         yield = string_cat(yield, enc);
         continue;
         }
@@ -6633,7 +6922,7 @@ while (*s != 0)
         while (*(++t) != 0)
           {
           if (*t < 0x21 || 0x7E < *t)
-            yield = string_catn(yield, string_sprintf("\\x%02x", *t), 4);
+            yield = string_fmt_append(yield, "\\x%02x", *t);
          else
            yield = string_catn(yield, t, 1);
           }
@@ -6646,12 +6935,10 @@ while (*s != 0)
         {
        int cnt = 0;
        int sep = 0;
-       uschar * cp;
        uschar buffer[256];
 
        while (string_nextinlist(CUSS &sub, &sep, buffer, sizeof(buffer)) != NULL) cnt++;
-       cp = string_sprintf("%d", cnt);
-        yield = string_cat(yield, cp);
+       yield = string_fmt_append(yield, "%d", cnt);
         continue;
         }
 
@@ -6829,16 +7116,11 @@ while (*s != 0)
         uschar * t = parse_extract_address(sub, &error, &start, &end, &domain,
           FALSE);
         if (t)
-          if (c != EOP_DOMAIN)
-            {
-            if (c == EOP_LOCAL_PART && domain != 0) end = start + domain - 1;
-            yield = string_catn(yield, sub+start, end-start);
-            }
-          else if (domain != 0)
-            {
-            domain += start;
-            yield = string_catn(yield, sub+domain, end-domain);
-            }
+         yield = c == EOP_DOMAIN
+           ? string_cat(yield, t + domain)
+           : c == EOP_LOCAL_PART && domain > 0
+           ? string_catn(yield, t, domain - 1 )
+           : string_cat(yield, t);
         continue;
         }
 
@@ -6858,11 +7140,11 @@ while (*s != 0)
               "missing in expanding ${addresses:%s}", --sub);
             goto EXPAND_FAILED;
             }
-        parse_allow_group = TRUE;
+        f.parse_allow_group = TRUE;
 
         for (;;)
           {
-          uschar *p = parse_find_address_end(sub, FALSE);
+          uschar * p = parse_find_address_end(sub, FALSE);
           uschar saveend = *p;
           *p = '\0';
           address = parse_extract_address(sub, &error, &start, &end, &domain,
@@ -6875,7 +7157,7 @@ while (*s != 0)
           list, add in a space if the new address begins with the separator
           character, or is an empty string. */
 
-          if (address != NULL)
+          if (address)
             {
             if (yield->ptr != save_ptr && address[0] == *outsep)
               yield = string_catn(yield, US" ", 1);
@@ -6907,7 +7189,7 @@ while (*s != 0)
         separator. */
 
         if (yield->ptr != save_ptr) yield->ptr--;
-        parse_allow_group = FALSE;
+        f.parse_allow_group = FALSE;
         continue;
         }
 
@@ -7017,9 +7299,9 @@ while (*s != 0)
       case EOP_RFC2047:
         {
         uschar buffer[2048];
-       const uschar *string = parse_quote_2047(sub, Ustrlen(sub), headers_charset,
-          buffer, sizeof(buffer), FALSE);
-        yield = string_cat(yield, string);
+        yield = string_cat(yield,
+                           parse_quote_2047(sub, Ustrlen(sub), headers_charset,
+                             buffer, sizeof(buffer), FALSE));
         continue;
         }
 
@@ -7065,12 +7347,13 @@ while (*s != 0)
         {
         int seq_len = 0, index = 0;
         int bytes_left = 0;
-       long codepoint = -1;
+        long codepoint = -1;
+        int complete;
         uschar seq_buff[4];                    /* accumulate utf-8 here */
 
         while (*sub != 0)
          {
-         int complete = 0;
+         complete = 0;
          uschar c = *sub++;
 
          if (bytes_left)
@@ -7135,6 +7418,13 @@ while (*s != 0)
                        /* ASCII character follows incomplete sequence */
              yield = string_catn(yield, &c, 1);
          }
+        /* If given a sequence truncated mid-character, we also want to report ?
+        * Eg, ${length_1:フィル} is one byte, not one character, so we expect
+        * ${utf8clean:${length_1:フィル}} to yield '?' */
+        if (bytes_left != 0)
+          {
+          yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1);
+          }
         continue;
         }
 
@@ -7212,13 +7502,12 @@ while (*s != 0)
 
       case EOP_ESCAPE8BIT:
        {
-       const uschar * s = sub;
        uschar c;
 
-       for (s = sub; (c = *s); s++)
+       for (const uschar * s = sub; (c = *s); s++)
          yield = c < 127 && c != '\\'
            ? string_catn(yield, s, 1)
-           : string_catn(yield, string_sprintf("\\%03o", c), 4);
+           : string_fmt_append(yield, "\\%03o", c);
        continue;
        }
 
@@ -7230,19 +7519,18 @@ while (*s != 0)
         uschar *save_sub = sub;
         uschar *error = NULL;
         int_eximarith_t n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE);
-        if (error != NULL)
+        if (error)
           {
           expand_string_message = string_sprintf("error in expression "
             "evaluation: %s (after processing \"%.*s\")", error,
            (int)(sub-save_sub), save_sub);
           goto EXPAND_FAILED;
           }
-        sprintf(CS var_buffer, PR_EXIM_ARITH, n);
-        yield = string_cat(yield, var_buffer);
+        yield = string_fmt_append(yield, PR_EXIM_ARITH, n);
         continue;
         }
 
-      /* Handle time period formating */
+      /* Handle time period formatting */
 
       case EOP_TIME_EVAL:
         {
@@ -7253,8 +7541,7 @@ while (*s != 0)
             "Exim time interval in \"%s\" operator", sub, name);
           goto EXPAND_FAILED;
           }
-        sprintf(CS var_buffer, "%d", n);
-        yield = string_cat(yield, var_buffer);
+        yield = string_fmt_append(yield, "%d", n);
         continue;
         }
 
@@ -7278,12 +7565,12 @@ while (*s != 0)
       case EOP_STR2B64:
       case EOP_BASE64:
        {
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
        uschar * s = vp && *(void **)vp->value
          ? tls_cert_der_b64(*(void **)vp->value)
-         : b64encode(sub, Ustrlen(sub));
+         : b64encode(CUS sub, Ustrlen(sub));
 #else
-       uschar * s = b64encode(sub, Ustrlen(sub));
+       uschar * s = b64encode(CUS sub, Ustrlen(sub));
 #endif
        yield = string_cat(yield, s);
        continue;
@@ -7306,12 +7593,8 @@ while (*s != 0)
       /* strlen returns the length of the string */
 
       case EOP_STRLEN:
-        {
-        uschar buff[24];
-        (void)sprintf(CS buff, "%d", Ustrlen(sub));
-        yield = string_cat(yield, buff);
+        yield = string_fmt_append(yield, "%d", Ustrlen(sub));
         continue;
-        }
 
       /* length_n or l_n takes just the first n characters or the whole string,
       whichever is the shorter;
@@ -7344,7 +7627,7 @@ while (*s != 0)
         int len;
         uschar *ret;
 
-        if (arg == NULL)
+        if (!arg)
           {
           expand_string_message = string_sprintf("missing values after %s",
             name);
@@ -7408,14 +7691,12 @@ while (*s != 0)
 
       case EOP_STAT:
         {
-        uschar *s;
         uschar smode[12];
         uschar **modetable[3];
-        int i;
         mode_t mode;
         struct stat st;
 
-        if ((expand_forbid & RDO_EXISTS) != 0)
+        if (expand_forbid & RDO_EXISTS)
           {
           expand_string_message = US"Use of the stat() expansion is not permitted";
           goto EXPAND_FAILED;
@@ -7442,20 +7723,20 @@ while (*s != 0)
         modetable[1] = ((mode & 02000) == 0)? mtable_normal : mtable_setid;
         modetable[2] = ((mode & 04000) == 0)? mtable_normal : mtable_setid;
 
-        for (i = 0; i < 3; i++)
+        for (int i = 0; i < 3; i++)
           {
           memcpy(CS(smode + 7 - i*3), CS(modetable[i][mode & 7]), 3);
           mode >>= 3;
           }
 
         smode[10] = 0;
-        s = string_sprintf("mode=%04lo smode=%s inode=%ld device=%ld links=%ld "
+        yield = string_fmt_append(yield,
+         "mode=%04lo smode=%s inode=%ld device=%ld links=%ld "
           "uid=%ld gid=%ld size=" OFF_T_FMT " atime=%ld mtime=%ld ctime=%ld",
           (long)(st.st_mode & 077777), smode, (long)st.st_ino,
           (long)st.st_dev, (long)st.st_nlink, (long)st.st_uid,
           (long)st.st_gid, st.st_size, (long)st.st_atime,
           (long)st.st_mtime, (long)st.st_ctime);
-        yield = string_cat(yield, s);
         continue;
         }
 
@@ -7463,14 +7744,11 @@ while (*s != 0)
 
       case EOP_RANDINT:
         {
-        int_eximarith_t max;
-        uschar *s;
+        int_eximarith_t max = expanded_string_integer(sub, TRUE);
 
-        max = expanded_string_integer(sub, TRUE);
-        if (expand_string_message != NULL)
+        if (expand_string_message)
           goto EXPAND_FAILED;
-        s = string_sprintf("%d", vaguely_random_number((int)max));
-        yield = string_cat(yield, s);
+        yield = string_fmt_append(yield, "%d", vaguely_random_number((int)max));
         continue;
         }
 
@@ -7496,9 +7774,9 @@ while (*s != 0)
       /* Unknown operator */
 
       default:
-      expand_string_message =
-        string_sprintf("unknown expansion operator \"%s\"", name);
-      goto EXPAND_FAILED;
+       expand_string_message =
+         string_sprintf("unknown expansion operator \"%s\"", name);
+       goto EXPAND_FAILED;
       }
     }
 
@@ -7579,19 +7857,28 @@ if (resetok) store_reset(yield->s + (yield->size = yield->ptr + 1));
 else if (resetok_p) *resetok_p = FALSE;
 
 DEBUG(D_expand)
-  {
-  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 (skipping)
-    debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-      "skipping: result is not used\n");
-  }
+  DEBUG(D_noutf8)
+    {
+    debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
+    debug_printf_indent("%sresult: %s\n",
+      skipping ? "|-----" : "\\_____", yield->s);
+    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 (skipping)
+      debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
+       "skipping: result is not used\n");
+    }
 expand_level--;
 return yield->s;
 
@@ -7613,16 +7900,25 @@ that is a bad idea, because expand_string_message is in dynamic store. */
 EXPAND_FAILED:
 if (left) *left = s;
 DEBUG(D_expand)
-  {
-  debug_printf_indent(UTF8_VERT_RIGHT "failed to expand: %s\n",
-    string);
-  debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-    "error message: %s\n",
-    expand_string_forcedfail ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
-    expand_string_message);
-  if (expand_string_forcedfail)
-    debug_printf_indent(UTF8_UP_RIGHT "failure was forced\n");
-  }
+  DEBUG(D_noutf8)
+    {
+    debug_printf_indent("|failed to expand: %s\n", string);
+    debug_printf_indent("%serror message: %s\n",
+      f.expand_string_forcedfail ? "|---" : "\\___", expand_string_message);
+    if (f.expand_string_forcedfail)
+      debug_printf_indent("\\failure was forced\n");
+    }
+  else
+    {
+    debug_printf_indent(UTF8_VERT_RIGHT "failed to expand: %s\n",
+      string);
+    debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
+      "error message: %s\n",
+      f.expand_string_forcedfail ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
+      expand_string_message);
+    if (f.expand_string_forcedfail)
+      debug_printf_indent(UTF8_UP_RIGHT "failure was forced\n");
+    }
 if (resetok_p && !resetok) *resetok_p = FALSE;
 expand_level--;
 return NULL;
@@ -7645,7 +7941,7 @@ if (Ustrpbrk(string, "$\\") != NULL)
   int old_pool = store_pool;
   uschar * s;
 
-  search_find_defer = FALSE;
+  f.search_find_defer = FALSE;
   malformed_header = FALSE;
   store_pool = POOL_MAIN;
     s = expand_string_internal(string, FALSE, NULL, FALSE, TRUE, NULL);
@@ -7838,7 +8134,7 @@ if (svalue == NULL) { *rvalue = bvalue; return OK; }
 expanded = expand_string(svalue);
 if (expanded == NULL)
   {
-  if (expand_string_forcedfail)
+  if (f.expand_string_forcedfail)
     {
     DEBUG(dbg_opt) debug_printf("expansion of \"%s\" forced failure\n", oname);
     *rvalue = bvalue;
@@ -7876,7 +8172,7 @@ expand_hide_passwords(uschar * s)
 {
 return (  (  Ustrstr(s, "failed to expand") != NULL
          || Ustrstr(s, "expansion of ")    != NULL
-         ) 
+         )
        && (  Ustrstr(s, "mysql")   != NULL
          || Ustrstr(s, "pgsql")   != NULL
          || Ustrstr(s, "redis")   != NULL
@@ -7886,7 +8182,7 @@ return (  (  Ustrstr(s, "failed to expand") != NULL
          || Ustrstr(s, "ldapi:")  != NULL
          || Ustrstr(s, "ldapdn:") != NULL
          || Ustrstr(s, "ldapm:")  != NULL
-       )  ) 
+       )  )
   ? US"Temporary internal error" : s;
 }
 
@@ -7957,23 +8253,21 @@ assert_no_variables(void * ptr, int len, const char * filename, int linenumber)
 {
 err_ctx e = { .region_start = ptr, .region_end = US ptr + len,
              .var_name = NULL, .var_data = NULL };
-int i;
-var_entry * v;
 
 /* check acl_ variables */
 tree_walk(acl_var_c, assert_variable_notin, &e);
 tree_walk(acl_var_m, assert_variable_notin, &e);
 
 /* check auth<n> variables */
-for (i = 0; i < AUTH_VARS; i++) if (auth_vars[i])
+for (int i = 0; i < AUTH_VARS; i++) if (auth_vars[i])
   assert_variable_notin(US"auth<n>", auth_vars[i], &e);
 
 /* check regex<n> variables */
-for (i = 0; i < REGEX_VARS; i++) if (regex_vars[i])
+for (int i = 0; i < REGEX_VARS; i++) if (regex_vars[i])
   assert_variable_notin(US"regex<n>", regex_vars[i], &e);
 
 /* check known-name variables */
-for (v = var_table; v < var_table + var_table_size; v++)
+for (var_entry * v = var_table; v < var_table + var_table_size; v++)
   if (v->type == vtype_stringptr)
     assert_variable_notin(US v->name, *(USS v->value), &e);
 
@@ -8010,9 +8304,8 @@ BOOL yield = n >= 0;
 if (n == 0) n = EXPAND_MAXN + 1;
 if (yield)
   {
-  int nn;
-  expand_nmax = (setup < 0)? 0 : setup + 1;
-  for (nn = (setup < 0)? 0 : 2; nn < n*2; nn += 2)
+  expand_nmax = setup < 0 ? 0 : setup + 1;
+  for (int nn = setup < 0 ? 0 : 2; nn < n*2; nn += 2)
     {
     expand_nstring[expand_nmax] = subject + ovector[nn];
     expand_nlength[expand_nmax++] = ovector[nn+1] - ovector[nn];
@@ -8025,7 +8318,6 @@ return yield;
 
 int main(int argc, uschar **argv)
 {
-int i;
 uschar buffer[1024];
 
 debug_selector = D_v;
@@ -8033,7 +8325,7 @@ debug_file = stderr;
 debug_fd = fileno(debug_file);
 big_buffer = malloc(big_buffer_size);
 
-for (i = 1; i < argc; i++)
+for (int i = 1; i < argc; i++)
   {
   if (argv[i][0] == '+')
     {
@@ -8095,9 +8387,9 @@ while (fgets(buffer, sizeof(buffer), stdin) != NULL)
     }
   else
     {
-    if (search_find_defer) printf("search_find deferred\n");
+    if (f.search_find_defer) printf("search_find deferred\n");
     printf("Failed: %s\n", expand_string_message);
-    if (expand_string_forcedfail) printf("Forced failure\n");
+    if (f.expand_string_forcedfail) printf("Forced failure\n");
     printf("\n");
     }
   }