ClamAV INSTREAM scanning by default, unless built with WITH_OLD_CLAMAV_STREAM.
[exim.git] / src / src / expand.c
index 11477431d0ab867ebaff1b3b8bafcb624677662b..6e47125bb1f177a3fa6af8a9807c9cd600e743da 100644 (file)
@@ -1,10 +1,10 @@
-/* $Cambridge: exim/src/src/expand.c,v 1.27 2005/06/16 20:01:29 tom Exp $ */
+/* $Cambridge: exim/src/src/expand.c,v 1.105 2009/11/16 19:50:36 nm4 Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* 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 +36,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 +106,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 +128,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,16 +156,19 @@ 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[] = {
   US"address",
+  US"addresses",
   US"base62",
   US"base62d",
   US"domain",
@@ -122,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",
@@ -134,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,
@@ -152,7 +220,9 @@ enum {
   EOP_NH,
   EOP_NHASH,
   EOP_QUOTE,
+  EOP_RANDINT,
   EOP_RFC2047,
+  EOP_RFC2047D,
   EOP_RXQUOTE,
   EOP_S,
   EOP_SHA1,
@@ -174,12 +244,15 @@ static uschar *cond_table[] = {
   US">",
   US">=",
   US"and",
+  US"bool",
   US"crypteq",
   US"def",
   US"eq",
   US"eqi",
   US"exists",
   US"first_delivery",
+  US"forall",
+  US"forany",
   US"ge",
   US"gei",
   US"gt",
@@ -195,6 +268,7 @@ static uschar *cond_table[] = {
   US"match",
   US"match_address",
   US"match_domain",
+  US"match_ip",
   US"match_local_part",
   US"or",
   US"pam",
@@ -212,12 +286,15 @@ enum {
   ECOND_NUM_G,
   ECOND_NUM_GE,
   ECOND_AND,
+  ECOND_BOOL,
   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,
@@ -233,6 +310,7 @@ enum {
   ECOND_MATCH,
   ECOND_MATCH_ADDRESS,
   ECOND_MATCH_DOMAIN,
+  ECOND_MATCH_IP,
   ECOND_MATCH_LOCAL_PART,
   ECOND_OR,
   ECOND_PAM,
@@ -270,11 +348,13 @@ enum {
   vtype_stringptr,      /* value is address of pointer to string */
   vtype_msgbody,        /* as stringptr, but read when first required */
   vtype_msgbody_end,    /* ditto, the end of the message */
-  vtype_msgheaders,     /* the message's headers */
+  vtype_msgheaders,     /* the message's headers, processed */
+  vtype_msgheaders_raw, /* the message's headers, unprocessed */
   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 */
@@ -288,34 +368,16 @@ 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. */
 
 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] },
+  /* WARNING: Do not invent variables whose names start acl_c or acl_m because
+     they will be confused with user-creatable ACL variables. */
   { "acl_verify_message",  vtype_stringptr,   &acl_verify_message },
   { "address_data",        vtype_stringptr,   &deliver_address_data },
   { "address_file",        vtype_stringptr,   &address_file },
@@ -338,24 +400,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 },
@@ -375,6 +451,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
@@ -393,11 +470,14 @@ 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 },
   { "message_body_size",   vtype_int,         &message_body_size },
+  { "message_exim_id",     vtype_stringptr,   &message_id },
   { "message_headers",     vtype_msgheaders,  NULL },
+  { "message_headers_raw", vtype_msgheaders_raw, NULL },
   { "message_id",          vtype_stringptr,   &message_id },
   { "message_linecount",   vtype_int,         &message_linecount },
   { "message_size",        vtype_int,         &message_size },
