Expansions: better error messages for parse errors, and better debug
[exim.git] / src / src / expand.c
index 5bb9df1b63dfe944bc8af18be5aafb9d340e792e..5189cdcfed5af230a5f80d60467f1271c1ec807c 100644 (file)
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* Copyright (c) University of Cambridge 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
@@ -480,10 +480,6 @@ static var_entry var_table[] = {
   { "dcc_header",          vtype_stringptr,   &dcc_header },
   { "dcc_result",          vtype_stringptr,   &dcc_result },
 #endif
-#ifdef WITH_OLD_DEMIME
-  { "demime_errorlevel",   vtype_int,         &demime_errorlevel },
-  { "demime_reason",       vtype_stringptr,   &demime_reason },
-#endif
 #ifndef DISABLE_DKIM
   { "dkim_algo",           vtype_dkim,        (void *)DKIM_ALGO },
   { "dkim_bodylength",     vtype_dkim,        (void *)DKIM_BODYLENGTH },
@@ -532,9 +528,6 @@ static var_entry var_table[] = {
   { "exim_path",           vtype_stringptr,   &exim_path },
   { "exim_uid",            vtype_uid,         &exim_uid },
   { "exim_version",        vtype_stringptr,   &version_string },
-#ifdef WITH_OLD_DEMIME
-  { "found_extension",     vtype_stringptr,   &found_extension },
-#endif
   { "headers_added",       vtype_string_func, &fn_hdrs_added },
   { "home",                vtype_stringptr,   &deliver_home },
   { "host",                vtype_stringptr,   &deliver_host },
@@ -543,6 +536,7 @@ static var_entry var_table[] = {
   { "host_lookup_deferred",vtype_int,         &host_lookup_deferred },
   { "host_lookup_failed",  vtype_int,         &host_lookup_failed },
   { "host_port",           vtype_int,         &deliver_host_port },
+  { "initial_cwd",         vtype_stringptr,   &initial_cwd },
   { "inode",               vtype_ino,         &deliver_inode },
   { "interface_address",   vtype_stringptr,   &interface_address },
   { "interface_port",      vtype_int,         &interface_port },
@@ -1675,9 +1669,8 @@ if (!enable_dollar_recipients) return NULL; else
   uschar * s = store_get(size);
   for (i = 0; i < recipients_count; i++)
     {
-    if (i != 0) s = string_cat(s, &size, &ptr, US", ", 2);
-    s = string_cat(s, &size, &ptr, recipients_list[i].address,
-      Ustrlen(recipients_list[i].address));
+    if (i != 0) s = string_catn(s, &size, &ptr, US", ", 2);
+    s = string_cat(s, &size, &ptr, recipients_list[i].address);
     }
   s[ptr] = 0;     /* string_cat() leaves room */
   return s;
@@ -1998,12 +1991,17 @@ for (i = 0; i < n; i++)
   {
   if (*s != '{')
     {
-    if (i < m) return 1;
+    if (i < m)
+      {
+      expand_string_message = string_sprintf("Not enough arguments for '%s' "
+       "(min is %d)", name, m);
+      return 1;
+      }
     sub[i] = NULL;
     break;
     }
-  sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, resetok);
-  if (sub[i] == NULL) return 3;
+  if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, resetok)))
+    return 3;
   if (*s++ != '}') return 1;
   while (isspace(*s)) s++;
   }
@@ -2011,10 +2009,11 @@ if (check_end && *s++ != '}')
   {
   if (s[-1] == '{')
     {
-    expand_string_message = string_sprintf("Too many arguments for \"%s\" "
+    expand_string_message = string_sprintf("Too many arguments for '%s' "
       "(max is %d)", name, n);
     return 2;
     }
+  expand_string_message = string_sprintf("missing '}' after '%s'", name);
   return 1;
   }
 
@@ -2057,7 +2056,7 @@ Load args from sub array to globals, and call acl_check().
 Sub array will be corrupted on return.
 
 Returns:       OK         access is granted by an ACCEPT verb
-               DISCARD    access is granted by a DISCARD verb
+               DISCARD    access is (apparently) granted by a DISCARD verb
               FAIL       access is denied
               FAIL_DROP  access is denied; drop the connection
               DEFER      can't tell at the moment
@@ -2372,7 +2371,7 @@ switch(cond_type)
       case 3: return NULL;
       }
 
-    *resetok = FALSE;
+    *resetok = FALSE;  /* eval_acl() might allocate; do not reclaim */
     if (yield != NULL) switch(eval_acl(sub, nelem(sub), &user_msg))
        {
        case OK:
@@ -2381,7 +2380,7 @@ switch(cond_type)
           lookup_value = NULL;
          if (user_msg)
            {
-            lookup_value = string_cat(NULL, &size, &ptr, user_msg, Ustrlen(user_msg));
+            lookup_value = string_cat(NULL, &size, &ptr, user_msg);
             lookup_value[ptr] = '\0';
            }
          *yield = cond == testfor;
@@ -2389,6 +2388,7 @@ switch(cond_type)
 
        case DEFER:
           expand_string_forcedfail = TRUE;
+         /*FALLTHROUGH*/
        default:
           expand_string_message = string_sprintf("error from acl \"%s\"", sub[0]);
          return NULL;
@@ -3171,6 +3171,7 @@ process_yesno(BOOL skipping, BOOL yes, uschar *save_lookup, const uschar **sptr,
 int rc = 0;
 const uschar *s = *sptr;    /* Local value */
 uschar *sub1, *sub2;
+const uschar * errwhere;
 
 /* If there are no following strings, we substitute the contents of $value for
 lookups and for extractions in the success case. For the ${if item, the string
@@ -3182,13 +3183,12 @@ if (*s == '}')
   {
   if (type[0] == 'i')
     {
-    if (yes) *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, US"true", 4);
+    if (yes) *yieldptr = string_catn(*yieldptr, sizeptr, ptrptr, US"true", 4);
     }
   else
     {
     if (yes && lookup_value)
-      *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, lookup_value,
-        Ustrlen(lookup_value));
+      *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, lookup_value);
     lookup_value = save_lookup;
     }
   s++;
@@ -3197,7 +3197,11 @@ if (*s == '}')
 
 /* The first following string must be braced. */
 
-if (*s++ != '{') goto FAILED_CURLY;
+if (*s++ != '{')
+  {
+  errwhere = US"'yes' part did not start with '{'";
+  goto FAILED_CURLY;
+  }
 
 /* Expand the first substring. Forced failures are noticed only if we actually
 want this string. Set skipping in the call in the fail case (this will always
@@ -3206,12 +3210,16 @@ 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 (*s++ != '}') goto FAILED_CURLY;
+if (*s++ != '}')
+  {
+  errwhere = US"'yes' part did not end with '}'";
+  goto FAILED_CURLY;
+  }
 
 /* If we want the first string, add it to the output */
 
 if (yes)
-  *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, sub1, Ustrlen(sub1));
+  *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, sub1);
 
 /* If this is called from a lookup/env or a (cert)extract, we want to restore
 $value to what it was at the start of the item, so that it has this value
@@ -3232,12 +3240,16 @@ 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 (*s++ != '}') goto FAILED_CURLY;
+  if (*s++ != '}')
+    {
+    errwhere = US"'no' part did not start with '{'";
+    goto FAILED_CURLY;
+    }
 
   /* If we want the second string, add it to the output */
 
   if (!yes)
