Handle ${run} returning more data than OS pipe buffer size.
[exim.git] / src / src / expand.c
index d049466f1897c264783e42068f1eb44388d96270..ec4dd71f94392407f12a78208b6af1c107825871 100644 (file)
@@ -1,10 +1,8 @@
-/* $Cambridge: exim/src/src/expand.c,v 1.70 2006/11/13 12:07:46 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2006 */
+/* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
 
 #include "exim.h"
 
+/* Recursively called function */
+
+static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL);
+
 #ifdef STAND_ALONE
 #ifndef SUPPORT_CRYPTEQ
 #define SUPPORT_CRYPTEQ
 #endif
 #endif
 
+#ifdef LOOKUP_LDAP
+#include "lookups/ldap.h"
+#endif
+
 #ifdef SUPPORT_CRYPTEQ
 #ifdef CRYPT_H
 #include <crypt.h>
@@ -28,15 +34,63 @@ extern char* crypt16(char*, char*);
 #endif
 #endif
 
-#ifdef LOOKUP_LDAP
-#include "lookups/ldap.h"
-#endif
-
-
-
-/* Recursively called function */
+/* The handling of crypt16() is a mess. I will record below the analysis of the
+mess that was sent to me. We decided, however, to make changing this very low
+priority, because in practice people are moving away from the crypt()
+algorithms nowadays, so it doesn't seem worth it.
+
+<quote>
+There is an algorithm named "crypt16" in Ultrix and Tru64.  It crypts
+the first 8 characters of the password using a 20-round version of crypt
+(standard crypt does 25 rounds).  It then crypts the next 8 characters,
+or an empty block if the password is less than 9 characters, using a
+20-round version of crypt and the same salt as was used for the first
+block.  Charaters after the first 16 are ignored.  It always generates
+a 16-byte hash, which is expressed together with the salt as a string
+of 24 base 64 digits.  Here are some links to peruse:
+
+        http://cvs.pld.org.pl/pam/pamcrypt/crypt16.c?rev=1.2
+        http://seclists.org/bugtraq/1999/Mar/0076.html
+
+There's a different algorithm named "bigcrypt" in HP-UX, Digital Unix,
+and OSF/1.  This is the same as the standard crypt if given a password
+of 8 characters or less.  If given more, it first does the same as crypt
+using the first 8 characters, then crypts the next 8 (the 9th to 16th)
+using as salt the first two base 64 digits from the first hash block.
+If the password is more than 16 characters then it crypts the 17th to 24th
+characters using as salt the first two base 64 digits from the second hash
+block.  And so on: I've seen references to it cutting off the password at
+40 characters (5 blocks), 80 (10 blocks), or 128 (16 blocks).  Some links:
+
+        http://cvs.pld.org.pl/pam/pamcrypt/bigcrypt.c?rev=1.2
+        http://seclists.org/bugtraq/1999/Mar/0109.html
+        http://h30097.www3.hp.com/docs/base_doc/DOCUMENTATION/HTML/AA-Q0R2D-
+             TET1_html/sec.c222.html#no_id_208
+
+Exim has something it calls "crypt16".  It will either use a native
+crypt16 or its own implementation.  A native crypt16 will presumably
+be the one that I called "crypt16" above.  The internal "crypt16"
+function, however, is a two-block-maximum implementation of what I called
+"bigcrypt".  The documentation matches the internal code.
+
+I suspect that whoever did the "crypt16" stuff for Exim didn't realise
+that crypt16 and bigcrypt were different things.
+
+Exim uses the LDAP-style scheme identifier "{crypt16}" to refer
+to whatever it is using under that name.  This unfortunately sets a
+precedent for using "{crypt16}" to identify two incompatible algorithms
+whose output can't be distinguished.  With "{crypt16}" thus rendered
+ambiguous, I suggest you deprecate it and invent two new identifiers
+for the two algorithms.
+
+Both crypt16 and bigcrypt are very poor algorithms, btw.  Hashing parts
+of the password separately means they can be cracked separately, so
+the double-length hash only doubles the cracking effort instead of
+squaring it.  I recommend salted SHA-1 ({SSHA}), or the Blowfish-based
+bcrypt ({CRYPT}$2a$).
+</quote>
+*/
 
-static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL);
 
 
 
