Remove whitespace
[exim.git] / src / src / expand.c
index 80971cb263e4de99a2d8e4d709c7b3f6b06d8a39..edef45dba2eee90b82774da9542343c0ccdbd666 100644 (file)
@@ -1,10 +1,8 @@
-/* $Cambridge: exim/src/src/expand.c,v 1.34 2005/06/22 10:17:23 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
 /* See the file NOTICE for conditions of use and distribution. */
 
 
 
 #include "exim.h"
 
 
 #include "exim.h"
 
+/* Recursively called function */
+
+static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL, BOOL);
+
 #ifdef STAND_ALONE
 #ifndef SUPPORT_CRYPTEQ
 #define SUPPORT_CRYPTEQ
 #endif
 #endif
 
 #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>
 #ifdef SUPPORT_CRYPTEQ
 #ifdef CRYPT_H
 #include <crypt.h>
@@ -28,15 +34,63 @@ extern char* crypt16(char*, char*);
 #endif
 #endif
 
 #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);
 
 
 
 
 
 
@@ -48,38 +102,46 @@ static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL);
 alphabetical order. */
 
 static uschar *item_table[] = {
 alphabetical order. */
 
 static uschar *item_table[] = {
+  US"acl",
   US"dlfunc",
   US"extract",
   US"dlfunc",
   US"extract",
+  US"filter",
   US"hash",
   US"hmac",
   US"if",
   US"length",
   US"lookup",
   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"nhash",
   US"perl",
   US"prvs",
   US"prvscheck",
   US"readfile",
   US"readsocket",
+  US"reduce",
   US"run",
   US"sg",
   US"substr",
   US"tr" };
 
 enum {
   US"run",
   US"sg",
   US"substr",
   US"tr" };
 
 enum {
+  EITEM_ACL,
   EITEM_DLFUNC,
   EITEM_EXTRACT,
   EITEM_DLFUNC,
   EITEM_EXTRACT,
+  EITEM_FILTER,
   EITEM_HASH,
   EITEM_HMAC,
   EITEM_IF,
   EITEM_LENGTH,
   EITEM_LOOKUP,
   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_NHASH,
   EITEM_PERL,
   EITEM_PRVS,
   EITEM_PRVSCHECK,
   EITEM_READFILE,
   EITEM_READSOCK,
+  EITEM_REDUCE,
   EITEM_RUN,
   EITEM_SG,
   EITEM_SUBSTR,
   EITEM_RUN,
   EITEM_SG,
   EITEM_SUBSTR,
@@ -94,16 +156,21 @@ static uschar *op_table_underscore[] = {
   US"from_utf8",
   US"local_part",
   US"quote_local_part",
   US"from_utf8",
   US"local_part",
   US"quote_local_part",
+  US"reverse_ip",
+  US"time_eval",
   US"time_interval"};
 
 enum {
   EOP_FROM_UTF8,
   EOP_LOCAL_PART,
   EOP_QUOTE_LOCAL_PART,
   US"time_interval"};
 
 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",
   EOP_TIME_INTERVAL };
 
 static uschar *op_table_main[] = {
   US"address",
+  US"addresses",
   US"base62",
   US"base62d",
   US"domain",
   US"base62",
   US"base62d",
   US"domain",
@@ -114,15 +181,20 @@ static uschar *op_table_main[] = {
   US"h",
   US"hash",
   US"hex2b64",
   US"h",
   US"hash",
   US"hex2b64",
+  US"hexquote",
   US"l",
   US"lc",
   US"length",
   US"l",
   US"lc",
   US"length",
+  US"listcount",
+  US"listnamed",
   US"mask",
   US"md5",
   US"nh",
   US"nhash",
   US"quote",
   US"mask",
   US"md5",
   US"nh",
   US"nhash",
   US"quote",
+  US"randint",
   US"rfc2047",
   US"rfc2047",
+  US"rfc2047d",
   US"rxquote",
   US"s",
   US"sha1",
   US"rxquote",
   US"s",
   US"sha1",
@@ -134,6 +206,7 @@ static uschar *op_table_main[] = {
 
 enum {
   EOP_ADDRESS =  sizeof(op_table_underscore)/sizeof(uschar *),
 
 enum {
   EOP_ADDRESS =  sizeof(op_table_underscore)/sizeof(uschar *),
+  EOP_ADDRESSES,
   EOP_BASE62,
   EOP_BASE62D,
   EOP_DOMAIN,
   EOP_BASE62,
   EOP_BASE62D,
   EOP_DOMAIN,
@@ -144,15 +217,20 @@ enum {
   EOP_H,
   EOP_HASH,
   EOP_HEX2B64,
   EOP_H,
   EOP_HASH,
   EOP_HEX2B64,
+  EOP_HEXQUOTE,
   EOP_L,
   EOP_LC,
   EOP_LENGTH,
   EOP_L,
   EOP_LC,
   EOP_LENGTH,
+  EOP_LISTCOUNT,
+  EOP_LISTNAMED,
   EOP_MASK,
   EOP_MD5,
   EOP_NH,
   EOP_NHASH,
   EOP_QUOTE,
   EOP_MASK,
   EOP_MD5,
   EOP_NH,
   EOP_NHASH,
   EOP_QUOTE,
+  EOP_RANDINT,
   EOP_RFC2047,
   EOP_RFC2047,
+  EOP_RFC2047D,
   EOP_RXQUOTE,
   EOP_S,
   EOP_SHA1,
   EOP_RXQUOTE,
   EOP_S,
   EOP_SHA1,
@@ -173,17 +251,24 @@ static uschar *cond_table[] = {
   US"==",     /* Backward compatibility */
   US">",
   US">=",
   US"==",     /* Backward compatibility */
   US">",
   US">=",
+  US"acl",
   US"and",
   US"and",
+  US"bool",
+  US"bool_lax",
   US"crypteq",
   US"def",
   US"eq",
   US"eqi",
   US"exists",
   US"first_delivery",
   US"crypteq",
   US"def",
   US"eq",
   US"eqi",
   US"exists",
   US"first_delivery",
+  US"forall",
+  US"forany",
   US"ge",
   US"gei",
   US"gt",
   US"gti",
   US"ge",
   US"gei",
   US"gt",
   US"gti",
+  US"inlist",
+  US"inlisti",
   US"isip",
   US"isip4",
   US"isip6",
   US"isip",
   US"isip4",
   US"isip6",
@@ -212,17 +297,24 @@ enum {
   ECOND_NUM_EE,
   ECOND_NUM_G,
   ECOND_NUM_GE,
   ECOND_NUM_EE,
   ECOND_NUM_G,
   ECOND_NUM_GE,
+  ECOND_ACL,
   ECOND_AND,
   ECOND_AND,
+  ECOND_BOOL,
+  ECOND_BOOL_LAX,
   ECOND_CRYPTEQ,
   ECOND_DEF,
   ECOND_STR_EQ,
   ECOND_STR_EQI,
   ECOND_EXISTS,
   ECOND_FIRST_DELIVERY,
   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,
   ECOND_STR_GTI,
   ECOND_STR_GE,
   ECOND_STR_GEI,
   ECOND_STR_GT,
   ECOND_STR_GTI,
+  ECOND_INLIST,
+  ECOND_INLISTI,
   ECOND_ISIP,
   ECOND_ISIP4,
   ECOND_ISIP6,
   ECOND_ISIP,
   ECOND_ISIP4,
   ECOND_ISIP6,
@@ -248,9 +340,9 @@ enum {
 /* Type for main variable table */
 
 typedef struct {
 /* 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
 } var_entry;
 
 /* Type for entries pointing to address/length pairs. Not currently
@@ -269,16 +361,18 @@ enum {
   vtype_ino,            /* value is address of ino_t (not always an int) */
   vtype_uid,            /* value is address of uid_t (not always an int) */
   vtype_gid,            /* value is address of gid_t (not always an int) */
   vtype_ino,            /* value is address of ino_t (not always an int) */
   vtype_uid,            /* value is address of uid_t (not always an int) */
   vtype_gid,            /* value is address of gid_t (not always an int) */
+  vtype_bool,           /* value is address of bool */
   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_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_localpart,      /* extract local part from string */
   vtype_domain,         /* extract domain from string */
-  vtype_recipients,     /* extract recipients from recipients list */
-                        /* (enabled only during system filtering */
+  vtype_string_func,   /* value is string returned by given function */
   vtype_todbsdin,       /* value not used; generate BSD inbox tod */
   vtype_tode,           /* value not used; generate tod in epoch format */
   vtype_todbsdin,       /* value not used; generate BSD inbox tod */
   vtype_tode,           /* value not used; generate tod in epoch format */
+  vtype_todel,          /* value not used; generate tod in epoch/usec format */
   vtype_todf,           /* value not used; generate full tod */
   vtype_todl,           /* value not used; generate log tod */
   vtype_todlf,          /* value not used; generate log file datestamp tod */
   vtype_todf,           /* value not used; generate full tod */
   vtype_todl,           /* value not used; generate log tod */
   vtype_todlf,          /* value not used; generate log file datestamp tod */
@@ -290,41 +384,39 @@ 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 */
   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
   };
 
   };
 
+static uschar * fn_recipients(void);
+
 /* This table must be kept in alphabetical order. */
 
 static var_entry var_table[] = {
 /* 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_arg1",            vtype_stringptr,   &acl_arg[0] },
+  { "acl_arg2",            vtype_stringptr,   &acl_arg[1] },
+  { "acl_arg3",            vtype_stringptr,   &acl_arg[2] },
+  { "acl_arg4",            vtype_stringptr,   &acl_arg[3] },
+  { "acl_arg5",            vtype_stringptr,   &acl_arg[4] },
+  { "acl_arg6",            vtype_stringptr,   &acl_arg[5] },
+  { "acl_arg7",            vtype_stringptr,   &acl_arg[6] },
+  { "acl_arg8",            vtype_stringptr,   &acl_arg[7] },
+  { "acl_arg9",            vtype_stringptr,   &acl_arg[8] },
+  { "acl_narg",            vtype_int,         &acl_narg },
   { "acl_verify_message",  vtype_stringptr,   &acl_verify_message },
   { "address_data",        vtype_stringptr,   &deliver_address_data },
   { "address_file",        vtype_stringptr,   &address_file },
   { "address_pipe",        vtype_stringptr,   &address_pipe },
   { "acl_verify_message",  vtype_stringptr,   &acl_verify_message },
   { "address_data",        vtype_stringptr,   &deliver_address_data },
   { "address_file",        vtype_stringptr,   &address_file },
   { "address_pipe",        vtype_stringptr,   &address_pipe },
+  { "authenticated_fail_id",vtype_stringptr,  &authenticated_fail_id },
   { "authenticated_id",    vtype_stringptr,   &authenticated_id },
   { "authenticated_sender",vtype_stringptr,   &authenticated_sender },
   { "authentication_failed",vtype_int,        &authentication_failed },
   { "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 },
 #ifdef EXPERIMENTAL_BRIGHTMAIL
   { "bmi_alt_location",    vtype_stringptr,   &bmi_alt_location },
   { "bmi_base64_tracker_verdict", vtype_stringptr, &bmi_base64_tracker_verdict },
@@ -340,24 +432,44 @@ static var_entry var_table[] = {
   { "compile_date",        vtype_stringptr,   &version_date },
   { "compile_number",      vtype_stringptr,   &version_cnumber },
   { "csa_status",          vtype_stringptr,   &csa_status },
   { "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 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
+#ifdef EXPERIMENTAL_DMARC
+  { "dmarc_ar_header",     vtype_stringptr,   &dmarc_ar_header },
+  { "dmarc_status",        vtype_stringptr,   &dmarc_status },
+  { "dmarc_status_text",   vtype_stringptr,   &dmarc_status_text },
+  { "dmarc_used_domain",   vtype_stringptr,   &dmarc_used_domain },
 #endif
   { "dnslist_domain",      vtype_stringptr,   &dnslist_domain },
 #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 },
   { "dnslist_text",        vtype_stringptr,   &dnslist_text },
   { "dnslist_value",       vtype_stringptr,   &dnslist_value },
   { "domain",              vtype_stringptr,   &deliver_domain },
@@ -368,6 +480,7 @@ static var_entry var_table[] = {
 #ifdef WITH_OLD_DEMIME
   { "found_extension",     vtype_stringptr,   &found_extension },
 #endif
 #ifdef WITH_OLD_DEMIME
   { "found_extension",     vtype_stringptr,   &found_extension },
 #endif
+  { "headers_added",       vtype_string_func, &fn_hdrs_added },
   { "home",                vtype_stringptr,   &deliver_home },
   { "host",                vtype_stringptr,   &deliver_host },
   { "host_address",        vtype_stringptr,   &deliver_host_address },
   { "home",                vtype_stringptr,   &deliver_home },
   { "host",                vtype_stringptr,   &deliver_host },
   { "host_address",        vtype_stringptr,   &deliver_host_address },
@@ -377,6 +490,7 @@ static var_entry var_table[] = {
   { "inode",               vtype_ino,         &deliver_inode },
   { "interface_address",   vtype_stringptr,   &interface_address },
   { "interface_port",      vtype_int,         &interface_port },
   { "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
   #ifdef LOOKUP_LDAP
   { "ldap_dn",             vtype_stringptr,   &eldap_dn },
   #endif
@@ -395,11 +509,14 @@ static var_entry var_table[] = {
 #ifdef WITH_CONTENT_SCAN
   { "malware_name",        vtype_stringptr,   &malware_name },
 #endif
 #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_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",     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 },
   { "message_id",          vtype_stringptr,   &message_id },
   { "message_linecount",   vtype_int,         &message_linecount },
   { "message_size",        vtype_int,         &message_size },
@@ -449,11 +566,13 @@ 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 },
   { "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 },
   { "recipient_verify_failure",vtype_stringptr,&recipient_verify_failure },
   { "received_protocol",   vtype_stringptr,   &received_protocol },
   { "received_time",       vtype_int,         &received_time },
   { "recipient_data",      vtype_stringptr,   &recipient_data },
   { "recipient_verify_failure",vtype_stringptr,&recipient_verify_failure },
-  { "recipients",          vtype_recipients,  NULL },
+  { "recipients",          vtype_string_func, &fn_recipients },
   { "recipients_count",    vtype_int,         &recipients_count },
 #ifdef WITH_CONTENT_SCAN
   { "regex_match_string",  vtype_stringptr,   &regex_match_string },
   { "recipients_count",    vtype_int,         &recipients_count },
 #ifdef WITH_CONTENT_SCAN
   { "regex_match_string",  vtype_stringptr,   &regex_match_string },
@@ -461,6 +580,7 @@ static var_entry var_table[] = {
   { "reply_address",       vtype_reply,       NULL },
   { "return_path",         vtype_stringptr,   &return_path },
   { "return_size_limit",   vtype_int,         &bounce_return_size_limit },
   { "reply_address",       vtype_reply,       NULL },
   { "return_path",         vtype_stringptr,   &return_path },
   { "return_size_limit",   vtype_int,         &bounce_return_size_limit },
+  { "router_name",         vtype_stringptr,   &router_name },
   { "runrc",               vtype_int,         &runrc },
   { "self_hostname",       vtype_stringptr,   &self_hostname },
   { "sender_address",      vtype_stringptr,   &sender_address },
   { "runrc",               vtype_int,         &runrc },
   { "self_hostname",       vtype_stringptr,   &self_hostname },
   { "sender_address",      vtype_stringptr,   &sender_address },
@@ -472,6 +592,7 @@ static var_entry var_table[] = {
   { "sender_helo_name",    vtype_stringptr,   &sender_helo_name },
   { "sender_host_address", vtype_stringptr,   &sender_host_address },
   { "sender_host_authenticated",vtype_stringptr, &sender_host_authenticated },
   { "sender_helo_name",    vtype_stringptr,   &sender_helo_name },
   { "sender_host_address", vtype_stringptr,   &sender_host_address },
   { "sender_host_authenticated",vtype_stringptr, &sender_host_authenticated },
+  { "sender_host_dnssec",  vtype_bool,        &sender_host_dnssec },
   { "sender_host_name",    vtype_host_lookup, NULL },
   { "sender_host_port",    vtype_int,         &sender_host_port },
   { "sender_ident",        vtype_stringptr,   &sender_ident },
   { "sender_host_name",    vtype_host_lookup, NULL },
   { "sender_host_port",    vtype_int,         &sender_host_port },
   { "sender_ident",        vtype_stringptr,   &sender_ident },
@@ -480,8 +601,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 },
   { "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_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] },
   { "sn0",                 vtype_filter_int,  &filter_sn[0] },
   { "sn1",                 vtype_filter_int,  &filter_sn[1] },
   { "sn2",                 vtype_filter_int,  &filter_sn[2] },
@@ -499,6 +625,7 @@ static var_entry var_table[] = {
   { "spam_score_int",      vtype_stringptr,   &spam_score_int },
 #endif
 #ifdef EXPERIMENTAL_SPF
   { "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 },
   { "spf_header_comment",  vtype_stringptr,   &spf_header_comment },
   { "spf_received",        vtype_stringptr,   &spf_received },
   { "spf_result",          vtype_stringptr,   &spf_result },
@@ -516,16 +643,41 @@ static var_entry var_table[] = {
   { "srs_status",          vtype_stringptr,   &srs_status },
 #endif
   { "thisaddress",         vtype_stringptr,   &filter_thisaddress },
   { "srs_status",          vtype_stringptr,   &srs_status },
 #endif
   { "thisaddress",         vtype_stringptr,   &filter_thisaddress },
-  { "tls_certificate_verified", vtype_int,    &tls_certificate_verified },
-  { "tls_cipher",          vtype_stringptr,   &tls_cipher },
-  { "tls_peerdn",          vtype_stringptr,   &tls_peerdn },
+
+  /* The non-(in,out) variables are now deprecated */
+  { "tls_bits",            vtype_int,         &tls_in.bits },
+  { "tls_certificate_verified", vtype_int,    &tls_in.certificate_verified },
+  { "tls_cipher",          vtype_stringptr,   &tls_in.cipher },
+
+  { "tls_in_bits",         vtype_int,         &tls_in.bits },
+  { "tls_in_certificate_verified", vtype_int, &tls_in.certificate_verified },
+  { "tls_in_cipher",       vtype_stringptr,   &tls_in.cipher },
+  { "tls_in_peerdn",       vtype_stringptr,   &tls_in.peerdn },
+#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS)
+  { "tls_in_sni",          vtype_stringptr,   &tls_in.sni },
+#endif
+  { "tls_out_bits",        vtype_int,         &tls_out.bits },
+  { "tls_out_certificate_verified", vtype_int,&tls_out.certificate_verified },
+  { "tls_out_cipher",      vtype_stringptr,   &tls_out.cipher },
+  { "tls_out_peerdn",      vtype_stringptr,   &tls_out.peerdn },
+#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS)
+  { "tls_out_sni",         vtype_stringptr,   &tls_out.sni },
+#endif
+
+  { "tls_peerdn",          vtype_stringptr,   &tls_in.peerdn },        /* mind the alphabetical order! */
+#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS)
+  { "tls_sni",             vtype_stringptr,   &tls_in.sni },   /* mind the alphabetical order! */
+#endif
+
   { "tod_bsdinbox",        vtype_todbsdin,    NULL },
   { "tod_epoch",           vtype_tode,        NULL },
   { "tod_bsdinbox",        vtype_todbsdin,    NULL },
   { "tod_epoch",           vtype_tode,        NULL },
+  { "tod_epoch_l",         vtype_todel,       NULL },
   { "tod_full",            vtype_todf,        NULL },
   { "tod_log",             vtype_todl,        NULL },
   { "tod_logfile",         vtype_todlf,       NULL },
   { "tod_zone",            vtype_todzone,     NULL },
   { "tod_zulu",            vtype_todzulu,     NULL },
   { "tod_full",            vtype_todf,        NULL },
   { "tod_log",             vtype_todl,        NULL },
   { "tod_logfile",         vtype_todlf,       NULL },
   { "tod_zone",            vtype_todzone,     NULL },
   { "tod_zulu",            vtype_todzulu,     NULL },
+  { "transport_name",      vtype_stringptr,   &transport_name },
   { "value",               vtype_stringptr,   &lookup_value },
   { "version_number",      vtype_stringptr,   &version_string },
   { "warn_message_delay",  vtype_stringptr,   &warnmsg_delay },
   { "value",               vtype_stringptr,   &lookup_value },
   { "version_number",      vtype_stringptr,   &version_string },
   { "warn_message_delay",  vtype_stringptr,   &warnmsg_delay },
@@ -542,9 +694,9 @@ static BOOL malformed_header;
 
 /* For textual hashes */
 
 
 /* For textual hashes */
 
-static char *hashcodes = "abcdefghijklmnopqrtsuvwxyz"
-                         "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-                         "0123456789";
+static const char *hashcodes = "abcdefghijklmnopqrtsuvwxyz"
+                               "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                               "0123456789";
 
 enum { HMAC_MD5, HMAC_SHA1 };
 
 
 enum { HMAC_MD5, HMAC_SHA1 };
 
@@ -643,8 +795,13 @@ return -1;
 
 /* This function is called to expand a string, and test the result for a "true"
 or "false" value. Failure of the expansion yields FALSE; logged unless it was a
 
 /* This function is called to expand a string, and test the result for a "true"
 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.
+forced fail or lookup defer.
+
+We used to release all store used, but this is not not safe due
+to ${dlfunc } and ${acl }.  In any case expand_string_internal()
+is reasonably careful to release what it can.
+
+The actual false-value tests should be replicated for ECOND_BOOL_LAX.
 
 Arguments:
   condition     the condition string
 
 Arguments:
   condition     the condition string
@@ -658,7 +815,6 @@ BOOL
 expand_check_condition(uschar *condition, uschar *m1, uschar *m2)
 {
 int rc;
 expand_check_condition(uschar *condition, uschar *m1, uschar *m2)
 {
 int rc;
-void *reset_point = store_get(0);
 uschar *ss = expand_string(condition);
 if (ss == NULL)
   {
 uschar *ss = expand_string(condition);
 if (ss == NULL)
   {
@@ -669,12 +825,86 @@ if (ss == NULL)
   }
 rc = ss[0] != 0 && Ustrcmp(ss, "0") != 0 && strcmpic(ss, US"no") != 0 &&
   strcmpic(ss, US"false") != 0;
   }
 rc = ss[0] != 0 && Ustrcmp(ss, "0") != 0 && strcmpic(ss, US"no") != 0 &&
   strcmpic(ss, US"false") != 0;
-store_reset(reset_point);
 return rc;
 }
 
 
 
 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 then this will not be used except as an emergency
+fallback.
+
+Arguments:
+  max       range maximum
+Returns     a random number in range [0, max-1]
+*/
+
+#ifdef SUPPORT_TLS
+# define vaguely_random_number vaguely_random_number_fallback
+#endif
+int
+vaguely_random_number(int max)
+{
+#ifdef SUPPORT_TLS
+# undef vaguely_random_number
+#endif
+  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
+}
+
+
+
+
 /*************************************************
 *             Pick out a name from a string      *
 *************************************************/
 /*************************************************
 *             Pick out a name from a string      *
 *************************************************/
@@ -1092,7 +1322,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,
   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_)
   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_)
@@ -1135,6 +1366,12 @@ for (i = 0; i < 2; i++)
           while (isspace(*t)) t++;          /* remove leading white space */
         ilen = h->slen - (t - h->text);     /* length to insert */
 
           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. */
         /* 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. */
@@ -1146,7 +1383,7 @@ for (i = 0; i < 2; i++)
         /* First pass - compute total store needed; second pass - compute
         total store used, including this header. */
 
         /* 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. */
 
         /* Second pass - concatentate the data, up to a maximum. Note that
         the loop stops when size hits the limit. */
@@ -1155,14 +1392,19 @@ for (i = 0; i < 2; i++)
           {
           if (size > header_insert_maxlen)
             {
           {
           if (size > header_insert_maxlen)
             {
-            ilen -= size - header_insert_maxlen;
+            ilen -= size - header_insert_maxlen - 1;
             comma = 0;
             }
           Ustrncpy(ptr, t, ilen);
           ptr += ilen;
             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';
             }
           }
             *ptr++ = '\n';
             }
           }
@@ -1170,8 +1412,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)
     {
 
   if (i == 0)
     {
@@ -1182,10 +1425,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)
 /* That's all we do for raw header expansion. */
 
 if (want_raw)
@@ -1193,17 +1432,19 @@ if (want_raw)
   *ptr = 0;
   }
 
   *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;
 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;
   *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"
   if (error != NULL)
     {
     DEBUG(D_any) debug_printf("*** error in RFC 2047 decoding: %s\n"
@@ -1218,6 +1459,34 @@ return yield;
 
 
 
 
 
 
+/*************************************************
+*               Return list of recipients        *
+*************************************************/
+/* A recipients list is available only during system message filtering,
+during ACL processing after DATA, and while expanding pipe commands
+generated from a system filter, but not elsewhere. */
+
+static uschar *
+fn_recipients(void)
+{
+if (!enable_dollar_recipients) return NULL; else
+  {
+  int size = 128;
+  int ptr = 0;
+  int i;
+  uschar * s = store_get(size);
+  for (i = 0; i < recipients_count; i++)
+    {
+    if (i != 0) s = string_cat(s, &size, &ptr, US", ", 2);
+    s = string_cat(s, &size, &ptr, recipients_list[i].address,
+      Ustrlen(recipients_list[i].address));
+    }
+  s[ptr] = 0;     /* string_cat() leaves room */
+  return s;
+  }
+}
+
+
 /*************************************************
 *               Find value of a variable         *
 *************************************************/
 /*************************************************
 *               Find value of a variable         *
 *************************************************/
@@ -1246,6 +1515,37 @@ find_variable(uschar *name, BOOL exists_only, BOOL skipping, int *newsize)
 int first = 0;
 int last = var_table_size;
 
 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;
 while (last > first)
   {
   uschar *s, *domain;
@@ -1257,57 +1557,12 @@ while (last > first)
   if (c < 0) { last = middle; continue; }
 
   /* Found an existing variable. If in skipping state, the value isn't needed,
   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"";
 
   switch (var_table[middle].type)
     {
 
   if (skipping) return US"";
 
   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 */
     case vtype_filter_int:
     if (!filter_running) return NULL;
     /* Fall through */
@@ -1328,6 +1583,10 @@ while (last > first)
     sprintf(CS var_buffer, "%ld", (long int)(*(uid_t *)(var_table[middle].value))); /* uid */
     return var_buffer;
 
     sprintf(CS var_buffer, "%ld", (long int)(*(uid_t *)(var_table[middle].value))); /* uid */
     return var_buffer;
 
+    case vtype_bool:
+    sprintf(CS var_buffer, "%s", *(BOOL *)(var_table[middle].value) ? "yes" : "no"); /* bool */
+    return var_buffer;
+
     case vtype_stringptr:                      /* Pointer to string */
     s = *((uschar **)(var_table[middle].value));
     return (s == NULL)? US"" : s;
     case vtype_stringptr:                      /* Pointer to string */
     s = *((uschar **)(var_table[middle].value));
     return (s == NULL)? US"" : s;
@@ -1337,7 +1596,7 @@ while (last > first)
     return var_buffer;
 
     case vtype_load_avg:
     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 */
     return var_buffer;
 
     case vtype_host_lookup:                    /* Lookup if not done so */
@@ -1352,8 +1611,8 @@ while (last > first)
     domain = Ustrrchr(s, '@');
     if (domain == NULL) return s;
     if (domain - s > sizeof(var_buffer) - 1)
     domain = Ustrrchr(s, '@');
     if (domain == NULL) return s;
     if (domain - s > sizeof(var_buffer) - 1)
-      log_write(0, LOG_MAIN|LOG_PANIC_DIE, "local part longer than %d in "
-        "string expansion", sizeof(var_buffer));
+      log_write(0, LOG_MAIN|LOG_PANIC_DIE, "local part longer than " SIZE_T_FMT
+          " in string expansion", sizeof(var_buffer));
     Ustrncpy(var_buffer, s, domain - s);
     var_buffer[domain - s] = 0;
     return var_buffer;
     Ustrncpy(var_buffer, s, domain - s);
     var_buffer[domain - s] = 0;
     return var_buffer;
@@ -1367,6 +1626,9 @@ while (last > first)
     case vtype_msgheaders:
     return find_header(NULL, exists_only, newsize, FALSE, NULL);
 
     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);
     case vtype_msgbody:                        /* Pointer to msgbody string */
     case vtype_msgbody_end:                    /* Ditto, the end of the msg */
     ss = (uschar **)(var_table[middle].value);
@@ -1393,9 +1655,15 @@ while (last > first)
       if (len > 0)
         {
         body[len] = 0;
       if (len > 0)
         {
         body[len] = 0;
-        while (len > 0)
+        if (message_body_newlines)   /* Separate loops for efficiency */
+          {
+          while (len > 0)
+            { if (body[--len] == 0) body[len] = ' '; }
+          }
+        else
           {
           {
-          if (body[--len] == '\n' || body[len] == 0) body[len] = ' ';
+          while (len > 0)
+            { if (body[--len] == '\n' || body[len] == 0) body[len] = ' '; }
           }
         }
       }
           }
         }
       }
@@ -1407,6 +1675,9 @@ while (last > first)
     case vtype_tode:                           /* Unix epoch time of day */
     return tod_stamp(tod_epoch);
 
     case vtype_tode:                           /* Unix epoch time of day */
     return tod_stamp(tod_epoch);
 
+    case vtype_todel:                          /* Unix epoch/usec time of day */
+    return tod_stamp(tod_epoch_l);
+
     case vtype_todf:                           /* Full time of day */
     return tod_stamp(tod_full);
 
     case vtype_todf:                           /* Full time of day */
     return tod_stamp(tod_full);
 
@@ -1420,35 +1691,32 @@ while (last > first)
     return tod_stamp(tod_zulu);
 
     case vtype_todlf:                          /* Log file datestamp tod */
     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 */
 
     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);
       headers_charset);
+    if (s != NULL) while (isspace(*s)) s++;
     if (s == NULL || *s == 0)
     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;
 
     return (s == NULL)? US"" : s;
 
-    /* A recipients list is available only during system message filtering,
-    during ACL processing after DATA, and while expanding pipe commands
-    generated from a system filter, but not elsewhere. */
-
-    case vtype_recipients:
-    if (!enable_dollar_recipients) return NULL; else
+    case vtype_string_func:
       {
       {
-      int size = 128;
-      int ptr = 0;
-      int i;
-      s = store_get(size);
-      for (i = 0; i < recipients_count; i++)
-        {
-        if (i != 0) s = string_cat(s, &size, &ptr, US", ", 2);
-        s = string_cat(s, &size, &ptr, recipients_list[i].address,
-          Ustrlen(recipients_list[i].address));
-        }
-      s[ptr] = 0;     /* string_cat() leaves room */
+      uschar * (*fn)() = var_table[middle].value;
+      return fn();
       }
       }
-    return s;
 
     case vtype_pspace:
       {
 
     case vtype_pspace:
       {
@@ -1465,6 +1733,12 @@ while (last > first)
       sprintf(CS var_buffer, "%d", inodes);
       }
     return var_buffer;
       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
+
     }
   }
 
     }
   }
 
@@ -1474,6 +1748,31 @@ return NULL;          /* Unknown variable name */
 
 
 
 
 
 
+void
+modify_variable(uschar *name, void * value)
+{
+int first = 0;
+int last = var_table_size;
+
+while (last > first)
+  {
+  int middle = (first + last)/2;
+  int c = Ustrcmp(name, var_table[middle].name);
+
+  if (c > 0) { first = middle + 1; continue; }
+  if (c < 0) { last = middle; continue; }
+
+  /* Found an existing variable; change the item it refers to */
+  var_table[middle].value = value;
+  return;
+  }
+return;          /* Unknown variable name, fail silently */
+}
+
+
+
+
+
 /*************************************************
 *           Read and expand substrings           *
 *************************************************/
 /*************************************************
 *           Read and expand substrings           *
 *************************************************/
@@ -1513,7 +1812,7 @@ for (i = 0; i < n; i++)
     sub[i] = NULL;
     break;
     }
     sub[i] = NULL;
     break;
     }