-    *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, sub2, Ustrlen(sub2));
+    *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, sub2);
   }
 
 /* If there is no second string, but the word "fail" is present when the use of
@@ -3255,7 +3267,11 @@ else if (*s != '}')
     if (!yes && !skipping)
       {
       while (isspace(*s)) s++;
-      if (*s++ != '}') goto FAILED_CURLY;
+      if (*s++ != '}')
+        {
+       errwhere = US"did not close with '}' after forcedfail";
+       goto FAILED_CURLY;
+       }
       expand_string_message =
         string_sprintf("\"%s\" failed and \"fail\" requested", type);
       expand_string_forcedfail = TRUE;
@@ -3273,23 +3289,30 @@ else if (*s != '}')
 /* All we have to do now is to check on the final closing brace. */
 
 while (isspace(*s)) s++;
-if (*s++ == '}') goto RETURN;
-
-/* Get here if there is a bracketing failure */
-
-FAILED_CURLY:
-rc++;
-
-/* Get here for other failures */
-
-FAILED:
-rc++;
+if (*s++ != '}')
+  {
+  errwhere = US"did not close with '}'";
+  goto FAILED_CURLY;
+  }
 
-/* Update the input pointer value before returning */
 
 RETURN:
+/* Update the input pointer value before returning */
 *sptr = s;
 return rc;
+
+FAILED_CURLY:
+  /* Get here if there is a bracketing failure */
+  expand_string_message = string_sprintf(
+    "curly-bracket problem in conditional yes/no parsing: %s\n"
+    " remaining string is '%s'", errwhere, --s);
+  rc = 2;
+  goto RETURN;
+
+FAILED:
+  /* Get here for other failures */
+  rc = 1;
+  goto RETURN;
 }
 
 
@@ -3406,9 +3429,9 @@ if (key_num == NULL)
 if (Ustrlen(key) > 64)
   return NULL;
 
-hash_source = string_cat(NULL,&size,&offset,key_num,1);
-string_cat(hash_source,&size,&offset,daystamp,3);
-string_cat(hash_source,&size,&offset,address,Ustrlen(address));
+hash_source = string_catn(NULL, &size, &offset, key_num, 1);
+hash_source = string_catn(hash_source, &size, &offset, daystamp, 3);
+hash_source = string_cat(hash_source, &size, &offset, address);
 hash_source[offset] = '\0';
 
 DEBUG(D_expand) debug_printf("prvs: hash source is '%s'\n", hash_source);
@@ -3472,9 +3495,9 @@ while (Ufgets(buffer, sizeof(buffer), f))
   {
   int len = Ustrlen(buffer);
   if (eol && buffer[len-1] == '\n') len--;
-  yield = string_cat(yield, sizep, ptrp, buffer, len);
+  yield = string_catn(yield, sizep, ptrp, buffer, len);
   if (buffer[len] != 0)
-    yield = string_cat(yield, sizep, ptrp, eol, eollen);
+    yield = string_catn(yield, sizep, ptrp, eol, eollen);
   }
 
 if (yield) yield[*ptrp] = 0;
@@ -3651,13 +3674,20 @@ eval_op_sum(uschar **sptr, BOOL decimal, uschar **error)
 {
 uschar *s = *sptr;
 int_eximarith_t x = eval_op_mult(&s, decimal, error);
-if (*error == NULL)
+if (!*error)
   {
   while (*s == '+' || *s == '-')
     {
     int op = *s++;
     int_eximarith_t y = eval_op_mult(&s, decimal, error);
-    if (*error != NULL) break;
+    if (*error) break;
+    if (  (x >=   EXIM_ARITH_MAX/2  && x >=   EXIM_ARITH_MAX/2)
+       || (x <= -(EXIM_ARITH_MAX/2) && y <= -(EXIM_ARITH_MAX/2)))
+      {                        /* over-conservative check */
+      *error = op == '+'
+       ? US"overflow in sum" : US"overflow in difference";
+      break;
+      }
     if (op == '+') x += y; else x -= y;
     }
   }
@@ -3828,6 +3858,9 @@ uschar *save_expand_nstring[EXPAND_MAXN+1];
 int save_expand_nlength[EXPAND_MAXN+1];
 BOOL resetok = TRUE;
 
+DEBUG(D_expand)
+  debug_printf("%s: %s\n", skipping ? "   scanning" : "considering", string);
+
 expand_string_forcedfail = FALSE;
 expand_string_message = US"";
 
@@ -3853,7 +3886,7 @@ while (*s != 0)
       {
       const uschar * t = s + 2;
       for (s = t; *s != 0; s++) if (*s == '\\' && s[1] == 'N') break;
-      yield = string_cat(yield, &size, &ptr, t, s - t);
+      yield = string_catn(yield, &size, &ptr, t, s - t);
       if (*s != 0) s += 2;
       }
 
@@ -3862,7 +3895,7 @@ while (*s != 0)
       uschar ch[1];
       ch[0] = string_interpret_escape(&s);
       s++;
-      yield = string_cat(yield, &size, &ptr, ch, 1);
+      yield = string_catn(yield, &size, &ptr, ch, 1);
       }
 
     continue;
@@ -3877,7 +3910,7 @@ while (*s != 0)
 
   if (*s != '$' || !honour_dollar)
     {
-    yield = string_cat(yield, &size, &ptr, s++, 1);
+    yield = string_catn(yield, &size, &ptr, s++, 1);
     continue;
     }
 
@@ -3955,7 +3988,7 @@ while (*s != 0)
       size = newsize;
       ptr = len;
       }
-    else yield = string_cat(yield, &size, &ptr, value, len);
+    else yield = string_catn(yield, &size, &ptr, value, len);
 
     continue;
     }
@@ -3965,7 +3998,7 @@ while (*s != 0)
     int n;
     s = read_cnumber(&n, s);
     if (n >= 0 && n <= expand_nmax)
-      yield = string_cat(yield, &size, &ptr, expand_nstring[n],
+      yield = string_catn(yield, &size, &ptr, expand_nstring[n],
         expand_nlength[n]);
     continue;
     }
@@ -3991,7 +4024,7 @@ while (*s != 0)
       goto EXPAND_FAILED;
       }
     if (n >= 0 && n <= expand_nmax)