@@ -447,6 +527,8 @@ static var_entry var_table[] = {
   { "rcpt_fail_count",     vtype_int,         &rcpt_fail_count },
   { "received_count",      vtype_int,         &received_count },
   { "received_for",        vtype_stringptr,   &received_for },
+  { "received_ip_address", vtype_stringptr,   &interface_address },
+  { "received_port",       vtype_int,         &interface_port },
   { "received_protocol",   vtype_stringptr,   &received_protocol },
   { "received_time",       vtype_int,         &received_time },
   { "recipient_data",      vtype_stringptr,   &recipient_data },
@@ -478,8 +560,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_argument", vtype_stringptr, &smtp_command_argument },
+  { "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] },
@@ -497,6 +584,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 },
@@ -673,6 +761,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      *
 *************************************************/
@@ -1090,7 +1247,8 @@ Arguments:
   newsize       return the size of memory block that was obtained; may be NULL
                 if exists_only is TRUE
   want_raw      TRUE if called for $rh_ or $rheader_ variables; no processing,
-                other than concatenating, will be done on the header
+                other than concatenating, will be done on the header. Also used
+                for $message_headers_raw.
   charset       name of charset to translate MIME words to; used only if
                 want_raw is false; if NULL, no translation is done (this is
                 used for $bh_ and $bheader_)
@@ -1133,6 +1291,12 @@ for (i = 0; i < 2; i++)
           while (isspace(*t)) t++;          /* remove leading white space */
         ilen = h->slen - (t - h->text);     /* length to insert */
 
+        /* Unless wanted raw, remove trailing whitespace, including the
+        newline. */
+
+        if (!want_raw)
+          while (ilen > 0 && isspace(t[ilen-1])) ilen--;
+
         /* Set comma = 1 if handling a single header and it's one of those
         that contains an address list, except when asked for raw headers. Only
         need to do this once. */
@@ -1144,7 +1308,7 @@ for (i = 0; i < 2; i++)
         /* First pass - compute total store needed; second pass - compute
         total store used, including this header. */
 
-        size += ilen + comma;
+        size += ilen + comma + 1;  /* +1 for the newline */
 
         /* Second pass - concatentate the data, up to a maximum. Note that
         the loop stops when size hits the limit. */
@@ -1153,14 +1317,19 @@ for (i = 0; i < 2; i++)
           {
           if (size > header_insert_maxlen)
             {
-            ilen -= size - header_insert_maxlen;
+            ilen -= size - header_insert_maxlen - 1;
             comma = 0;
             }
           Ustrncpy(ptr, t, ilen);
           ptr += ilen;
-          if (comma != 0 && ilen > 0)
+
+          /* For a non-raw header, put in the comma if needed, then add
+          back the newline we removed above, provided there was some text in
+          the header. */
+
+          if (!want_raw && ilen > 0)
             {
-            ptr[-1] = ',';
+            if (comma != 0) *ptr++ = ',';
             *ptr++ = '\n';
             }
           }
@@ -1168,8 +1337,9 @@ for (i = 0; i < 2; i++)
       }
     }
 
-  /* At end of first pass, truncate size if necessary, and get the buffer
-  to hold the data, returning the buffer size. */
+  /* At end of first pass, return NULL if no header found. Then truncate size
+  if necessary, and get the buffer to hold the data, returning the buffer size.
+  */
 
   if (i == 0)
     {
@@ -1180,10 +1350,6 @@ for (i = 0; i < 2; i++)
     }
   }
 
-/* Remove a redundant added comma if present */
-
-if (comma != 0 && ptr > yield) ptr -= 2;
-
 /* That's all we do for raw header expansion. */
 
 if (want_raw)
@@ -1191,17 +1357,19 @@ if (want_raw)
   *ptr = 0;
   }
 
-/* Otherwise, we remove trailing whitespace, including newlines. Then we do RFC
-2047 decoding, translating the charset if requested. The rfc2047_decode2()
+/* Otherwise, remove a final newline and a redundant added comma. Then we do
+RFC 2047 decoding, translating the charset if requested. The rfc2047_decode2()
 function can return an error with decoded data if the charset translation
 fails. If decoding fails, it returns NULL. */
 
 else
   {
   uschar *decoded, *error;
-  while (ptr > yield && isspace(ptr[-1])) ptr--;
+  if (ptr > yield && ptr[-1] == '\n') ptr--;
+  if (ptr > yield && comma != 0 && 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"
@@ -1244,6 +1412,37 @@ find_variable(uschar *name, BOOL exists_only, BOOL skipping, int *newsize)
 int first = 0;
 int last = var_table_size;
 
+/* Handle ACL variables, whose names are of the form acl_cxxx or acl_mxxx.
+Originally, xxx had to be a number in the range 0-9 (later 0-19), but from
+release 4.64 onwards arbitrary names are permitted, as long as the first 5
+characters are acl_c or acl_m and the sixth is either a digit or an underscore
+(this gave backwards compatibility at the changeover). There may be built-in
+variables whose names start acl_ but they should never start in this way. This
+slightly messy specification is a consequence of the history, needless to say.
+
+If an ACL variable does not exist, treat it as empty, unless strict_acl_vars is
+set, in which case give an error. */
+
+if ((Ustrncmp(name, "acl_c", 5) == 0 || Ustrncmp(name, "acl_m", 5) == 0) &&
+     !isalpha(name[5]))
+  {
+  tree_node *node =
+    tree_search((name[4] == 'c')? acl_var_c : acl_var_m, name + 4);
+  return (node == NULL)? (strict_acl_vars? NULL : US"") : node->data.ptr;
+  }
+
+/* Handle $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;
@@ -1255,7 +1454,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"";
 
@@ -1264,52 +1463,7 @@ while (last > first)
     case vtype_filter_int:
     if (!filter_running) return NULL;
     /* Fall through */
-
-#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 = "0"; break;
-        case DK_EXIM_ADDRESS_FROM_FROM: s = "from"; break;
-        case DK_EXIM_ADDRESS_FROM_SENDER: s = "sender"; break;
-      }
-
-    if (Ustrcmp(var_table[middle].name, "dk_status") == 0)
-      switch(dk_verify_block->result) {
-        case DK_EXIM_RESULT_ERR: s = "error"; break;
-        case DK_EXIM_RESULT_BAD_FORMAT: s = "bad format"; break;
-        case DK_EXIM_RESULT_NO_KEY: s = "no key"; break;
-        case DK_EXIM_RESULT_NO_SIGNATURE: s = "no signature"; break;
-        case DK_EXIM_RESULT_REVOKED: s = "revoked"; break;
-        case DK_EXIM_RESULT_NON_PARTICIPANT: s = "non-participant"; break;
-        case DK_EXIM_RESULT_GOOD: s = "good"; break;
-        case DK_EXIM_RESULT_BAD: s = "bad"; break;
-      }
-
-    if (Ustrcmp(var_table[middle].name, "dk_signsall") == 0)
-      s = (dk_verify_block->signsall)? "1" : "0";
-
-    if (Ustrcmp(var_table[middle].name, "dk_testing") == 0)
-      s = (dk_verify_block->testing)? "1" : "0";
-
-    if (Ustrcmp(var_table[middle].name, "dk_is_signed") == 0)
-      s = (dk_verify_block->is_signed)? "1" : "0";
-
-    return (s == NULL)? US"" : s;
-#endif
-
+    /* VVVVVVVVVVVV */
     case vtype_int:
     sprintf(CS var_buffer, "%d", *(int *)(var_table[middle].value)); /* Integer */
     return var_buffer;
@@ -1335,7 +1489,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 */
@@ -1365,6 +1519,9 @@ while (last > first)
     case vtype_msgheaders:
     return find_header(NULL, exists_only, newsize, FALSE, NULL);
 
+    case vtype_msgheaders_raw:
+    return find_header(NULL, exists_only, newsize, TRUE, NULL);
+
     case vtype_msgbody:                        /* Pointer to msgbody string */
     case vtype_msgbody_end:                    /* Ditto, the end of the msg */
     ss = (uschar **)(var_table[middle].value);
@@ -1391,9 +1548,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] = ' '; }
           }
         }
       }