-  sub[i] = expand_string_internal(s+1, TRUE, &s, skipping);
+  sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE);
   if (sub[i] == NULL) return 3;
   if (*s++ != '}') return 1;
   while (isspace(*s)) s++;
   if (sub[i] == NULL) return 3;
   if (*s++ != '}') return 1;
   while (isspace(*s)) s++;
@@ -1536,6 +1835,85 @@ 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");
+}
+
+
+
+/*
+Load args from sub array to globals, and call acl_check().
+Sub array will be corrupted on return.
+
+Returns:       OK         access is granted by an ACCEPT verb
+               DISCARD    access is granted by a DISCARD verb
+              FAIL       access is denied
+              FAIL_DROP  access is denied; drop the connection
+              DEFER      can't tell at the moment
+              ERROR      disaster
+*/
+static int
+eval_acl(uschar ** sub, int nsub, uschar ** user_msgp)
+{
+int i;
+uschar *tmp;
+int sav_narg = acl_narg;
+int ret;
+extern int acl_where;
+
+if(--nsub > sizeof(acl_arg)/sizeof(*acl_arg)) nsub = sizeof(acl_arg)/sizeof(*acl_arg);
+for (i = 0; i < nsub && sub[i+1]; i++)
+  {
+  tmp = acl_arg[i];
+  acl_arg[i] = sub[i+1];       /* place callers args in the globals */
+  sub[i+1] = tmp;              /* stash the old args using our caller's storage */
+  }
+acl_narg = i;
+while (i < nsub)
+  {
+  sub[i+1] = acl_arg[i];
+  acl_arg[i++] = NULL;
+  }
+
+DEBUG(D_expand)
+  debug_printf("expanding: acl: %s  arg: %s%s\n",
+    sub[0],
+    acl_narg>0 ? acl_arg[0] : US"<none>",
+    acl_narg>1 ? " +more"   : "");
+
+ret = acl_eval(acl_where, sub[0], user_msgp, &tmp);
+
+for (i = 0; i < nsub; i++)
+  acl_arg[i] = sub[i+1];       /* restore old args */
+acl_narg = sav_narg;
+
+return ret;
+}
+
+
+
+
 /*************************************************
 *        Read and evaluate a condition           *
 *************************************************/
 /*************************************************
 *        Read and evaluate a condition           *
 *************************************************/