-      yield = string_cat(yield, &size, &ptr, expand_nstring[n],
+      yield = string_catn(yield, &size, &ptr, expand_nstring[n],
         expand_nlength[n]);
     continue;
     }
@@ -4043,11 +4076,12 @@ while (*s != 0)
          DEBUG(D_expand)
            debug_printf("acl expansion yield: %s\n", user_msg);
          if (user_msg)
-            yield = string_cat(yield, &size, &ptr, user_msg, Ustrlen(user_msg));
+            yield = string_cat(yield, &size, &ptr, user_msg);
          continue;
 
        case DEFER:
           expand_string_forcedfail = TRUE;
+         /*FALLTHROUGH*/
        default:
           expand_string_message = string_sprintf("error from acl \"%s\"", sub[0]);
          goto EXPAND_FAILED;
@@ -4134,7 +4168,7 @@ while (*s != 0)
                          sub_arg[1][0], sub_arg[2], &expand_string_message)))
        goto EXPAND_FAILED;
       if (!skipping)
-       yield = string_cat(yield, &size, &ptr, encoded, Ustrlen(encoded));
+       yield = string_cat(yield, &size, &ptr, encoded);
       continue;
       }
 #endif
@@ -4170,8 +4204,12 @@ while (*s != 0)
       if (*s == '{')                                   /*}*/
         {
         key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
-        if (key == NULL) goto EXPAND_FAILED;           /*{*/
-        if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+        if (!key) goto EXPAND_FAILED;                  /*{{*/
+        if (*s++ != '}')
+         {
+         expand_string_message = US"missing '}' after lookup key";
+         goto EXPAND_FAILED_CURLY;
+         }
         while (isspace(*s)) s++;
         }
       else key = NULL;
@@ -4234,10 +4272,18 @@ while (*s != 0)
       queries that also require a file name (e.g. sqlite), the file name comes
       first. */
 
-      if (*s != '{') goto EXPAND_FAILED_CURLY;
+      if (*s != '{')
+        {
+       expand_string_message = US"missing '{' for lookup file-or-query arg";
+       goto EXPAND_FAILED_CURLY;
+       }
       filename = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
       if (filename == NULL) goto EXPAND_FAILED;
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+       expand_string_message = US"missing '}' closing lookup file-or-query arg";
+       goto EXPAND_FAILED_CURLY;
+       }
       while (isspace(*s)) s++;
 
       /* If this isn't a single-key+file lookup, re-arrange the variables
@@ -4245,15 +4291,13 @@ while (*s != 0)
       there is just a "key", and no file name. For the special query-style +
       file types, the query (i.e. "key") starts with a file name. */
 
-      if (key == NULL)
+      if (!key)
         {
         while (isspace(*filename)) filename++;
         key = filename;
 
         if (mac_islookup(stype, lookup_querystyle))
-          {
           filename = NULL;
-          }
         else
           {
           if (*filename != '/')
@@ -4463,14 +4507,14 @@ while (*s != 0)
       /* Now separate the domain from the local part */
       *domain++ = '\0';
 
-      yield = string_cat(yield,&size,&ptr,US"prvs=",5);
-      string_cat(yield,&size,&ptr,(sub_arg[2] != NULL) ? sub_arg[2] : US"0", 1);
-      string_cat(yield,&size,&ptr,prvs_daystamp(7),3);
-      string_cat(yield,&size,&ptr,p,6);
-      string_cat(yield,&size,&ptr,US"=",1);
-      string_cat(yield,&size,&ptr,sub_arg[0],Ustrlen(sub_arg[0]));
-      string_cat(yield,&size,&ptr,US"@",1);
-      string_cat(yield,&size,&ptr,domain,Ustrlen(domain));
+      yield = string_catn(yield, &size, &ptr, US"prvs=", 5);
+      yield = string_catn(yield, &size, &ptr, sub_arg[2] ? sub_arg[2] : US"0", 1);
+      yield = string_catn(yield, &size, &ptr, prvs_daystamp(7), 3);
+      yield = string_catn(yield, &size, &ptr, p, 6);
+      yield = string_catn(yield, &size, &ptr, US"=", 1);
+      yield = string_cat (yield, &size, &ptr, sub_arg[0]);
+      yield = string_catn(yield, &size, &ptr, US"@", 1);
+      yield = string_cat (yield, &size, &ptr, domain);
 
       continue;
       }
@@ -4524,9 +4568,9 @@ while (*s != 0)
         DEBUG(D_expand) debug_printf("prvscheck domain: %s\n", domain);
 
         /* Set up expansion variables */
-        prvscheck_address = string_cat(NULL, &mysize, &myptr, local_part, Ustrlen(local_part));
-        string_cat(prvscheck_address,&mysize,&myptr,US"@",1);
-        string_cat(prvscheck_address,&mysize,&myptr,domain,Ustrlen(domain));
+        prvscheck_address = string_cat (NULL, &mysize, &myptr, local_part);
+        prvscheck_address = string_catn(prvscheck_address, &mysize, &myptr, US"@", 1);
+        prvscheck_address = string_cat (prvscheck_address, &mysize, &myptr, domain);
         prvscheck_address[myptr] = '\0';
         prvscheck_keynum = string_copy(key_num);
 
@@ -4592,10 +4636,8 @@ while (*s != 0)
           case 3: goto EXPAND_FAILED;
           }
 
-        if (sub_arg[0] == NULL || *sub_arg[0] == '\0')
-          yield = string_cat(yield,&size,&ptr,prvscheck_address,Ustrlen(prvscheck_address));
-        else
-          yield = string_cat(yield,&size,&ptr,sub_arg[0],Ustrlen(sub_arg[0]));
+       yield = string_cat(yield, &size, &ptr,
+         !sub_arg[0] || !*sub_arg[0] ? prvscheck_address : sub_arg[0]);
 
         /* Reset the "internal" variables afterwards, because they are in
         dynamic store that will be reclaimed if the expansion succeeded. */
@@ -4842,10 +4884,20 @@ while (*s != 0)
         {
         if (expand_string_internal(s+1, TRUE, &s, TRUE, TRUE, &resetok) == NULL)
           goto EXPAND_FAILED;
-        if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+        if (*s++ != '}')
+         {
+         expand_string_message = US"missing '}' closing failstring for readsocket";
+         goto EXPAND_FAILED_CURLY;
+         }
         while (isspace(*s)) s++;
         }
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+
+    readsock_done:
+      if (*s++ != '}')
+        {
+       expand_string_message = US"missing '}' closing readsocket";
+       goto EXPAND_FAILED_CURLY;
+       }
       continue;
 
       /* Come here on failure to create socket, connect socket, write to the
@@ -4855,13 +4907,16 @@ while (*s != 0)
       SOCK_FAIL:
       if (*s != '{') goto EXPAND_FAILED;
       DEBUG(D_any) debug_printf("%s\n", expand_string_message);
-      arg = expand_string_internal(s+1, TRUE, &s, FALSE, TRUE, &resetok);
-      if (arg == NULL) goto EXPAND_FAILED;
-      yield = string_cat(yield, &size, &ptr, arg, Ustrlen(arg));
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (!(arg = expand_string_internal(s+1, TRUE, &s, FALSE, TRUE, &resetok)))
+        goto EXPAND_FAILED;
+      yield = string_cat(yield, &size, &ptr, arg);
+      if (*s++ != '}')
+        {
+       expand_string_message = US"missing '}' closing failstring for readsocket";
+       goto EXPAND_FAILED_CURLY;
+       }
       while (isspace(*s)) s++;
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
-      continue;
+      goto readsock_done;
       }
 
     /* Handle "run" to execute a program. */
