TLS: add variables for the IETF standard name for the connection ciphersuite
[exim.git] / src / src / expand.c
index cd753ef0615e402856dea2b04c10719eee397623..e3615cdb05fd1d08bd5da970e876e631266d602f 100644 (file)
@@ -312,7 +312,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 +362,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,
@@ -741,6 +749,7 @@ 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 },
@@ -751,6 +760,7 @@ static var_entry var_table[] = {
   { "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
@@ -1133,20 +1143,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))));
@@ -1257,17 +1267,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);
 }
 
@@ -1301,7 +1311,6 @@ static uschar *
 expand_getcertele(uschar * field, uschar * certvar)
 {
 var_entry * vp;
-certfield * cp;
 
 if (!(vp = find_var_ent(certvar)))
   {
@@ -1323,9 +1332,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) == ','
@@ -1561,10 +1570,9 @@ find_header(uschar *name, int *newsize, unsigned flags, uschar *charset)
 BOOL found = !name;
 int len = name ? Ustrlen(name) : 0;
 BOOL comma = FALSE;
-header_line * h;
 gstring * g = NULL;
 
-for (h = header_list; h; h = h->next)
+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))
       {
@@ -1687,7 +1695,7 @@ else
   return g;
 
 if (sender_host_address)
-  g = string_append(g, 2, US" smtp.client-ip=", sender_host_address);
+  g = string_append(g, 2, US" smtp.remote-ip=", sender_host_address);
 return g;
 }
 
@@ -1705,11 +1713,10 @@ fn_recipients(void)
 {
 uschar * s;
 gstring * g = NULL;
-int i;
 
 if (!f.enable_dollar_recipients) return NULL;
 
-for (i = 0; i < recipients_count; i++)
+for (int i = 0; i < recipients_count; i++)
   {
   s = recipients_list[i].address;
   g = string_append2_listele_n(g, US", ", s, Ustrlen(s));
@@ -1957,7 +1964,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;
@@ -2028,11 +2035,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 != '{')
     {
@@ -2148,6 +2154,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           *
 *************************************************/
@@ -2174,7 +2263,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];
@@ -2524,7 +2614,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
@@ -2741,16 +2831,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);
@@ -2779,16 +2868,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);
@@ -2951,8 +3039,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;
@@ -2993,10 +3087,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))
         {
@@ -3008,8 +3114,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;
@@ -3145,8 +3251,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];
@@ -3174,9 +3279,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];
@@ -3465,7 +3569,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];
@@ -3490,7 +3593,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];
@@ -3505,7 +3608,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];
@@ -3558,11 +3661,12 @@ static gstring *
 cat_file_tls(void * tls_ctx, gstring * yield, uschar * eol)
 {
 int rc;
-uschar * s;
 uschar buffer[1024];
 
+/*XXX could we read direct into a pre-grown string? */
+
 while ((rc = tls_read(tls_ctx, buffer, sizeof(buffer))) > 0)
-  for (s = buffer; rc--; s++)
+  for (uschar * s = buffer; rc--; s++)
     yield = eol && *s == '\n'
       ? string_cat(yield, eol) : string_catn(yield, s, 1);
 
@@ -3925,11 +4029,15 @@ 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);
 
 f.expand_string_forcedfail = FALSE;
 expand_string_message = US"";
@@ -4218,15 +4326,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;
 
@@ -4723,7 +4837,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;
 
@@ -4930,6 +5044,7 @@ while (*s != 0)
             port = ntohs(service_info->s_port);
             }
 
+         /*XXX we trust that the request is idempotent.  Hmm. */
          fd = ip_connectedsocket(SOCK_STREAM, server_name, port, port,
                  timeout, &host, &expand_string_message,
                  do_tls ? NULL : &reqstr);
@@ -4960,9 +5075,9 @@ while (*s != 0)
          server_name = US sockun.sun_path;
 
           sigalrm_seen = FALSE;
-          alarm(timeout);
+          ALARM(timeout);
           rc = connect(fd, (struct sockaddr *)(&sockun), sizeof(sockun));