@@ -1421,10 +1584,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,
@@ -1463,6 +1638,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
+
     }
   }
 
@@ -1534,6 +1715,33 @@ return 0;
 
 
 
+/*************************************************
+*     Elaborate message for bad variable         *
+*************************************************/
+
+/* For the "unknown variable" message, take a look at the variable's name, and
+give additional information about possible ACL variables. The extra information
+is added on to expand_string_message.
+
+Argument:   the name of the variable
+Returns:    nothing
+*/
+
+static void
+check_variable_error_message(uschar *name)
+{
+if (Ustrncmp(name, "acl_", 4) == 0)
+  expand_string_message = string_sprintf("%s (%s)", expand_string_message,
+    (name[4] == 'c' || name[4] == 'm')?
+      (isalpha(name[5])?
+        US"6th character of a user-defined ACL variable must be a digit or underscore" :
+        US"strict_acl_vars is set"    /* Syntax is OK, it has to be this */
+      ) :
+      US"user-defined ACL variables must start acl_c or acl_m");
+}
+
+
+
 /*************************************************
 *        Read and evaluate a condition           *
 *************************************************/
@@ -1615,7 +1823,9 @@ switch(cond_type)
 
   s = read_name(name, 256, s+1, US"_");
 
-  /* Test for a header's existence */
+  /* Test for a header's existence. If the name contains a closing brace
+  character, this may be a user error where the terminating colon has been
+  omitted. Set a flag to adjust a subsequent error message in this case. */
 
   if (Ustrncmp(name, "h_", 2) == 0 ||
       Ustrncmp(name, "rh_", 3) == 0 ||
@@ -1625,6 +1835,7 @@ switch(cond_type)
       Ustrncmp(name, "bheader_", 8) == 0)
     {
     s = read_header_name(name, 256, s);
+    if (Ustrchr(name, '}') != NULL) malformed_header = TRUE;
     if (yield != NULL) *yield =
       (find_header(name, TRUE, NULL, FALSE, NULL) != NULL) == testfor;
     }
@@ -1640,6 +1851,7 @@ switch(cond_type)
       expand_string_message = (name[0] == 0)?
         string_sprintf("variable name omitted after \"def:\"") :
         string_sprintf("unknown variable \"%s\" after \"def:\"", name);
+      check_variable_error_message(name);
       return NULL;
       }
     if (yield != NULL) *yield = (value[0] != 0) == testfor;
@@ -1705,7 +1917,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;
 
@@ -1801,6 +2013,7 @@ switch(cond_type)
                        variables if it succeeds
   match_address:     matches in an address list
   match_domain:      matches in a domain list