@@ -1558,11 +1936,12 @@ eval_condition(uschar *s, BOOL *yield)
 BOOL testfor = TRUE;
 BOOL tempcond, combined_cond;
 BOOL *subcondptr;
 BOOL testfor = TRUE;
 BOOL tempcond, combined_cond;
 BOOL *subcondptr;
+BOOL sub2_honour_dollar = TRUE;
 int i, rc, cond_type, roffset;
 int i, rc, cond_type, roffset;
-int num[2];
+int_eximarith_t num[2];
 struct stat statbuf;
 uschar name[256];
 struct stat statbuf;
 uschar name[256];
-uschar *sub[4];
+uschar *sub[10];
 
 const pcre *re;
 const uschar *rerror;
 
 const pcre *re;
 const uschar *rerror;
@@ -1617,7 +1996,9 @@ switch(cond_type)
 
   s = read_name(name, 256, s+1, US"_");
 
 
   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 ||
 
   if (Ustrncmp(name, "h_", 2) == 0 ||
       Ustrncmp(name, "rh_", 3) == 0 ||
@@ -1627,6 +2008,8 @@ switch(cond_type)
       Ustrncmp(name, "bheader_", 8) == 0)
     {
     s = read_header_name(name, 256, s);
       Ustrncmp(name, "bheader_", 8) == 0)
     {
     s = read_header_name(name, 256, s);
+    /* {-for-text-editors */
+    if (Ustrchr(name, '}') != NULL) malformed_header = TRUE;
     if (yield != NULL) *yield =
       (find_header(name, TRUE, NULL, FALSE, NULL) != NULL) == testfor;
     }
     if (yield != NULL) *yield =
       (find_header(name, TRUE, NULL, FALSE, NULL) != NULL) == testfor;
     }
