Check for overflow in numeric expansion conditions; forbid negative
[exim.git] / src / src / expand.c
index 80971cb263e4de99a2d8e4d709c7b3f6b06d8a39..1517c0625726b102b3fc07b69094613878b32ad1 100644 (file)
@@ -1,10 +1,10 @@
-/* $Cambridge: exim/src/src/expand.c,v 1.34 2005/06/22 10:17:23 ph10 Exp $ */
+/* $Cambridge: exim/src/src/expand.c,v 1.60 2006/09/18 14:49:23 ph10 Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2006 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
@@ -94,12 +94,14 @@ static uschar *op_table_underscore[] = {
   US"from_utf8",
   US"local_part",
   US"quote_local_part",
+  US"time_eval",
   US"time_interval"};
 
 enum {
   EOP_FROM_UTF8,
   EOP_LOCAL_PART,
   EOP_QUOTE_LOCAL_PART,
+  EOP_TIME_EVAL,
   EOP_TIME_INTERVAL };
 
 static uschar *op_table_main[] = {
@@ -298,26 +300,6 @@ enum {
 /* This table must be kept in alphabetical order. */
 
 static var_entry var_table[] = {
-  { "acl_c0",              vtype_stringptr,   &acl_var[0] },
-  { "acl_c1",              vtype_stringptr,   &acl_var[1] },
-  { "acl_c2",              vtype_stringptr,   &acl_var[2] },
-  { "acl_c3",              vtype_stringptr,   &acl_var[3] },
-  { "acl_c4",              vtype_stringptr,   &acl_var[4] },
-  { "acl_c5",              vtype_stringptr,   &acl_var[5] },
-  { "acl_c6",              vtype_stringptr,   &acl_var[6] },
-  { "acl_c7",              vtype_stringptr,   &acl_var[7] },
-  { "acl_c8",              vtype_stringptr,   &acl_var[8] },
-  { "acl_c9",              vtype_stringptr,   &acl_var[9] },
-  { "acl_m0",              vtype_stringptr,   &acl_var[10] },
-  { "acl_m1",              vtype_stringptr,   &acl_var[11] },
-  { "acl_m2",              vtype_stringptr,   &acl_var[12] },
-  { "acl_m3",              vtype_stringptr,   &acl_var[13] },
-  { "acl_m4",              vtype_stringptr,   &acl_var[14] },
-  { "acl_m5",              vtype_stringptr,   &acl_var[15] },
-  { "acl_m6",              vtype_stringptr,   &acl_var[16] },
-  { "acl_m7",              vtype_stringptr,   &acl_var[17] },
-  { "acl_m8",              vtype_stringptr,   &acl_var[18] },
-  { "acl_m9",              vtype_stringptr,   &acl_var[19] },
   { "acl_verify_message",  vtype_stringptr,   &acl_verify_message },
   { "address_data",        vtype_stringptr,   &deliver_address_data },
   { "address_file",        vtype_stringptr,   &address_file },
@@ -399,6 +381,7 @@ static var_entry var_table[] = {
   { "message_body",        vtype_msgbody,     &message_body },
   { "message_body_end",    vtype_msgbody_end, &message_body_end },
   { "message_body_size",   vtype_int,         &message_body_size },
+  { "message_exim_id",     vtype_stringptr,   &message_id },
   { "message_headers",     vtype_msgheaders,  NULL },
   { "message_id",          vtype_stringptr,   &message_id },
   { "message_linecount",   vtype_int,         &message_linecount },
@@ -481,7 +464,8 @@ static var_entry var_table[] = {
   { "sender_rcvhost",      vtype_stringptr,   &sender_rcvhost },
   { "sender_verify_failure",vtype_stringptr,  &sender_verify_failure },
   { "smtp_active_hostname", vtype_stringptr,  &smtp_active_hostname },
-  { "smtp_command_argument", vtype_stringptr, &smtp_command_argument },
+  { "smtp_command",        vtype_stringptr,   &smtp_cmd_buffer },
+  { "smtp_command_argument", vtype_stringptr, &smtp_cmd_argument },
   { "sn0",                 vtype_filter_int,  &filter_sn[0] },
   { "sn1",                 vtype_filter_int,  &filter_sn[1] },
   { "sn2",                 vtype_filter_int,  &filter_sn[2] },
@@ -1203,7 +1187,8 @@ else
   uschar *decoded, *error;
   while (ptr > yield && isspace(ptr[-1])) ptr--;
   *ptr = 0;
-  decoded = rfc2047_decode2(yield, TRUE, charset, '?', NULL, newsize, &error);
+  decoded = rfc2047_decode2(yield, check_rfc2047_length, charset, '?', NULL,
+    newsize, &error);
   if (error != NULL)
     {
     DEBUG(D_any) debug_printf("*** error in RFC 2047 decoding: %s\n"
@@ -1246,6 +1231,48 @@ find_variable(uschar *name, BOOL exists_only, BOOL skipping, int *newsize)
 int first = 0;
 int last = var_table_size;
 
+/* Handle ACL variables, which are not in the table because their number may
+vary depending on a build-time setting. If the variable's name is not of the
+form acl_mddd or acl_cddd, where the d's are digits, fall through to look for
+other names that start with acl_. */
+
+if (Ustrncmp(name, "acl_", 4) == 0)
+  {
+  uschar *endptr;
+  int offset = -1;
+  int max = 0;
+
+  if (name[4] == 'm')
+    {
+    offset = ACL_CVARS;
+    max = ACL_MVARS;
+    }
+  else if (name[4] == 'c')
+    {
+    offset = 0;
+    max = ACL_CVARS;
+    }
+
+  if (offset >= 0)
+    {
+    int n = Ustrtoul(name + 5, &endptr, 10);
+    if (*endptr == 0 && n < max)
+      return (acl_var[offset + n] == NULL)? US"" : acl_var[offset + n];
+    }
+  }
+
+/* Similarly for $auth<n> variables. */
+
+if (Ustrncmp(name, "auth", 4) == 0)
+  {
+  uschar *endptr;
+  int n = Ustrtoul(name + 4, &endptr, 10);
+  if (*endptr == 0 && n != 0 && n <= AUTH_VARS)
+    return (auth_vars[n-1] == NULL)? US"" : auth_vars[n-1];
+  }
+
+/* For all other variables, search the table */
+
 while (last > first)
   {
   uschar *s, *domain;
@@ -1257,7 +1284,7 @@ while (last > first)
   if (c < 0) { last = middle; continue; }
 
   /* Found an existing variable. If in skipping state, the value isn't needed,
-  and we want to avoid processing (such as looking up up the host name). */
+  and we want to avoid processing (such as looking up the host name). */
 
   if (skipping) return US"";
 
@@ -1423,10 +1450,22 @@ while (last > first)
     return tod_stamp(tod_log_datestamp);
 
     case vtype_reply:                          /* Get reply address */
-    s = find_header(US"reply-to:", exists_only, newsize, FALSE,
+    s = find_header(US"reply-to:", exists_only, newsize, TRUE,
       headers_charset);
+    if (s != NULL) while (isspace(*s)) s++;
     if (s == NULL || *s == 0)
-      s = find_header(US"from:", exists_only, newsize, FALSE, headers_charset);
+      {
+      *newsize = 0;                            /* For the *s==0 case */
+      s = find_header(US"from:", exists_only, newsize, TRUE, headers_charset);
+      }
+    if (s != NULL)
+      {
+      uschar *t;
+      while (isspace(*s)) s++;
+      for (t = s; *t != 0; t++) if (*t == '\n') *t = ' ';
+      while (t > s && isspace(t[-1])) t--;
+      *t = 0;
+      }
     return (s == NULL)? US"" : s;
 
     /* A recipients list is available only during system message filtering,
@@ -1707,7 +1746,7 @@ switch(cond_type)
     case ECOND_ISIP4:
     case ECOND_ISIP6:
     rc = string_is_ip_address(sub[0], NULL);
-    *yield = ((cond_type == ECOND_ISIP)? (rc > 0) :
+    *yield = ((cond_type == ECOND_ISIP)? (rc != 0) :
              (cond_type == ECOND_ISIP4)? (rc == 4) : (rc == 6)) == testfor;
     break;
 
@@ -1854,25 +1893,8 @@ switch(cond_type)
 
     if (!isalpha(name[0]))
       {
-      uschar *endptr;
-      num[i] = (int)Ustrtol((const uschar *)sub[i], &endptr, 10);
-      if (tolower(*endptr) == 'k')
-        {
-        num[i] *= 1024;
-        endptr++;
-        }
-      else if (tolower(*endptr) == 'm')
-        {
-        num[i] *= 1024*1024;
-        endptr++;
-        }
-      while (isspace(*endptr)) endptr++;
-      if (*endptr != 0)
-        {
-        expand_string_message = string_sprintf("\"%s\" is not a number",
-          sub[i]);
-        return NULL;
-        }
+      num[i] = expand_string_integer(sub[i], FALSE);
+      if (expand_string_message != NULL) return NULL;
       }
     }
 
@@ -1967,7 +1989,7 @@ switch(cond_type)
     goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_IP:       /* Match IP address in a host list */
-    if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) <= 0)
+    if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) == 0)
       {
       expand_string_message = string_sprintf("\"%s\" is not an IP address",
         sub[0]);
@@ -2529,8 +2551,8 @@ Returns:  pointer to string containing the last three
 static uschar *
 prvs_daystamp(int day_offset)
 {
-uschar *days = store_get(16);
-(void)string_format(days, 16, TIME_T_FMT,
+uschar *days = store_get(32);                /* Need at least 24 for cases */
+(void)string_format(days, 32, TIME_T_FMT,    /* where TIME_T_FMT is %lld */
   (time(NULL) + day_offset*86400)/86400);
 return (Ustrlen(days) >= 3) ? &days[Ustrlen(days)-3] : US"100";
 }
@@ -2745,12 +2767,14 @@ uschar *s = *sptr;
 int x = eval_term(&s, decimal, error);
 if (*error == NULL)
   {
-  while (*s == '*' || *s == '/')
+  while (*s == '*' || *s == '/' || *s == '%')
     {
     int op = *s++;
     int y = eval_term(&s, decimal, error);
     if (*error != NULL) break;
-    if (op == '*') x *= y; else x /= y;
+    if (op == '*') x *= y;
+      else if (op == '/') x /= y;
+      else x %= y;
     }
   }
 *sptr = s;
@@ -3125,7 +3149,7 @@ while (*s != 0)
       /* Check that a key was provided for those lookup types that need it,
       and was not supplied for those that use the query style. */
 
-      if (!mac_islookup(stype, lookup_querystyle))
+      if (!mac_islookup(stype, lookup_querystyle|lookup_absfilequery))
         {
         if (key == NULL)
           {
@@ -3145,7 +3169,9 @@ while (*s != 0)
         }
 
       /* Get the next string in brackets and expand it. It is the file name for
-      single-key+file lookups, and the whole query otherwise. */
+      single-key+file lookups, and the whole query otherwise. In the case of
+      queries that also require a file name (e.g. sqlite), the file name comes
+      first. */
 
       if (*s != '{') goto EXPAND_FAILED_CURLY;
       filename = expand_string_internal(s+1, TRUE, &s, skipping);
@@ -3154,12 +3180,30 @@ while (*s != 0)
       while (isspace(*s)) s++;
 
       /* If this isn't a single-key+file lookup, re-arrange the variables
-      to be appropriate for the search_ functions. */
+      to be appropriate for the search_ functions. For query-style lookups,
+      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)
         {
+        while (isspace(*filename)) filename++;
         key = filename;
-        filename = NULL;
+
+        if (mac_islookup(stype, lookup_querystyle))
+          {
+          filename = NULL;
+          }
+        else
+          {
+          if (*filename != '/')
+            {
+            expand_string_message = string_sprintf(
+              "absolute file name expected for \"%s\" lookup", name);
+            goto EXPAND_FAILED;
+            }
+          while (*key != 0 && !isspace(*key)) key++;
+          if (*key != 0) *key++ = 0;
+          }
         }
 
       /* If skipping, don't do the next bit - just lookup_value == NULL, as if
@@ -3333,15 +3377,24 @@ while (*s != 0)
       domain = Ustrrchr(sub_arg[0],'@');
       if ( (domain == NULL) || (domain == sub_arg[0]) || (Ustrlen(domain) == 1) )
         {
-        expand_string_message = US"first parameter must be a qualified email address";
+        expand_string_message = US"prvs first argument must be a qualified email address";
+        goto EXPAND_FAILED;
+        }
+
+      /* Calculate the hash. The second argument must be a single-digit
+      key number, or unset. */
+
+      if (sub_arg[2] != NULL &&
+          (!isdigit(sub_arg[2][0]) || sub_arg[2][1] != 0))
+        {
+        expand_string_message = US"prvs second argument must be a single digit";
         goto EXPAND_FAILED;
         }
 
-      /* Calculate the hash */
       p = prvs_hmac_sha1(sub_arg[0],sub_arg[1],sub_arg[2],prvs_daystamp(7));
       if (p == NULL)
         {
-        expand_string_message = US"hmac-sha1 conversion failed";
+        expand_string_message = US"prvs hmac-sha1 conversion failed";
         goto EXPAND_FAILED;
         }
 
@@ -3368,18 +3421,23 @@ while (*s != 0)
       int mysize = 0, myptr = 0;
       const pcre *re;
       uschar *p;
-      /* Ugliness: We want to expand parameter 1 first, then set
+
+      /* TF: Ugliness: We want to expand parameter 1 first, then set
          up expansion variables that are used in the expansion of
          parameter 2. So we clone the string for the first
-         expansion, where we only expand paramter 1. */
-      uschar *s_backup = string_copy(s);
+         expansion, where we only expand parameter 1.
+
+         PH: Actually, that isn't necessary. The read_subs() function is
+         designed to work this way for the ${if and ${lookup expansions. I've
+         tidied the code.
+      */
 
       /* Reset expansion variables */
       prvscheck_result = NULL;
       prvscheck_address = NULL;
       prvscheck_keynum = NULL;
 
-      switch(read_subs(sub_arg, 1, 1, &s_backup, skipping, FALSE, US"prvs"))
+      switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, US"prvs"))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -3389,7 +3447,8 @@ while (*s != 0)
       re = regex_must_compile(US"^prvs\\=(.+)\\/([0-9])([0-9]{3})([A-F0-9]{6})\\@(.+)$",
                               TRUE,FALSE);
 
-      if (regex_match_and_setup(re,sub_arg[0],0,-1)) {
+      if (regex_match_and_setup(re,sub_arg[0],0,-1))
+        {
         uschar *local_part = string_copyn(expand_nstring[1],expand_nlength[1]);
         uschar *key_num = string_copyn(expand_nstring[2],expand_nlength[2]);
         uschar *daystamp = string_copyn(expand_nstring[3],expand_nlength[3]);
@@ -3409,21 +3468,19 @@ while (*s != 0)
         prvscheck_address[myptr] = '\0';
         prvscheck_keynum = string_copy(key_num);
 
-        /* Now re-expand all arguments in the usual manner */
-        switch(read_subs(sub_arg, 3, 3, &s, skipping, TRUE, US"prvs"))
+        /* Now expand the second argument */
+        switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, US"prvs"))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
           case 3: goto EXPAND_FAILED;
           }
 
-        if (*sub_arg[2] == '\0')
-          yield = string_cat(yield,&size,&ptr,prvscheck_address,Ustrlen(prvscheck_address));
-        else
-          yield = string_cat(yield,&size,&ptr,sub_arg[2],Ustrlen(sub_arg[2]));
-
         /* Now we have the key and can check the address. */
-        p = prvs_hmac_sha1(prvscheck_address, sub_arg[1], prvscheck_keynum, daystamp);
+
+        p = prvs_hmac_sha1(prvscheck_address, sub_arg[0], prvscheck_keynum,
+          daystamp);
+
         if (p == NULL)
           {
           expand_string_message = US"hmac-sha1 conversion failed";
@@ -3432,14 +3489,15 @@ while (*s != 0)
 
         DEBUG(D_expand) debug_printf("prvscheck: received hash is %s\n", hash);
         DEBUG(D_expand) debug_printf("prvscheck:      own hash is %s\n", p);
+
         if (Ustrcmp(p,hash) == 0)
           {
           /* Success, valid BATV address. Now check the expiry date. */
           uschar *now = prvs_daystamp(0);
           unsigned int inow = 0,iexpire = 1;
 
-          sscanf(CS now,"%u",&inow);
-          sscanf(CS daystamp,"%u",&iexpire);
+          (void)sscanf(CS now,"%u",&inow);
+          (void)sscanf(CS daystamp,"%u",&iexpire);
 
           /* When "iexpire" is < 7, a "flip" has occured.
              Adjust "inow" accordingly. */
@@ -3461,12 +3519,35 @@ while (*s != 0)
           prvscheck_result = NULL;
           DEBUG(D_expand) debug_printf("prvscheck: hash failure, $pvrs_result unset\n");
           }
-      }
+
+        /* Now expand the final argument. We leave this till now so that
+        it can include $prvscheck_result. */
+
+        switch(read_subs(sub_arg, 1, 0, &s, skipping, TRUE, US"prvs"))
+          {
+          case 1: goto EXPAND_FAILED_CURLY;
+          case 2:
+          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]));
+
+        /* Reset the "internal" variables afterwards, because they are in
+        dynamic store that will be reclaimed if the expansion succeeded. */
+
+        prvscheck_address = NULL;
+        prvscheck_keynum = NULL;
+        }
       else
         {
         /* Does not look like a prvs encoded address, return the empty string.
-           We need to make sure all subs are expanded first. */
-        switch(read_subs(sub_arg, 3, 3, &s, skipping, TRUE, US"prvs"))
+           We need to make sure all subs are expanded first, so as to skip over
+           the entire item. */
+
+        switch(read_subs(sub_arg, 2, 1, &s, skipping, TRUE, US"prvs"))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
@@ -3511,7 +3592,7 @@ while (*s != 0)
         }
 
       yield = cat_file(f, yield, &size, &ptr, sub_arg[1]);
-      fclose(f);
+      (void)fclose(f);
       continue;
       }
 