-          alarm(0);
+          ALARM_CLR(0);
           if (sigalrm_seen)
             {
             expand_string_message = US "socket connect timed out";
@@ -5035,13 +5150,13 @@ while (*s != 0)
        if (!tls_ctx)
          fp = fdopen(fd, "rb");
         sigalrm_seen = FALSE;
-        alarm(timeout);
+        ALARM(timeout);
         yield =
 #ifdef SUPPORT_TLS
          tls_ctx ? cat_file_tls(tls_ctx, yield, sub_arg[3]) :
 #endif
                    cat_file(fp, yield, sub_arg[3]);
-        alarm(0);
+        ALARM_CLR(0);
 
 #ifdef SUPPORT_TLS
        if (tls_ctx)
@@ -5174,9 +5289,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
@@ -5260,7 +5375,6 @@ while (*s != 0)
     case EITEM_NHASH:
     case EITEM_SUBSTR:
       {
-      int i;
       int len;
       uschar *ret;
       int val[2] = { 0, -1 };
@@ -5293,9 +5407,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))
           {
@@ -5333,7 +5446,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;
@@ -5395,7 +5508,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];
@@ -5414,7 +5527,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];
@@ -5476,7 +5589,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
@@ -5502,7 +5614,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];
@@ -5546,8 +5658,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;
@@ -5555,17 +5665,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";
@@ -5573,13 +5700,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";
@@ -5587,13 +5714,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(
@@ -5604,7 +5731,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)
             {
@@ -5635,7 +5762,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;
              }
             }
@@ -5651,9 +5778,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. */
@@ -5683,7 +5897,6 @@ while (*s != 0)
 
     case EITEM_LISTEXTRACT:
       {
-      int i;
       int field_number = 1;
       uschar *save_lookup_value = lookup_value;
       uschar *sub[2];
@@ -5692,10 +5905,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);
@@ -5753,7 +5966,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. */
@@ -6269,8 +6482,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)
@@ -6499,7 +6711,6 @@ while (*s != 0)
 
       case EOP_BASE62D:
         {
-        uschar buf[16];
         uschar *tt = sub;
         unsigned long int n = 0;
         while (*tt != 0)
@@ -6514,8 +6725,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;
         }
 
@@ -6563,12 +6773,10 @@ 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;
 
@@ -6584,12 +6792,10 @@ 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;
 
@@ -6604,7 +6810,6 @@ while (*s != 0)
          {
          hctx h;
          blob b;
-         char st[3];
 
          if (!exim_sha_init(&h, HASH_SHA2_256))
            {
@@ -6614,10 +6819,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++);
          }
 #else
          expand_string_message = US"sha256 only supported with TLS";
@@ -6629,7 +6831,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
@@ -6646,10 +6847,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
@@ -6667,7 +6865,7 @@ while (*s != 0)
         uschar *out = sub;
         uschar *enc;
 
-        for (enc = sub; *enc != 0; enc++)
+        for (enc = sub; *enc; enc++)
           {
           if (!isxdigit(*enc))
             {
@@ -6690,9 +6888,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;
@@ -6700,7 +6896,7 @@ while (*s != 0)
             }
           }
 
-        enc = b64encode(sub, out - sub);
+        enc = b64encode(CUS sub, out - sub);
         yield = string_cat(yield, enc);
         continue;
         }
@@ -6713,7 +6909,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);
           }
@@ -6726,12 +6922,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;
         }
 
@@ -7097,9 +7291,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;
         }
 
@@ -7300,13 +7494,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;
        }
 
@@ -7318,19 +7511,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:
         {
@@ -7341,8 +7533,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;
         }
 
@@ -7369,9 +7560,9 @@ while (*s != 0)
 #ifdef SUPPORT_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;
@@ -7394,12 +7585,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;
@@ -7432,7 +7619,7 @@ while (*s != 0)
         int len;
         uschar *ret;
 
-        if (arg == NULL)
+        if (!arg)
           {
           expand_string_message = string_sprintf("missing values after %s",
             name);
@@ -7496,14 +7683,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;
@@ -7530,20 +7715,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;
         }
 
@@ -7551,14 +7736,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;
         }
 
@@ -7584,9 +7766,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;
       }
     }
 
@@ -7667,19 +7849,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;
 
@@ -7701,16 +7892,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",
-    f.expand_string_forcedfail ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
-    expand_string_message);
-  if (f.expand_string_forcedfail)
-    debug_printf_indent(UTF8_UP_RIGHT "failure was forced\n");
-  }
+  DEBUG(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;
@@ -8045,23 +8245,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);
 
@@ -8098,9 +8296,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];
@@ -8113,7 +8310,6 @@ return yield;
 
 int main(int argc, uschar **argv)
 {
-int i;
 uschar buffer[1024];
 
 debug_selector = D_v;
@@ -8121,7 +8317,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] == '+')
     {