@@ -50,17 +104,20 @@ alphabetical order. */
 static uschar *item_table[] = {
   US"dlfunc",
   US"extract",
+  US"filter",
   US"hash",
   US"hmac",
   US"if",
   US"length",
   US"lookup",
+  US"map",
   US"nhash",
   US"perl",
   US"prvs",
   US"prvscheck",
   US"readfile",
   US"readsocket",
+  US"reduce",
   US"run",
   US"sg",
   US"substr",
@@ -69,17 +126,20 @@ static uschar *item_table[] = {
 enum {
   EITEM_DLFUNC,
   EITEM_EXTRACT,
+  EITEM_FILTER,
   EITEM_HASH,
   EITEM_HMAC,
   EITEM_IF,
   EITEM_LENGTH,
   EITEM_LOOKUP,
+  EITEM_MAP,
   EITEM_NHASH,
   EITEM_PERL,
   EITEM_PRVS,
   EITEM_PRVSCHECK,
   EITEM_READFILE,
   EITEM_READSOCK,
+  EITEM_REDUCE,
   EITEM_RUN,
   EITEM_SG,
   EITEM_SUBSTR,
@@ -94,6 +154,7 @@ static uschar *op_table_underscore[] = {
   US"from_utf8",
   US"local_part",
   US"quote_local_part",
+  US"reverse_ip",
   US"time_eval",
   US"time_interval"};
 
@@ -101,11 +162,13 @@ enum {
   EOP_FROM_UTF8,
   EOP_LOCAL_PART,
   EOP_QUOTE_LOCAL_PART,
+  EOP_REVERSE_IP,
   EOP_TIME_EVAL,
   EOP_TIME_INTERVAL };
 
 static uschar *op_table_main[] = {
   US"address",
+  US"addresses",
   US"base62",
   US"base62d",
   US"domain",
@@ -124,7 +187,9 @@ static uschar *op_table_main[] = {
   US"nh",
   US"nhash",
   US"quote",
+  US"randint",
   US"rfc2047",
+  US"rfc2047d",
   US"rxquote",
   US"s",
   US"sha1",
@@ -136,6 +201,7 @@ static uschar *op_table_main[] = {
 
 enum {
   EOP_ADDRESS =  sizeof(op_table_underscore)/sizeof(uschar *),
+  EOP_ADDRESSES,
   EOP_BASE62,
   EOP_BASE62D,
   EOP_DOMAIN,
@@ -154,7 +220,9 @@ enum {
   EOP_NH,
   EOP_NHASH,
   EOP_QUOTE,
+  EOP_RANDINT,
   EOP_RFC2047,
+  EOP_RFC2047D,
   EOP_RXQUOTE,
   EOP_S,
   EOP_SHA1,
@@ -176,12 +244,16 @@ static uschar *cond_table[] = {
   US">",
   US">=",
   US"and",
+  US"bool",
+  US"bool_lax",
   US"crypteq",
   US"def",
   US"eq",
   US"eqi",
   US"exists",
   US"first_delivery",
+  US"forall",
+  US"forany",
   US"ge",
   US"gei",
   US"gt",
@@ -215,12 +287,16 @@ enum {
   ECOND_NUM_G,
   ECOND_NUM_GE,
   ECOND_AND,
+  ECOND_BOOL,
+  ECOND_BOOL_LAX,
   ECOND_CRYPTEQ,
   ECOND_DEF,
   ECOND_STR_EQ,
   ECOND_STR_EQI,
   ECOND_EXISTS,
   ECOND_FIRST_DELIVERY,
+  ECOND_FORALL,
+  ECOND_FORANY,
   ECOND_STR_GE,
   ECOND_STR_GEI,
   ECOND_STR_GT,
@@ -250,9 +326,9 @@ enum {
 /* Type for main variable table */
 
 typedef struct {
-  char *name;
-  int   type;
-  void *value;
+  const char *name;
+  int         type;
+  void       *value;
 } var_entry;
 
 /* Type for entries pointing to address/length pairs. Not currently
@@ -279,7 +355,8 @@ enum {
   vtype_localpart,      /* extract local part from string */
   vtype_domain,         /* extract domain from string */
   vtype_recipients,     /* extract recipients from recipients list */
-                        /* (enabled only during system filtering */
+                        /* (available only in system filters, ACLs, and */
+                        /* local_scan()) */
   vtype_todbsdin,       /* value not used; generate BSD inbox tod */
   vtype_tode,           /* value not used; generate tod in epoch format */
   vtype_todf,           /* value not used; generate full tod */
@@ -293,9 +370,9 @@ enum {
   vtype_load_avg,       /* value not used; result is int from os_getloadavg */
   vtype_pspace,         /* partition space; value is T/F for spool/log */
   vtype_pinodes         /* partition inodes; value is T/F for spool/log */
-#ifdef EXPERIMENTAL_DOMAINKEYS
,vtype_dk_verify       /* Serve request out of DomainKeys verification structure */
-#endif
+  #ifndef DISABLE_DKIM
 ,vtype_dkim           /* Lookup of value in DKIM signature */
+  #endif
   };
 
 /* This table must be kept in alphabetical order. */
@@ -310,6 +387,9 @@ static var_entry var_table[] = {
   { "authenticated_id",    vtype_stringptr,   &authenticated_id },
   { "authenticated_sender",vtype_stringptr,   &authenticated_sender },
   { "authentication_failed",vtype_int,        &authentication_failed },
+#ifdef WITH_CONTENT_SCAN
+  { "av_failed",           vtype_int,         &av_failed },
+#endif
 #ifdef EXPERIMENTAL_BRIGHTMAIL
   { "bmi_alt_location",    vtype_stringptr,   &bmi_alt_location },
   { "bmi_base64_tracker_verdict", vtype_stringptr, &bmi_base64_tracker_verdict },
@@ -325,24 +405,38 @@ static var_entry var_table[] = {
   { "compile_date",        vtype_stringptr,   &version_date },
   { "compile_number",      vtype_stringptr,   &version_cnumber },
   { "csa_status",          vtype_stringptr,   &csa_status },
+#ifdef EXPERIMENTAL_DCC
+  { "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
-#ifdef EXPERIMENTAL_DOMAINKEYS
-  { "dk_domain",           vtype_stringptr,   &dk_signing_domain },
-  { "dk_is_signed",        vtype_dk_verify,   NULL },
-  { "dk_result",           vtype_dk_verify,   NULL },
-  { "dk_selector",         vtype_stringptr,   &dk_signing_selector },
-  { "dk_sender",           vtype_dk_verify,   NULL },
-  { "dk_sender_domain",    vtype_dk_verify,   NULL },
-  { "dk_sender_local_part",vtype_dk_verify,   NULL },
-  { "dk_sender_source",    vtype_dk_verify,   NULL },
-  { "dk_signsall",         vtype_dk_verify,   NULL },
-  { "dk_status",           vtype_dk_verify,   NULL },
-  { "dk_testing",          vtype_dk_verify,   NULL },
+#ifndef DISABLE_DKIM
+  { "dkim_algo",           vtype_dkim,        (void *)DKIM_ALGO },
+  { "dkim_bodylength",     vtype_dkim,        (void *)DKIM_BODYLENGTH },
+  { "dkim_canon_body",     vtype_dkim,        (void *)DKIM_CANON_BODY },
+  { "dkim_canon_headers",  vtype_dkim,        (void *)DKIM_CANON_HEADERS },
+  { "dkim_copiedheaders",  vtype_dkim,        (void *)DKIM_COPIEDHEADERS },
+  { "dkim_created",        vtype_dkim,        (void *)DKIM_CREATED },
+  { "dkim_cur_signer",     vtype_stringptr,   &dkim_cur_signer },
+  { "dkim_domain",         vtype_stringptr,   &dkim_signing_domain },
+  { "dkim_expires",        vtype_dkim,        (void *)DKIM_EXPIRES },
+  { "dkim_headernames",    vtype_dkim,        (void *)DKIM_HEADERNAMES },
+  { "dkim_identity",       vtype_dkim,        (void *)DKIM_IDENTITY },
+  { "dkim_key_granularity",vtype_dkim,        (void *)DKIM_KEY_GRANULARITY },
+  { "dkim_key_nosubdomains",vtype_dkim,       (void *)DKIM_NOSUBDOMAINS },
+  { "dkim_key_notes",      vtype_dkim,        (void *)DKIM_KEY_NOTES },
+  { "dkim_key_srvtype",    vtype_dkim,        (void *)DKIM_KEY_SRVTYPE },
+  { "dkim_key_testing",    vtype_dkim,        (void *)DKIM_KEY_TESTING },
+  { "dkim_selector",       vtype_stringptr,   &dkim_signing_selector },
+  { "dkim_signers",        vtype_stringptr,   &dkim_signers },
+  { "dkim_verify_reason",  vtype_dkim,        (void *)DKIM_VERIFY_REASON },
+  { "dkim_verify_status",  vtype_dkim,        (void *)DKIM_VERIFY_STATUS},
 #endif
   { "dnslist_domain",      vtype_stringptr,   &dnslist_domain },
+  { "dnslist_matched",     vtype_stringptr,   &dnslist_matched },
   { "dnslist_text",        vtype_stringptr,   &dnslist_text },
   { "dnslist_value",       vtype_stringptr,   &dnslist_value },
   { "domain",              vtype_stringptr,   &deliver_domain },
@@ -362,6 +456,7 @@ static var_entry var_table[] = {
   { "inode",               vtype_ino,         &deliver_inode },
   { "interface_address",   vtype_stringptr,   &interface_address },
   { "interface_port",      vtype_int,         &interface_port },
+  { "item",                vtype_stringptr,   &iterate_item },
   #ifdef LOOKUP_LDAP
   { "ldap_dn",             vtype_stringptr,   &eldap_dn },
   #endif
@@ -380,6 +475,7 @@ static var_entry var_table[] = {
 #ifdef WITH_CONTENT_SCAN
   { "malware_name",        vtype_stringptr,   &malware_name },
 #endif
+  { "max_received_linelength", vtype_int,     &max_received_linelength },
   { "message_age",         vtype_int,         &message_age },
   { "message_body",        vtype_msgbody,     &message_body },
   { "message_body_end",    vtype_msgbody_end, &message_body_end },
@@ -469,9 +565,13 @@ static var_entry var_table[] = {
   { "sender_rate_period",  vtype_stringptr,   &sender_rate_period },
   { "sender_rcvhost",      vtype_stringptr,   &sender_rcvhost },
   { "sender_verify_failure",vtype_stringptr,  &sender_verify_failure },
+  { "sending_ip_address",  vtype_stringptr,   &sending_ip_address },
+  { "sending_port",        vtype_int,         &sending_port },
   { "smtp_active_hostname", vtype_stringptr,  &smtp_active_hostname },
   { "smtp_command",        vtype_stringptr,   &smtp_cmd_buffer },
   { "smtp_command_argument", vtype_stringptr, &smtp_cmd_argument },
+  { "smtp_count_at_connection_start", vtype_int, &smtp_accept_count },
+  { "smtp_notquit_reason", vtype_stringptr,   &smtp_notquit_reason },
   { "sn0",                 vtype_filter_int,  &filter_sn[0] },
   { "sn1",                 vtype_filter_int,  &filter_sn[1] },
   { "sn2",                 vtype_filter_int,  &filter_sn[2] },
@@ -489,6 +589,7 @@ static var_entry var_table[] = {
   { "spam_score_int",      vtype_stringptr,   &spam_score_int },
 #endif
 #ifdef EXPERIMENTAL_SPF
+  { "spf_guess",           vtype_stringptr,   &spf_guess },
   { "spf_header_comment",  vtype_stringptr,   &spf_header_comment },
   { "spf_received",        vtype_stringptr,   &spf_received },
   { "spf_result",          vtype_stringptr,   &spf_result },
@@ -532,9 +633,9 @@ static BOOL malformed_header;
 
 /* For textual hashes */
 
-static char *hashcodes = "abcdefghijklmnopqrtsuvwxyz"
-                         "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-                         "0123456789";
+static const char *hashcodes = "abcdefghijklmnopqrtsuvwxyz"
+                               "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                               "0123456789";
 
 enum { HMAC_MD5, HMAC_SHA1 };
 
@@ -636,6 +737,8 @@ or "false" value. Failure of the expansion yields FALSE; logged unless it was a
 forced fail or lookup defer. All store used by the function can be released on
 exit.
 
+The actual false-value tests should be replicated for ECOND_BOOL_LAX.
+
 Arguments:
   condition     the condition string
   m1            text to be incorporated in panic error
@@ -665,6 +768,75 @@ return rc;
 
 
 
+/*************************************************
+*        Pseudo-random number generation         *
+*************************************************/
+
+/* Pseudo-random number generation.  The result is not "expected" to be
+cryptographically strong but not so weak that someone will shoot themselves
+in the foot using it as a nonce in some email header scheme or whatever
+weirdness they'll twist this into.  The result should ideally handle fork().
+
+However, if we're stuck unable to provide this, then we'll fall back to
+appallingly bad randomness.
+
+If SUPPORT_TLS is defined and OpenSSL is used, then this will not be used.
+The GNUTLS randomness functions found do not seem amenable to extracting
+random numbers outside of a TLS context.  Any volunteers?
+
+Arguments:
+  max       range maximum
+Returns     a random number in range [0, max-1]
+*/
+
+#if !defined(SUPPORT_TLS) || defined(USE_GNUTLS)
+int
+pseudo_random_number(int max)
+{
+  static pid_t pid = 0;
+  pid_t p2;
+#if defined(HAVE_SRANDOM) && !defined(HAVE_SRANDOMDEV)
+  struct timeval tv;
+#endif
+
+  p2 = getpid();
+  if (p2 != pid)
+    {
+    if (pid != 0)
+      {
+
+#ifdef HAVE_ARC4RANDOM
+      /* cryptographically strong randomness, common on *BSD platforms, not
+      so much elsewhere.  Alas. */
+      arc4random_stir();
+#elif defined(HAVE_SRANDOM) || defined(HAVE_SRANDOMDEV)
+#ifdef HAVE_SRANDOMDEV
+      /* uses random(4) for seeding */
+      srandomdev();
+#else
+      gettimeofday(&tv, NULL);
+      srandom(tv.tv_sec | tv.tv_usec | getpid());
+#endif
+#else
+      /* Poor randomness and no seeding here */
+#endif
+
+      }
+    pid = p2;
+    }
+
+#ifdef HAVE_ARC4RANDOM
+  return arc4random() % max;
+#elif defined(HAVE_SRANDOM) || defined(HAVE_SRANDOMDEV)
+  return random() % max;
+#else
+  /* This one returns a 16-bit number, definitely not crypto-strong */
+  return random_number(max);
+#endif
+}
+
+#endif
+
 /*************************************************
 *             Pick out a name from a string      *
 *************************************************/
@@ -1295,51 +1467,6 @@ while (last > first)
 
   switch (var_table[middle].type)
     {
-#ifdef EXPERIMENTAL_DOMAINKEYS
-
-    case vtype_dk_verify:
-    if (dk_verify_block == NULL) return US"";
-    s = NULL;
-    if (Ustrcmp(var_table[middle].name, "dk_result") == 0)
-      s = dk_verify_block->result_string;
-    if (Ustrcmp(var_table[middle].name, "dk_sender") == 0)
-      s = dk_verify_block->address;
-    if (Ustrcmp(var_table[middle].name, "dk_sender_domain") == 0)
-      s = dk_verify_block->domain;
-    if (Ustrcmp(var_table[middle].name, "dk_sender_local_part") == 0)
-      s = dk_verify_block->local_part;
-
-    if (Ustrcmp(var_table[middle].name, "dk_sender_source") == 0)
-      switch(dk_verify_block->address_source) {
-        case DK_EXIM_ADDRESS_NONE: s = US"0"; break;
-        case DK_EXIM_ADDRESS_FROM_FROM: s = US"from"; break;
-        case DK_EXIM_ADDRESS_FROM_SENDER: s = US"sender"; break;
-      }
-
-    if (Ustrcmp(var_table[middle].name, "dk_status") == 0)
-      switch(dk_verify_block->result) {
-        case DK_EXIM_RESULT_ERR: s = US"error"; break;
-        case DK_EXIM_RESULT_BAD_FORMAT: s = US"bad format"; break;
-        case DK_EXIM_RESULT_NO_KEY: s = US"no key"; break;
-        case DK_EXIM_RESULT_NO_SIGNATURE: s = US"no signature"; break;
-        case DK_EXIM_RESULT_REVOKED: s = US"revoked"; break;
-        case DK_EXIM_RESULT_NON_PARTICIPANT: s = US"non-participant"; break;
-        case DK_EXIM_RESULT_GOOD: s = US"good"; break;
-        case DK_EXIM_RESULT_BAD: s = US"bad"; break;
-      }
-
-    if (Ustrcmp(var_table[middle].name, "dk_signsall") == 0)
-      s = (dk_verify_block->signsall)? US"1" : US"0";
-
-    if (Ustrcmp(var_table[middle].name, "dk_testing") == 0)
-      s = (dk_verify_block->testing)? US"1" : US"0";
-
-    if (Ustrcmp(var_table[middle].name, "dk_is_signed") == 0)
-      s = (dk_verify_block->is_signed)? US"1" : US"0";
-
-    return (s == NULL)? US"" : s;
-#endif
-
     case vtype_filter_int:
     if (!filter_running) return NULL;
     /* Fall through */
@@ -1369,7 +1496,7 @@ while (last > first)
     return var_buffer;
 
     case vtype_load_avg:
-    sprintf(CS var_buffer, "%d", os_getloadavg()); /* load_average */
+    sprintf(CS var_buffer, "%d", OS_GETLOADAVG()); /* load_average */
     return var_buffer;
 
     case vtype_host_lookup:                    /* Lookup if not done so */
@@ -1428,9 +1555,15 @@ while (last > first)
       if (len > 0)
         {
         body[len] = 0;
-        while (len > 0)
+        if (message_body_newlines)   /* Separate loops for efficiency */
           {
-          if (body[--len] == '\n' || body[len] == 0) body[len] = ' ';
+          while (len > 0)
+            { if (body[--len] == 0) body[len] = ' '; }
+          }
+        else
+          {
+          while (len > 0)
+            { if (body[--len] == '\n' || body[len] == 0) body[len] = ' '; }
           }
         }
       }
@@ -1455,7 +1588,7 @@ while (last > first)
     return tod_stamp(tod_zulu);
 
     case vtype_todlf:                          /* Log file datestamp tod */
-    return tod_stamp(tod_log_datestamp);
+    return tod_stamp(tod_log_datestamp_daily);
 
     case vtype_reply:                          /* Get reply address */
     s = find_header(US"reply-to:", exists_only, newsize, TRUE,
@@ -1512,6 +1645,12 @@ while (last > first)
       sprintf(CS var_buffer, "%d", inodes);
       }
     return var_buffer;
+
+    #ifndef DISABLE_DKIM
+    case vtype_dkim:
+    return dkim_exim_expand_query((int)(long)var_table[middle].value);
+    #endif
+
     }
   }
 
@@ -1930,10 +2069,19 @@ switch(cond_type)
     conditions that compare numbers do not start with a letter. This just saves
     checking for them individually. */
 
-    if (!isalpha(name[0]))
+    if (!isalpha(name[0]) && yield != NULL)
       {
-      num[i] = expand_string_integer(sub[i], FALSE);
-      if (expand_string_message != NULL) return NULL;
+      if (sub[i][0] == 0)
+        {
+        num[i] = 0;
+        DEBUG(D_expand)
+          debug_printf("empty string cast to zero for numerical comparison\n");
+        }
+      else
+        {
+        num[i] = expand_string_integer(sub[i], FALSE);
+        if (expand_string_message != NULL) return NULL;
+        }
       }
     }
 
@@ -2279,6 +2427,142 @@ switch(cond_type)
   return ++s;
 
 
+  /* forall/forany: iterates a condition with different values */
+
+  case ECOND_FORALL:
+  case ECOND_FORANY:
+    {
+    int sep = 0;
+    uschar *save_iterate_item = iterate_item;
+
+    while (isspace(*s)) s++;
+    if (*s++ != '{') goto COND_FAILED_CURLY_START;
+    sub[0] = expand_string_internal(s, TRUE, &s, (yield == NULL));
+    if (sub[0] == NULL) return NULL;
+    if (*s++ != '}') goto COND_FAILED_CURLY_END;
+
+    while (isspace(*s)) s++;
+    if (*s++ != '{') goto COND_FAILED_CURLY_START;
+
+    sub[1] = s;
+
+    /* Call eval_condition once, with result discarded (as if scanning a
+    "false" part). This allows us to find the end of the condition, because if
+    the list it empty, we won't actually evaluate the condition for real. */
+
+    s = eval_condition(sub[1], NULL);
+    if (s == NULL)
+      {
+      expand_string_message = string_sprintf("%s inside \"%s\" condition",
+        expand_string_message, name);
+      return NULL;
+      }
+    while (isspace(*s)) s++;
+
+    if (*s++ != '}')
+      {
+      expand_string_message = string_sprintf("missing } at end of condition "
+        "inside \"%s\"", name);
+      return NULL;
+      }
+
+    if (yield != NULL) *yield = !testfor;
+    while ((iterate_item = string_nextinlist(&sub[0], &sep, NULL, 0)) != NULL)
+      {
+      DEBUG(D_expand) debug_printf("%s: $item = \"%s\"\n", name, iterate_item);
+      if (eval_condition(sub[1], &tempcond) == NULL)
+        {
+        expand_string_message = string_sprintf("%s inside \"%s\" condition",
+          expand_string_message, name);
+        iterate_item = save_iterate_item;
+        return NULL;
+        }
+      DEBUG(D_expand) debug_printf("%s: condition evaluated to %s\n", name,
+        tempcond? "true":"false");
+
+      if (yield != NULL) *yield = (tempcond == testfor);
+      if (tempcond == (cond_type == ECOND_FORANY)) break;
+      }
+
+    iterate_item = save_iterate_item;
+    return s;
+    }
+
+
+  /* The bool{} expansion condition maps a string to boolean.
+  The values supported should match those supported by the ACL condition
+  (acl.c, ACLC_CONDITION) so that we keep to a minimum the different ideas
+  of true/false.  Note that Router "condition" rules have a different
+  interpretation, where general data can be used and only a few values
+  map to FALSE.
+  Note that readconf.c boolean matching, for boolean configuration options,
+  only matches true/yes/false/no.
+  The bool_lax{} condition matches the Router logic, which is much more
+  liberal. */
+  case ECOND_BOOL:
+  case ECOND_BOOL_LAX:
+    {
+    uschar *sub_arg[1];
+    uschar *t, *t2;
+    uschar *ourname;
+    size_t len;
+    BOOL boolvalue = FALSE;
+    while (isspace(*s)) s++;
+    if (*s != '{') goto COND_FAILED_CURLY_START;
+    ourname = cond_type == ECOND_BOOL_LAX ? US"bool_lax" : US"bool";
+    switch(read_subs(sub_arg, 1, 1, &s, yield == NULL, FALSE, ourname))
+      {
+      case 1: expand_string_message = string_sprintf(
+                  "too few arguments or bracketing error for %s",
+                  ourname);
+      /*FALLTHROUGH*/
+      case 2:
+      case 3: return NULL;
+      }
+    t = sub_arg[0];
+    while (isspace(*t)) t++;
+    len = Ustrlen(t);
+    if (len)
+      {
+      /* trailing whitespace: seems like a good idea to ignore it too */
+      t2 = t + len - 1;
+      while (isspace(*t2)) t2--;
+      if (t2 != (t + len))
+        {
+        *++t2 = '\0';
+        len = t2 - t;
+        }
+      }
+    DEBUG(D_expand)
+      debug_printf("considering %s: %s\n", ourname, len ? t : US"<empty>");
+    /* logic for the lax case from expand_check_condition(), which also does
+    expands, and the logic is both short and stable enough that there should
+    be no maintenance burden from replicating it. */
+    if (len == 0)
+      boolvalue = FALSE;
+    else if (Ustrspn(t, "0123456789") == len)
+      {
+      boolvalue = (Uatoi(t) == 0) ? FALSE : TRUE;
+      /* expand_check_condition only does a literal string "0" check */
+      if ((cond_type == ECOND_BOOL_LAX) && (len > 1))
+        boolvalue = TRUE;
+      }
+    else if (strcmpic(t, US"true") == 0 || strcmpic(t, US"yes") == 0)
+      boolvalue = TRUE;
+    else if (strcmpic(t, US"false") == 0 || strcmpic(t, US"no") == 0)
+      boolvalue = FALSE;
+    else if (cond_type == ECOND_BOOL_LAX)
+      boolvalue = TRUE;
+    else
+      {
+      expand_string_message = string_sprintf("unrecognised boolean "
+       "value \"%s\"", t);
+      return NULL;
+      }
+    if (yield != NULL) *yield = (boolvalue == testfor);
+    return s;
+    }
+
   /* Unknown condition */
 
   default:
@@ -2823,9 +3107,47 @@ if (*error == NULL)
     int op = *s++;
     int y = eval_op_unary(&s, decimal, error);
     if (*error != NULL) break;
-    if (op == '*') x *= y;
-      else if (op == '/') x /= y;
-      else x %= y;
+    /* SIGFPE both on div/mod by zero and on INT_MIN / -1, which would give
+     * a value of INT_MAX+1. Note that INT_MIN * -1 gives INT_MIN for me, which
+     * is a bug somewhere in [gcc 4.2.1, FreeBSD, amd64].  In fact, -N*-M where
+     * -N*M is INT_MIN will yielf INT_MIN.
+     * Since we don't support floating point, this is somewhat simpler.
+     * Ideally, we'd return an error, but since we overflow for all other
+     * arithmetic, consistency suggests otherwise, but what's the correct value
+     * to use?  There is none.
+     * The C standard guarantees overflow for unsigned arithmetic but signed
+     * overflow invokes undefined behaviour; in practice, this is overflow
+     * except for converting INT_MIN to INT_MAX+1.  We also can't guarantee
+     * that long/longlong larger than int are available, or we could just work
+     * with larger types.  We should consider whether to guarantee 32bit eval
+     * and 64-bit working variables, with errors returned.  For now ...
+     * So, the only SIGFPEs occur with a non-shrinking div/mod, thus -1; we
+     * can just let the other invalid results occur otherwise, as they have
+     * until now.  For this one case, we can coerce.
+     */
+    if (y == -1 && x == INT_MIN && op != '*')
+      {
+      DEBUG(D_expand)
+        debug_printf("Integer exception dodging: %d%c-1 coerced to %d\n",
+            INT_MIN, op, INT_MAX);
+      x = INT_MAX;
+      continue;
+      }
+    if (op == '*')
+      x *= y;
+    else
+      {
+      if (y == 0)
+        {
+        *error = (op == '/') ? US"divide by zero" : US"modulo by zero";
+        x = 0;
+        break;
+        }
+      if (op == '/')
+        x /= y;
+      else
+        x %= y;
+      }
     }
   }
 *sptr = s;
@@ -2972,6 +3294,12 @@ we reset the store before processing it; if the result is in fresh store, we
 use that without copying. This is helpful for expanding strings like
 $message_headers which can get very long.
 
+There's a problem if a ${dlfunc item has side-effects that cause allocation,
+since resetting the store at the end of the expansion will free store that was
+allocated by the plugin code as well as the slop after the expanded string. So
+we skip any resets if ${dlfunc has been used. This is an unfortunate
+consequence of string expansion becoming too powerful.
+
 Arguments:
   string         the string to be expanded
   ket_ends       true if expansion is to stop at }
@@ -2997,6 +3325,7 @@ uschar *yield = store_get(size);
 uschar *s = string;
 uschar *save_expand_nstring[EXPAND_MAXN+1];
 int save_expand_nlength[EXPAND_MAXN+1];
+BOOL resetok = TRUE;
 
 expand_string_forcedfail = FALSE;
 expand_string_message = US"";
@@ -3069,7 +3398,7 @@ while (*s != 0)
 
     if (ptr == 0 && yield != NULL)
       {
-      store_reset(yield);
+      if (resetok) store_reset(yield);
       yield = NULL;
       size = 0;
       }
@@ -3385,8 +3714,8 @@ while (*s != 0)
         if (search_find_defer)
           {
           expand_string_message =
-            string_sprintf("lookup of \"%s\" gave DEFER: %s", key,
-              search_error_message);
+            string_sprintf("lookup of \"%s\" gave DEFER: %s",
+              string_printing2(key, FALSE), search_error_message);
           goto EXPAND_FAILED;
           }
         if (expand_setup > 0) expand_nmax = expand_setup;
@@ -3553,11 +3882,11 @@ while (*s != 0)
       *domain++ = '\0';
 
       yield = string_cat(yield,&size,&ptr,US"prvs=",5);
-      string_cat(yield,&size,&ptr,sub_arg[0],Ustrlen(sub_arg[0]));
-      string_cat(yield,&size,&ptr,US"/",1);
       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));
 
@@ -3595,15 +3924,15 @@ while (*s != 0)
         case 3: goto EXPAND_FAILED;
         }
 
-      re = regex_must_compile(US"^prvs\\=(.+)\\/([0-9])([0-9]{3})([A-F0-9]{6})\\@(.+)$",
+      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))
         {
-        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]);
-        uschar *hash = string_copyn(expand_nstring[4],expand_nlength[4]);
+        uschar *local_part = string_copyn(expand_nstring[4],expand_nlength[4]);
+        uschar *key_num = string_copyn(expand_nstring[1],expand_nlength[1]);
+        uschar *daystamp = string_copyn(expand_nstring[2],expand_nlength[2]);
+        uschar *hash = string_copyn(expand_nstring[3],expand_nlength[3]);
         uschar *domain = string_copyn(expand_nstring[5],expand_nlength[5]);
 
         DEBUG(D_expand) debug_printf("prvscheck localpart: %s\n", local_part);
@@ -3654,7 +3983,7 @@ while (*s != 0)
              Adjust "inow" accordingly. */
           if ( (iexpire < 7) && (inow >= 993) ) inow = 0;
 
-          if (iexpire > inow)
+          if (iexpire >= inow)
             {
             prvscheck_result = US"1";
             DEBUG(D_expand) debug_printf("prvscheck: success, $pvrs_result set to 1\n");
@@ -3914,6 +4243,7 @@ while (*s != 0)
 
         else
           {
+          int rc;
           if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
             {
             expand_string_message = string_sprintf("failed to create socket: %s",
@@ -3924,7 +4254,17 @@ while (*s != 0)
           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)
+
+          sigalrm_seen = FALSE;
+          alarm(timeout);
+          rc = connect(fd, (struct sockaddr *)(&sockun), sizeof(sockun));
+          alarm(0);
+          if (sigalrm_seen)
+            {
+            expand_string_message = US "socket connect timed out";
+            goto SOCK_FAIL;
+            }
+          if (rc < 0)
             {
             expand_string_message = string_sprintf("failed to connect to socket "
               "%s: %s", sub_arg[0], strerror(errno));
@@ -4064,13 +4404,24 @@ while (*s != 0)
 
         (void)close(fd_in);
 
+        /* Read the pipe to get the command's output into $value (which is kept
+        in lookup_value). Read during execution, so that if the output exceeds
+        the OS pipe buffer limit, we don't block forever. */
+
+        f = fdopen(fd_out, "rb");
+        sigalrm_seen = FALSE;
+        alarm(60);
+        lookup_value = cat_file(f, lookup_value, &lsize, &lptr, NULL);
+        alarm(0);
+        (void)fclose(f);
+
         /* Wait for the process to finish, applying the timeout, and inspect its
         return code for serious disasters. Simple non-zero returns are passed on.
         */
 
-        if ((runrc = child_close(pid, 60)) < 0)
+        if (sigalrm_seen == TRUE || (runrc = child_close(pid, 30)) < 0)
           {
-          if (runrc == -256)
+          if (sigalrm_seen == TRUE || runrc == -256)
             {
             expand_string_message = string_sprintf("command timed out");
             killpg(pid, SIGKILL);       /* Kill the whole process group */
@@ -4086,14 +4437,6 @@ while (*s != 0)
 
           goto EXPAND_FAILED;
           }
-
-        /* Read the pipe to get the command's output into $value (which is kept
-        in lookup_value). */
-
-        f = fdopen(fd_out, "rb");
-        lookup_value = NULL;
-        lookup_value = cat_file(f, lookup_value, &lsize, &lptr, NULL);
-        (void)fclose(f);
         }
 
       /* Process the yes/no strings; $value may be useful in both cases */
@@ -4477,7 +4820,7 @@ while (*s != 0)
             while (len > 0 && isspace(p[len-1])) len--;
             p[len] = 0;
 
-            if (*p == 0)
+            if (*p == 0 && !skipping)
               {
               expand_string_message = US"first argument of \"extract\" must "
                 "not be empty";
@@ -4534,6 +4877,184 @@ while (*s != 0)
       }
 
 
+    /* Handle list operations */
+
+    case EITEM_FILTER:
+    case EITEM_MAP:
+    case EITEM_REDUCE:
+      {
+      int sep = 0;
+      int save_ptr = ptr;
+      uschar outsep[2] = { '\0', '\0' };
+      uschar *list, *expr, *temp;
+      uschar *save_iterate_item = iterate_item;
+      uschar *save_lookup_value = lookup_value;
+
+      while (isspace(*s)) s++;
+      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+
+      list = expand_string_internal(s, TRUE, &s, skipping);
+      if (list == NULL) goto EXPAND_FAILED;
+      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+
+      if (item_type == EITEM_REDUCE)
+        {
+        while (isspace(*s)) s++;
+        if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+        temp = expand_string_internal(s, TRUE, &s, skipping);
+        if (temp == NULL) goto EXPAND_FAILED;
+        lookup_value = temp;
+        if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+        }
+
+      while (isspace(*s)) s++;
+      if (*s++ != '{') goto EXPAND_FAILED_CURLY;
+
+      expr = s;
+
+      /* For EITEM_FILTER, call eval_condition once, with result discarded (as
+      if scanning a "false" part). This allows us to find the end of the
+      condition, because if the list is empty, we won't actually evaluate the
+      condition for real. For EITEM_MAP and EITEM_REDUCE, do the same, using
+      the normal internal expansion function. */
+
+      if (item_type == EITEM_FILTER)
+        {
+        temp = eval_condition(expr, NULL);
+        if (temp != NULL) s = temp;
+        }
+      else
+        {
+        temp = expand_string_internal(s, TRUE, &s, TRUE);
+        }
+
+      if (temp == NULL)
+        {
+        expand_string_message = string_sprintf("%s inside \"%s\" item",
+          expand_string_message, name);
+        goto EXPAND_FAILED;
+        }
+
+      while (isspace(*s)) s++;
+      if (*s++ != '}')
+        {
+        expand_string_message = string_sprintf("missing } at end of condition "
+          "or expression inside \"%s\"", name);
+        goto EXPAND_FAILED;
+        }
+
+      while (isspace(*s)) s++;
+      if (*s++ != '}')
+        {
+        expand_string_message = string_sprintf("missing } at end of \"%s\"",
+          name);
+        goto EXPAND_FAILED;
+        }
+
+      /* If we are skipping, we can now just move on to the next item. When
+      processing for real, we perform the iteration. */
+
+      if (skipping) continue;
+      while ((iterate_item = string_nextinlist(&list, &sep, NULL, 0)) != NULL)
+        {
+        *outsep = (uschar)sep;      /* Separator as a string */
+
+        DEBUG(D_expand) debug_printf("%s: $item = \"%s\"\n", name, iterate_item);
+
+        if (item_type == EITEM_FILTER)
+          {
+          BOOL condresult;
+          if (eval_condition(expr, &condresult) == NULL)
+            {
+            iterate_item = save_iterate_item;
+            lookup_value = save_lookup_value;
+            expand_string_message = string_sprintf("%s inside \"%s\" condition",
+              expand_string_message, name);
+            goto EXPAND_FAILED;
+            }
+          DEBUG(D_expand) debug_printf("%s: condition is %s\n", name,
+            condresult? "true":"false");
+          if (condresult)
+            temp = iterate_item;    /* TRUE => include this item */
+          else
+            continue;               /* FALSE => skip this item */
+          }
+
+        /* EITEM_MAP and EITEM_REDUCE */
+
+        else
+          {
+          temp = expand_string_internal(expr, TRUE, NULL, skipping);
+          if (temp == NULL)
+            {
+            iterate_item = save_iterate_item;
+            expand_string_message = string_sprintf("%s inside \"%s\" item",
+              expand_string_message, name);
+            goto EXPAND_FAILED;
+            }
+          if (item_type == EITEM_REDUCE)
+            {
+            lookup_value = temp;      /* Update the value of $value */
+            continue;                 /* and continue the iteration */
+            }
+          }
+
+        /* We reach here for FILTER if the condition is true, always for MAP,
+        and never for REDUCE. The value in "temp" is to be added to the output
+        list that is being created, ensuring that any occurrences of the
+        separator character are doubled. Unless we are dealing with the first
+        item of the output list, add in a space if the new item begins with the
+        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);
+
+        /* Add the string in "temp" to the output list that we are building,
+        This is done in chunks by searching for the separator character. */
+
+        for (;;)
+          {
+          size_t seglen = Ustrcspn(temp, outsep);
+            yield = string_cat(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);
+          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);
+        }   /* End of iteration over the list loop */
+
+      /* REDUCE has generated no output above: output the final value of
+      $value. */
+
+      if (item_type == EITEM_REDUCE)
+        {
+        yield = string_cat(yield, &size, &ptr, lookup_value,
+          Ustrlen(lookup_value));
+        lookup_value = save_lookup_value;  /* Restore $value */
+        }
+
+      /* FILTER and MAP generate lists: if they have generated anything, remove
+      the redundant final separator. Even though an empty item at the end of a
+      list does not count, this is tidier. */
+
+      else if (ptr != save_ptr) ptr--;
+
+      /* Restore preserved $item */
+
+      iterate_item = save_iterate_item;
+      continue;
+      }
+
+
     /* If ${dlfunc support is configured, handle calling dynamically-loaded
     functions, unless locked out at this time. Syntax is ${dlfunc{file}{func}}
     or ${dlfunc{file}{func}{arg}} or ${dlfunc{file}{func}{arg1}{arg2}} or up to
@@ -4610,8 +5131,10 @@ while (*s != 0)
       returns OK, we have a replacement string; if it returns DEFER then
       expansion has failed in a non-forced manner; if it returns FAIL then
       failure was forced; if it returns ERROR or any other value there's a
-      problem, so panic slightly. */
+      problem, so panic slightly. In any case, assume that the function has
+      side-effects on the store that must be preserved. */
 
+      resetok = FALSE;
       result = NULL;
       for (argc = 0; argv[argc] != NULL; argc++);
       status = func(&result, argc - 2, &argv[2]);
@@ -4894,6 +5417,69 @@ while (*s != 0)
         continue;
         }
 
+      case EOP_ADDRESSES:
+        {
+        uschar outsep[2] = { ':', '\0' };
+        uschar *address, *error;
+        int save_ptr = ptr;
+        int start, end, domain;  /* Not really used */
+
+        while (isspace(*sub)) sub++;
+        if (*sub == '>') { *outsep = *++sub; ++sub; }
+        parse_allow_group = TRUE;
+
+        for (;;)
+          {
+          uschar *p = parse_find_address_end(sub, FALSE);
+          uschar saveend = *p;
+          *p = '\0';
+          address = parse_extract_address(sub, &error, &start, &end, &domain,
+            FALSE);
+          *p = saveend;
+
+          /* Add the address to the output list that we are building. This is
+          done in chunks by searching for the separator character. At the
+          start, unless we are dealing with the first address of the output
+          list, add in a space if the new address begins with the separator
+          character, or is an empty string. */
+
+          if (address != NULL)
+            {
+            if (ptr != save_ptr && address[0] == *outsep)
+              yield = string_cat(yield, &size, &ptr, US" ", 1);
+
+            for (;;)
+              {
+              size_t seglen = Ustrcspn(address, outsep);
+              yield = string_cat(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);
+              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);
+            }
+
+          if (saveend == '\0') break;
+          sub = p + 1;
+          }
+
+        /* If we have generated anything, remove the redundant final
+        separator. */
+
+        if (ptr != save_ptr) ptr--;
+        parse_allow_group = FALSE;
+        continue;
+        }
+
+
       /* quote puts a string in quotes if it is empty or contains anything
       other than alphamerics, underscore, dot, or hyphen.
 
@@ -4962,8 +5548,8 @@ while (*s != 0)
           goto EXPAND_FAILED;
           }
 
-        if (lookup_list[n].quote != NULL)
-          sub = (lookup_list[n].quote)(sub, opt);
+        if (lookup_list[n]->quote != NULL)
+          sub = (lookup_list[n]->quote)(sub, opt);
         else if (opt != NULL) sub = NULL;
 
         if (sub == NULL)
@@ -5005,6 +5591,23 @@ while (*s != 0)
         continue;
         }
 
+      /* RFC 2047 decode */
+
+      case EOP_RFC2047D:
+        {
+        int len;
+        uschar *error;
+        uschar *decoded = rfc2047_decode(sub, check_rfc2047_length,
+          headers_charset, '?', &len, &error);
+        if (error != NULL)
+          {
+          expand_string_message = error;
+          goto EXPAND_FAILED;
+          }
+        yield = string_cat(yield, &size, &ptr, decoded, len);
+        continue;
+        }
+
       /* from_utf8 converts UTF-8 to 8859-1, turning non-existent chars into
       underscores */
 
@@ -5247,6 +5850,40 @@ while (*s != 0)
         continue;
         }
 
+      /* pseudo-random number less than N */
+
+      case EOP_RANDINT:
+        {
+        int max;
+        uschar *s;
+
+        max = expand_string_integer(sub, TRUE);
+        if (expand_string_message != NULL)
+          goto EXPAND_FAILED;
+        s = string_sprintf("%d", pseudo_random_number(max));
+        yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+        continue;
+        }
+
+      /* Reverse IP, including IPv6 to dotted-nibble */
+
+      case EOP_REVERSE_IP:
+        {
+        int family, maskptr;
+        uschar reversed[128];
+
+        family = string_is_ip_address(sub, &maskptr);
+        if (family == 0)
+          {
+          expand_string_message = string_sprintf(
+              "reverse_ip() not given an IP address [%s]", sub);
+          goto EXPAND_FAILED;
+          }
+        invert_address(reversed, sub);
+        yield = string_cat(yield, &size, &ptr, reversed, Ustrlen(reversed));
+        continue;
+        }
+
       /* Unknown operator */
 
       default:
@@ -5269,7 +5906,7 @@ while (*s != 0)
     int newsize = 0;
     if (ptr == 0)
       {
-      store_reset(yield);
+      if (resetok) store_reset(yield);
       yield = NULL;
       size = 0;
       }
@@ -5324,7 +5961,7 @@ if (left != NULL) *left = s;
 In many cases the final string will be the first one that was got and so there
 will be optimal store usage. */
 
-store_reset(yield + ptr + 1);
+if (resetok) store_reset(yield + ptr + 1);
 DEBUG(D_expand)
   {
   debug_printf("expanding: %.*s\n   result: %s\n", (int)(s - string), string,
@@ -5432,7 +6069,24 @@ systems, so we set it zero ourselves. */
 
 errno = 0;
 expand_string_message = NULL;               /* Indicates no error */
-value = strtol(CS s, CSS &endptr, 0);
+
+/* Before Exim 4.64, strings consisting entirely of whitespace compared
+equal to 0.  Unfortunately, people actually relied upon that, so preserve
+the behaviour explicitly.  Stripping leading whitespace is a harmless
+noop change since strtol skips it anyway (provided that there is a number
+to find at all). */
+if (isspace(*s))
+  {
+  while (isspace(*s)) ++s;
+  if (*s == '\0')
+    {
+      DEBUG(D_expand)
+       debug_printf("treating blank string as number 0\n");
+      return 0;
+    }
+  }
+
+value = strtol(CS s, CSS &endptr, 10);
 
 if (endptr == s)
   {