@@ -3557,28 +3638,148 @@ while (*s != 0)
         }
       else sub_arg[3] = NULL;                     /* No eol if no timeout */
 
-      /* If skipping, we don't actually do anything */
+      /* If skipping, we don't actually do anything. Otherwise, arrange to
+      connect to either an IP or a Unix socket. */
 
       if (!skipping)
         {
-        /* Make a connection to the socket */
+        /* Handle an IP (internet) domain */
 
-        if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
+        if (Ustrncmp(sub_arg[0], "inet:", 5) == 0)
           {
-          expand_string_message = string_sprintf("failed to create socket: %s",
-            strerror(errno));
-          goto SOCK_FAIL;
+          BOOL connected = FALSE;
+          int namelen, port;
+          host_item shost;
+          host_item *h;
+          uschar *server_name = sub_arg[0] + 5;
+          uschar *port_name = Ustrrchr(server_name, ':');
+
+          /* Sort out the port */
+
+          if (port_name == NULL)
+            {
+            expand_string_message =
+              string_sprintf("missing port for readsocket %s", sub_arg[0]);
+            goto EXPAND_FAILED;
+            }
+          *port_name++ = 0;           /* Terminate server name */
+
+          if (isdigit(*port_name))
+            {
+            uschar *end;
+            port = Ustrtol(port_name, &end, 0);
+            if (end != port_name + Ustrlen(port_name))
+              {
+              expand_string_message =
+                string_sprintf("invalid port number %s", port_name);
+              goto EXPAND_FAILED;
+              }
+            }
+          else
+            {
+            struct servent *service_info = getservbyname(CS port_name, "tcp");
+            if (service_info == NULL)
+              {
+              expand_string_message = string_sprintf("unknown port \"%s\"",
+                port_name);
+              goto EXPAND_FAILED;
+              }
+            port = ntohs(service_info->s_port);
+            }
+
+          /* Sort out the server. */
+
+          shost.next = NULL;
+          shost.address = NULL;
+          shost.port = port;
+          shost.mx = -1;
+
+          namelen = Ustrlen(server_name);
+
+          /* Anything enclosed in [] must be an IP address. */
+
+          if (server_name[0] == '[' &&
+              server_name[namelen - 1] == ']')
+            {
+            server_name[namelen - 1] = 0;
+            server_name++;
+            if (string_is_ip_address(server_name, NULL) == 0)
+              {
+              expand_string_message =
+                string_sprintf("malformed IP address \"%s\"", server_name);
+              goto EXPAND_FAILED;
+              }
+            shost.name = shost.address = server_name;
+            }
+
+          /* Otherwise check for an unadorned IP address */
+
+          else if (string_is_ip_address(server_name, NULL) != 0)
+            shost.name = shost.address = server_name;
+
+          /* Otherwise lookup IP address(es) from the name */
+
+          else
+            {
+            shost.name = server_name;
+            if (host_find_byname(&shost, NULL, NULL, FALSE) != HOST_FOUND)
+              {
+              expand_string_message =
+                string_sprintf("no IP address found for host %s", shost.name);
+              goto EXPAND_FAILED;
+              }
+            }
+
+          /* Try to connect to the server - test each IP till one works */
+
+          for (h = &shost; h != NULL; h = h->next)
+            {
+            int af = (Ustrchr(h->address, ':') != 0)? AF_INET6 : AF_INET;
+            if ((fd = ip_socket(SOCK_STREAM, af)) == -1)
+              {
+              expand_string_message = string_sprintf("failed to create socket: "
+                "%s", strerror(errno));
+              goto SOCK_FAIL;
+              }
+
+            if (ip_connect(fd, af, h->address, port, timeout) == 0)
+              {
+              connected = TRUE;
+              break;
+              }
+            }
+
+          if (!connected)
+            {
+            expand_string_message = string_sprintf("failed to connect to "
+              "socket %s: couldn't connect to any host", sub_arg[0],
+              strerror(errno));
+            goto SOCK_FAIL;
+            }
           }
 
-        sockun.sun_family = AF_UNIX;
-        sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1),
-          sub_arg[0]);
-        if(connect(fd, (struct sockaddr *)(&sockun), sizeof(sockun)) == -1)
+        /* Handle a Unix domain socket */
+
+        else
           {
-          expand_string_message = string_sprintf("failed to connect to socket "
-            "%s: %s", sub_arg[0], strerror(errno));
-          goto SOCK_FAIL;
+          if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
+            {
+            expand_string_message = string_sprintf("failed to create socket: %s",
+              strerror(errno));
+            goto SOCK_FAIL;
+            }
+
+          sockun.sun_family = AF_UNIX;
+          sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1),
+            sub_arg[0]);
+          if(connect(fd, (struct sockaddr *)(&sockun), sizeof(sockun)) == -1)
+            {
+            expand_string_message = string_sprintf("failed to connect to socket "
+              "%s: %s", sub_arg[0], strerror(errno));
+            goto SOCK_FAIL;
+            }
           }