@@ -4882,16 +4937,22 @@ while (*s != 0)
         }
 
       while (isspace(*s)) s++;
-      if (*s != '{') goto EXPAND_FAILED_CURLY;
+      if (*s != '{')
+        {
+       expand_string_message = US"missing '{' for command arg of run";
+       goto EXPAND_FAILED_CURLY;
+       }
       arg = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
       if (arg == NULL) goto EXPAND_FAILED;
       while (isspace(*s)) s++;
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+       expand_string_message = US"missing '}' closing command arg of run";
+       goto EXPAND_FAILED_CURLY;
+       }
 
       if (skipping)   /* Just pretend it worked when we're skipping */
-        {
         runrc = 0;
-        }
       else
         {
         if (!transport_set_up_command(&argv,    /* anchor for arg list */
@@ -4988,7 +5049,7 @@ while (*s != 0)
         case 3: goto EXPAND_FAILED;
         }
 
-      yield = string_cat(yield, &size, &ptr, sub[0], Ustrlen(sub[0]));
+      yield = string_cat(yield, &size, &ptr, sub[0]);
       o2m = Ustrlen(sub[2]) - 1;
 
       if (o2m >= 0) for (; oldptr < ptr; oldptr++)
@@ -5065,7 +5126,7 @@ while (*s != 0)
           extract_substr(sub[2], val[0], val[1], &len);
 
       if (ret == NULL) goto EXPAND_FAILED;
-      yield = string_cat(yield, &size, &ptr, ret, len);
+      yield = string_catn(yield, &size, &ptr, ret, len);
       continue;
       }
 
@@ -5173,7 +5234,7 @@ while (*s != 0)
       DEBUG(D_any) debug_printf("HMAC[%s](%.*s,%.*s)=%.*s\n", sub[0],
         (int)keylen, keyptr, Ustrlen(sub[2]), sub[2], hashlen*2, finalhash_hex);
 
-      yield = string_cat(yield, &size, &ptr, finalhash_hex, hashlen*2);
+      yield = string_catn(yield, &size, &ptr, finalhash_hex, hashlen*2);
       }
 
     continue;
@@ -5244,7 +5305,7 @@ while (*s != 0)
             emptyopt = 0;
             continue;
             }
-          yield = string_cat(yield, &size, &ptr, subject+moffset, slen-moffset);
+          yield = string_catn(yield, &size, &ptr, subject+moffset, slen-moffset);
           break;
           }
 
@@ -5261,11 +5322,11 @@ while (*s != 0)
 
         /* Copy the characters before the match, plus the expanded insertion. */
 
-        yield = string_cat(yield, &size, &ptr, subject + moffset,
+        yield = string_catn(yield, &size, &ptr, subject + moffset,
           ovector[0] - moffset);
         insert = expand_string(sub[2]);
         if (insert == NULL) goto EXPAND_FAILED;
-        yield = string_cat(yield, &size, &ptr, insert, Ustrlen(insert));
+        yield = string_cat(yield, &size, &ptr, insert);
 
         moffset = ovector[1];
         moffsetextra = 0;
@@ -5306,16 +5367,44 @@ while (*s != 0)
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
 
-      /* Read the arguments */
+      /* While skipping we cannot rely on the data for expansions being
+      available (eg. $item) hence cannot decide on numeric vs. keyed.
+      Just read as many arguments as there are. */
 
-      for (i = 0; i < j; i++)
+      if (skipping)
+       {
+        while (isspace(*s)) s++;
+        while (*s == '{')
+         {
+          if (!expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok))
+           goto EXPAND_FAILED;                                 /*{*/
+          if (*s++ != '}')
+           {
+           expand_string_message = US"missing '{' for arg of extract";
+           goto EXPAND_FAILED_CURLY;
+           }
+         while (isspace(*s)) s++;
+         }
+       if (*s != '}')
+         {
+         expand_string_message = US"missing '}' closing extract";
+         goto EXPAND_FAILED_CURLY;
+         }
+       }
+
+      else for (i = 0; i < j; i++) /* Read the proper number of arguments */
         {
         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 (*s++ != '}') goto EXPAND_FAILED_CURLY;
+          if (*s++ != '}')
+           {
+           expand_string_message = string_sprintf(
+             US"missing '}' closing arg %d of extract", i+1);
+           goto EXPAND_FAILED_CURLY;
+           }
 
           /* After removal of leading and trailing white space, the first
           argument must not be empty; if it consists entirely of digits
@@ -5335,31 +5424,33 @@ while (*s != 0)
             while (len > 0 && isspace(p[len-1])) len--;
             p[len] = 0;
 
-            if (!skipping)
+           if (*p == 0)
              {
-             if (*p == 0)
-               {
-               expand_string_message = US"first argument of \"extract\" must "
-                 "not be empty";
-               goto EXPAND_FAILED;
-               }
+             expand_string_message = US"first argument of \"extract\" must "
+               "not be empty";
+             goto EXPAND_FAILED;
+             }
 
-             if (*p == '-')
-               {
-               field_number = -1;
-               p++;
-               }
-             while (*p != 0 && isdigit(*p)) x = x * 10 + *p++ - '0';
-             if (*p == 0)
-               {
-               field_number *= x;
-               j = 3;               /* Need 3 args */
-               field_number_set = TRUE;
-               }
+           if (*p == '-')
+             {
+             field_number = -1;
+             p++;
+             }
+           while (*p != 0 && isdigit(*p)) x = x * 10 + *p++ - '0';
+           if (*p == 0)
+             {
+             field_number *= x;
+             j = 3;               /* Need 3 args */
+             field_number_set = TRUE;
              }
             }
           }
-        else goto EXPAND_FAILED_CURLY;
+        else
+         {
+         expand_string_message = string_sprintf(
+           US"missing '{' for arg %d of extract", i+1);
+         goto EXPAND_FAILED_CURLY;
+         }
         }
 
       /* Extract either the numbered or the keyed substring into $value. If
@@ -5412,11 +5503,20 @@ while (*s != 0)
         {
         while (isspace(*s)) s++;
         if (*s != '{')                                 /*}*/
