Add $auth1, $auth2, $auth3 variables.
[users/jgh/exim.git] / src / src / expand.c
index 49af95fd955013665dd01af7116cf83676ef54fd..1d82a150ca949a0c092e16f77e51c56a329efae6 100644 (file)
@@ -1,10 +1,10 @@
-/* $Cambridge: exim/src/src/expand.c,v 1.12 2005/01/25 14:16:33 ph10 Exp $ */
+/* $Cambridge: exim/src/src/expand.c,v 1.54 2006/02/10 14:25:43 ph10 Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2006 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
@@ -48,6 +48,7 @@ static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL);
 alphabetical order. */
 
 static uschar *item_table[] = {
+  US"dlfunc",
   US"extract",
   US"hash",
   US"hmac",
@@ -55,9 +56,9 @@ static uschar *item_table[] = {
   US"length",
   US"lookup",
   US"nhash",
-  #ifdef EXIM_PERL
-    US"perl",
-  #endif
+  US"perl",
+  US"prvs",
+  US"prvscheck",
   US"readfile",
   US"readsocket",
   US"run",
@@ -66,6 +67,7 @@ static uschar *item_table[] = {
   US"tr" };
 
 enum {
+  EITEM_DLFUNC,
   EITEM_EXTRACT,
   EITEM_HASH,
   EITEM_HMAC,
@@ -73,9 +75,9 @@ enum {
   EITEM_LENGTH,
   EITEM_LOOKUP,
   EITEM_NHASH,
-  #ifdef EXIM_PERL
-    EITEM_PERL,
-  #endif
+  EITEM_PERL,
+  EITEM_PRVS,
+  EITEM_PRVSCHECK,
   EITEM_READFILE,
   EITEM_READSOCK,
   EITEM_RUN,
@@ -193,6 +195,7 @@ static uschar *cond_table[] = {
   US"match",
   US"match_address",
   US"match_domain",
+  US"match_ip",
   US"match_local_part",
   US"or",
   US"pam",
@@ -231,6 +234,7 @@ enum {
   ECOND_MATCH,
   ECOND_MATCH_ADDRESS,
   ECOND_MATCH_DOMAIN,
+  ECOND_MATCH_IP,
   ECOND_MATCH_LOCAL_PART,
   ECOND_OR,
   ECOND_PAM,
@@ -285,32 +289,15 @@ enum {
   vtype_host_lookup,    /* value not used; get host name */
   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_pinodes         /* partition inodes; value is T/F for spool/log */
+#ifdef EXPERIMENTAL_DOMAINKEYS
+ ,vtype_dk_verify       /* Serve request out of DomainKeys verification structure */
+#endif
   };
 
 /* This table must be kept in alphabetical order. */
 
 static var_entry var_table[] = {
-  { "acl_c0",              vtype_stringptr,   &acl_var[0] },
-  { "acl_c1",              vtype_stringptr,   &acl_var[1] },
-  { "acl_c2",              vtype_stringptr,   &acl_var[2] },
-  { "acl_c3",              vtype_stringptr,   &acl_var[3] },
-  { "acl_c4",              vtype_stringptr,   &acl_var[4] },
-  { "acl_c5",              vtype_stringptr,   &acl_var[5] },
-  { "acl_c6",              vtype_stringptr,   &acl_var[6] },
-  { "acl_c7",              vtype_stringptr,   &acl_var[7] },
-  { "acl_c8",              vtype_stringptr,   &acl_var[8] },
-  { "acl_c9",              vtype_stringptr,   &acl_var[9] },
-  { "acl_m0",              vtype_stringptr,   &acl_var[10] },
-  { "acl_m1",              vtype_stringptr,   &acl_var[11] },
-  { "acl_m2",              vtype_stringptr,   &acl_var[12] },
-  { "acl_m3",              vtype_stringptr,   &acl_var[13] },
-  { "acl_m4",              vtype_stringptr,   &acl_var[14] },
-  { "acl_m5",              vtype_stringptr,   &acl_var[15] },
-  { "acl_m6",              vtype_stringptr,   &acl_var[16] },
-  { "acl_m7",              vtype_stringptr,   &acl_var[17] },
-  { "acl_m8",              vtype_stringptr,   &acl_var[18] },
-  { "acl_m9",              vtype_stringptr,   &acl_var[19] },
   { "acl_verify_message",  vtype_stringptr,   &acl_verify_message },
   { "address_data",        vtype_stringptr,   &deliver_address_data },
   { "address_file",        vtype_stringptr,   &address_file },
@@ -332,9 +319,23 @@ static var_entry var_table[] = {
   { "caller_uid",          vtype_uid,         &real_uid },
   { "compile_date",        vtype_stringptr,   &version_date },
   { "compile_number",      vtype_stringptr,   &version_cnumber },
+  { "csa_status",          vtype_stringptr,   &csa_status },
 #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 },
 #endif
   { "dnslist_domain",      vtype_stringptr,   &dnslist_domain },
   { "dnslist_text",        vtype_stringptr,   &dnslist_text },
@@ -346,7 +347,7 @@ static var_entry var_table[] = {
   { "exim_uid",            vtype_uid,         &exim_uid },
 #ifdef WITH_OLD_DEMIME
   { "found_extension",     vtype_stringptr,   &found_extension },
-#endif 
+#endif
   { "home",                vtype_stringptr,   &deliver_home },
   { "host",                vtype_stringptr,   &deliver_host },
   { "host_address",        vtype_stringptr,   &deliver_host_address },
@@ -369,7 +370,7 @@ static var_entry var_table[] = {
   { "local_user_uid",      vtype_uid,         &local_user_uid },
   { "localhost_number",    vtype_int,         &host_number },
   { "log_inodes",          vtype_pinodes,     (void *)FALSE },
-  { "log_space",           vtype_pspace,      (void *)FALSE },  
+  { "log_space",           vtype_pspace,      (void *)FALSE },
   { "mailstore_basename",  vtype_stringptr,   &mailstore_basename },
 #ifdef WITH_CONTENT_SCAN
   { "malware_name",        vtype_stringptr,   &malware_name },
@@ -378,8 +379,10 @@ static var_entry var_table[] = {
   { "message_body",        vtype_msgbody,     &message_body },
   { "message_body_end",    vtype_msgbody_end, &message_body_end },
   { "message_body_size",   vtype_int,         &message_body_size },
+  { "message_exim_id",     vtype_stringptr,   &message_id },
   { "message_headers",     vtype_msgheaders,  NULL },
   { "message_id",          vtype_stringptr,   &message_id },
+  { "message_linecount",   vtype_int,         &message_linecount },
   { "message_size",        vtype_int,         &message_size },
 #ifdef WITH_CONTENT_SCAN
   { "mime_anomaly_level",  vtype_int,         &mime_anomaly_level },
@@ -417,6 +420,9 @@ static var_entry var_table[] = {
   { "parent_local_part",   vtype_stringptr,   &deliver_localpart_parent },
   { "pid",                 vtype_pid,         NULL },
   { "primary_hostname",    vtype_stringptr,   &primary_hostname },
+  { "prvscheck_address",   vtype_stringptr,   &prvscheck_address },
+  { "prvscheck_keynum",    vtype_stringptr,   &prvscheck_keynum },
+  { "prvscheck_result",    vtype_stringptr,   &prvscheck_result },
   { "qualify_domain",      vtype_stringptr,   &qualify_domain_sender },
   { "qualify_recipient",   vtype_stringptr,   &qualify_domain_recipient },
   { "rcpt_count",          vtype_int,         &rcpt_count },
@@ -425,8 +431,9 @@ static var_entry var_table[] = {
   { "received_count",      vtype_int,         &received_count },
   { "received_for",        vtype_stringptr,   &received_for },
   { "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 }, 
+  { "recipient_verify_failure",vtype_stringptr,&recipient_verify_failure },
   { "recipients",          vtype_recipients,  NULL },
   { "recipients_count",    vtype_int,         &recipients_count },
 #ifdef WITH_CONTENT_SCAN
@@ -449,10 +456,14 @@ static var_entry var_table[] = {
   { "sender_host_name",    vtype_host_lookup, NULL },
   { "sender_host_port",    vtype_int,         &sender_host_port },
   { "sender_ident",        vtype_stringptr,   &sender_ident },
+  { "sender_rate",         vtype_stringptr,   &sender_rate },
+  { "sender_rate_limit",   vtype_stringptr,   &sender_rate_limit },
+  { "sender_rate_period",  vtype_stringptr,   &sender_rate_period },
   { "sender_rcvhost",      vtype_stringptr,   &sender_rcvhost },
-  { "sender_verify_failure",vtype_stringptr,  &sender_verify_failure }, 
-  { "smtp_active_hostname", vtype_stringptr,  &smtp_active_hostname }, 
-  { "smtp_command_argument", vtype_stringptr, &smtp_command_argument },
+  { "sender_verify_failure",vtype_stringptr,  &sender_verify_failure },
+  { "smtp_active_hostname", vtype_stringptr,  &smtp_active_hostname },
+  { "smtp_command",        vtype_stringptr,   &smtp_cmd_buffer },
+  { "smtp_command_argument", vtype_stringptr, &smtp_cmd_argument },
   { "sn0",                 vtype_filter_int,  &filter_sn[0] },
   { "sn1",                 vtype_filter_int,  &filter_sn[1] },
   { "sn2",                 vtype_filter_int,  &filter_sn[2] },
@@ -477,7 +488,7 @@ static var_entry var_table[] = {
 #endif
   { "spool_directory",     vtype_stringptr,   &spool_directory },
   { "spool_inodes",        vtype_pinodes,     (void *)TRUE },
-  { "spool_space",         vtype_pspace,      (void *)TRUE },  
+  { "spool_space",         vtype_pspace,      (void *)TRUE },
 #ifdef EXPERIMENTAL_SRS
   { "srs_db_address",      vtype_stringptr,   &srs_db_address },
   { "srs_db_key",          vtype_stringptr,   &srs_db_key },
@@ -1174,7 +1185,8 @@ else
   uschar *decoded, *error;
   while (ptr > yield && isspace(ptr[-1])) ptr--;
   *ptr = 0;
-  decoded = rfc2047_decode2(yield, TRUE, charset, '?', NULL, newsize, &error);
+  decoded = rfc2047_decode2(yield, check_rfc2047_length, charset, '?', NULL,
+    newsize, &error);
   if (error != NULL)
     {
     DEBUG(D_any) debug_printf("*** error in RFC 2047 decoding: %s\n"
@@ -1217,6 +1229,48 @@ find_variable(uschar *name, BOOL exists_only, BOOL skipping, int *newsize)
 int first = 0;
 int last = var_table_size;
 
+/* Handle ACL variables, which are not in the table because their number may
+vary depending on a build-time setting. If the variable's name is not of the
+form acl_mddd or acl_cddd, where the d's are digits, fall through to look for
+other names that start with acl_. */
+
+if (Ustrncmp(name, "acl_", 4) == 0)
+  {
+  uschar *endptr;
+  int offset = -1;
+  int max = 0;
+
+  if (name[4] == 'm')
+    {
+    offset = ACL_CVARS;
+    max = ACL_MVARS;
+    }
+  else if (name[4] == 'c')
+    {
+    offset = 0;
+    max = ACL_CVARS;
+    }
+
+  if (offset >= 0)
+    {
+    int n = Ustrtoul(name + 5, &endptr, 10);
+    if (*endptr == 0 && n < max)
+      return (acl_var[offset + n] == NULL)? US"" : acl_var[offset + n];
+    }
+  }
+
+/* Similarly for $auth<n> variables. */
+
+if (Ustrncmp(name, "auth", 4) == 0)
+  {
+  uschar *endptr;
+  int n = Ustrtoul(name + 4, &endptr, 10);
+  if (*endptr == 0 && n != 0 && n <= AUTH_VARS)
+    return (auth_vars[n-1] == NULL)? US"" : auth_vars[n-1];
+  }
+
+/* For all other variables, search the table */
+
 while (last > first)
   {
   uschar *s, *domain;
@@ -1228,16 +1282,61 @@ while (last > first)
   if (c < 0) { last = middle; continue; }
 
   /* Found an existing variable. If in skipping state, the value isn't needed,
-  and we want to avoid processing (such as looking up up the host name). */
+  and we want to avoid processing (such as looking up the host name). */
 
   if (skipping) return US"";
 
   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 */
-
+    /* VVVVVVVVVVVV */
     case vtype_int:
     sprintf(CS var_buffer, "%d", *(int *)(var_table[middle].value)); /* Integer */
     return var_buffer;
@@ -1299,7 +1398,7 @@ while (last > first)
     if (*ss == NULL && deliver_datafile >= 0)  /* Read body when needed */
       {
       uschar *body;
-      int start_offset = SPOOL_DATA_START_OFFSET;
+      off_t start_offset = SPOOL_DATA_START_OFFSET;
       int len = message_body_visible;
       if (len > message_size) len = message_size;
       *ss = body = store_malloc(len+1);
@@ -1349,10 +1448,22 @@ while (last > first)
     return tod_stamp(tod_log_datestamp);
 
     case vtype_reply:                          /* Get reply address */
-    s = find_header(US"reply-to:", exists_only, newsize, FALSE,
+    s = find_header(US"reply-to:", exists_only, newsize, TRUE,
       headers_charset);
+    if (s != NULL) while (isspace(*s)) s++;
     if (s == NULL || *s == 0)
-      s = find_header(US"from:", exists_only, newsize, FALSE, headers_charset);
+      {
+      *newsize = 0;                            /* For the *s==0 case */
+      s = find_header(US"from:", exists_only, newsize, TRUE, headers_charset);
+      }
+    if (s != NULL)
+      {
+      uschar *t;
+      while (isspace(*s)) s++;
+      for (t = s; *t != 0; t++) if (*t == '\n') *t = ' ';
+      while (t > s && isspace(t[-1])) t--;
+      *t = 0;
+      }
     return (s == NULL)? US"" : s;
 
     /* A recipients list is available only during system message filtering,
@@ -1375,19 +1486,19 @@ while (last > first)
       s[ptr] = 0;     /* string_cat() leaves room */
       }
     return s;
-    
+
     case vtype_pspace:
       {
       int inodes;
-      sprintf(CS var_buffer, "%d", 
-        receive_statvfs(var_table[middle].value == (void *)TRUE, &inodes));  
+      sprintf(CS var_buffer, "%d",
+        receive_statvfs(var_table[middle].value == (void *)TRUE, &inodes));
       }
     return var_buffer;
-    
+
     case vtype_pinodes:
       {
       int inodes;
-      (void) receive_statvfs(var_table[middle].value == (void *)TRUE, &inodes);  
+      (void) receive_statvfs(var_table[middle].value == (void *)TRUE, &inodes);
       sprintf(CS var_buffer, "%d", inodes);
       }
     return var_buffer;
@@ -1531,8 +1642,8 @@ if (name[0] == 0)
 cond_type = chop_match(name, cond_table, sizeof(cond_table)/sizeof(uschar *));
 switch(cond_type)
   {
-  /* def: tests for a non-zero or non-NULL variable, or for an existing
-  header */
+  /* def: tests for a non-empty variable, or for the existence of a header. If
+  yield == NULL we are in a skipping state, and don't care about the answer. */
 
   case ECOND_DEF:
   if (*s != ':')
@@ -1557,8 +1668,8 @@ switch(cond_type)
       (find_header(name, TRUE, NULL, FALSE, NULL) != NULL) == testfor;
     }
 
-  /* Test for a variable's having a non-empty value. If yield == NULL we
-  are in a skipping state, and don't care about the answer. */
+  /* Test for a variable's having a non-empty value. A non-existent variable
+  causes an expansion failure. */
 
   else
     {
@@ -1570,8 +1681,7 @@ switch(cond_type)
         string_sprintf("unknown variable \"%s\" after \"def:\"", name);
       return NULL;
       }
-    if (yield != NULL)
-      *yield = (value[0] != 0 && Ustrcmp(value, "0") != 0) == testfor;
+    if (yield != NULL) *yield = (value[0] != 0) == testfor;
     }
 
   return s;
@@ -1634,7 +1744,7 @@ switch(cond_type)
     case ECOND_ISIP4:
     case ECOND_ISIP6:
     rc = string_is_ip_address(sub[0], NULL);
-    *yield = ((cond_type == ECOND_ISIP)? (rc > 0) :
+    *yield = ((cond_type == ECOND_ISIP)? (rc != 0) :
              (cond_type == ECOND_ISIP4)? (rc == 4) : (rc == 6)) == testfor;
     break;
 
@@ -1730,6 +1840,7 @@ switch(cond_type)
                        variables if it succeeds
   match_address:     matches in an address list
   match_domain:      matches in a domain list
+  match_ip:          matches a host list that is restricted to IP addresses
   match_local_part:  matches in a local part list
   crypteq:           encrypts plaintext and compares against an encrypted text,
                        using crypt(), crypt16(), MD5 or SHA-1
@@ -1738,6 +1849,7 @@ switch(cond_type)
   case ECOND_MATCH:
   case ECOND_MATCH_ADDRESS:
   case ECOND_MATCH_DOMAIN:
+  case ECOND_MATCH_IP:
   case ECOND_MATCH_LOCAL_PART:
   case ECOND_CRYPTEQ:
 
@@ -1891,11 +2003,46 @@ switch(cond_type)
       MCL_DOMAIN + MCL_NOEXPAND, TRUE, NULL);
     goto MATCHED_SOMETHING;
 
+    case ECOND_MATCH_IP:       /* Match IP address in a host list */
+    if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) == 0)
+      {
+      expand_string_message = string_sprintf("\"%s\" is not an IP address",
+        sub[0]);
+      return NULL;
+      }
+    else
+      {
+      unsigned int *nullcache = NULL;
+      check_host_block cb;
+
+      cb.host_name = US"";
+      cb.host_address = sub[0];
+
+      /* If the host address starts off ::ffff: it is an IPv6 address in
+      IPv4-compatible mode. Find the IPv4 part for checking against IPv4
+      addresses. */
+
+      cb.host_ipv4 = (Ustrncmp(cb.host_address, "::ffff:", 7) == 0)?
+        cb.host_address + 7 : cb.host_address;
+
+      rc = match_check_list(
+             &sub[1],                   /* the list */
+             0,                         /* separator character */
+             &hostlist_anchor,          /* anchor pointer */
+             &nullcache,                /* cache pointer */
+             check_host,                /* function for testing */
+             &cb,                       /* argument for function */
+             MCL_HOST,                  /* type of check */
+             sub[0],                    /* text for debugging */
+             NULL);                     /* where to pass back data */
+      }
+    goto MATCHED_SOMETHING;
+
     case ECOND_MATCH_LOCAL_PART:
     rc = match_isinlist(sub[0], &(sub[1]), 0, &localpartlist_anchor, NULL,
       MCL_LOCALPART + MCL_NOEXPAND, TRUE, NULL);
     /* Fall through */
-
+    /* VVVVVVVVVVVV */
     MATCHED_SOMETHING:
     switch(rc)
       {
@@ -2235,7 +2382,7 @@ uschar *sub1, *sub2;
 
 /* If there are no following strings, we substitute the contents of $value for
 lookups and for extractions in the success case. For the ${if item, the string
-"true" is substituted. In the fail case, nothing is substituted for all three 
+"true" is substituted. In the fail case, nothing is substituted for all three
 items. */
 
 while (isspace(*s)) s++;
@@ -2243,10 +2390,10 @@ if (*s == '}')
   {
   if (type[0] == 'i')
     {
-    if (yes) *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, US"true", 4); 
+    if (yes) *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, US"true", 4);
     }
   else
-    {      
+    {
     if (yes && lookup_value != NULL)
       *yieldptr = string_cat(*yieldptr, sizeptr, ptrptr, lookup_value,
         Ustrlen(lookup_value));
@@ -2256,11 +2403,15 @@ if (*s == '}')
   goto RETURN;
   }
 
+/* The first following string must be braced. */
+
+if (*s++ != '{') goto FAILED_CURLY;
+
 /* Expand the first substring. Forced failures are noticed only if we actually
 want this string. Set skipping in the call in the fail case (this will always
 be the case if we were already skipping). */
 
-sub1 = expand_string_internal(s+1, TRUE, &s, !yes);
+sub1 = expand_string_internal(s, TRUE, &s, !yes);
 if (sub1 == NULL && (yes || !expand_string_forcedfail)) goto FAILED;
 expand_string_forcedfail = FALSE;
 if (*s++ != '}') goto FAILED_CURLY;
@@ -2272,8 +2423,8 @@ if (yes)
 
 /* If this is called from a lookup or an extract, we want to restore $value to
 what it was at the start of the item, so that it has this value during the
-second string expansion. For the call from "if" to this function, save_lookup
-is set to lookup_value, so that this statement does nothing. */
+second string expansion. For the call from "if" or "run" to this function,
+save_lookup is set to lookup_value, so that this statement does nothing. */
 
 lookup_value = save_lookup;
 
@@ -2350,8 +2501,6 @@ return rc;
 
 
 
-
-
 /*************************************************
 *    Handle MD5 or SHA-1 computation for HMAC    *
 *************************************************/
@@ -2397,6 +2546,110 @@ else
 
 
 
+/********************************************************
+* prvs: Get last three digits of days since Jan 1, 1970 *
+********************************************************/
+
+/* This is needed to implement the "prvs" BATV reverse
+   path signing scheme
+
+Argument: integer "days" offset to add or substract to
+          or from the current number of days.
+
+Returns:  pointer to string containing the last three
+          digits of the number of days since Jan 1, 1970,
+          modified by the offset argument, NULL if there
+          was an error in the conversion.
+
+*/
+
+static uschar *
+prvs_daystamp(int day_offset)
+{
+uschar *days = store_get(16);
+(void)string_format(days, 16, TIME_T_FMT,
+  (time(NULL) + day_offset*86400)/86400);
+return (Ustrlen(days) >= 3) ? &days[Ustrlen(days)-3] : US"100";
+}
+
+
+
+/********************************************************
+*   prvs: perform HMAC-SHA1 computation of prvs bits    *
+********************************************************/
+
+/* This is needed to implement the "prvs" BATV reverse
+   path signing scheme
+
+Arguments:
+  address RFC2821 Address to use
+      key The key to use (must be less than 64 characters
+          in size)
+  key_num Single-digit key number to use. Defaults to
+          '0' when NULL.
+
+Returns:  pointer to string containing the first three
+          bytes of the final hash in hex format, NULL if
+          there was an error in the process.
+*/
+
+static uschar *
+prvs_hmac_sha1(uschar *address, uschar *key, uschar *key_num, uschar *daystamp)
+{
+uschar *hash_source, *p;
+int size = 0,offset = 0,i;
+sha1 sha1_base;
+void *use_base = &sha1_base;
+uschar innerhash[20];
+uschar finalhash[20];
+uschar innerkey[64];
+uschar outerkey[64];
+uschar *finalhash_hex = store_get(40);
+
+if (key_num == NULL)
+  key_num = US"0";
+
+if (Ustrlen(key) > 64)
+  return NULL;
+
+hash_source = string_cat(NULL,&size,&offset,key_num,1);
+string_cat(hash_source,&size,&offset,daystamp,3);
+string_cat(hash_source,&size,&offset,address,Ustrlen(address));
+hash_source[offset] = '\0';
+
+DEBUG(D_expand) debug_printf("prvs: hash source is '%s'\n", hash_source);
+
+memset(innerkey, 0x36, 64);
+memset(outerkey, 0x5c, 64);
+
+for (i = 0; i < Ustrlen(key); i++)
+  {
+  innerkey[i] ^= key[i];
+  outerkey[i] ^= key[i];
+  }
+
+chash_start(HMAC_SHA1, use_base);
+chash_mid(HMAC_SHA1, use_base, innerkey);
+chash_end(HMAC_SHA1, use_base, hash_source, offset, innerhash);
+
+chash_start(HMAC_SHA1, use_base);
+chash_mid(HMAC_SHA1, use_base, outerkey);
+chash_end(HMAC_SHA1, use_base, innerhash, 20, finalhash);
+
+p = finalhash_hex;
+for (i = 0; i < 3; i++)
+  {
+  *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
+  *p++ = hex_digits[finalhash[i] & 0x0f];
+  }
+*p = '\0';
+
+return finalhash_hex;
+}
+
+
+
+
 /*************************************************
 *        Join a file onto the output string      *
 *************************************************/
@@ -2529,12 +2782,14 @@ uschar *s = *sptr;
 int x = eval_term(&s, decimal, error);
 if (*error == NULL)
   {
-  while (*s == '*' || *s == '/')
+  while (*s == '*' || *s == '/' || *s == '%')
     {
     int op = *s++;
     int y = eval_term(&s, decimal, error);
     if (*error != NULL) break;
-    if (op == '*') x *= y; else x /= y;
+    if (op == '*') x *= y;
+      else if (op == '/') x /= y;
+      else x %= y;
     }
   }
 *sptr = s;
@@ -2909,7 +3164,7 @@ while (*s != 0)
       /* Check that a key was provided for those lookup types that need it,
       and was not supplied for those that use the query style. */
 
-      if (!mac_islookup(stype, lookup_querystyle))
+      if (!mac_islookup(stype, lookup_querystyle|lookup_absfilequery))
         {
         if (key == NULL)
           {
@@ -2929,7 +3184,9 @@ while (*s != 0)
         }
 
       /* Get the next string in brackets and expand it. It is the file name for
-      single-key+file lookups, and the whole query otherwise. */
+      single-key+file lookups, and the whole query otherwise. In the case of
+      queries that also require a file name (e.g. sqlite), the file name comes
+      first. */
 
       if (*s != '{') goto EXPAND_FAILED_CURLY;
       filename = expand_string_internal(s+1, TRUE, &s, skipping);
@@ -2938,12 +3195,30 @@ while (*s != 0)
       while (isspace(*s)) s++;
 
       /* If this isn't a single-key+file lookup, re-arrange the variables
-      to be appropriate for the search_ functions. */
+      to be appropriate for the search_ functions. For query-style lookups,
+      there is just a "key", and no file name. For the special query-style +
+      file types, the query (i.e. "key") starts with a file name. */
 
       if (key == NULL)
         {
+        while (isspace(*filename)) filename++;
         key = filename;
-        filename = NULL;
+
+        if (mac_islookup(stype, lookup_querystyle))
+          {
+          filename = NULL;
+          }
+        else
+          {
+          if (*filename != '/')
+            {
+            expand_string_message = string_sprintf(
+              "absolute file name expected for \"%s\" lookup", name);
+            goto EXPAND_FAILED;
+            }
+          while (*key != 0 && !isspace(*key)) key++;
+          if (*key != 0) *key++ = 0;
+          }
         }
 
       /* If skipping, don't do the next bit - just lookup_value == NULL, as if
@@ -3011,10 +3286,15 @@ while (*s != 0)
     or ${perl{sub}{arg1}{arg2}} or up to a maximum of EXIM_PERL_MAX_ARGS
     arguments (defined below). */
 
-    #ifdef EXIM_PERL
     #define EXIM_PERL_MAX_ARGS 8
 
     case EITEM_PERL:
+    #ifndef EXIM_PERL
+    expand_string_message = US"\"${perl\" encountered, but this facility "
+      "is not included in this binary";
+    goto EXPAND_FAILED;
+
+    #else   /* EXIM_PERL */
       {
       uschar *sub_arg[EXIM_PERL_MAX_ARGS + 2];
       uschar *new_yield;
@@ -3090,6 +3370,209 @@ while (*s != 0)
       }
     #endif /* EXIM_PERL */
 
+    /* Transform email address to "prvs" scheme to use
+       as BATV-signed return path */
+
+    case EITEM_PRVS:
+      {
+      uschar *sub_arg[3];
+      uschar *p,*domain;
+
+      switch(read_subs(sub_arg, 3, 2, &s, skipping, TRUE, US"prvs"))
+        {
+        case 1: goto EXPAND_FAILED_CURLY;
+        case 2:
+        case 3: goto EXPAND_FAILED;
+        }
+
+      /* If skipping, we don't actually do anything */
+      if (skipping) continue;
+
+      /* sub_arg[0] is the address */
+      domain = Ustrrchr(sub_arg[0],'@');
+      if ( (domain == NULL) || (domain == sub_arg[0]) || (Ustrlen(domain) == 1) )
+        {
+        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;
+        }
+
+      p = prvs_hmac_sha1(sub_arg[0],sub_arg[1],sub_arg[2],prvs_daystamp(7));
+      if (p == NULL)
+        {
+        expand_string_message = US"prvs hmac-sha1 conversion failed";
+        goto EXPAND_FAILED;
+        }
+
+      /* Now separate the domain from the local part */
+      *domain++ = '\0';
+
+      yield = string_cat(yield,&size,&ptr,US"prvs=",5);
+      string_cat(yield,&size,&ptr,sub_arg[0],Ustrlen(sub_arg[0]));
+      string_cat(yield,&size,&ptr,US"/",1);
+      string_cat(yield,&size,&ptr,(sub_arg[2] != NULL) ? sub_arg[2] : US"0", 1);
+      string_cat(yield,&size,&ptr,prvs_daystamp(7),3);
+      string_cat(yield,&size,&ptr,p,6);
+      string_cat(yield,&size,&ptr,US"@",1);
+      string_cat(yield,&size,&ptr,domain,Ustrlen(domain));
+
+      continue;
+      }
+
+    /* Check a prvs-encoded address for validity */
+
+    case EITEM_PRVSCHECK:
+      {
+      uschar *sub_arg[3];
+      int mysize = 0, myptr = 0;
+      const pcre *re;
+      uschar *p;
+
+      /* TF: Ugliness: We want to expand parameter 1 first, then set
+         up expansion variables that are used in the expansion of
+         parameter 2. So we clone the string for the first
+         expansion, where we only expand parameter 1.
+
+         PH: Actually, that isn't necessary. The read_subs() function is
+         designed to work this way for the ${if and ${lookup expansions. I've
+         tidied the code.
+      */
+
+      /* Reset expansion variables */
+      prvscheck_result = NULL;
+      prvscheck_address = NULL;
+      prvscheck_keynum = NULL;
+
+      switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, US"prvs"))
+        {
+        case 1: goto EXPAND_FAILED_CURLY;
+        case 2:
+        case 3: goto EXPAND_FAILED;
+        }
+
+      re = regex_must_compile(US"^prvs\\=(.+)\\/([0-9])([0-9]{3})([A-F0-9]{6})\\@(.+)$",
+                              TRUE,FALSE);
+
+      if (regex_match_and_setup(re,sub_arg[0],0,-1))
+        {
+        uschar *local_part = string_copyn(expand_nstring[1],expand_nlength[1]);
+        uschar *key_num = string_copyn(expand_nstring[2],expand_nlength[2]);
+        uschar *daystamp = string_copyn(expand_nstring[3],expand_nlength[3]);
+        uschar *hash = string_copyn(expand_nstring[4],expand_nlength[4]);
+        uschar *domain = string_copyn(expand_nstring[5],expand_nlength[5]);
+
+        DEBUG(D_expand) debug_printf("prvscheck localpart: %s\n", local_part);
+        DEBUG(D_expand) debug_printf("prvscheck key number: %s\n", key_num);
+        DEBUG(D_expand) debug_printf("prvscheck daystamp: %s\n", daystamp);
+        DEBUG(D_expand) debug_printf("prvscheck hash: %s\n", hash);
+        DEBUG(D_expand) debug_printf("prvscheck domain: %s\n", domain);
+
+        /* Set up expansion variables */
+        prvscheck_address = string_cat(NULL, &mysize, &myptr, local_part, Ustrlen(local_part));
+        string_cat(prvscheck_address,&mysize,&myptr,US"@",1);
+        string_cat(prvscheck_address,&mysize,&myptr,domain,Ustrlen(domain));
+        prvscheck_address[myptr] = '\0';
+        prvscheck_keynum = string_copy(key_num);
+
+        /* 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;
+          }
+
+        /* Now we have the key and can check the address. */
+
+        p = prvs_hmac_sha1(prvscheck_address, sub_arg[0], prvscheck_keynum,
+          daystamp);
+
+        if (p == NULL)
+          {
+          expand_string_message = US"hmac-sha1 conversion failed";
+          goto EXPAND_FAILED;
+          }
+
+        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;
+
+          (void)sscanf(CS now,"%u",&inow);
+          (void)sscanf(CS daystamp,"%u",&iexpire);
+
+          /* When "iexpire" is < 7, a "flip" has occured.
+             Adjust "inow" accordingly. */
+          if ( (iexpire < 7) && (inow >= 993) ) inow = 0;
+
+          if (iexpire > inow)
+            {
+            prvscheck_result = US"1";
+            DEBUG(D_expand) debug_printf("prvscheck: success, $pvrs_result set to 1\n");
+            }
+            else
+            {
+            prvscheck_result = NULL;
+            DEBUG(D_expand) debug_printf("prvscheck: signature expired, $pvrs_result unset\n");
+            }
+          }
+        else
+          {
+          prvscheck_result = NULL;
+          DEBUG(D_expand) debug_printf("prvscheck: hash failure, $pvrs_result unset\n");
+          }
+
+        /* Now expand the final argument. We leave this till now so that
+        it can include $prvscheck_result. */
+
+        switch(read_subs(sub_arg, 1, 0, &s, skipping, TRUE, US"prvs"))
+          {
+          case 1: goto EXPAND_FAILED_CURLY;
+          case 2:
+          case 3: goto EXPAND_FAILED;
+          }
+
+        if (sub_arg[0] == NULL || *sub_arg[0] == '\0')
+          yield = string_cat(yield,&size,&ptr,prvscheck_address,Ustrlen(prvscheck_address));
+        else
+          yield = string_cat(yield,&size,&ptr,sub_arg[0],Ustrlen(sub_arg[0]));
+
+        /* Reset the "internal" variables afterwards, because they are in
+        dynamic store that will be reclaimed if the expansion succeeded. */
+
+        prvscheck_address = NULL;
+        prvscheck_keynum = NULL;
+        }
+      else
+        {
+        /* Does not look like a prvs encoded address, return the empty string.
+           We need to make sure all subs are expanded first, 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 3: goto EXPAND_FAILED;
+          }
+        }
+
+      continue;
+      }
+
     /* Handle "readfile" to insert an entire file */
 
     case EITEM_READFILE:
@@ -3124,7 +3607,7 @@ while (*s != 0)
         }
 
       yield = cat_file(f, yield, &size, &ptr, sub_arg[1]);
-      fclose(f);
+      (void)fclose(f);
       continue;
       }
 
@@ -3217,7 +3700,7 @@ while (*s != 0)
         alarm(timeout);
         yield = cat_file(f, yield, &size, &ptr, sub_arg[3]);
         alarm(0);
-        fclose(f);
+        (void)fclose(f);
 
         /* After a timeout, we restore the pointer in the result, that is,
         make sure we add nothing from the socket. */
@@ -3264,7 +3747,6 @@ while (*s != 0)
     case EITEM_RUN:
       {
       FILE *f;
-      uschar *old_lookup_value = NULL;
       uschar *arg;
       uschar **argv;
       pid_t pid;
@@ -3315,7 +3797,7 @@ while (*s != 0)
 
         /* Nothing is written to the standard input. */
 
-        close(fd_in);
+        (void)close(fd_in);
 
         /* Wait for the process to finish, applying the timeout, and inspect its
         return code for serious disasters. Simple non-zero returns are passed on.
@@ -3344,18 +3826,17 @@ while (*s != 0)
         in lookup_value). */
 
         f = fdopen(fd_out, "rb");
-        old_lookup_value = lookup_value;
         lookup_value = NULL;
         lookup_value = cat_file(f, lookup_value, &lsize, &lptr, NULL);
-        fclose(f);
+        (void)fclose(f);
         }
 
-      /* Process the yes/no strings */
+      /* Process the yes/no strings; $value may be useful in both cases */
 
       switch(process_yesno(
                skipping,                     /* were previously skipping */
                runrc == 0,                   /* success/failure indicator */
-               old_lookup_value,             /* value to reset for string2 */
+               lookup_value,                 /* value to reset for string2 */
                &s,                           /* input pointer */
                &yield,                       /* output pointer */
                &size,                        /* output size */
@@ -3733,8 +4214,8 @@ while (*s != 0)
 
             if (*p == 0)
               {
-              expand_string_message = US"first argument of \"expand\" must not "
-                "be empty";
+              expand_string_message = US"first argument of \"extract\" must "
+                "not be empty";
               goto EXPAND_FAILED;
               }
 
@@ -3786,6 +4267,106 @@ while (*s != 0)
 
       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
+    a maximum of EXPAND_DLFUNC_MAX_ARGS arguments (defined below). */
+
+    #define EXPAND_DLFUNC_MAX_ARGS 8
+
+    case EITEM_DLFUNC:
+    #ifndef EXPAND_DLFUNC
+    expand_string_message = US"\"${dlfunc\" encountered, but this facility "
+      "is not included in this binary";
+    goto EXPAND_FAILED;
+
+    #else   /* EXPAND_DLFUNC */
+      {
+      tree_node *t;
+      exim_dlfunc_t *func;
+      uschar *result;
+      int status, argc;
+      uschar *argv[EXPAND_DLFUNC_MAX_ARGS + 3];
+
+      if ((expand_forbid & RDO_DLFUNC) != 0)
+        {
+        expand_string_message =
+          US"dynamically-loaded functions are not permitted";
+        goto EXPAND_FAILED;
+        }
+
+      switch(read_subs(argv, EXPAND_DLFUNC_MAX_ARGS + 2, 2, &s, skipping,
+           TRUE, US"dlfunc"))
+        {
+        case 1: goto EXPAND_FAILED_CURLY;
+        case 2:
+        case 3: goto EXPAND_FAILED;
+        }
+
+      /* If skipping, we don't actually do anything */
+
+      if (skipping) continue;
+
+      /* Look up the dynamically loaded object handle in the tree. If it isn't
+      found, dlopen() the file and put the handle in the tree for next time. */
+
+      t = tree_search(dlobj_anchor, argv[0]);
+      if (t == NULL)
+        {
+        void *handle = dlopen(CS argv[0], RTLD_LAZY);
+        if (handle == NULL)
+          {
+          expand_string_message = string_sprintf("dlopen \"%s\" failed: %s",
+            argv[0], dlerror());
+          log_write(0, LOG_MAIN|LOG_PANIC, "%s", expand_string_message);
+          goto EXPAND_FAILED;
+          }
+        t = store_get_perm(sizeof(tree_node) + Ustrlen(argv[0]));
+        Ustrcpy(t->name, argv[0]);
+        t->data.ptr = handle;
+        (void)tree_insertnode(&dlobj_anchor, t);
+        }
+
+      /* Having obtained the dynamically loaded object handle, look up the
+      function pointer. */
+
+      func = (exim_dlfunc_t *)dlsym(t->data.ptr, CS argv[1]);
+      if (func == NULL)
+        {
+        expand_string_message = string_sprintf("dlsym \"%s\" in \"%s\" failed: "
+          "%s", argv[1], argv[0], dlerror());
+        log_write(0, LOG_MAIN|LOG_PANIC, "%s", expand_string_message);
+        goto EXPAND_FAILED;
+        }
+
+      /* Call the function and work out what to do with the result. If it
+      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. */
+
+      result = NULL;
+      for (argc = 0; argv[argc] != NULL; argc++);
+      status = func(&result, argc - 2, &argv[2]);
+      if(status == OK)
+        {
+        if (result == NULL) result = US"";
+        yield = string_cat(yield, &size, &ptr, result, Ustrlen(result));
+        continue;
+        }
+      else
+        {
+        expand_string_message = result == NULL ? US"(no message)" : result;
+        if(status == FAIL_FORCED) expand_string_forcedfail = TRUE;
+          else if(status != FAIL)
+            log_write(0, LOG_MAIN|LOG_PANIC, "dlfunc{%s}{%s} failed (%d): %s",
+              argv[0], argv[1], status, expand_string_message);
+        goto EXPAND_FAILED;
+        }
+      }
+    #endif /* EXPAND_DLFUNC */
     }
 
   /* Control reaches here if the name is not recognized as one of the more
@@ -3846,6 +4427,8 @@ while (*s != 0)
         continue;
         }
 
+      /* Note that for Darwin and Cygwin, BASE_62 actually has the value 36 */
+
       case EOP_BASE62D:
         {
         uschar buf[16];
@@ -3857,10 +4440,11 @@ while (*s != 0)
           if (t == NULL)
             {
             expand_string_message = string_sprintf("argument for base62d "
-              "operator is \"%s\", which is not a base 62 number", sub);
+              "operator is \"%s\", which is not a base %d number", sub,
+              BASE_62);
             goto EXPAND_FAILED;
             }
-          n = n * 62 + (t - base62_chars);
+          n = n * BASE_62 + (t - base62_chars);
           }
         (void)sprintf(CS buf, "%ld", n);
         yield = string_cat(yield, &size, &ptr, buf, Ustrlen(buf));
@@ -4340,6 +4924,12 @@ while (*s != 0)
         mode_t mode;
         struct stat st;
 
+        if ((expand_forbid & RDO_EXISTS) != 0)
+          {
+          expand_string_message = US"Use of the stat() expansion is not permitted";
+          goto EXPAND_FAILED;
+          }
+
         if (stat(CS sub, &st) < 0)
           {
           expand_string_message = string_sprintf("stat(%s) failed: %s",
@@ -4369,10 +4959,10 @@ while (*s != 0)
 
         smode[10] = 0;
         s = string_sprintf("mode=%04lo smode=%s inode=%ld device=%ld links=%ld "
-          "uid=%ld gid=%ld size=%ld atime=%ld mtime=%ld ctime=%ld",
+          "uid=%ld gid=%ld size=" OFF_T_FMT " atime=%ld mtime=%ld ctime=%ld",
           (long)(st.st_mode & 077777), smode, (long)st.st_ino,
           (long)st.st_dev, (long)st.st_nlink, (long)st.st_uid,
-          (long)st.st_gid, (long)st.st_size, (long)st.st_atime,
+          (long)st.st_gid, st.st_size, (long)st.st_atime,
           (long)st.st_mtime, (long)st.st_ctime);
         yield = string_cat(yield, &size, &ptr, s, Ustrlen(s));
         continue;
@@ -4598,7 +5188,6 @@ return -2;
 }
 
 
-
 /*************************************************
 **************************************************
 *             Stand-alone test program           *