+
         DEBUG(D_expand) debug_printf("connected to socket %s\n", sub_arg[0]);
 
         /* Write the request string, if not empty */
@@ -3604,7 +3805,7 @@ while (*s != 0)
         alarm(timeout);
         yield = cat_file(f, yield, &size, &ptr, sub_arg[3]);
         alarm(0);
-        fclose(f);
+        (void)fclose(f);
 
         /* After a timeout, we restore the pointer in the result, that is,
         make sure we add nothing from the socket. */
@@ -3612,7 +3813,7 @@ while (*s != 0)
         if (sigalrm_seen)
           {
           ptr = save_ptr;
-          expand_string_message = US"socket read timed out";
+          expand_string_message = US "socket read timed out";
           goto SOCK_FAIL;
           }
         }
@@ -3701,7 +3902,7 @@ while (*s != 0)
 
         /* Nothing is written to the standard input. */
 
-        close(fd_in);
+        (void)close(fd_in);
 
         /* Wait for the process to finish, applying the timeout, and inspect its
         return code for serious disasters. Simple non-zero returns are passed on.
@@ -3732,7 +3933,7 @@ while (*s != 0)
         f = fdopen(fd_out, "rb");
         lookup_value = NULL;
         lookup_value = cat_file(f, lookup_value, &lsize, &lptr, NULL);
-        fclose(f);
+        (void)fclose(f);
         }
 
       /* Process the yes/no strings; $value may be useful in both cases */