@@ -1642,6 +2025,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);
       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;
       return NULL;
       }
     if (yield != NULL) *yield = (value[0] != 0) == testfor;
@@ -1684,10 +2068,11 @@ switch(cond_type)
   case ECOND_PWCHECK:
 
   while (isspace(*s)) s++;
   case ECOND_PWCHECK:
 
   while (isspace(*s)) s++;
-  if (*s != '{') goto COND_FAILED_CURLY_START;
+  if (*s != '{') goto COND_FAILED_CURLY_START;         /* }-for-text-editors */
 
 
-  sub[0] = expand_string_internal(s+1, TRUE, &s, yield == NULL);
+  sub[0] = expand_string_internal(s+1, TRUE, &s, yield == NULL, TRUE);
   if (sub[0] == NULL) return NULL;
   if (sub[0] == NULL) return NULL;
+  /* {-for-text-editors */
   if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
   if (yield == NULL) return s;   /* No need to run the test if skipping */
   if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
   if (yield == NULL) return s;   /* No need to run the test if skipping */
@@ -1707,7 +2092,7 @@ switch(cond_type)
     case ECOND_ISIP4:
     case ECOND_ISIP6:
     rc = string_is_ip_address(sub[0], NULL);
     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;
 
              (cond_type == ECOND_ISIP4)? (rc == 4) : (rc == 6)) == testfor;
     break;
 
@@ -1763,19 +2148,71 @@ switch(cond_type)
   return s;
 
 
   return s;
 
 
+  /* call ACL (in a conditional context).  Accept true, deny false.
+  Defer is a forced-fail.  Anything set by message= goes to $value.
+  Up to ten parameters are used; we use the braces round the name+args
+  like the saslauthd condition does, to permit a variable number of args.
+  See also the expansion-item version EITEM_ACL and the traditional
+  acl modifier ACLC_ACL.
+  */
+
+  case ECOND_ACL:
+    /* ${if acl {{name}{arg1}{arg2}...}  {yes}{no}} */
+    {
+    uschar *user_msg;
+    BOOL cond = FALSE;
+    int size = 0;
+    int ptr = 0;
+
+    while (isspace(*s)) s++;
+    if (*s++ != '{') goto COND_FAILED_CURLY_START;     /*}*/
+
+    switch(read_subs(sub, sizeof(sub)/sizeof(*sub), 1,
+      &s, yield == NULL, TRUE, US"acl"))
+      {
+      case 1: expand_string_message = US"too few arguments or bracketing "
+        "error for acl";
+      case 2:
+      case 3: return NULL;
+      }
+
+    if (yield != NULL) switch(eval_acl(sub, sizeof(sub)/sizeof(*sub), &user_msg))
+       {
+       case OK:
+         cond = TRUE;
+       case FAIL:
+          lookup_value = NULL;
+         if (user_msg)
+           {
+            lookup_value = string_cat(NULL, &size, &ptr, user_msg, Ustrlen(user_msg));
+            lookup_value[ptr] = '\0';
+           }
+         *yield = cond == testfor;
+         break;
+
+       case DEFER:
+          expand_string_forcedfail = TRUE;
+       default:
+          expand_string_message = string_sprintf("error from acl \"%s\"", sub[0]);
+         return NULL;
+       }
+    return s;
+    }
+
+
   /* saslauthd: does Cyrus saslauthd authentication. Four parameters are used:
 
      ${if saslauthd {{username}{password}{service}{realm}}  {yes}[no}}
 
   However, the last two are optional. That is why the whole set is enclosed
   /* saslauthd: does Cyrus saslauthd authentication. Four parameters are used:
 
      ${if saslauthd {{username}{password}{service}{realm}}  {yes}[no}}
 
   However, the last two are optional. That is why the whole set is enclosed
-  in their own set or braces. */
+  in their own set of braces. */
 
   case ECOND_SASLAUTHD:
   #ifndef CYRUS_SASLAUTHD_SOCKET
   goto COND_FAILED_NOT_COMPILED;
   #else
   while (isspace(*s)) s++;
 
   case ECOND_SASLAUTHD:
   #ifndef CYRUS_SASLAUTHD_SOCKET
   goto COND_FAILED_NOT_COMPILED;
   #else
   while (isspace(*s)) s++;
-  if (*s++ != '{') goto COND_FAILED_CURLY_START;
+  if (*s++ != '{') goto COND_FAILED_CURLY_START;       /* }-for-text-editors */
   switch(read_subs(sub, 4, 2, &s, yield == NULL, TRUE, US"saslauthd"))
     {
     case 1: expand_string_message = US"too few arguments or bracketing "
   switch(read_subs(sub, 4, 2, &s, yield == NULL, TRUE, US"saslauthd"))
     {
     case 1: expand_string_message = US"too few arguments or bracketing "
@@ -1799,22 +2236,30 @@ switch(cond_type)
   /* symbolic operators for numeric and string comparison, and a number of
   other operators, all requiring two arguments.
 
   /* symbolic operators for numeric and string comparison, and a number of
   other operators, all requiring two arguments.
 
+  crypteq:           encrypts plaintext and compares against an encrypted text,
+                       using crypt(), crypt16(), MD5 or SHA-1
+  inlist/inlisti:    checks if first argument is in the list of the second
   match:             does a regular expression match and sets up the numerical
                        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
   match:             does a regular expression match and sets up the numerical
                        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
   */
 
   */
 
-  case ECOND_MATCH:
   case ECOND_MATCH_ADDRESS:
   case ECOND_MATCH_DOMAIN:
   case ECOND_MATCH_IP:
   case ECOND_MATCH_LOCAL_PART:
   case ECOND_MATCH_ADDRESS:
   case ECOND_MATCH_DOMAIN:
   case ECOND_MATCH_IP:
   case ECOND_MATCH_LOCAL_PART:
+#ifndef EXPAND_LISTMATCH_RHS
+    sub2_honour_dollar = FALSE;
+#endif
+    /* FALLTHROUGH */
+
   case ECOND_CRYPTEQ:
   case ECOND_CRYPTEQ:
+  case ECOND_INLIST:
+  case ECOND_INLISTI:
+  case ECOND_MATCH:
 
   case ECOND_NUM_L:     /* Numerical comparisons */
   case ECOND_NUM_LE:
 
   case ECOND_NUM_L:     /* Numerical comparisons */
   case ECOND_NUM_LE:
@@ -1836,6 +2281,13 @@ switch(cond_type)
 
   for (i = 0; i < 2; i++)
     {
 
   for (i = 0; i < 2; i++)
     {
+    /* Sometimes, we don't expand substrings; too many insecure configurations
+    created using match_address{}{} and friends, where the second param
+    includes information from untrustworthy sources. */
+    BOOL honour_dollar = TRUE;
+    if ((i > 0) && !sub2_honour_dollar)
+      honour_dollar = FALSE;
+
     while (isspace(*s)) s++;
     if (*s != '{')
       {
     while (isspace(*s)) s++;
     if (*s != '{')
       {
@@ -1844,7 +2296,8 @@ switch(cond_type)
         "after \"%s\"", name);
       return NULL;
       }
         "after \"%s\"", name);
       return NULL;
       }
-    sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL);
+    sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL,
+        honour_dollar);
     if (sub[i] == NULL) return NULL;
     if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
     if (sub[i] == NULL) return NULL;
     if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
@@ -1852,26 +2305,18 @@ switch(cond_type)
     conditions that compare numbers do not start with a letter. This just saves
     checking for them individually. */
 
     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')
+      if (sub[i][0] == 0)
         {
         {
-        num[i] *= 1024;
-        endptr++;
+        num[i] = 0;
+        DEBUG(D_expand)
+          debug_printf("empty string cast to zero for numerical comparison\n");
         }
         }
-      else if (tolower(*endptr) == 'm')
-        {
-        num[i] *= 1024*1024;
-        endptr++;
-        }
-      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;
         }
       }
     }
         }
       }
     }
@@ -1886,63 +2331,63 @@ switch(cond_type)
     {
     case ECOND_NUM_E:
     case ECOND_NUM_EE:
     {
     case ECOND_NUM_E:
     case ECOND_NUM_EE:
-    *yield = (num[0] == num[1]) == testfor;
+    tempcond = (num[0] == num[1]);
     break;
 
     case ECOND_NUM_G:
     break;
 
     case ECOND_NUM_G:
-    *yield = (num[0] > num[1]) == testfor;
+    tempcond = (num[0] > num[1]);
     break;
 
     case ECOND_NUM_GE:
     break;
 
     case ECOND_NUM_GE:
-    *yield = (num[0] >= num[1]) == testfor;
+    tempcond = (num[0] >= num[1]);
     break;
 
     case ECOND_NUM_L:
     break;
 
     case ECOND_NUM_L:
-    *yield = (num[0] < num[1]) == testfor;
+    tempcond = (num[0] < num[1]);
     break;
 
     case ECOND_NUM_LE:
     break;
 
     case ECOND_NUM_LE:
-    *yield = (num[0] <= num[1]) == testfor;
+    tempcond = (num[0] <= num[1]);
     break;
 
     case ECOND_STR_LT:
     break;
 
     case ECOND_STR_LT:
-    *yield = (Ustrcmp(sub[0], sub[1]) < 0) == testfor;
+    tempcond = (Ustrcmp(sub[0], sub[1]) < 0);
     break;
 
     case ECOND_STR_LTI:
     break;
 
     case ECOND_STR_LTI:
-    *yield = (strcmpic(sub[0], sub[1]) < 0) == testfor;
+    tempcond = (strcmpic(sub[0], sub[1]) < 0);
     break;
 
     case ECOND_STR_LE:
     break;
 
     case ECOND_STR_LE:
-    *yield = (Ustrcmp(sub[0], sub[1]) <= 0) == testfor;
+    tempcond = (Ustrcmp(sub[0], sub[1]) <= 0);
     break;
 
     case ECOND_STR_LEI:
     break;
 
     case ECOND_STR_LEI:
-    *yield = (strcmpic(sub[0], sub[1]) <= 0) == testfor;
+    tempcond = (strcmpic(sub[0], sub[1]) <= 0);
     break;
 
     case ECOND_STR_EQ:
     break;
 
     case ECOND_STR_EQ:
-    *yield = (Ustrcmp(sub[0], sub[1]) == 0) == testfor;
+    tempcond = (Ustrcmp(sub[0], sub[1]) == 0);
     break;
 
     case ECOND_STR_EQI:
     break;
 
     case ECOND_STR_EQI:
-    *yield = (strcmpic(sub[0], sub[1]) == 0) == testfor;
+    tempcond = (strcmpic(sub[0], sub[1]) == 0);
     break;
 
     case ECOND_STR_GT:
     break;
 
     case ECOND_STR_GT:
-    *yield = (Ustrcmp(sub[0], sub[1]) > 0) == testfor;
+    tempcond = (Ustrcmp(sub[0], sub[1]) > 0);
     break;
 
     case ECOND_STR_GTI:
     break;
 
     case ECOND_STR_GTI:
-    *yield = (strcmpic(sub[0], sub[1]) > 0) == testfor;
+    tempcond = (strcmpic(sub[0], sub[1]) > 0);
     break;
 
     case ECOND_STR_GE:
     break;
 
     case ECOND_STR_GE:
-    *yield = (Ustrcmp(sub[0], sub[1]) >= 0) == testfor;
+    tempcond = (Ustrcmp(sub[0], sub[1]) >= 0);
     break;
 
     case ECOND_STR_GEI:
     break;
 
     case ECOND_STR_GEI:
-    *yield = (strcmpic(sub[0], sub[1]) >= 0) == testfor;
+    tempcond = (strcmpic(sub[0], sub[1]) >= 0);
     break;
 
     case ECOND_MATCH:   /* Regular expression match */
     break;
 
     case ECOND_MATCH:   /* Regular expression match */
@@ -1954,7 +2399,7 @@ switch(cond_type)
         "\"%s\": %s at offset %d", sub[1], rerror, roffset);
       return NULL;
       }
         "\"%s\": %s at offset %d", sub[1], rerror, roffset);
       return NULL;
       }
-    *yield = regex_match_and_setup(re, sub[0], 0, -1) == testfor;
+    tempcond = regex_match_and_setup(re, sub[0], 0, -1);
     break;
 
     case ECOND_MATCH_ADDRESS:  /* Match in an address list */
     break;
 
     case ECOND_MATCH_ADDRESS:  /* Match in an address list */