+  match_ip:          matches a host list that is restricted to IP addresses
   match_local_part:  matches in a local part list
   crypteq:           encrypts plaintext and compares against an encrypted text,
                        using crypt(), crypt16(), MD5 or SHA-1
@@ -1809,6 +2022,7 @@ switch(cond_type)
   case ECOND_MATCH:
   case ECOND_MATCH_ADDRESS:
   case ECOND_MATCH_DOMAIN:
+  case ECOND_MATCH_IP:
   case ECOND_MATCH_LOCAL_PART:
   case ECOND_CRYPTEQ:
 
@@ -1848,26 +2062,18 @@ 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)
       {
-      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')
+      if (sub[i][0] == 0)
         {
-        num[i] *= 1024*1024;
-        endptr++;
+        num[i] = 0;
+        DEBUG(D_expand)
+          debug_printf("empty string cast to zero for numerical comparison\n");
         }
-      while (isspace(*endptr)) endptr++;
-      if (*endptr != 0)
+      else
         {
-        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;
         }
       }
     }
@@ -1962,11 +2168,46 @@ switch(cond_type)
       MCL_DOMAIN + MCL_NOEXPAND, TRUE, NULL);
     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)
+      {
+      expand_string_message = string_sprintf("\"%s\" is not an IP address",
+        sub[0]);
+      return NULL;
+      }
+    else
+      {
+      unsigned int *nullcache = NULL;
+      check_host_block cb;
+
+      cb.host_name = US"";
+      cb.host_address = sub[0];
+
+      /* If the host address starts off ::ffff: it is an IPv6 address in
+      IPv4-compatible mode. Find the IPv4 part for checking against IPv4
+      addresses. */
+
+      cb.host_ipv4 = (Ustrncmp(cb.host_address, "::ffff:", 7) == 0)?
+        cb.host_address + 7 : cb.host_address;
+
+      rc = match_check_list(
+             &sub[1],                   /* the list */
+             0,                         /* separator character */
+             &hostlist_anchor,          /* anchor pointer */
+             &nullcache,                /* cache pointer */
+             check_host,                /* function for testing */
+             &cb,                       /* argument for function */
+             MCL_HOST,                  /* type of check */
+             sub[0],                    /* text for debugging */
+             NULL);                     /* where to pass back data */
+      }
+    goto MATCHED_SOMETHING;
+
     case ECOND_MATCH_LOCAL_PART:
     rc = match_isinlist(sub[0], &(sub[1]), 0, &localpartlist_anchor, NULL,
       MCL_LOCALPART + MCL_NOEXPAND, TRUE, NULL);
     /* Fall through */
-
+    /* VVVVVVVVVVVV */
     MATCHED_SOMETHING:
     switch(rc)
       {
@@ -2179,6 +2420,115 @@ 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. */
+  case ECOND_BOOL:
+    {
+    uschar *sub_arg[1];
+    uschar *t;
+    size_t len;
+    BOOL boolvalue = FALSE;
+    while (isspace(*s)) s++;
+    if (*s != '{') goto COND_FAILED_CURLY_START;
+    switch(read_subs(sub_arg, 1, 1, &s, yield == NULL, FALSE, US"bool"))
+      {
+      case 1: expand_string_message = US"too few arguments or bracketing "
+        "error for bool";
+      /*FALLTHROUGH*/
+      case 2:
+      case 3: return NULL;
+      }
+    t = sub_arg[0];
+    while (isspace(*t)) t++;
+    len = Ustrlen(t);
+    DEBUG(D_expand)
+      debug_printf("considering bool: %s\n", len ? t : US"<empty>");
+    if (len == 0)
+      boolvalue = FALSE;
+    else if (Ustrspn(t, "0123456789") == len)
+      boolvalue = (Uatoi(t) == 0) ? FALSE : 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
+      {
+      expand_string_message = string_sprintf("unrecognised boolean "
+       "value \"%s\"", t);
+      return NULL;
+      }
+    if (yield != NULL) *yield = (boolvalue != 0);
+    return s;
+    }
+
   /* Unknown condition */
 
   default:
@@ -2425,8 +2775,6 @@ return rc;
 
 
 
-
-
 /*************************************************
 *    Handle MD5 or SHA-1 computation for HMAC    *
 *************************************************/
@@ -2472,110 +2820,204 @@ else
 
 
 
-/*************************************************
-*        Join a file onto the output string      *
-*************************************************/
+/********************************************************
+* prvs: Get last three digits of days since Jan 1, 1970 *
+********************************************************/
 
-/* This is used for readfile and after a run expansion. It joins the contents
-of a file onto the output string, globally replacing newlines with a given
-string (optionally). The file is closed at the end.
+/* This is needed to implement the "prvs" BATV reverse
+   path signing scheme
 
-Arguments:
-  f            the FILE
-  yield        pointer to the expandable string
-  sizep        pointer to the current size
-  ptrp         pointer to the current position
-  eol          newline replacement string, or NULL
+Argument: integer "days" offset to add or substract to
+          or from the current number of days.
+
+Returns:  pointer to string containing the last three
+          digits of the number of days since Jan 1, 1970,
+          modified by the offset argument, NULL if there
+          was an error in the conversion.
 
-Returns:       new value of string pointer
 */
 
 static uschar *
-cat_file(FILE *f, uschar *yield, int *sizep, int *ptrp, uschar *eol)
+prvs_daystamp(int day_offset)
 {
-int eollen;
-uschar buffer[1024];
-
-eollen = (eol == NULL)? 0 : Ustrlen(eol);
+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";
+}
 
-while (Ufgets(buffer, sizeof(buffer), f) != NULL)
-  {
-  int len = Ustrlen(buffer);
-  if (eol != NULL && buffer[len-1] == '\n') len--;
-  yield = string_cat(yield, sizep, ptrp, buffer, len);
-  if (buffer[len] != 0)
-    yield = string_cat(yield, sizep, ptrp, eol, eollen);
-  }
 
-if (yield != NULL) yield[*ptrp] = 0;
 
-return yield;
-}
+/********************************************************
+*   prvs: perform HMAC-SHA1 computation of prvs bits    *
+********************************************************/
 