@@ -4331,6 +4532,8 @@ while (*s != 0)
         continue;
         }
 
+      /* Note that for Darwin and Cygwin, BASE_62 actually has the value 36 */
+
       case EOP_BASE62D:
         {
         uschar buf[16];
@@ -4342,10 +4545,11 @@ while (*s != 0)
           if (t == NULL)
             {
             expand_string_message = string_sprintf("argument for base62d "
-              "operator is \"%s\", which is not a base 62 number", sub);
+              "operator is \"%s\", which is not a base %d number", sub,
+              BASE_62);
             goto EXPAND_FAILED;
             }
-          n = n * 62 + (t - base62_chars);
+          n = n * BASE_62 + (t - base62_chars);
           }
         (void)sprintf(CS buf, "%ld", n);
         yield = string_cat(yield, &size, &ptr, buf, Ustrlen(buf));
@@ -4636,7 +4840,7 @@ while (*s != 0)
         {
         uschar buffer[2048];
         uschar *string = parse_quote_2047(sub, Ustrlen(sub), headers_charset,
-          buffer, sizeof(buffer));
+          buffer, sizeof(buffer), FALSE);
         yield = string_cat(yield, &size, &ptr, string, Ustrlen(string));
         continue;
         }
@@ -4689,6 +4893,20 @@ while (*s != 0)
 
       /* Handle time period formating */
 