+         {
+         expand_string_message = string_sprintf(
+           US"missing '{' for arg %d of listextract", i+1);
          goto EXPAND_FAILED_CURLY;
+         }
 
        sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
        if (!sub[i])     goto EXPAND_FAILED;            /*{*/
-       if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+       if (*s++ != '}')
+         {
+         expand_string_message = string_sprintf(
+           US"missing '}' closing arg %d of listextract", i+1);
+         goto EXPAND_FAILED_CURLY;
+         }
 
        /* After removal of leading and trailing white space, the first
        argument must be numeric and nonempty. */
@@ -5499,10 +5599,17 @@ while (*s != 0)
       /* Read the field argument */
       while (isspace(*s)) s++;
       if (*s != '{')                                   /*}*/
+       {
+       expand_string_message = US"missing '{' for field arg of certextract";
        goto EXPAND_FAILED_CURLY;
+       }
       sub[0] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
       if (!sub[0])     goto EXPAND_FAILED;             /*{*/
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+       expand_string_message = US"missing '}' closing field arg of certextract";
+       goto EXPAND_FAILED_CURLY;
+       }
       /* strip spaces fore & aft */
       {
       int len;
@@ -5519,7 +5626,10 @@ while (*s != 0)
       /* inspect the cert argument */
       while (isspace(*s)) s++;
       if (*s != '{')                                   /*}*/
+       {
+       expand_string_message = US"missing '{' for cert variable arg of certextract";
        goto EXPAND_FAILED_CURLY;
+       }
       if (*++s != '$')
         {
        expand_string_message = US"second argument of \"certextract\" must "
@@ -5528,7 +5638,11 @@ while (*s != 0)
        }
       sub[1] = expand_string_internal(s+1, TRUE, &s, skipping, FALSE, &resetok);
       if (!sub[1])     goto EXPAND_FAILED;             /*{*/
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+       expand_string_message = US"missing '}' closing cert variable arg of certextract";
+       goto EXPAND_FAILED_CURLY;
+       }
 
       if (skipping)
        lookup_value = NULL;
@@ -5572,25 +5686,48 @@ while (*s != 0)
       uschar *save_lookup_value = lookup_value;
 
       while (isspace(*s)) s++;
-      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '{')
+        {
+       expand_string_message =
+         string_sprintf("missing '{' for first arg of %s", name);
+       goto EXPAND_FAILED_CURLY;
+       }
 
       list = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
       if (list == NULL) goto EXPAND_FAILED;
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+       expand_string_message =
+         string_sprintf("missing '}' closing first arg of %s", name);
+       goto EXPAND_FAILED_CURLY;
+       }
 
       if (item_type == EITEM_REDUCE)
         {
        uschar * t;
         while (isspace(*s)) s++;
-        if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+        if (*s++ != '{')
+         {
+         expand_string_message = US"missing '{' for second arg of reduce";
+         goto EXPAND_FAILED_CURLY;
+         }
         t = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
         if (!t) goto EXPAND_FAILED;
         lookup_value = t;
-        if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+        if (*s++ != '}')
+         {
+         expand_string_message = US"missing '}' closing second arg of reduce";
+         goto EXPAND_FAILED_CURLY;
+         }
         }
 
       while (isspace(*s)) s++;
-      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '{')
+        {
+       expand_string_message =
+         string_sprintf("missing '{' for last arg of %s", name);
+       goto EXPAND_FAILED_CURLY;
+       }
 
       expr = s;
 
@@ -5688,7 +5825,7 @@ while (*s != 0)
         separator character, or is an empty string. */
 
         if (ptr != save_ptr && (temp[0] == *outsep || temp[0] == 0))
-          yield = string_cat(yield, &size, &ptr, US" ", 1);
+          yield = string_catn(yield, &size, &ptr, US" ", 1);
 
         /* Add the string in "temp" to the output list that we are building,
         This is done in chunks by searching for the separator character. */
@@ -5696,21 +5833,22 @@ while (*s != 0)
         for (;;)
           {
           size_t seglen = Ustrcspn(temp, outsep);
-            yield = string_cat(yield, &size, &ptr, temp, seglen + 1);
+
+         yield = string_catn(yield, &size, &ptr, temp, seglen + 1);
 
           /* If we got to the end of the string we output one character
           too many; backup and end the loop. Otherwise arrange to double the
           separator. */
 
           if (temp[seglen] == '\0') { ptr--; break; }
-          yield = string_cat(yield, &size, &ptr, outsep, 1);
+          yield = string_catn(yield, &size, &ptr, outsep, 1);
           temp += seglen + 1;
           }
 
         /* Output a separator after the string: we will remove the redundant
         final one at the end. */
 
-        yield = string_cat(yield, &size, &ptr, outsep, 1);
+        yield = string_catn(yield, &size, &ptr, outsep, 1);
         }   /* End of iteration over the list loop */
 
       /* REDUCE has generated no output above: output the final value of
@@ -5718,8 +5856,7 @@ while (*s != 0)
 
       if (item_type == EITEM_REDUCE)
         {
-        yield = string_cat(yield, &size, &ptr, lookup_value,
-          Ustrlen(lookup_value));
+        yield = string_cat(yield, &size, &ptr, lookup_value);
         lookup_value = save_lookup_value;  /* Restore $value */
         }
 
@@ -5745,28 +5882,52 @@ while (*s != 0)
       uschar *save_iterate_item = iterate_item;
 
       while (isspace(*s)) s++;
-      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '{')
+        {
+        expand_string_message = US"missing '{' for list arg of sort";
+       goto EXPAND_FAILED_CURLY;
+       }
 
       srclist = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
       if (!srclist) goto EXPAND_FAILED;
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+        expand_string_message = US"missing '}' closing list arg of sort";
+       goto EXPAND_FAILED_CURLY;
+       }
 
       while (isspace(*s)) s++;
-      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '{')
+        {
+        expand_string_message = US"missing '{' for comparator arg of sort";
+       goto EXPAND_FAILED_CURLY;
+       }
 
       cmp = expand_string_internal(s, TRUE, &s, skipping, FALSE, &resetok);
       if (!cmp) goto EXPAND_FAILED;
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+        expand_string_message = US"missing '}' closing comparator arg of sort";
+       goto EXPAND_FAILED_CURLY;
+       }
 
       while (isspace(*s)) s++;
-      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '{')
+        {
+        expand_string_message = US"missing '{' for extractor arg of sort";
+       goto EXPAND_FAILED_CURLY;
+       }
 
       xtract = s;
       tmp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok);
       if (!tmp) goto EXPAND_FAILED;
       xtract = string_copyn(xtract, s - xtract);
 
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+        expand_string_message = US"missing '}' closing extractor arg of sort";
+       goto EXPAND_FAILED_CURLY;
+       }
                                                        /*{*/
       if (*s++ != '}')
         {                                              /*{*/
@@ -5862,7 +6023,7 @@ while (*s != 0)
        }
 
       if (dstlist)