@@ -1967,7 +2412,7 @@ switch(cond_type)
     goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_IP:       /* Match IP address in a host list */
     goto MATCHED_SOMETHING;
 
     case ECOND_MATCH_IP:       /* Match IP address in a host list */
-    if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) <= 0)
+    if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) == 0)
       {
       expand_string_message = string_sprintf("\"%s\" is not an IP address",
         sub[0]);
       {
       expand_string_message = string_sprintf("\"%s\" is not an IP address",
         sub[0]);
@@ -2010,11 +2455,11 @@ switch(cond_type)
     switch(rc)
       {
       case OK:
     switch(rc)
       {
       case OK:
-      *yield = testfor;
+      tempcond = TRUE;
       break;
 
       case FAIL:
       break;
 
       case FAIL:
-      *yield = !testfor;
+      tempcond = FALSE;
       break;
 
       case DEFER:
       break;
 
       case DEFER:
@@ -2028,6 +2473,7 @@ switch(cond_type)
     /* Various "encrypted" comparisons. If the second string starts with
     "{" then an encryption type is given. Default to crypt() or crypt16()
     (build-time choice). */
     /* Various "encrypted" comparisons. If the second string starts with
     "{" then an encryption type is given. Default to crypt() or crypt16()
     (build-time choice). */
+    /* }-for-text-editors */
 
     case ECOND_CRYPTEQ:
     #ifndef SUPPORT_CRYPTEQ
 
     case ECOND_CRYPTEQ:
     #ifndef SUPPORT_CRYPTEQ
@@ -2052,7 +2498,7 @@ switch(cond_type)
         uschar *coded = auth_b64encode((uschar *)digest, 16);
         DEBUG(D_auth) debug_printf("crypteq: using MD5+B64 hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
         uschar *coded = auth_b64encode((uschar *)digest, 16);
         DEBUG(D_auth) debug_printf("crypteq: using MD5+B64 hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
-        *yield = (Ustrcmp(coded, sub[1]+5) == 0) == testfor;
+        tempcond = (Ustrcmp(coded, sub[1]+5) == 0);
         }
       else if (sublen == 32)
         {
         }
       else if (sublen == 32)
         {
@@ -2062,13 +2508,13 @@ switch(cond_type)
         coded[32] = 0;
         DEBUG(D_auth) debug_printf("crypteq: using MD5+hex hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
         coded[32] = 0;
         DEBUG(D_auth) debug_printf("crypteq: using MD5+hex hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+5);
-        *yield = (strcmpic(coded, sub[1]+5) == 0) == testfor;
+        tempcond = (strcmpic(coded, sub[1]+5) == 0);
         }
       else
         {
         DEBUG(D_auth) debug_printf("crypteq: length for MD5 not 24 or 32: "
           "fail\n  crypted=%s\n", sub[1]+5);
         }
       else
         {
         DEBUG(D_auth) debug_printf("crypteq: length for MD5 not 24 or 32: "
           "fail\n  crypted=%s\n", sub[1]+5);
-        *yield = !testfor;
+        tempcond = FALSE;
         }
       }
 
         }
       }
 
@@ -2090,7 +2536,7 @@ switch(cond_type)
         uschar *coded = auth_b64encode((uschar *)digest, 20);
         DEBUG(D_auth) debug_printf("crypteq: using SHA1+B64 hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
         uschar *coded = auth_b64encode((uschar *)digest, 20);
         DEBUG(D_auth) debug_printf("crypteq: using SHA1+B64 hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
-        *yield = (Ustrcmp(coded, sub[1]+6) == 0) == testfor;
+        tempcond = (Ustrcmp(coded, sub[1]+6) == 0);
         }
       else if (sublen == 40)
         {
         }
       else if (sublen == 40)
         {
@@ -2100,17 +2546,18 @@ switch(cond_type)
         coded[40] = 0;
         DEBUG(D_auth) debug_printf("crypteq: using SHA1+hex hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
         coded[40] = 0;
         DEBUG(D_auth) debug_printf("crypteq: using SHA1+hex hashing\n"
           "  subject=%s\n  crypted=%s\n", coded, sub[1]+6);
-        *yield = (strcmpic(coded, sub[1]+6) == 0) == testfor;
+        tempcond = (strcmpic(coded, sub[1]+6) == 0);
         }
       else
         {
         DEBUG(D_auth) debug_printf("crypteq: length for SHA-1 not 28 or 40: "
           "fail\n  crypted=%s\n", sub[1]+6);
         }
       else
         {
         DEBUG(D_auth) debug_printf("crypteq: length for SHA-1 not 28 or 40: "
           "fail\n  crypted=%s\n", sub[1]+6);
-        *yield = !testfor;
+       tempcond = FALSE;
         }
       }
 
     else   /* {crypt} or {crypt16} and non-{ at start */
         }
       }
 
     else   /* {crypt} or {crypt16} and non-{ at start */
+           /* }-for-text-editors */
       {
       int which = 0;
       uschar *coded;
       {
       int which = 0;
       uschar *coded;
@@ -2125,7 +2572,7 @@ switch(cond_type)
         sub[1] += 9;
         which = 2;
         }
         sub[1] += 9;
         which = 2;
         }
-      else if (sub[1][0] == '{')
+      else if (sub[1][0] == '{')               /* }-for-text-editors */
         {
         expand_string_message = string_sprintf("unknown encryption mechanism "
           "in \"%s\"", sub[1]);
         {
         expand_string_message = string_sprintf("unknown encryption mechanism "
           "in \"%s\"", sub[1]);
@@ -2152,13 +2599,37 @@ switch(cond_type)
       salt), force failure. Otherwise we get false positives: with an empty
       string the yield of crypt() is an empty string! */
 
       salt), force failure. Otherwise we get false positives: with an empty
       string the yield of crypt() is an empty string! */
 
-      *yield = (Ustrlen(sub[1]) < 2)? !testfor :
-        (Ustrcmp(coded, sub[1]) == 0) == testfor;
+      tempcond = (Ustrlen(sub[1]) < 2)? FALSE :
+        (Ustrcmp(coded, sub[1]) == 0);
       }
     break;
     #endif  /* SUPPORT_CRYPTEQ */
       }
     break;
     #endif  /* SUPPORT_CRYPTEQ */
+
+    case ECOND_INLIST:
+    case ECOND_INLISTI:
+      {
+      int sep = 0;
+      uschar *save_iterate_item = iterate_item;
+      int (*compare)(const uschar *, const uschar *);
+
+      tempcond = FALSE;
+      if (cond_type == ECOND_INLISTI)
+        compare = strcmpic;
+      else
+        compare = (int (*)(const uschar *, const uschar *)) strcmp;
+
+      while ((iterate_item = string_nextinlist(&sub[1], &sep, NULL, 0)) != NULL)
+        if (compare(sub[0], iterate_item) == 0)
+          {
+          tempcond = TRUE;
+          break;
+          }
+      iterate_item = save_iterate_item;
+      }
+
     }   /* Switch for comparison conditions */
 
     }   /* Switch for comparison conditions */
 
+  *yield = tempcond == testfor;
   return s;    /* End of comparison conditions */
 
 
   return s;    /* End of comparison conditions */
 
 
@@ -2170,13 +2641,14 @@ switch(cond_type)
   combined_cond = (cond_type == ECOND_AND);
 
   while (isspace(*s)) s++;
   combined_cond = (cond_type == ECOND_AND);
 
   while (isspace(*s)) s++;