+/* This is needed to implement the "prvs" BATV reverse
+   path signing scheme
 
+Arguments:
+  address RFC2821 Address to use
+      key The key to use (must be less than 64 characters
+          in size)
+  key_num Single-digit key number to use. Defaults to
+          '0' when NULL.
 
+Returns:  pointer to string containing the first three
+          bytes of the final hash in hex format, NULL if
+          there was an error in the process.
+*/
 
-/*************************************************
-*          Evaluate numeric expression           *
-*************************************************/
+static uschar *
+prvs_hmac_sha1(uschar *address, uschar *key, uschar *key_num, uschar *daystamp)
+{
+uschar *hash_source, *p;
+int size = 0,offset = 0,i;
+sha1 sha1_base;
+void *use_base = &sha1_base;
+uschar innerhash[20];
+uschar finalhash[20];
+uschar innerkey[64];
+uschar outerkey[64];
+uschar *finalhash_hex = store_get(40);
 
-/* This is a set of mutually recursive functions that evaluate a simple
-arithmetic expression involving only + - * / and parentheses. The only one that
-is called from elsewhere is eval_expr, whose interface is:
+if (key_num == NULL)
+  key_num = US"0";
 
-Arguments:
-  sptr          pointer to the pointer to the string - gets updated
-  decimal       TRUE if numbers are to be assumed decimal
-  error         pointer to where to put an error message - must be NULL on input
-  endket        TRUE if ')' must terminate - FALSE for external call
+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[offset] = '\0';
 
-Returns:        on success: the value of the expression, with *error still NULL
-                on failure: an undefined value, with *error = a message
-*/
+DEBUG(D_expand) debug_printf("prvs: hash source is '%s'\n", hash_source);
 
-static int eval_sumterm(uschar **, BOOL, uschar **);
+memset(innerkey, 0x36, 64);
+memset(outerkey, 0x5c, 64);
 
-static int
-eval_expr(uschar **sptr, BOOL decimal, uschar **error, BOOL endket)
-{
-uschar *s = *sptr;
-int x = eval_sumterm(&s, decimal, error);
-if (*error == NULL)
+for (i = 0; i < Ustrlen(key); i++)
   {
-  while (*s == '+' || *s == '-')
-    {
-    int op = *s++;
-    int y = eval_sumterm(&s, decimal, error);
-    if (*error != NULL) break;
-    if (op == '+') x += y; else x -= y;
-    }
-  if (*error == NULL)
+  innerkey[i] ^= key[i];
+  outerkey[i] ^= key[i];
+  }
+
+chash_start(HMAC_SHA1, use_base);
+chash_mid(HMAC_SHA1, use_base, innerkey);
+chash_end(HMAC_SHA1, use_base, hash_source, offset, innerhash);
+
+chash_start(HMAC_SHA1, use_base);
+chash_mid(HMAC_SHA1, use_base, outerkey);
+chash_end(HMAC_SHA1, use_base, innerhash, 20, finalhash);
+
+p = finalhash_hex;
+for (i = 0; i < 3; i++)
+  {
+  *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
+  *p++ = hex_digits[finalhash[i] & 0x0f];
+  }
+*p = '\0';
+
+return finalhash_hex;
+}
+
+
+
+
+/*************************************************
+*        Join a file onto the output string      *
+*************************************************/
+
+/* This is used for readfile and after a run expansion. It joins the contents
+of a file onto the output string, globally replacing newlines with a given
+string (optionally). The file is closed at the end.
+
+Arguments:
+  f            the FILE
+  yield        pointer to the expandable string
+  sizep        pointer to the current size
+  ptrp         pointer to the current position
+  eol          newline replacement string, or NULL
+
+Returns:       new value of string pointer
+*/
+
+static uschar *
+cat_file(FILE *f, uschar *yield, int *sizep, int *ptrp, uschar *eol)
+{
+int eollen;
+uschar buffer[1024];
+
+eollen = (eol == NULL)? 0 : Ustrlen(eol);
+
+while (Ufgets(buffer, sizeof(buffer), f) != NULL)
+  {
+  int len = Ustrlen(buffer);
+  if (eol != NULL && buffer[len-1] == '\n') len--;
+  yield = string_cat(yield, sizep, ptrp, buffer, len);
+  if (buffer[len] != 0)
+    yield = string_cat(yield, sizep, ptrp, eol, eollen);
+  }
+
+if (yield != NULL) yield[*ptrp] = 0;
+
+return yield;
+}
+
+
+
+
+/*************************************************
+*          Evaluate numeric expression           *
+*************************************************/
+
+/* This is a set of mutually recursive functions that evaluate an arithmetic
+expression involving + - * / % & | ^ ~ << >> and parentheses. The only one of
+these functions that is called from elsewhere is eval_expr, whose interface is:
+
+Arguments:
+  sptr        pointer to the pointer to the string - gets updated
+  decimal     TRUE if numbers are to be assumed decimal
+  error       pointer to where to put an error message - must be NULL on input
+  endket      TRUE if ')' must terminate - FALSE for external call
+
+Returns:      on success: the value of the expression, with *error still NULL
+              on failure: an undefined value, with *error = a message
+*/
+
+static int eval_op_or(uschar **, BOOL, uschar **);
+
+
+static int
+eval_expr(uschar **sptr, BOOL decimal, uschar **error, BOOL endket)
+{
+uschar *s = *sptr;
+int x = eval_op_or(&s, decimal, error);
+if (*error == NULL)
+  {
+  if (endket)
     {
-    if (endket)
-      {
-      if (*s != ')')
-        *error = US"expecting closing parenthesis";
-      else
-        while (isspace(*(++s)));
-      }
-    else if (*s != 0) *error = US"expecting + or -";
+    if (*s != ')')
+      *error = US"expecting closing parenthesis";
+    else
+      while (isspace(*(++s)));
     }
+  else if (*s != 0) *error = US"expecting operator";
   }
-
 *sptr = s;
 return x;
 }
 
+
 static int
-eval_term(uschar **sptr, BOOL decimal, uschar **error)
+eval_number(uschar **sptr, BOOL decimal, uschar **error)
 {
 register int c;
 int n;
 uschar *s = *sptr;
 while (isspace(*s)) s++;
 c = *s;
-if (isdigit(c) || ((c == '-' || c == '+') && isdigit(s[1])))
+if (isdigit(c))
   {
   int count;
   (void)sscanf(CS s, (decimal? "%d%n" : "%i%n"), &n, &count);
@@ -2598,18 +3040,102 @@ else
 return n;
 }
 
-static int eval_sumterm(uschar **sptr, BOOL decimal, uschar **error)
+
+static int eval_op_unary(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int x;
+while (isspace(*s)) s++;
+if (*s == '+' || *s == '-' || *s == '~')
+  {
+  int op = *s++;
+  x = eval_op_unary(&s, decimal, error);
+  if (op == '-') x = -x;
+    else if (op == '~') x = ~x;
+  }
+else
+  {
+  x = eval_number(&s, decimal, error);
+  }
+*sptr = s;
+return x;
+}
+
+
+static int eval_op_mult(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int x = eval_op_unary(&s, decimal, error);
+if (*error == NULL)
+  {
+  while (*s == '*' || *s == '/' || *s == '%')
+    {
+    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;
+    }
+  }
+*sptr = s;
+return x;
+}
+
+
+static int eval_op_sum(uschar **sptr, BOOL decimal, uschar **error)
 {
 uschar *s = *sptr;
-int x = eval_term(&s, decimal, error);
+int x = eval_op_mult(&s, decimal, error);
 if (*error == NULL)
   {
-  while (*s == '*' || *s == '/')
+  while (*s == '+' || *s == '-')
     {
     int op = *s++;
-    int y = eval_term(&s, decimal, error);
+    int y = eval_op_mult(&s, decimal, error);
+    if (*error != NULL) break;
+    if (op == '+') x += y; else x -= y;
+    }
+  }
+*sptr = s;
+return x;
+}
+
+
+static int eval_op_shift(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int x = eval_op_sum(&s, decimal, error);
+if (*error == NULL)
+  {
+  while ((*s == '<' || *s == '>') && s[1] == s[0])
+    {
+    int y;
+    int op = *s++;
+    s++;
+    y = eval_op_sum(&s, decimal, error);
+    if (*error != NULL) break;
+    if (op == '<') x <<= y; else x >>= y;
+    }
+  }
+*sptr = s;
+return x;
+}
+
+
+static int eval_op_and(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int x = eval_op_shift(&s, decimal, error);
+if (*error == NULL)
+  {
+  while (*s == '&')
+    {
+    int y;
+    s++;
+    y = eval_op_shift(&s, decimal, error);
     if (*error != NULL) break;
-    if (op == '*') x *= y; else x /= y;
+    x &= y;
     }
   }
 *sptr = s;
@@ -2617,6 +3143,45 @@ return x;
 }
 
 
+static int eval_op_xor(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int x = eval_op_and(&s, decimal, error);
+if (*error == NULL)
+  {
+  while (*s == '^')
+    {
+    int y;
+    s++;
+    y = eval_op_and(&s, decimal, error);
+    if (*error != NULL) break;
+    x ^= y;
+    }
+  }
+*sptr = s;
+return x;
+}
+
+
+static int eval_op_or(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int x = eval_op_xor(&s, decimal, error);
+if (*error == NULL)
+  {
+  while (*s == '|')
+    {
+    int y;
+    s++;
+    y = eval_op_xor(&s, decimal, error);
+    if (*error != NULL) break;
+    x |= y;
+    }
+  }
+*sptr = s;
+return x;
+}
+
 
 
 /*************************************************
@@ -2657,6 +3222,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 }
@@ -2682,6 +3253,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"";
@@ -2754,7 +3326,7 @@ while (*s != 0)
 
     if (ptr == 0 && yield != NULL)
       {
-      store_reset(yield);
+      if (resetok) store_reset(yield);
       yield = NULL;
       size = 0;
       }
@@ -2774,7 +3346,7 @@ while (*s != 0)
       value = find_header(name, FALSE, &newsize, want_raw, charset);
 
       /* If we didn't find the header, and the header contains a closing brace
-      characters, this may be a user error where the terminating colon
+      character, this may be a user error where the terminating colon
       has been omitted. Set a flag to adjust the error message in this case.
       But there is no error here - nothing gets inserted. */
 
@@ -2794,6 +3366,7 @@ while (*s != 0)
         {
         expand_string_message =
           string_sprintf("unknown variable name \"%s\"", name);
+          check_variable_error_message(name);
         goto EXPAND_FAILED;
         }
       }
@@ -2984,7 +3557,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)
           {
@@ -3004,7 +3577,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);
@@ -3013,12 +3588,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
@@ -3192,15 +3785,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;
         }
 