-       yield = string_cat(yield, &size, &ptr, dstlist, Ustrlen(dstlist));
+       yield = string_cat(yield, &size, &ptr, dstlist);
 
       /* Restore preserved $item */
       iterate_item = save_iterate_item;
@@ -5960,7 +6121,7 @@ while (*s != 0)
       if(status == OK)
         {
         if (result == NULL) result = US"";
-        yield = string_cat(yield, &size, &ptr, result, Ustrlen(result));
+        yield = string_cat(yield, &size, &ptr, result);
         continue;
         }
       else
@@ -5986,7 +6147,11 @@ while (*s != 0)
 
       key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
       if (!key) goto EXPAND_FAILED;                    /*{*/
-      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      if (*s++ != '}')
+        {
+        expand_string_message = US"missing '{' for name arg of env";
+       goto EXPAND_FAILED_CURLY;
+       }
 
       lookup_value = US getenv(CS key);
 
@@ -6050,7 +6215,12 @@ while (*s != 0)
          sub = expand_string_internal(s+2, TRUE, &s1, skipping,
                  FALSE, &resetok);
          if (!sub)       goto EXPAND_FAILED;           /*{*/
-         if (*s1 != '}') goto EXPAND_FAILED_CURLY;
+         if (*s1 != '}')
+           {
+           expand_string_message =
+             string_sprintf("missing '}' closing cert arg of %s", name);
+           goto EXPAND_FAILED_CURLY;
+           }
          if ((vp = find_var_ent(sub)) && vp->type == vtype_cert)
            {
            s = s1+1;
@@ -6090,7 +6260,7 @@ while (*s != 0)
           goto EXPAND_FAILED;
           }
         t = string_base62(n);
-        yield = string_cat(yield, &size, &ptr, t, Ustrlen(t));
+        yield = string_cat(yield, &size, &ptr, t);
         continue;
         }
 
@@ -6114,7 +6284,7 @@ while (*s != 0)
           n = n * BASE_62 + (t - base62_chars);
           }
         (void)sprintf(CS buf, "%ld", n);
-        yield = string_cat(yield, &size, &ptr, buf, Ustrlen(buf));
+        yield = string_cat(yield, &size, &ptr, buf);
         continue;
         }
 
@@ -6128,7 +6298,7 @@ while (*s != 0)
               expand_string_message);
           goto EXPAND_FAILED;
           }
-        yield = string_cat(yield, &size, &ptr, expanded, Ustrlen(expanded));
+        yield = string_cat(yield, &size, &ptr, expanded);
         continue;
         }
 
@@ -6137,7 +6307,7 @@ while (*s != 0)
         int count = 0;
         uschar *t = sub - 1;
         while (*(++t) != 0) { *t = tolower(*t); count++; }
-        yield = string_cat(yield, &size, &ptr, sub, count);
+        yield = string_catn(yield, &size, &ptr, sub, count);
         continue;
         }
 
@@ -6146,7 +6316,7 @@ while (*s != 0)
         int count = 0;
         uschar *t = sub - 1;
         while (*(++t) != 0) { *t = toupper(*t); count++; }
-        yield = string_cat(yield, &size, &ptr, sub, count);
+        yield = string_catn(yield, &size, &ptr, sub, count);
         continue;
         }
 
@@ -6155,7 +6325,7 @@ while (*s != 0)
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_md5(*(void **)vp->value);
-         yield = string_cat(yield, &size, &ptr, cp, Ustrlen(cp));
+         yield = string_cat(yield, &size, &ptr, cp);
          }
        else
 #endif
@@ -6167,7 +6337,7 @@ while (*s != 0)
          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, &size, &ptr, US st, (int)strlen(st));
+         yield = string_cat(yield, &size, &ptr, US st);
          }
         continue;
 
@@ -6176,7 +6346,7 @@ while (*s != 0)
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_sha1(*(void **)vp->value);
-         yield = string_cat(yield, &size, &ptr, cp, Ustrlen(cp));
+         yield = string_cat(yield, &size, &ptr, cp);
          }
        else
 #endif
@@ -6188,7 +6358,7 @@ while (*s != 0)
          sha1_start(&base);
          sha1_end(&base, sub, Ustrlen(sub), digest);
          for(j = 0; j < 20; j++) sprintf(st+2*j, "%02X", digest[j]);
-         yield = string_cat(yield, &size, &ptr, US st, (int)strlen(st));
+         yield = string_cat(yield, &size, &ptr, US st);
          }
         continue;
 
@@ -6197,7 +6367,7 @@ while (*s != 0)
        if (vp && *(void **)vp->value)
          {
          uschar * cp = tls_cert_fprt_sha256(*(void **)vp->value);
-         yield = string_cat(yield, &size, &ptr, cp, (int)Ustrlen(cp));
+         yield = string_cat(yield, &size, &ptr, cp);
          }
        else
 #endif
@@ -6248,7 +6418,7 @@ while (*s != 0)
           }
 
         enc = b64encode(sub, out - sub);
-        yield = string_cat(yield, &size, &ptr, enc, Ustrlen(enc));
+        yield = string_cat(yield, &size, &ptr, enc);
         continue;
         }
 
@@ -6260,10 +6430,10 @@ while (*s != 0)
         while (*(++t) != 0)
           {
           if (*t < 0x21 || 0x7E < *t)
-            yield = string_cat(yield, &size, &ptr,
+            yield = string_catn(yield, &size, &ptr,
              string_sprintf("\\x%02x", *t), 4);
          else
-           yield = string_cat(yield, &size, &ptr, t, 1);
+           yield = string_catn(yield, &size, &ptr, t, 1);
           }
        continue;
        }
@@ -6279,7 +6449,7 @@ while (*s != 0)
 
        while (string_nextinlist(CUSS &sub, &sep, buffer, sizeof(buffer)) != NULL) cnt++;
        cp = string_sprintf("%d", cnt);
-        yield = string_cat(yield, &size, &ptr, cp, Ustrlen(cp));
+        yield = string_cat(yield, &size, &ptr, cp);
         continue;
         }
 
@@ -6333,7 +6503,7 @@ while (*s != 0)
          {
          uschar * buf = US" : ";
          if (needsep)
-           yield = string_cat(yield, &size, &ptr, buf, 3);
+           yield = string_catn(yield, &size, &ptr, buf, 3);
          else
            needsep = TRUE;
 
@@ -6349,21 +6519,21 @@ while (*s != 0)
            tok[0] = sep; tok[1] = ':'; tok[2] = 0;
            while ((cp= strpbrk((const char *)item, tok)))
              {
-              yield = string_cat(yield, &size, &ptr, item, cp-(char *)item);
+              yield = string_catn(yield, &size, &ptr, item, cp-(char *)item);
              if (*cp++ == ':') /* colon in a non-colon-sep list item, needs doubling */
                {
-                yield = string_cat(yield, &size, &ptr, US"::", 2);
+                yield = string_catn(yield, &size, &ptr, US"::", 2);
                item = (uschar *)cp;
                }
              else              /* sep in item; should already be doubled; emit once */
                {
-                yield = string_cat(yield, &size, &ptr, (uschar *)tok, 1);
+                yield = string_catn(yield, &size, &ptr, (uschar *)tok, 1);
                if (*cp == sep) cp++;
                item = (uschar *)cp;
                }
              }
            }