-  if (*s++ != '{') goto COND_FAILED_CURLY_START;
+  if (*s++ != '{') goto COND_FAILED_CURLY_START;       /* }-for-text-editors */
 
   for (;;)
     {
     while (isspace(*s)) s++;
 
   for (;;)
     {
     while (isspace(*s)) s++;
+    /* {-for-text-editors */
     if (*s == '}') break;
     if (*s == '}') break;
-    if (*s != '{')
+    if (*s != '{')                                     /* }-for-text-editors */
       {
       expand_string_message = string_sprintf("each subcondition "
         "inside an \"%s{...}\" condition must be in its own {}", name);
       {
       expand_string_message = string_sprintf("each subcondition "
         "inside an \"%s{...}\" condition must be in its own {}", name);
@@ -2192,8 +2664,10 @@ switch(cond_type)
       }
     while (isspace(*s)) s++;
 
       }
     while (isspace(*s)) s++;
 
+    /* {-for-text-editors */
     if (*s++ != '}')
       {
     if (*s++ != '}')
       {
+      /* {-for-text-editors */
       expand_string_message = string_sprintf("missing } at end of condition "
         "inside \"%s\" group", name);
       return NULL;
       expand_string_message = string_sprintf("missing } at end of condition "
         "inside \"%s\" group", name);
       return NULL;
@@ -2218,41 +2692,180 @@ switch(cond_type)
   return ++s;
 
 
   return ++s;
 
 
-  /* Unknown condition */
+  /* forall/forany: iterates a condition with different values */
 
 
-  default:
-  expand_string_message = string_sprintf("unknown condition \"%s\"", name);
-  return NULL;
-  }   /* End switch on condition type */
+  case ECOND_FORALL:
+  case ECOND_FORANY:
+    {
+    int sep = 0;
+    uschar *save_iterate_item = iterate_item;
 
 
-/* Missing braces at start and end of data */
+    while (isspace(*s)) s++;
+    if (*s++ != '{') goto COND_FAILED_CURLY_START;     /* }-for-text-editors */
+    sub[0] = expand_string_internal(s, TRUE, &s, (yield == NULL), TRUE);
+    if (sub[0] == NULL) return NULL;
+    /* {-for-text-editors */
+    if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
 
-COND_FAILED_CURLY_START:
-expand_string_message = string_sprintf("missing { after \"%s\"", name);
-return NULL;
+    while (isspace(*s)) s++;
+    if (*s++ != '{') goto COND_FAILED_CURLY_START;     /* }-for-text-editors */
 
 
-COND_FAILED_CURLY_END:
-expand_string_message = string_sprintf("missing } at end of \"%s\" condition",
-  name);
-return NULL;
+    sub[1] = s;
 
 
-/* A condition requires code that is not compiled */
+    /* 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. */
 
 
-#if !defined(SUPPORT_PAM) || !defined(RADIUS_CONFIG_FILE) || \
-    !defined(LOOKUP_LDAP) || !defined(CYRUS_PWCHECK_SOCKET) || \
-    !defined(SUPPORT_CRYPTEQ) || !defined(CYRUS_SASLAUTHD_SOCKET)
-COND_FAILED_NOT_COMPILED:
-expand_string_message = string_sprintf("support for \"%s\" not compiled",
-  name);
-return NULL;
-#endif
-}
+    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++;
 
 
+    /* {-for-text-editors */
+    if (*s++ != '}')
+      {
+      /* {-for-text-editors */
+      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;
+      }
 
 
-/*************************************************
-*          Save numerical variables              *
+    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;       /* }-for-text-editors */
+    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:
+  expand_string_message = string_sprintf("unknown condition \"%s\"", name);
+  return NULL;
+  }   /* End switch on condition type */
+
+/* Missing braces at start and end of data */
+
+COND_FAILED_CURLY_START:
+expand_string_message = string_sprintf("missing { after \"%s\"", name);
+return NULL;
+
+COND_FAILED_CURLY_END:
+expand_string_message = string_sprintf("missing } at end of \"%s\" condition",
+  name);
+return NULL;
+
+/* A condition requires code that is not compiled */
+
+#if !defined(SUPPORT_PAM) || !defined(RADIUS_CONFIG_FILE) || \
+    !defined(LOOKUP_LDAP) || !defined(CYRUS_PWCHECK_SOCKET) || \
+    !defined(SUPPORT_CRYPTEQ) || !defined(CYRUS_SASLAUTHD_SOCKET)
+COND_FAILED_NOT_COMPILED:
+expand_string_message = string_sprintf("support for \"%s\" not compiled",
+  name);
+return NULL;
+#endif
+}
+
+
+
+
+/*************************************************
+*          Save numerical variables              *
 *************************************************/
 
 /* This function is called from items such as "if" that want to preserve and
 *************************************************/
 
 /* This function is called from items such as "if" that want to preserve and
@@ -2374,7 +2987,7 @@ if (*s++ != '{') goto FAILED_CURLY;
 want this string. Set skipping in the call in the fail case (this will always
 be the case if we were already skipping). */
 
 want this string. Set skipping in the call in the fail case (this will always
 be the case if we were already skipping). */
 
-sub1 = expand_string_internal(s, TRUE, &s, !yes);
+sub1 = expand_string_internal(s, TRUE, &s, !yes, TRUE);
 if (sub1 == NULL && (yes || !expand_string_forcedfail)) goto FAILED;
 expand_string_forcedfail = FALSE;
 if (*s++ != '}') goto FAILED_CURLY;
 if (sub1 == NULL && (yes || !expand_string_forcedfail)) goto FAILED;
 expand_string_forcedfail = FALSE;
 if (*s++ != '}') goto FAILED_CURLY;
@@ -2399,7 +3012,7 @@ already skipping. */
 while (isspace(*s)) s++;
 if (*s == '{')
   {
 while (isspace(*s)) s++;
 if (*s == '{')
   {
-  sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping);
+  sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping, TRUE);
   if (sub2 == NULL && (!yes || !expand_string_forcedfail)) goto FAILED;
   expand_string_forcedfail = FALSE;
   if (*s++ != '}') goto FAILED_CURLY;
   if (sub2 == NULL && (!yes || !expand_string_forcedfail)) goto FAILED;
   expand_string_forcedfail = FALSE;
   if (*s++ != '}') goto FAILED_CURLY;
@@ -2529,8 +3142,8 @@ Returns:  pointer to string containing the last three
 static uschar *
 prvs_daystamp(int day_offset)
 {
 static uschar *
 prvs_daystamp(int day_offset)
 {
-uschar *days = store_get(16);
-(void)string_format(days, 16, TIME_T_FMT,
+uschar *days = store_get(32);                /* Need at least 24 for cases */
+(void)string_format(days, 32, TIME_T_FMT,    /* where TIME_T_FMT is %lld */
   (time(NULL) + day_offset*86400)/86400);
 return (Ustrlen(days) >= 3) ? &days[Ustrlen(days)-3] : US"100";
 }
   (time(NULL) + day_offset*86400)/86400);
 return (Ustrlen(days) >= 3) ? &days[Ustrlen(days)-3] : US"100";
 }
@@ -2660,69 +3273,64 @@ return yield;
 *          Evaluate numeric expression           *
 *************************************************/
 
 *          Evaluate numeric expression           *
 *************************************************/
 
-/* 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:
+/* 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:
 
 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
-
+  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
+Returns:      on success: the value of the expression, with *error still NULL
+              on failure: an undefined value, with *error = a message
 */
 
 */
 
-static int eval_sumterm(uschar **, BOOL, uschar **);
+static int_eximarith_t eval_op_or(uschar **, BOOL, uschar **);
 
 
-static int
+
+static int_eximarith_t
 eval_expr(uschar **sptr, BOOL decimal, uschar **error, BOOL endket)
 {
 uschar *s = *sptr;
 eval_expr(uschar **sptr, BOOL decimal, uschar **error, BOOL endket)
 {
 uschar *s = *sptr;
-int x = eval_sumterm(&s, decimal, error);
+int_eximarith_t x = eval_op_or(&s, decimal, error);
 if (*error == NULL)
   {
 if (*error == NULL)
   {
-  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)
+  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;
 }
 
 *sptr = s;
 return x;
 }
 
-static int
-eval_term(uschar **sptr, BOOL decimal, uschar **error)
+
+static int_eximarith_t
+eval_number(uschar **sptr, BOOL decimal, uschar **error)
 {
 register int c;
 {
 register int c;
-int n;
+int_eximarith_t n;
 uschar *s = *sptr;
 while (isspace(*s)) s++;
 c = *s;
 uschar *s = *sptr;
 while (isspace(*s)) s++;
 c = *s;
-if (isdigit(c) || ((c == '-' || c == '+') && isdigit(s[1])))
+if (isdigit(c))
   {
   int count;
   {
   int count;
-  (void)sscanf(CS s, (decimal? "%d%n" : "%i%n"), &n, &count);
+  (void)sscanf(CS s, (decimal? SC_EXIM_DEC "%n" : SC_EXIM_ARITH "%n"), &n, &count);
   s += count;
   s += count;
-  if (tolower(*s) == 'k') { n *= 1024; s++; }
-    else if (tolower(*s) == 'm') { n *= 1024*1024; s++; }
+  switch (tolower(*s))
+    {
+    default: break;
+    case 'k': n *= 1024; s++; break;
+    case 'm': n *= 1024*1024; s++; break;
+    case 'g': n *= 1024*1024*1024; s++; break;
+    }
   while (isspace (*s)) s++;
   }
 else if (c == '(')
   while (isspace (*s)) s++;
   }
 else if (c == '(')
@@ -2739,18 +3347,82 @@ else
 return n;
 }
 
 return n;
 }
 
-static int eval_sumterm(uschar **sptr, BOOL decimal, uschar **error)
+
+static int_eximarith_t
+eval_op_unary(uschar **sptr, BOOL decimal, uschar **error)
 {
 uschar *s = *sptr;
 {
 uschar *s = *sptr;
-int x = eval_term(&s, decimal, error);
+int_eximarith_t 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_eximarith_t
+eval_op_mult(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int_eximarith_t x = eval_op_unary(&s, decimal, error);
 if (*error == NULL)
   {
 if (*error == NULL)
   {
-  while (*s == '*' || *s == '/')
+  while (*s == '*' || *s == '/' || *s == '%')
     {
     int op = *s++;
     {
     int op = *s++;
-    int y = eval_term(&s, decimal, error);
+    int_eximarith_t y = eval_op_unary(&s, decimal, error);
     if (*error != NULL) break;
     if (*error != NULL) break;
-    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 == EXIM_ARITH_MIN && op != '*')
+      {
+      DEBUG(D_expand)
+        debug_printf("Integer exception dodging: " PR_EXIM_ARITH "%c-1 coerced to " PR_EXIM_ARITH "\n",
+            EXIM_ARITH_MIN, op, EXIM_ARITH_MAX);
+      x = EXIM_ARITH_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;
     }
   }
 *sptr = s;
@@ -2758,6 +3430,110 @@ return x;
 }
 
 
 }
 
 
+static int_eximarith_t
+eval_op_sum(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int_eximarith_t x = eval_op_mult(&s, decimal, error);
+if (*error == NULL)
+  {
+  while (*s == '+' || *s == '-')
+    {
+    int op = *s++;
+    int_eximarith_t y = eval_op_mult(&s, decimal, error);
+    if (*error != NULL) break;
+    if (op == '+') x += y; else x -= y;
+    }
+  }
+*sptr = s;
+return x;
+}
+
+
+static int_eximarith_t
+eval_op_shift(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int_eximarith_t x = eval_op_sum(&s, decimal, error);
+if (*error == NULL)
+  {
+  while ((*s == '<' || *s == '>') && s[1] == s[0])
+    {
+    int_eximarith_t 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_eximarith_t
+eval_op_and(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int_eximarith_t x = eval_op_shift(&s, decimal, error);
+if (*error == NULL)
+  {
+  while (*s == '&')
+    {
+    int_eximarith_t y;
+    s++;
+    y = eval_op_shift(&s, decimal, error);
+    if (*error != NULL) break;
+    x &= y;
+    }
+  }
+*sptr = s;
+return x;
+}
+
+
+static int_eximarith_t
+eval_op_xor(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int_eximarith_t x = eval_op_and(&s, decimal, error);
+if (*error == NULL)
+  {
+  while (*s == '^')
+    {
+    int_eximarith_t y;
+    s++;
+    y = eval_op_and(&s, decimal, error);
+    if (*error != NULL) break;
+    x ^= y;
+    }
+  }
+*sptr = s;
+return x;
+}
+
+
+static int_eximarith_t
+eval_op_or(uschar **sptr, BOOL decimal, uschar **error)
+{
+uschar *s = *sptr;
+int_eximarith_t x = eval_op_xor(&s, decimal, error);
+if (*error == NULL)
+  {
+  while (*s == '|')
+    {
+    int_eximarith_t y;
+    s++;
+    y = eval_op_xor(&s, decimal, error);
+    if (*error != NULL) break;
+    x |= y;
+    }
+  }
+*sptr = s;
+return x;
+}
+
 
 
 /*************************************************
 
 
 /*************************************************
@@ -2798,6 +3574,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.
 
 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. The same applies for ${acl. 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 }
 Arguments:
   string         the string to be expanded
   ket_ends       true if expansion is to stop at }
@@ -2805,6 +3587,8 @@ Arguments:
                  expansion is placed here (typically used with ket_ends)
   skipping       TRUE for recursive calls when the value isn't actually going
                  to be used (to allow for optimisation)
                  expansion is placed here (typically used with ket_ends)
   skipping       TRUE for recursive calls when the value isn't actually going
                  to be used (to allow for optimisation)
+  honour_dollar  TRUE if $ is to be expanded,
+                 FALSE if it's just another character
 
 Returns:         NULL if expansion fails:
                    expand_string_forcedfail is set TRUE if failure was forced
 
 Returns:         NULL if expansion fails:
                    expand_string_forcedfail is set TRUE if failure was forced
@@ -2814,7 +3598,7 @@ Returns:         NULL if expansion fails:
 
 static uschar *
 expand_string_internal(uschar *string, BOOL ket_ends, uschar **left,
 
 static uschar *
 expand_string_internal(uschar *string, BOOL ket_ends, uschar **left,
-  BOOL skipping)
+  BOOL skipping, BOOL honour_dollar)
 {
 int ptr = 0;
 int size = Ustrlen(string)+ 64;
 {
 int ptr = 0;
 int size = Ustrlen(string)+ 64;
@@ -2823,6 +3607,7 @@ uschar *yield = store_get(size);
 uschar *s = string;
 uschar *save_expand_nstring[EXPAND_MAXN+1];
 int save_expand_nlength[EXPAND_MAXN+1];
 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"";
 
 expand_string_forcedfail = FALSE;
 expand_string_message = US"";
@@ -2869,7 +3654,7 @@ while (*s != 0)
 
   if (ket_ends && *s == '}') break;
 
 
   if (ket_ends && *s == '}') break;
 
-  if (*s != '$')
+  if (*s != '$' || !honour_dollar)
     {
     yield = string_cat(yield, &size, &ptr, s++, 1);
     continue;
     {
     yield = string_cat(yield, &size, &ptr, s++, 1);
     continue;
@@ -2895,7 +3680,7 @@ while (*s != 0)
 
     if (ptr == 0 && yield != NULL)
       {
 
     if (ptr == 0 && yield != NULL)
       {
-      store_reset(yield);
+      if (resetok) store_reset(yield);
       yield = NULL;
       size = 0;
       }
       yield = NULL;
       size = 0;
       }
@@ -2915,7 +3700,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
       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. */
 
       has been omitted. Set a flag to adjust the error message in this case.
       But there is no error here - nothing gets inserted. */
 
@@ -2935,6 +3720,7 @@ while (*s != 0)
         {
         expand_string_message =
           string_sprintf("unknown variable name \"%s\"", name);
         {
         expand_string_message =
           string_sprintf("unknown variable name \"%s\"", name);
+          check_variable_error_message(name);
         goto EXPAND_FAILED;
         }
       }
         goto EXPAND_FAILED;
         }
       }
@@ -3008,6 +3794,46 @@ while (*s != 0)
 
   switch(item_type)
     {
 
   switch(item_type)
     {
+    /* Call an ACL from an expansion.  We feed data in via $acl_arg1 - $acl_arg9.
+    If the ACL returns accept or reject we return content set by "message ="
+    There is currently no limit on recursion; this would have us call
+    acl_check_internal() directly and get a current level from somewhere.
+    See also the acl expansion condition ECOND_ACL and the traditional
+    acl modifier ACLC_ACL.
+    Assume that the function has side-effects on the store that must be preserved.
+    */
+
+    case EITEM_ACL:
+      /* ${acl {name} {arg1}{arg2}...} */
+      {
+      uschar *sub[10]; /* name + arg1-arg9 (which must match number of acl_arg[]) */
+      uschar *user_msg;
+
+      switch(read_subs(sub, 10, 1, &s, skipping, TRUE, US"acl"))
+        {
+        case 1: goto EXPAND_FAILED_CURLY;
+        case 2:
+        case 3: goto EXPAND_FAILED;
+        }
+      if (skipping) continue;
+
+      resetok = FALSE;
+      switch(eval_acl(sub, sizeof(sub)/sizeof(*sub), &user_msg))
+       {
+       case OK:
+       case FAIL:
+         if (user_msg)
+            yield = string_cat(yield, &size, &ptr, user_msg, Ustrlen(user_msg));
+         continue;
+
+       case DEFER:
+          expand_string_forcedfail = TRUE;
+       default:
+          expand_string_message = string_sprintf("error from acl \"%s\"", sub[0]);
+         goto EXPAND_FAILED;
+       }
+      }
+
     /* Handle conditionals - preserve the values of the numerical expansion
     variables in case they get changed by a regular expression match in the
     condition. If not, they retain their external settings. At the end
     /* Handle conditionals - preserve the values of the numerical expansion
     variables in case they get changed by a regular expression match in the
     condition. If not, they retain their external settings. At the end
@@ -3084,7 +3910,7 @@ while (*s != 0)
       while (isspace(*s)) s++;
       if (*s == '{')
         {
       while (isspace(*s)) s++;
       if (*s == '{')
         {
-        key = expand_string_internal(s+1, TRUE, &s, skipping);
+        key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE);
         if (key == NULL) goto EXPAND_FAILED;
         if (*s++ != '}') goto EXPAND_FAILED_CURLY;
         while (isspace(*s)) s++;
         if (key == NULL) goto EXPAND_FAILED;
         if (*s++ != '}') goto EXPAND_FAILED_CURLY;
         while (isspace(*s)) s++;
@@ -3125,7 +3951,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. */
 
       /* 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)
           {
         {
         if (key == NULL)
           {
@@ -3145,21 +3971,41 @@ while (*s != 0)
         }
 
       /* Get the next string in brackets and expand it. It is the file name for
         }
 
       /* 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;
 
       if (*s != '{') goto EXPAND_FAILED_CURLY;
-      filename = expand_string_internal(s+1, TRUE, &s, skipping);
+      filename = expand_string_internal(s+1, TRUE, &s, skipping, TRUE);
       if (filename == NULL) goto EXPAND_FAILED;
       if (*s++ != '}') goto EXPAND_FAILED_CURLY;
       while (isspace(*s)) s++;
 
       /* If this isn't a single-key+file lookup, re-arrange the variables
       if (filename == NULL) goto EXPAND_FAILED;
       if (*s++ != '}') goto EXPAND_FAILED_CURLY;
       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)
         {
 
       if (key == NULL)
         {
+        while (isspace(*filename)) filename++;
         key = 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
         }
 
       /* If skipping, don't do the next bit - just lookup_value == NULL, as if
@@ -3190,8 +4036,8 @@ while (*s != 0)
         if (search_find_defer)
           {
           expand_string_message =
         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;
           goto EXPAND_FAILED;
           }
         if (expand_setup > 0) expand_nmax = expand_setup;
@@ -3333,15 +4179,24 @@ while (*s != 0)
       domain = Ustrrchr(sub_arg[0],'@');
       if ( (domain == NULL) || (domain == sub_arg[0]) || (Ustrlen(domain) == 1) )
         {
       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;
         }
 
         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)
         {
       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;
         }
 
         goto EXPAND_FAILED;
         }
 
@@ -3349,11 +4204,11 @@ while (*s != 0)
       *domain++ = '\0';
 
       yield = string_cat(yield,&size,&ptr,US"prvs=",5);
       *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,(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));
 
       string_cat(yield,&size,&ptr,US"@",1);
       string_cat(yield,&size,&ptr,domain,Ustrlen(domain));
 
@@ -3368,32 +4223,38 @@ while (*s != 0)
       int mysize = 0, myptr = 0;
       const pcre *re;
       uschar *p;
       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
          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;
 
 
       /* 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;
         }
 
         {
         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);
 
                               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);
         uschar *domain = string_copyn(expand_nstring[5],expand_nlength[5]);
 
         DEBUG(D_expand) debug_printf("prvscheck localpart: %s\n", local_part);
@@ -3409,21 +4270,19 @@ while (*s != 0)
         prvscheck_address[myptr] = '\0';
         prvscheck_keynum = string_copy(key_num);
 
         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;
           }
 
           {
           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. */
         /* 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";
         if (p == NULL)
           {
           expand_string_message = US"hmac-sha1 conversion failed";
@@ -3432,20 +4291,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);
 
         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;
 
         if (Ustrcmp(p,hash) == 0)
           {
           /* Success, valid BATV address. Now check the expiry date. */
           uschar *now = prvs_daystamp(0);
           unsigned int inow = 0,iexpire = 1;
 
-          sscanf(CS now,"%u",&inow);
-          sscanf(CS daystamp,"%u",&iexpire);
+          (void)sscanf(CS now,"%u",&inow);
+          (void)sscanf(CS daystamp,"%u",&iexpire);
 
           /* When "iexpire" is < 7, a "flip" has occured.
              Adjust "inow" accordingly. */
           if ( (iexpire < 7) && (inow >= 993) ) inow = 0;
 
 
           /* 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");
             {
             prvscheck_result = US"1";
             DEBUG(D_expand) debug_printf("prvscheck: success, $pvrs_result set to 1\n");
@@ -3461,12 +4321,35 @@ while (*s != 0)
           prvscheck_result = NULL;
           DEBUG(D_expand) debug_printf("prvscheck: hash failure, $pvrs_result unset\n");
           }
           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.
       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:
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
@@ -3511,7 +4394,7 @@ while (*s != 0)
         }
 
       yield = cat_file(f, yield, &size, &ptr, sub_arg[1]);
         }
 
       yield = cat_file(f, yield, &size, &ptr, sub_arg[1]);
-      fclose(f);
+      (void)fclose(f);
       continue;
       }
 
       continue;
       }
 
@@ -3557,28 +4440,160 @@ while (*s != 0)
         }
       else sub_arg[3] = NULL;                     /* No eol if no timeout */
 
         }
       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)
         {
 
       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)
+        /* Handle a Unix domain socket */
+
+        else
           {
           {
-          expand_string_message = string_sprintf("failed to connect to socket "
-            "%s: %s", sub_arg[0], strerror(errno));
-          goto SOCK_FAIL;
+          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 */
         DEBUG(D_expand) debug_printf("connected to socket %s\n", sub_arg[0]);
 
         /* Write the request string, if not empty */
@@ -3596,6 +4611,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. */
 
         /* Now we need to read from the socket, under a timeout. The function
         that reads a file can be used. */
 
@@ -3604,7 +4627,7 @@ while (*s != 0)
         alarm(timeout);
         yield = cat_file(f, yield, &size, &ptr, sub_arg[3]);
         alarm(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. */
 
         /* After a timeout, we restore the pointer in the result, that is,
         make sure we add nothing from the socket. */
@@ -3612,7 +4635,7 @@ while (*s != 0)
         if (sigalrm_seen)
           {
           ptr = save_ptr;
         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;
           }
         }
           goto SOCK_FAIL;
           }
         }
@@ -3622,7 +4645,7 @@ while (*s != 0)
 
       if (*s == '{')
         {
 
       if (*s == '{')
         {
-        if (expand_string_internal(s+1, TRUE, &s, TRUE) == NULL)
+        if (expand_string_internal(s+1, TRUE, &s, TRUE, TRUE) == NULL)
           goto EXPAND_FAILED;
         if (*s++ != '}') goto EXPAND_FAILED_CURLY;
         while (isspace(*s)) s++;
           goto EXPAND_FAILED;
         if (*s++ != '}') goto EXPAND_FAILED_CURLY;
         while (isspace(*s)) s++;
@@ -3637,7 +4660,7 @@ while (*s != 0)
       SOCK_FAIL:
       if (*s != '{') goto EXPAND_FAILED;
       DEBUG(D_any) debug_printf("%s\n", expand_string_message);
       SOCK_FAIL:
       if (*s != '{') goto EXPAND_FAILED;
       DEBUG(D_any) debug_printf("%s\n", expand_string_message);
-      arg = expand_string_internal(s+1, TRUE, &s, FALSE);
+      arg = expand_string_internal(s+1, TRUE, &s, FALSE, TRUE);
       if (arg == NULL) goto EXPAND_FAILED;
       yield = string_cat(yield, &size, &ptr, arg, Ustrlen(arg));
       if (*s++ != '}') goto EXPAND_FAILED_CURLY;
       if (arg == NULL) goto EXPAND_FAILED;
       yield = string_cat(yield, &size, &ptr, arg, Ustrlen(arg));
       if (*s++ != '}') goto EXPAND_FAILED_CURLY;
@@ -3666,7 +4689,7 @@ while (*s != 0)
 
       while (isspace(*s)) s++;
       if (*s != '{') goto EXPAND_FAILED_CURLY;
 
       while (isspace(*s)) s++;
       if (*s != '{') goto EXPAND_FAILED_CURLY;
-      arg = expand_string_internal(s+1, TRUE, &s, skipping);
+      arg = expand_string_internal(s+1, TRUE, &s, skipping, TRUE);
       if (arg == NULL) goto EXPAND_FAILED;
       while (isspace(*s)) s++;
       if (*s++ != '}') goto EXPAND_FAILED_CURLY;
       if (arg == NULL) goto EXPAND_FAILED;
       while (isspace(*s)) s++;
       if (*s++ != '}') goto EXPAND_FAILED_CURLY;
@@ -3701,15 +4724,26 @@ while (*s != 0)
 
         /* Nothing is written to the standard input. */
 
 
         /* Nothing is written to the standard input. */
 
-        close(fd_in);
+        (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.
         */
 
 
         /* 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 */
             {
             expand_string_message = string_sprintf("command timed out");
             killpg(pid, SIGKILL);       /* Kill the whole process group */
@@ -3725,14 +4759,6 @@ while (*s != 0)
 
           goto EXPAND_FAILED;
           }
 
           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);
-        fclose(f);
         }
 
       /* Process the yes/no strings; $value may be useful in both cases */
         }
 
       /* Process the yes/no strings; $value may be useful in both cases */
@@ -4094,7 +5120,7 @@ while (*s != 0)
         while (isspace(*s)) s++;
         if (*s == '{')
           {
         while (isspace(*s)) s++;
         if (*s == '{')
           {
-          sub[i] = expand_string_internal(s+1, TRUE, &s, skipping);
+          sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE);
           if (sub[i] == NULL) goto EXPAND_FAILED;
           if (*s++ != '}') goto EXPAND_FAILED_CURLY;
 
           if (sub[i] == NULL) goto EXPAND_FAILED;
           if (*s++ != '}') goto EXPAND_FAILED_CURLY;
 
@@ -4116,7 +5142,7 @@ while (*s != 0)
             while (len > 0 && isspace(p[len-1])) len--;
             p[len] = 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";
               {
               expand_string_message = US"first argument of \"extract\" must "
                 "not be empty";
@@ -4173,6 +5199,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, TRUE);
+      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, TRUE);
+        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, 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, TRUE);
+          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
     /* 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
@@ -4249,8 +5453,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
       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]);
       result = NULL;
       for (argc = 0; argv[argc] != NULL; argc++);
       status = func(&result, argc - 2, &argv[2]);
@@ -4282,7 +5488,7 @@ while (*s != 0)
     {
     int c;
     uschar *arg = NULL;
     {
     int c;
     uschar *arg = NULL;
-    uschar *sub = expand_string_internal(s+1, TRUE, &s, skipping);
+    uschar *sub = expand_string_internal(s+1, TRUE, &s, skipping, TRUE);
     if (sub == NULL) goto EXPAND_FAILED;
     s++;
 
     if (sub == NULL) goto EXPAND_FAILED;
     s++;
 
@@ -4331,6 +5537,8 @@ while (*s != 0)
         continue;
         }
 
         continue;
         }
 
+      /* Note that for Darwin and Cygwin, BASE_62 actually has the value 36 */
+
       case EOP_BASE62D:
         {
         uschar buf[16];
       case EOP_BASE62D:
         {
         uschar buf[16];
@@ -4342,10 +5550,11 @@ while (*s != 0)
           if (t == NULL)
             {
             expand_string_message = string_sprintf("argument for base62d "
           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;
             }
             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));
           }
         (void)sprintf(CS buf, "%ld", n);
         yield = string_cat(yield, &size, &ptr, buf, Ustrlen(buf));
@@ -4354,7 +5563,7 @@ while (*s != 0)
 
       case EOP_EXPAND:
         {
 
       case EOP_EXPAND:
         {
-        uschar *expanded = expand_string_internal(sub, FALSE, NULL, skipping);
+        uschar *expanded = expand_string_internal(sub, FALSE, NULL, skipping, TRUE);
         if (expanded == NULL)
           {
           expand_string_message =
         if (expanded == NULL)
           {
           expand_string_message =
@@ -4458,6 +5667,122 @@ while (*s != 0)
         continue;
         }
 
         continue;
         }
 
+      /* Convert octets outside 0x21..0x7E to \xXX form */
+
+      case EOP_HEXQUOTE:
+       {
+        uschar *t = sub - 1;
+        while (*(++t) != 0)
+          {
+          if (*t < 0x21 || 0x7E < *t)
+            yield = string_cat(yield, &size, &ptr,
+             string_sprintf("\\x%02x", *t), 4);
+         else
+           yield = string_cat(yield, &size, &ptr, t, 1);
+          }
+       continue;
+       }
+
+      /* count the number of list elements */
+
+      case EOP_LISTCOUNT:
+        {
+       int cnt = 0;
+       int sep = 0;
+       uschar * cp;
+       uschar buffer[256];
+
+       while (string_nextinlist(&sub, &sep, buffer, sizeof(buffer)) != NULL) cnt++;
+       cp = string_sprintf("%d", cnt);
+        yield = string_cat(yield, &size, &ptr, cp, Ustrlen(cp));
+        continue;
+        }
+
+      /* expand a named list given the name */
+      /* handles nested named lists; requotes as colon-sep list */
+
+      case EOP_LISTNAMED:
+       {
+       tree_node *t = NULL;
+       uschar * list;
+       int sep = 0;
+       uschar * item;
+       uschar * suffix = US"";
+       BOOL needsep = FALSE;
+       uschar buffer[256];
+
+       if (*sub == '+') sub++;
+       if (arg == NULL)        /* no-argument version */
+         {
+         if (!(t = tree_search(addresslist_anchor, sub)) &&
+             !(t = tree_search(domainlist_anchor,  sub)) &&
+             !(t = tree_search(hostlist_anchor,    sub)))
+           t = tree_search(localpartlist_anchor, sub);
+         }
+       else switch(*arg)       /* specific list-type version */
+         {
+         case 'a': t = tree_search(addresslist_anchor,   sub); suffix = US"_a"; break;
+         case 'd': t = tree_search(domainlist_anchor,    sub); suffix = US"_d"; break;
+         case 'h': t = tree_search(hostlist_anchor,      sub); suffix = US"_h"; break;
+         case 'l': t = tree_search(localpartlist_anchor, sub); suffix = US"_l"; break;
+         default:
+            expand_string_message = string_sprintf("bad suffix on \"list\" operator");
+           goto EXPAND_FAILED;
+         }
+
+       if(!t)
+         {
+          expand_string_message = string_sprintf("\"%s\" is not a %snamed list",
+            sub, !arg?""
+             : *arg=='a'?"address "
+             : *arg=='d'?"domain "
+             : *arg=='h'?"host "
+             : *arg=='l'?"localpart "
+             : 0);
+         goto EXPAND_FAILED;
+         }
+
+       list = ((namedlist_block *)(t->data.ptr))->string;
+
+       while ((item = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL)
+         {
+         uschar * buf = US" : ";
+         if (needsep)
+           yield = string_cat(yield, &size, &ptr, buf, 3);
+         else
+           needsep = TRUE;
+
+         if (*item == '+')     /* list item is itself a named list */
+           {
+           uschar * sub = string_sprintf("${listnamed%s:%s}", suffix, item);
+           item = expand_string_internal(sub, FALSE, NULL, FALSE, TRUE);
+           }
+         else if (sep != ':')  /* item from non-colon-sep list, re-quote for colon list-separator */
+           {
+           char * cp;
+           char tok[3];
+           tok[0] = sep; tok[1] = ':'; tok[2] = 0;
+           while ((cp= strpbrk((const char *)item, tok)))
+             {
+              yield = string_cat(yield, &size, &ptr, item, cp-(char *)item);
+             if (*cp++ == ':') /* colon in a non-colon-sep list item, needs doubling */
+               {
+                yield = string_cat(yield, &size, &ptr, US"::", 2);
+               item = (uschar *)cp;
+               }
+             else              /* sep in item; should already be doubled; emit once */
+               {
+                yield = string_cat(yield, &size, &ptr, (uschar *)tok, 1);
+               if (*cp == sep) cp++;
+               item = (uschar *)cp;
+               }
+             }
+           }
+          yield = string_cat(yield, &size, &ptr, item, Ustrlen(item));
+         }
+        continue;
+       }
+
       /* mask applies a mask to an IP address; for example the result of
       ${mask:131.111.10.206/28} is 131.111.10.192/28. */
 
       /* mask applies a mask to an IP address; for example the result of
       ${mask:131.111.10.206/28} is 131.111.10.192/28. */
 
@@ -4530,6 +5855,69 @@ while (*s != 0)
         continue;
         }
 
         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.
 
       /* quote puts a string in quotes if it is empty or contains anything
       other than alphamerics, underscore, dot, or hyphen.
 
@@ -4598,8 +5986,8 @@ while (*s != 0)
           goto EXPAND_FAILED;
           }
 
           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)
         else if (opt != NULL) sub = NULL;
 
         if (sub == NULL)
@@ -4636,11 +6024,28 @@ while (*s != 0)
         {
         uschar buffer[2048];
         uschar *string = parse_quote_2047(sub, Ustrlen(sub), headers_charset,
         {
         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;
         }
 
         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 */
 
       /* from_utf8 converts UTF-8 to 8859-1, turning non-existent chars into
       underscores */
 
@@ -4674,7 +6079,7 @@ while (*s != 0)
         {
         uschar *save_sub = sub;
         uschar *error = NULL;
         {
         uschar *save_sub = sub;
         uschar *error = NULL;
-        int n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE);
+        int_eximarith_t n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE);
         if (error != NULL)
           {
           expand_string_message = string_sprintf("error in expression "
         if (error != NULL)
           {
           expand_string_message = string_sprintf("error in expression "
@@ -4682,13 +6087,27 @@ while (*s != 0)
               save_sub);
           goto EXPAND_FAILED;
           }
               save_sub);
           goto EXPAND_FAILED;
           }
-        sprintf(CS var_buffer, "%d", n);
+        sprintf(CS var_buffer, PR_EXIM_ARITH, n);
         yield = string_cat(yield, &size, &ptr, var_buffer, Ustrlen(var_buffer));
         continue;
         }
 
       /* Handle time period formating */
 
         yield = string_cat(yield, &size, &ptr, var_buffer, Ustrlen(var_buffer));
         continue;
         }
 
       /* 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;
       case EOP_TIME_INTERVAL:
         {
         int n;
@@ -4825,6 +6244,12 @@ while (*s != 0)
         mode_t mode;
         struct stat st;
 
         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",
         if (stat(CS sub, &st) < 0)
           {
           expand_string_message = string_sprintf("stat(%s) failed: %s",
@@ -4863,6 +6288,40 @@ while (*s != 0)
         continue;
         }
 
         continue;
         }
 
+      /* vaguely random number less than N */
+
+      case EOP_RANDINT:
+        {
+        int_eximarith_t max;
+        uschar *s;
+
+        max = expand_string_integer(sub, TRUE);
+        if (expand_string_message != NULL)
+          goto EXPAND_FAILED;
+        s = string_sprintf("%d", vaguely_random_number((int)max));
+        yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
+        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:
       /* Unknown operator */
 
       default:
@@ -4885,7 +6344,7 @@ while (*s != 0)
     int newsize = 0;
     if (ptr == 0)
       {
     int newsize = 0;
     if (ptr == 0)
       {
-      store_reset(yield);
+      if (resetok) store_reset(yield);
       yield = NULL;
       size = 0;
       }
       yield = NULL;
       size = 0;
       }
@@ -4894,6 +6353,7 @@ while (*s != 0)
       {
       expand_string_message =
         string_sprintf("unknown variable in \"${%s}\"", name);
       {
       expand_string_message =
         string_sprintf("unknown variable in \"${%s}\"", name);
+      check_variable_error_message(name);
       goto EXPAND_FAILED;
       }
     len = Ustrlen(value);
       goto EXPAND_FAILED;
       }
     len = Ustrlen(value);
@@ -4939,7 +6399,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. */
 
 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,
 DEBUG(D_expand)
   {
   debug_printf("expanding: %.*s\n   result: %s\n", (int)(s - string), string,
@@ -4987,7 +6447,7 @@ expand_string(uschar *string)
 search_find_defer = FALSE;
 malformed_header = FALSE;
 return (Ustrpbrk(string, "$\\") == NULL)? string :
 search_find_defer = FALSE;
 malformed_header = FALSE;
 return (Ustrpbrk(string, "$\\") == NULL)? string :
-  expand_string_internal(string, FALSE, NULL, FALSE);
+  expand_string_internal(string, FALSE, NULL, FALSE, TRUE);
 }
 
 
 }
 
 
@@ -5019,22 +6479,26 @@ return yield;
 
 /* Expand a string, and convert the result into an integer.
 
 
 /* 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
 
 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)
+int_eximarith_t
+expand_string_integer(uschar *string, BOOL isplus)
 {
 {
-long int value;
+int_eximarith_t value;
 uschar *s = expand_string(string);
 uschar *msg = US"invalid integer \"%s\"";
 uschar *endptr;
 
 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
 if (s == NULL) return -1;
 
 /* On an overflow, strtol() returns LONG_MAX or LONG_MIN, and sets errno
@@ -5042,39 +6506,62 @@ 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;
 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 = strtoll(CS s, CSS &endptr, 10);
 
 if (endptr == s)
   {
   msg = US"integer expected but \"%s\" found";
   }
 
 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
   {
 else
   {
-  /* Ensure we can cast this down to an int */
-  if (value > INT_MAX  || value < INT_MIN) errno = ERANGE;
-
-  if (errno != ERANGE)
+  switch (tolower(*endptr))
     {
     {
-    if (tolower(*endptr) == 'k')
-      {
-      if (value > INT_MAX/1024 || value < INT_MIN/1024) errno = ERANGE;
-        else value *= 1024;
+    default:
+      break;
+    case 'k':
+      if (value > EXIM_ARITH_MAX/1024 || value < EXIM_ARITH_MIN/1024) errno = ERANGE;
+      else value *= 1024;
       endptr++;
       endptr++;
-      }
-    else if (tolower(*endptr) == 'm')
-      {
-      if (value > INT_MAX/(1024*1024) || value < INT_MIN/(1024*1024))
-        errno = ERANGE;
+      break;
+    case 'm':
+      if (value > EXIM_ARITH_MAX/(1024*1024) || value < EXIM_ARITH_MIN/(1024*1024)) errno = ERANGE;
       else value *= 1024*1024;
       endptr++;
       else value *= 1024*1024;
       endptr++;
-      }
+      break;
+    case 'g':
+      if (value > EXIM_ARITH_MAX/(1024*1024*1024) || value < EXIM_ARITH_MIN/(1024*1024*1024)) errno = ERANGE;
+      else value *= 1024*1024*1024;
+      endptr++;
+      break;
     }
   if (errno == ERANGE)
     msg = US"absolute value of integer \"%s\" is too large (overflow)";
   else
     {
     while (isspace(*endptr)) endptr++;
     }
   if (errno == ERANGE)
     msg = US"absolute value of integer \"%s\" is too large (overflow)";
   else
     {
     while (isspace(*endptr)) endptr++;
-    if (*endptr == 0) return (int)value;
+    if (*endptr == 0) return value;
     }
   }
 
     }
   }