+      case EOP_TIME_EVAL:
+        {
+        int n = readconf_readtime(sub, 0, FALSE);
+        if (n < 0)
+          {
+          expand_string_message = string_sprintf("string \"%s\" is not an "
+            "Exim time interval in \"%s\" operator", sub, name);
+          goto EXPAND_FAILED;
+          }
+        sprintf(CS var_buffer, "%d", n);
+        yield = string_cat(yield, &size, &ptr, var_buffer, Ustrlen(var_buffer));
+        continue;
+        }
+
       case EOP_TIME_INTERVAL:
         {
         int n;
@@ -4825,6 +5043,12 @@ while (*s != 0)
         mode_t mode;
         struct stat st;
 
+        if ((expand_forbid & RDO_EXISTS) != 0)
+          {
+          expand_string_message = US"Use of the stat() expansion is not permitted";
+          goto EXPAND_FAILED;
+          }
+
         if (stat(CS sub, &st) < 0)
           {
           expand_string_message = string_sprintf("stat(%s) failed: %s",
@@ -5019,22 +5243,26 @@ return yield;
 
 /* Expand a string, and convert the result into an integer.
 
-Argument: the string to be expanded
+Arguments:
+  string  the string to be expanded
+  isplus  TRUE if a non-negative number is expected
 
 Returns:  the integer value, or
           -1 for an expansion error               ) in both cases, message in
           -2 for an integer interpretation error  ) expand_string_message
-
+          expand_string_message is set NULL for an OK integer
 */
 
 int
-expand_string_integer(uschar *string)
+expand_string_integer(uschar *string, BOOL isplus)
 {
 long int value;
 uschar *s = expand_string(string);
 uschar *msg = US"invalid integer \"%s\"";
 uschar *endptr;
 
+/* If expansion failed, expand_string_message will be set. */
+
 if (s == NULL) return -1;
 
 /* On an overflow, strtol() returns LONG_MAX or LONG_MIN, and sets errno
@@ -5042,12 +5270,17 @@ to ERANGE. When there isn't an overflow, errno is not changed, at least on some
 systems, so we set it zero ourselves. */
 
 errno = 0;
+expand_string_message = NULL;               /* Indicates no error */
 value = strtol(CS s, CSS &endptr, 0);
 
 if (endptr == s)
   {
   msg = US"integer expected but \"%s\" found";
   }
+else if (value < 0 && isplus)
+  {
+  msg = US"non-negative integer expected but \"%s\" found";
+  }
 else
   {
   /* Ensure we can cast this down to an int */