-          yield = string_cat(yield, &size, &ptr, item, Ustrlen(item));
+          yield = string_cat(yield, &size, &ptr, item);
          }
         continue;
        }
@@ -6411,7 +6581,7 @@ while (*s != 0)
 
         /* Convert to masked textual format and add to output. */
 
-        yield = string_cat(yield, &size, &ptr, buffer,
+        yield = string_catn(yield, &size, &ptr, buffer,
           host_nmtoa(count, binary, mask, buffer, '.'));
         continue;
         }
@@ -6441,7 +6611,7 @@ while (*s != 0)
            goto EXPAND_FAILED;
          }
 
-       yield = string_cat(yield, &size, &ptr, buffer,
+       yield = string_catn(yield, &size, &ptr, buffer,
                  c == EOP_IPV6NORM
                    ? ipv6_nmtoa(binary, buffer)
                    : host_nmtoa(4, binary, -1, buffer, ':')
@@ -6462,12 +6632,12 @@ while (*s != 0)
           if (c != EOP_DOMAIN)
             {
             if (c == EOP_LOCAL_PART && domain != 0) end = start + domain - 1;
-            yield = string_cat(yield, &size, &ptr, sub+start, end-start);
+            yield = string_catn(yield, &size, &ptr, sub+start, end-start);
             }
           else if (domain != 0)
             {
             domain += start;
-            yield = string_cat(yield, &size, &ptr, sub+domain, end-domain);
+            yield = string_catn(yield, &size, &ptr, sub+domain, end-domain);
             }
           }
         continue;
@@ -6502,25 +6672,25 @@ while (*s != 0)
           if (address != NULL)
             {
             if (ptr != save_ptr && address[0] == *outsep)
-              yield = string_cat(yield, &size, &ptr, US" ", 1);
+              yield = string_catn(yield, &size, &ptr, US" ", 1);
 
             for (;;)
               {
               size_t seglen = Ustrcspn(address, outsep);
-              yield = string_cat(yield, &size, &ptr, address, seglen + 1);
+              yield = string_catn(yield, &size, &ptr, address, seglen + 1);
 
               /* If we got to the end of the string we output one character
               too many. */
 
               if (address[seglen] == '\0') { ptr--; break; }
-              yield = string_cat(yield, &size, &ptr, outsep, 1);
+              yield = string_catn(yield, &size, &ptr, outsep, 1);
               address += seglen + 1;
               }
 
             /* Output a separator after the string: we will remove the
             redundant final one at the end. */
 
-            yield = string_cat(yield, &size, &ptr, outsep, 1);
+            yield = string_catn(yield, &size, &ptr, outsep, 1);
             }
 
           if (saveend == '\0') break;
@@ -6567,24 +6737,24 @@ while (*s != 0)
 
         if (needs_quote)
           {
-          yield = string_cat(yield, &size, &ptr, US"\"", 1);
+          yield = string_catn(yield, &size, &ptr, US"\"", 1);
           t = sub - 1;
           while (*(++t) != 0)
             {
             if (*t == '\n')
-              yield = string_cat(yield, &size, &ptr, US"\\n", 2);
+              yield = string_catn(yield, &size, &ptr, US"\\n", 2);
             else if (*t == '\r')
-              yield = string_cat(yield, &size, &ptr, US"\\r", 2);
+              yield = string_catn(yield, &size, &ptr, US"\\r", 2);
             else
               {
               if (*t == '\\' || *t == '"')
-                yield = string_cat(yield, &size, &ptr, US"\\", 1);
-              yield = string_cat(yield, &size, &ptr, t, 1);
+                yield = string_catn(yield, &size, &ptr, US"\\", 1);
+              yield = string_catn(yield, &size, &ptr, t, 1);
               }
             }
-          yield = string_cat(yield, &size, &ptr, US"\"", 1);
+          yield = string_catn(yield, &size, &ptr, US"\"", 1);
           }
-        else yield = string_cat(yield, &size, &ptr, sub, Ustrlen(sub));
+        else yield = string_cat(yield, &size, &ptr, sub);
         continue;
         }
 
@@ -6616,7 +6786,7 @@ while (*s != 0)
           goto EXPAND_FAILED;
           }
 
-        yield = string_cat(yield, &size, &ptr, sub, Ustrlen(sub));
+        yield = string_cat(yield, &size, &ptr, sub);
         continue;
         }
 
@@ -6629,8 +6799,8 @@ while (*s != 0)
         while (*(++t) != 0)
           {
           if (!isalnum(*t))
-            yield = string_cat(yield, &size, &ptr, US"\\", 1);
-          yield = string_cat(yield, &size, &ptr, t, 1);
+            yield = string_catn(yield, &size, &ptr, US"\\", 1);
+          yield = string_catn(yield, &size, &ptr, t, 1);
           }
         continue;
         }
@@ -6643,7 +6813,7 @@ while (*s != 0)
         uschar buffer[2048];
        const uschar *string = parse_quote_2047(sub, Ustrlen(sub), headers_charset,
           buffer, sizeof(buffer), FALSE);
-        yield = string_cat(yield, &size, &ptr, string, Ustrlen(string));
+        yield = string_cat(yield, &size, &ptr, string);
         continue;
         }
 
@@ -6660,7 +6830,7 @@ while (*s != 0)
           expand_string_message = error;
           goto EXPAND_FAILED;
           }
-        yield = string_cat(yield, &size, &ptr, decoded, len);
+        yield = string_catn(yield, &size, &ptr, decoded, len);
         continue;
         }
 
@@ -6676,7 +6846,7 @@ while (*s != 0)
           GETUTF8INC(c, sub);
           if (c > 255) c = '_';
           buff[0] = c;
-          yield = string_cat(yield, &size, &ptr, buff, 1);
+          yield = string_catn(yield, &size, &ptr, buff, 1);
           }
         continue;
         }
@@ -6711,7 +6881,7 @@ while (*s != 0)
                  complete = -1;        /* error (RFC3629 limit) */
                else
                  {             /* finished; output utf-8 sequence */
-                 yield = string_cat(yield, &size, &ptr, seq_buff, seq_len);
+                 yield = string_catn(yield, &size, &ptr, seq_buff, seq_len);
                  index = 0;
                  }
              }