@@ -3208,11 +3810,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));
 
@@ -3227,32 +3829,38 @@ 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:
         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]);
+      if (regex_match_and_setup(re,sub_arg[0],0,-1))
+        {
+        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);
@@ -3263,26 +3871,24 @@ while (*s != 0)
 
         /* Set up expansion variables */
         prvscheck_address = string_cat(NULL, &mysize, &myptr, local_part, Ustrlen(local_part));
-        string_cat(prvscheck_address,&mysize,&myptr,"@",1);
+        string_cat(prvscheck_address,&mysize,&myptr,US"@",1);
         string_cat(prvscheck_address,&mysize,&myptr,domain,Ustrlen(domain));
         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";
@@ -3291,20 +3897,21 @@ 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(now,"%u",&inow);
-          sscanf(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. */
           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");
@@ -3320,12 +3927,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:
@@ -3370,7 +4000,7 @@ while (*s != 0)
         }
 
       yield = cat_file(f, yield, &size, &ptr, sub_arg[1]);
-      fclose(f);
+      (void)fclose(f);
       continue;
       }
 
@@ -3416,28 +4046,160 @@ 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, HOST_FIND_QUALIFY_SINGLE, 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)
-          {
-          expand_string_message = string_sprintf("failed to connect to socket "
-            "%s: %s", sub_arg[0], strerror(errno));
-          goto SOCK_FAIL;
+        /* Handle a Unix domain socket */
+
+        else
+          {
+          int rc;
+          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]);
+
+          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));
+            goto SOCK_FAIL;
+            }
           }