@@ -6720,7 +6890,7 @@ while (*s != 0)
            {
            if((c & 0x80) == 0) /* 1-byte sequence, US-ASCII, keep it */
              {
-             yield = string_cat(yield, &size, &ptr, &c, 1);
+             yield = string_catn(yield, &size, &ptr, &c, 1);
              continue;
              }
            if((c & 0xe0) == 0xc0)              /* 2-byte sequence */
@@ -6753,11 +6923,11 @@ while (*s != 0)
          if (complete != 0)
            {
            bytes_left = index = 0;
-           yield = string_cat(yield, &size, &ptr, UTF8_REPLACEMENT_CHAR, 1);
+           yield = string_catn(yield, &size, &ptr, UTF8_REPLACEMENT_CHAR, 1);
            }
          if ((complete == 1) && ((c & 0x80) == 0))
                        /* ASCII character follows incomplete sequence */
-             yield = string_cat(yield, &size, &ptr, &c, 1);
+             yield = string_catn(yield, &size, &ptr, &c, 1);
          }
         continue;
         }
@@ -6774,7 +6944,7 @@ while (*s != 0)
            string_printing(sub), error);
          goto EXPAND_FAILED;
          }
-       yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+       yield = string_cat(yield, &size, &ptr, s);
         continue;
        }
 
@@ -6789,7 +6959,7 @@ while (*s != 0)
            string_printing(sub), error);
          goto EXPAND_FAILED;
          }
-       yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+       yield = string_cat(yield, &size, &ptr, s);
         continue;
        }
 
@@ -6804,7 +6974,7 @@ while (*s != 0)
            string_printing(sub), error);
          goto EXPAND_FAILED;
          }
-       yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+       yield = string_cat(yield, &size, &ptr, s);
        DEBUG(D_expand) debug_printf("yield: '%s'\n", yield);
         continue;
        }
@@ -6820,7 +6990,7 @@ while (*s != 0)
            string_printing(sub), error);
          goto EXPAND_FAILED;
          }
-       yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+       yield = string_cat(yield, &size, &ptr, s);
         continue;
        }
 #endif /* EXPERIMENTAL_INTERNATIONAL */
@@ -6830,7 +7000,7 @@ while (*s != 0)
       case EOP_ESCAPE:
         {
         const uschar *t = string_printing(sub);
-        yield = string_cat(yield, &size, &ptr, t, Ustrlen(t));
+        yield = string_cat(yield, &size, &ptr, t);
         continue;
         }
 
@@ -6850,7 +7020,7 @@ while (*s != 0)
           goto EXPAND_FAILED;
           }
         sprintf(CS var_buffer, PR_EXIM_ARITH, n);
-        yield = string_cat(yield, &size, &ptr, var_buffer, Ustrlen(var_buffer));
+        yield = string_cat(yield, &size, &ptr, var_buffer);
         continue;
         }
 
@@ -6866,7 +7036,7 @@ while (*s != 0)
           goto EXPAND_FAILED;
           }
         sprintf(CS var_buffer, "%d", n);
-        yield = string_cat(yield, &size, &ptr, var_buffer, Ustrlen(var_buffer));
+        yield = string_cat(yield, &size, &ptr, var_buffer);
         continue;
         }
 
@@ -6881,7 +7051,7 @@ while (*s != 0)
           goto EXPAND_FAILED;
           }
         t = readconf_printtime(n);
-        yield = string_cat(yield, &size, &ptr, t, Ustrlen(t));
+        yield = string_cat(yield, &size, &ptr, t);
         continue;
         }
 
@@ -6897,7 +7067,7 @@ while (*s != 0)
 #else
        uschar * s = b64encode(sub, Ustrlen(sub));
 #endif
-       yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+       yield = string_cat(yield, &size, &ptr, s);
        continue;
        }
 
@@ -6911,7 +7081,7 @@ while (*s != 0)
             "well-formed for \"%s\" operator", sub, name);
           goto EXPAND_FAILED;
           }
-        yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+        yield = string_cat(yield, &size, &ptr, s);
         continue;
         }
 
@@ -6921,7 +7091,7 @@ while (*s != 0)
         {
         uschar buff[24];
         (void)sprintf(CS buff, "%d", Ustrlen(sub));
-        yield = string_cat(yield, &size, &ptr, buff, Ustrlen(buff));
+        yield = string_cat(yield, &size, &ptr, buff);
         continue;
         }
 
@@ -7012,7 +7182,7 @@ while (*s != 0)
              extract_substr(sub, value1, value2, &len);
 
         if (ret == NULL) goto EXPAND_FAILED;
-        yield = string_cat(yield, &size, &ptr, ret, len);
+        yield = string_catn(yield, &size, &ptr, ret, len);
         continue;
         }
 
@@ -7067,7 +7237,7 @@ while (*s != 0)
           (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, &size, &ptr, s, Ustrlen(s));
+        yield = string_cat(yield, &size, &ptr, s);
         continue;
         }
 
@@ -7082,7 +7252,7 @@ while (*s != 0)
         if (expand_string_message != NULL)
           goto EXPAND_FAILED;
         s = string_sprintf("%d", vaguely_random_number((int)max));
-        yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+        yield = string_cat(yield, &size, &ptr, s);
         continue;
         }
 
@@ -7101,7 +7271,7 @@ while (*s != 0)
           goto EXPAND_FAILED;
           }
         invert_address(reversed, sub);
-        yield = string_cat(yield, &size, &ptr, reversed, Ustrlen(reversed));
+        yield = string_cat(yield, &size, &ptr, reversed);
         continue;
         }
 
@@ -7146,7 +7316,7 @@ while (*s != 0)
       size = newsize;
       ptr = len;
       }
-    else yield = string_cat(yield, &size, &ptr, value, len);
+    else yield = string_catn(yield, &size, &ptr, value, len);
     continue;
     }
 
@@ -7187,9 +7357,9 @@ else if (resetok_p) *resetok_p = FALSE;
 
 DEBUG(D_expand)
   {
-  debug_printf("expanding: %.*s\n   result: %s\n", (int)(s - string), string,
+  debug_printf("  expanding: %.*s\n     result: %s\n", (int)(s - string), string,
     yield);
-  if (skipping) debug_printf("skipping: result is not used\n");
+  if (skipping) debug_printf("   skipping: result is not used\n");
   }
 return yield;
 
@@ -7198,10 +7368,12 @@ to update the pointer to the terminator, for cases of nested calls with "fail".
 */
 
 EXPAND_FAILED_CURLY:
-expand_string_message = malformed_header?
-  US"missing or misplaced { or } - could be header name not terminated by colon"
-  :
-  US"missing or misplaced { or }";
+if (malformed_header)
+  expand_string_message =
+    US"missing or misplaced { or } - could be header name not terminated by colon";
+
+else if (!expand_string_message || !*expand_string_message)
+  expand_string_message = US"missing or misplaced { or }";
 
 /* At one point, Exim reset the store to yield (if yield was not NULL), but
 that is a bad idea, because expand_string_message is in dynamic store. */