+
         DEBUG(D_expand) debug_printf("connected to socket %s\n", sub_arg[0]);
 
         /* Write the request string, if not empty */
@@ -3455,6 +4217,14 @@ while (*s != 0)
             }
           }
 
+        /* Shut down the sending side of the socket. This helps some servers to
+        recognise that it is their turn to do some work. Just in case some
+        system doesn't have this function, make it conditional. */
+
+        #ifdef SHUT_WR
+        shutdown(fd, SHUT_WR);
+        #endif
+
         /* Now we need to read from the socket, under a timeout. The function
         that reads a file can be used. */
 
@@ -3463,7 +4233,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. */
@@ -3471,7 +4241,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;
           }
         }
@@ -3560,7 +4330,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.
@@ -3591,7 +4361,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 */
@@ -3975,7 +4745,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";
@@ -4032,6 +4802,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
@@ -4108,8 +5056,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]);
@@ -4190,6 +5140,8 @@ while (*s != 0)
         continue;
         }
 
+      /* Note that for Darwin and Cygwin, BASE_62 actually has the value 36 */
+
       case EOP_BASE62D:
         {
         uschar buf[16];
@@ -4201,10 +5153,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));
@@ -4389,6 +5342,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.
 
@@ -4495,11 +5511,28 @@ 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;
         }
 
+      /* 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 */
 
@@ -4548,6 +5581,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;
@@ -4684,6 +5731,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",
@@ -4722,6 +5775,21 @@ 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;
+        }
+
       /* Unknown operator */
 
       default:
@@ -4744,7 +5812,7 @@ while (*s != 0)
     int newsize = 0;
     if (ptr == 0)
       {
-      store_reset(yield);
+      if (resetok) store_reset(yield);
       yield = NULL;
       size = 0;
       }
@@ -4753,6 +5821,7 @@ while (*s != 0)
       {
       expand_string_message =
         string_sprintf("unknown variable in \"${%s}\"", name);
+      check_variable_error_message(name);
       goto EXPAND_FAILED;
       }
     len = Ustrlen(value);
@@ -4798,7 +5867,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,
@@ -4878,22 +5947,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
@@ -4901,12 +5974,34 @@ 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;
-value = strtol(CS s, CSS &endptr, 0);
+expand_string_message = NULL;               /* Indicates no error */
+
+/* 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)
   {
   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 */
@@ -4941,103 +6036,6 @@ expand_string_message = string_sprintf(CS msg, s);
 return -2;
 }
 
-/********************************************************
-* prvs: Get last three digits of days since Jan 1, 1970 *
-********************************************************/
-
-/* This is needed to implement the "prvs" BATV reverse
-   path signing scheme
-
-Argument: integer "days" offset to add or substract to
-          or from the current number of days.
-
-Returns:  pointer to string containing the last three
-          digits of the number of days since Jan 1, 1970,
-          modified by the offset argument, NULL if there
-          was an error in the conversion.
-
-*/
-
-uschar *
-prvs_daystamp(int day_offset)
-{
-uschar *days = store_get(10);
-snprintf(days, 9, "%lld", (((long long)time(NULL))+(day_offset*86400))/86400);
-return (Ustrlen(days) >= 3) ? &days[Ustrlen(days)-3] : NULL;
-}
-
-/********************************************************
-*   prvs: perform HMAC-SHA1 computation of prvs bits    *
-********************************************************/
-
-/* This is needed to implement the "prvs" BATV reverse
-   path signing scheme
-
-Arguments:
-  address RFC2821 Address to use
-      key The key to use (must be less than 64 characters
-          in size)
-  key_num Single-digit key number to use. Defaults to
-          '0' when NULL.
-
-Returns:  pointer to string containing the first three
-          bytes of the final hash in hex format, NULL if
-          there was an error in the process.
-*/
-
-uschar *
-prvs_hmac_sha1(uschar *address, uschar *key, uschar *key_num, uschar *daystamp)
-{
-uschar *hash_source, *p;
-int size = 0,offset = 0,i;
-sha1 sha1_base;
-void *use_base = &sha1_base;
-uschar innerhash[20];
-uschar finalhash[20];
-uschar innerkey[64];
-uschar outerkey[64];
-uschar *finalhash_hex = store_get(40);
-
-if (key_num == NULL)
-  key_num = US"0";
-
-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[offset] = '\0';
-
-DEBUG(D_expand) debug_printf("prvs: hash source is '%s'\n", hash_source);
-
-memset(innerkey, 0x36, 64);
-memset(outerkey, 0x5c, 64);
-
-for (i = 0; i < Ustrlen(key); i++)
-  {
-  innerkey[i] ^= key[i];
-  outerkey[i] ^= key[i];
-  }
-
-chash_start(HMAC_SHA1, use_base);
-chash_mid(HMAC_SHA1, use_base, innerkey);
-chash_end(HMAC_SHA1, use_base, hash_source, offset, innerhash);
-
-chash_start(HMAC_SHA1, use_base);
-chash_mid(HMAC_SHA1, use_base, outerkey);
-chash_end(HMAC_SHA1, use_base, innerhash, 20, finalhash);
-
-p = finalhash_hex;
-for (i = 0; i < 3; i++)
-  {
-  *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
-  *p++ = hex_digits[finalhash[i] & 0x0f];
-  }
-*p = '\0';
-
-return finalhash_hex;
-}
 
 /*************************************************
 **************************************************