Add client-ip info to non-pass iprev ${authres } lines
[exim.git] / src / src / expand.c
index f44ddf8b828c2c8f60cd2a8976b5862dd671a736..b9eeb7c46766352e5e09a7c5bf3d7e1407e5072d 100644 (file)
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2017 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
@@ -17,22 +17,22 @@ static uschar *expand_string_internal(const uschar *, BOOL, const uschar **, BOO
 static int_eximarith_t expanded_string_integer(const uschar *, BOOL);
 
 #ifdef STAND_ALONE
-#ifndef SUPPORT_CRYPTEQ
-#define SUPPORT_CRYPTEQ
-#endif
+# ifndef SUPPORT_CRYPTEQ
+#  define SUPPORT_CRYPTEQ
+# endif
 #endif
 
 #ifdef LOOKUP_LDAP
-#include "lookups/ldap.h"
+# include "lookups/ldap.h"
 #endif
 
 #ifdef SUPPORT_CRYPTEQ
-#ifdef CRYPT_H
-#include <crypt.h>
-#endif
-#ifndef HAVE_CRYPT16
+# ifdef CRYPT_H
+#  include <crypt.h>
+# endif
+# ifndef HAVE_CRYPT16
 extern char* crypt16(char*, char*);
-#endif
+# endif
 #endif
 
 /* The handling of crypt16() is a mess. I will record below the analysis of the
@@ -103,6 +103,7 @@ alphabetical order. */
 
 static uschar *item_table[] = {
   US"acl",
+  US"authresults",
   US"certextract",
   US"dlfunc",
   US"env",
@@ -133,6 +134,7 @@ static uschar *item_table[] = {
 
 enum {
   EITEM_ACL,
+  EITEM_AUTHRESULTS,
   EITEM_CERTEXTRACT,
   EITEM_DLFUNC,
   EITEM_ENV,
@@ -459,6 +461,12 @@ static var_entry var_table[] = {
   { "address_data",        vtype_stringptr,   &deliver_address_data },
   { "address_file",        vtype_stringptr,   &address_file },
   { "address_pipe",        vtype_stringptr,   &address_pipe },
+#ifdef EXPERIMENTAL_ARC
+  { "arc_domains",         vtype_string_func, &fn_arc_domains },
+  { "arc_oldest_pass",     vtype_int,         &arc_oldest_pass },
+  { "arc_state",           vtype_stringptr,   &arc_state },
+  { "arc_state_reason",    vtype_stringptr,   &arc_state_reason },
+#endif
   { "authenticated_fail_id",vtype_stringptr,  &authenticated_fail_id },
   { "authenticated_id",    vtype_stringptr,   &authenticated_id },
   { "authenticated_sender",vtype_stringptr,   &authenticated_sender },
@@ -512,7 +520,6 @@ static var_entry var_table[] = {
   { "dkim_verify_status",  vtype_stringptr,   &dkim_verify_status },
 #endif
 #ifdef EXPERIMENTAL_DMARC
-  { "dmarc_ar_header",     vtype_stringptr,   &dmarc_ar_header },
   { "dmarc_domain_policy", vtype_stringptr,   &dmarc_domain_policy },
   { "dmarc_status",        vtype_stringptr,   &dmarc_status },
   { "dmarc_status_text",   vtype_stringptr,   &dmarc_status_text },
@@ -557,7 +564,9 @@ static var_entry var_table[] = {
   { "local_part_data",     vtype_stringptr,   &deliver_localpart_data },
   { "local_part_prefix",   vtype_stringptr,   &deliver_localpart_prefix },
   { "local_part_suffix",   vtype_stringptr,   &deliver_localpart_suffix },
+#ifdef HAVE_LOCAL_SCAN
   { "local_scan_data",     vtype_stringptr,   &local_scan_data },
+#endif
   { "local_user_gid",      vtype_gid,         &local_user_gid },
   { "local_user_uid",      vtype_uid,         &local_user_uid },
   { "localhost_number",    vtype_int,         &host_number },
@@ -700,11 +709,12 @@ static var_entry var_table[] = {
   { "spam_score",          vtype_stringptr,   &spam_score },
   { "spam_score_int",      vtype_stringptr,   &spam_score_int },
 #endif
-#ifdef EXPERIMENTAL_SPF
+#ifdef SUPPORT_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_result_guessed",  vtype_bool,        &spf_result_guessed },
   { "spf_smtp_comment",    vtype_stringptr,   &spf_smtp_comment },
 #endif
   { "spool_directory",     vtype_stringptr,   &spool_directory },
@@ -738,7 +748,7 @@ static var_entry var_table[] = {
   { "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 },
-#ifdef EXPERIMENTAL_DANE
+#ifdef SUPPORT_DANE
   { "tls_out_dane",        vtype_bool,        &tls_out.dane_verified },
 #endif
   { "tls_out_ocsp",        vtype_int,         &tls_out.ocsp },
@@ -748,7 +758,7 @@ static var_entry var_table[] = {
 #if defined(SUPPORT_TLS)
   { "tls_out_sni",         vtype_stringptr,   &tls_out.sni },
 #endif
-#ifdef EXPERIMENTAL_DANE
+#ifdef SUPPORT_DANE
   { "tls_out_tlsa_usage",  vtype_int,         &tls_out.tlsa_usage },
 #endif
 
@@ -807,6 +817,10 @@ static uschar *mtable_setid[] =
 static uschar *mtable_sticky[] =
   { US"--T", US"--t", US"-wT", US"-wt", US"r-T", US"r-t", US"rwT", US"rwt" };
 
+/* flags for find_header() */
+#define FH_EXISTS_ONLY BIT(0)
+#define FH_WANT_RAW    BIT(1)
+#define FH_WANT_LIST   BIT(2)
 
 
 /*************************************************
@@ -1511,22 +1525,25 @@ can also return a concatenation of all the header lines. When concatenating
 specific headers that contain lists of addresses, a comma is inserted between
 them. Otherwise we use a straight concatenation. Because some messages can have
 pathologically large number of lines, there is a limit on the length that is
-returned. Also, to avoid massive store use which would result from using
-string_cat() as it copies and extends strings, we do a preliminary pass to find
-out exactly how much store will be needed. On "normal" messages this will be
-pretty trivial.
+returned.
 
 Arguments:
   name          the name of the header, without the leading $header_ or $h_,
                 or NULL if a concatenation of all headers is required
-  exists_only   TRUE if called from a def: test; don't need to build a string;
-                just return a string that is not "" and not "0" if the header
-                exists
   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. Also used
-                for $message_headers_raw.
+  flags                FH_EXISTS_ONLY
+                 set if called from a def: test; don't need to build a string;
+                 just return a string that is not "" and not "0" if the header
+                 exists
+               FH_WANT_RAW
+                 set if called for $rh_ or $rheader_ items; no processing,
+                 other than concatenating, will be done on the header. Also used
+                 for $message_headers_raw.
+               FH_WANT_LIST
+                 Double colon chars in the content, and replace newline with
+                 colon between each element when concatenating; returning a
+                 colon-sep list (elements might contain newlines)
   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_)
@@ -1536,124 +1553,141 @@ Returns:        NULL if the header does not exist, else a pointer to a new
 */
 
 static uschar *
-find_header(uschar *name, BOOL exists_only, int *newsize, BOOL want_raw,
-  uschar *charset)
+find_header(uschar *name, int *newsize, unsigned flags, uschar *charset)
 {
-BOOL found = name == NULL;
-int comma = 0;
-int len = found? 0 : Ustrlen(name);
-int i;
-uschar *yield = NULL;
-uschar *ptr = NULL;
-
-/* Loop for two passes - saves code repetition */
-
-for (i = 0; i < 2; i++)
-  {
-  int size = 0;
-  header_line *h;
-
-  for (h = header_list; size < header_insert_maxlen && h; h = h->next)
-    if (h->type != htype_old && h->text)  /* NULL => Received: placeholder */
-      if (!name || (len <= h->slen && strncmpic(name, h->text, len) == 0))
-        {
-        int ilen;
-        uschar *t;
-
-        if (exists_only) return US"1";      /* don't need actual string */
-        found = TRUE;
-        t = h->text + len;                  /* text to insert */
-        if (!want_raw)                      /* unless wanted raw, */
-          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--;
+BOOL found = !name;
+int len = name ? Ustrlen(name) : 0;
+BOOL comma = FALSE;
+header_line * h;
+gstring * g = NULL;
 
-        /* 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. */
+for (h = header_list; h; h = h->next)
+  if (h->type != htype_old && h->text)  /* NULL => Received: placeholder */
+    if (!name || (len <= h->slen && strncmpic(name, h->text, len) == 0))
+      {
+      uschar * s, * t;
+      size_t inc;
 
-        if (!want_raw && name && comma == 0 &&
-            Ustrchr("BCFRST", h->type) != NULL)
-          comma = 1;
+      if (flags & FH_EXISTS_ONLY)
+       return US"1";  /* don't need actual string */
 
-        /* First pass - compute total store needed; second pass - compute
-        total store used, including this header. */
+      found = TRUE;
+      s = h->text + len;               /* text to insert */
+      if (!(flags & FH_WANT_RAW))      /* unless wanted raw, */
+       while (isspace(*s)) s++;        /* remove leading white space */
+      t = h->text + h->slen;           /* end-point */
 
-        size += ilen + comma + 1;  /* +1 for the newline */
+      /* Unless wanted raw, remove trailing whitespace, including the
+      newline. */
 
-        /* Second pass - concatenate the data, up to a maximum. Note that
-        the loop stops when size hits the limit. */
+      if (flags & FH_WANT_LIST)
+       while (t > s && t[-1] == '\n') t--;
+      else if (!(flags & FH_WANT_RAW))
+       {
+       while (t > s && isspace(t[-1])) t--;
 
-        if (i != 0)
-          {
-          if (size > header_insert_maxlen)
-            {
-            ilen -= size - header_insert_maxlen - 1;
-            comma = 0;
-            }
-          Ustrncpy(ptr, t, ilen);
-          ptr += ilen;
+       /* Set comma 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. */
 
-          /* 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 (name && !comma && Ustrchr("BCFRST", h->type)) comma = TRUE;
+       }
 
-          if (!want_raw && ilen > 0)
-            {
-            if (comma != 0) *ptr++ = ',';
-            *ptr++ = '\n';
-            }
-          }
-        }
+      /* Trim the header roughly if we're approaching limits */
+      inc = t - s;
+      if ((g ? g->ptr : 0) + inc > header_insert_maxlen)
+       inc = header_insert_maxlen - (g ? g->ptr : 0);
+
+      /* For raw just copy the data; for a list, add the data as a colon-sep
+      list-element; for comma-list add as an unchecked comma,newline sep
+      list-elemment; for other nonraw add as an unchecked newline-sep list (we
+      stripped trailing WS above including the newline). We ignore the potential
+      expansion due to colon-doubling, just leaving the loop if the limit is met
+      or exceeded. */
+
+      if (flags & FH_WANT_LIST)
+        g = string_append_listele_n(g, ':', s, (unsigned)inc);
+      else if (flags & FH_WANT_RAW)
+       {
+       g = string_catn(g, s, (unsigned)inc);
+       (void) string_from_gstring(g);
+       }
+      else if (inc > 0)
+       if (comma)
+         g = string_append2_listele_n(g, US",\n", s, (unsigned)inc);
+       else
+         g = string_append2_listele_n(g, US"\n", s, (unsigned)inc);
 
-  /* 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 (g && g->ptr >= header_insert_maxlen) break;
+      }
 
-  if (i == 0)
-    {
-    if (!found) return NULL;
-    if (size > header_insert_maxlen) size = header_insert_maxlen;
-    *newsize = size + 1;
-    ptr = yield = store_get(*newsize);
-    }
-  }
+if (!found) return NULL;       /* No header found */
+if (!g) return US"";
 
 /* That's all we do for raw header expansion. */
 
-if (want_raw)
-  *ptr = 0;
+*newsize = g->size;
+if (flags & FH_WANT_RAW)
+  return g->s;
 
-/* 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. */
+/* Otherwise 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;
-  if (ptr > yield && ptr[-1] == '\n') ptr--;
-  if (ptr > yield && comma != 0 && ptr[-1] == ',') ptr--;
-  *ptr = 0;
-  decoded = rfc2047_decode2(yield, check_rfc2047_length, charset, '?', NULL,
+
+  decoded = rfc2047_decode2(g->s, check_rfc2047_length, charset, '?', NULL,
     newsize, &error);
-  if (error != NULL)
+  if (error)
     {
     DEBUG(D_any) debug_printf("*** error in RFC 2047 decoding: %s\n"
-      "    input was: %s\n", error, yield);
+      "    input was: %s\n", error, g->s);
     }
-  if (decoded != NULL) yield = decoded;
+  return decoded ? decoded : g->s;
   }
+}
 
-return yield;
+
+
+
+/* Append a "local" element to an Authentication-Results: header
+if this was a non-smtp message.
+*/
+
+static gstring *
+authres_local(gstring * g, const uschar * sysname)
+{
+if (!authentication_local)
+  return g;
+g = string_append(g, 3, US";\n\tlocal=pass (non-smtp, ", sysname, US")");
+if (authenticated_id) g = string_append(g, 2, " u=", authenticated_id);
+return g;
 }
 
 
+/* Append an "iprev" element to an Authentication-Results: header
+if we have attempted to get the calling host's name.
+*/
+
+static gstring *
+authres_iprev(gstring * g)
+{
+if (sender_host_name)
+  g = string_append(g, 3, US";\n\tiprev=pass (", sender_host_name, US")");
+else if (host_lookup_deferred)
+  g = string_catn(g, US";\n\tiprev=temperror", 19);
+else if (host_lookup_failed)
+  g = string_catn(g, US";\n\tiprev=fail", 13);
+else 
+  return g;
+
+if (sender_host_address)
+  g = string_append(g, 2, US" smtp.client-ip=", sender_host_address);
+return g;
+}
+
 
 
 /*************************************************
@@ -1666,6 +1700,7 @@ generated from a system filter, but not elsewhere. */
 static uschar *
 fn_recipients(void)
 {
+uschar * s;
 gstring * g = NULL;
 int i;
 
@@ -1673,11 +1708,10 @@ if (!enable_dollar_recipients) return NULL;
 
 for (i = 0; i < recipients_count; i++)
   {
-  /*XXX variant of list_appendele? */
-  if (i != 0) g = string_catn(g, US", ", 2);
-  g = string_cat(g, recipients_list[i].address);
+  s = recipients_list[i].address;
+  g = string_append2_listele_n(g, US", ", s, Ustrlen(s));
   }
-return string_from_gstring(g);
+return g ? g->s : NULL;
 }
 
 
@@ -1797,10 +1831,10 @@ switch (vp->type)
     return var_buffer;
 
   case vtype_host_lookup:                    /* Lookup if not done so */
-    if (sender_host_name == NULL && sender_host_address != NULL &&
-       !host_lookup_failed && host_name_lookup() == OK)
+    if (  !sender_host_name && sender_host_address
+       && !host_lookup_failed && host_name_lookup() == OK)
       host_build_sender_fullhost();
-    return (sender_host_name == NULL)? US"" : sender_host_name;
+    return sender_host_name ? sender_host_name : US"";
 
   case vtype_localpart:                      /* Get local part from address */
     s = *((uschar **)(val));
@@ -1821,10 +1855,11 @@ switch (vp->type)
     return (domain == NULL)? US"" : domain + 1;
 
   case vtype_msgheaders:
-    return find_header(NULL, exists_only, newsize, FALSE, NULL);
+    return find_header(NULL, newsize, exists_only ? FH_EXISTS_ONLY : 0, NULL);
 
   case vtype_msgheaders_raw:
-    return find_header(NULL, exists_only, newsize, TRUE, NULL);
+    return find_header(NULL, newsize,
+               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW, NULL);
 
   case vtype_msgbody:                        /* Pointer to msgbody string */
   case vtype_msgbody_end:                    /* Ditto, the end of the msg */
@@ -1889,15 +1924,18 @@ switch (vp->type)
     return tod_stamp(tod_log_datestamp_daily);
 
   case vtype_reply:                          /* Get reply address */
-    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"reply-to:", newsize,
+               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
+               headers_charset);
+    if (s) while (isspace(*s)) s++;
+    if (!s || !*s)
       {
       *newsize = 0;                            /* For the *s==0 case */
-      s = find_header(US"from:", exists_only, newsize, TRUE, headers_charset);
+      s = find_header(US"from:", newsize,
+               exists_only ? FH_EXISTS_ONLY|FH_WANT_RAW : FH_WANT_RAW,
+               headers_charset);
       }
-    if (s != NULL)
+    if (s)
       {
       uschar *t;
       while (isspace(*s)) s++;
@@ -1905,7 +1943,7 @@ switch (vp->type)
       while (t > s && isspace(t[-1])) t--;
       *t = 0;
       }
-    return (s == NULL)? US"" : s;
+    return s ? s : US"";
 
   case vtype_string_func:
     {
@@ -2184,50 +2222,52 @@ switch(cond_type)
   yield == NULL we are in a skipping state, and don't care about the answer. */
 
   case ECOND_DEF:
-  if (*s != ':')
     {
-    expand_string_message = US"\":\" expected after \"def\"";
-    return NULL;
-    }
+    uschar * t;
 
-  s = read_name(name, 256, s+1, US"_");
+    if (*s != ':')
+      {
+      expand_string_message = US"\":\" expected after \"def\"";
+      return NULL;
+      }
 
-  /* 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. */
+    s = read_name(name, 256, s+1, US"_");
 
-  if (Ustrncmp(name, "h_", 2) == 0 ||
-      Ustrncmp(name, "rh_", 3) == 0 ||
-      Ustrncmp(name, "bh_", 3) == 0 ||
-      Ustrncmp(name, "header_", 7) == 0 ||
-      Ustrncmp(name, "rheader_", 8) == 0 ||
-      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;
-    }
+    /* 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. */
 
-  /* Test for a variable's having a non-empty value. A non-existent variable
-  causes an expansion failure. */
+    if (  ( *(t = name) == 'h'
+         || (*t == 'r' || *t == 'l' || *t == 'b') && *++t == 'h'
+         )
+       && (*++t == '_' || Ustrncmp(t, "eader_", 6) == 0)
+       )
+      {
+      s = read_header_name(name, 256, s);
+      /* {-for-text-editors */
+      if (Ustrchr(name, '}') != NULL) malformed_header = TRUE;
+      if (yield) *yield =
+       (find_header(name, NULL, FH_EXISTS_ONLY, NULL) != NULL) == testfor;
+      }
 
-  else
-    {
-    uschar *value = find_variable(name, TRUE, yield == NULL, NULL);
-    if (value == NULL)
+    /* Test for a variable's having a non-empty value. A non-existent variable
+    causes an expansion failure. */
+
+    else
       {
-      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 (!(t = find_variable(name, TRUE, yield == NULL, NULL)))
+       {
+       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) *yield = (t[0] != 0) == testfor;
       }
-    if (yield != NULL) *yield = (value[0] != 0) == testfor;
-    }
 
-  return s;
+    return s;
+    }
 
 
   /* first_delivery tests for first delivery attempt */
@@ -2498,9 +2538,12 @@ switch(cond_type)
         "after \"%s\"", name);
       return NULL;
       }
-    sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL,
-        honour_dollar, resetok);
-    if (sub[i] == NULL) return NULL;
+    if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL,
+        honour_dollar, resetok)))
+      return NULL;
+    DEBUG(D_expand) if (i == 1 && !sub2_honour_dollar && Ustrchr(sub[1], '$'))
+      debug_printf_indent("WARNING: the second arg is NOT expanded,"
+                       " for security reasons\n");
     if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
     /* Convert to numerical if required; we know that the names of all the
@@ -3931,6 +3974,7 @@ while (*s != 0)
     int len;
     int newsize = 0;
     gstring * g = NULL;
+    uschar * t;
 
     s = read_name(name, sizeof(name), s, US"_");
 
@@ -3948,17 +3992,19 @@ while (*s != 0)
 
     /* Header */
 
-    if (Ustrncmp(name, "h_", 2) == 0 ||
-        Ustrncmp(name, "rh_", 3) == 0 ||
-        Ustrncmp(name, "bh_", 3) == 0 ||
-        Ustrncmp(name, "header_", 7) == 0 ||
-        Ustrncmp(name, "rheader_", 8) == 0 ||
-        Ustrncmp(name, "bheader_", 8) == 0)
+    if (  ( *(t = name) == 'h'
+          || (*t == 'r' || *t == 'l' || *t == 'b') && *++t == 'h'
+         )
+       && (*++t == '_' || Ustrncmp(t, "eader_", 6) == 0)
+       )
       {
-      BOOL want_raw = (name[0] == 'r')? TRUE : FALSE;
-      uschar *charset = (name[0] == 'b')? NULL : headers_charset;
+      unsigned flags = *name == 'r' ? FH_WANT_RAW
+                     : *name == 'l' ? FH_WANT_RAW|FH_WANT_LIST
+                     : 0;
+      uschar * charset = *name == 'b' ? NULL : headers_charset;
+
       s = read_header_name(name, sizeof(name), s);
-      value = find_header(name, FALSE, &newsize, want_raw, charset);
+      value = find_header(name, &newsize, flags, charset);
 
       /* If we didn't find the header, and the header contains a closing brace
       character, this may be a user error where the terminating colon
@@ -4097,6 +4143,41 @@ while (*s != 0)
        }
       }
 
+    case EITEM_AUTHRESULTS:
+      /* ${authresults {mysystemname}} */
+      {
+      uschar *sub_arg[1];
+
+      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, skipping, TRUE, name,
+                     &resetok))
+        {
+        case 1: goto EXPAND_FAILED_CURLY;
+        case 2:
+        case 3: goto EXPAND_FAILED;
+        }
+
+      yield = string_append(yield, 3,
+                       US"Authentication-Results: ", sub_arg[0], US"; none");
+      yield->ptr -= 6;
+
+      yield = authres_local(yield, sub_arg[0]);
+      yield = authres_iprev(yield);
+      yield = authres_smtpauth(yield);
+#ifdef SUPPORT_SPF
+      yield = authres_spf(yield);
+#endif
+#ifndef DISABLE_DKIM
+      yield = authres_dkim(yield);
+#endif
+#ifdef EXPERIMENTAL_DMARC
+      yield = authres_dmarc(yield);
+#endif
+#ifdef EXPERIMENTAL_ARC
+      yield = authres_arc(yield);
+#endif
+      continue;
+      }
+
     /* 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
@@ -4494,25 +4575,25 @@ while (*s != 0)
       if (skipping) continue;
 
       /* sub_arg[0] is the address */
-      domain = Ustrrchr(sub_arg[0],'@');
-      if ( (domain == NULL) || (domain == sub_arg[0]) || (Ustrlen(domain) == 1) )
+      if (  !(domain = Ustrrchr(sub_arg[0],'@'))
+        || 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
+      /* Calculate the hash. The third 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))
+      if (  sub_arg[2]
+         && (!isdigit(sub_arg[2][0]) || sub_arg[2][1] != 0))
         {
-        expand_string_message = US"prvs second argument must be a single digit";
+        expand_string_message = US"prvs third 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)
+      p = prvs_hmac_sha1(sub_arg[0], sub_arg[1], sub_arg[2], prvs_daystamp(7));
+      if (!p)
         {
         expand_string_message = US"prvs hmac-sha1 conversion failed";
         goto EXPAND_FAILED;
@@ -4628,7 +4709,7 @@ while (*s != 0)
             prvscheck_result = US"1";
             DEBUG(D_expand) debug_printf_indent("prvscheck: success, $pvrs_result set to 1\n");
             }
-            else
+         else
             {
             prvscheck_result = NULL;
             DEBUG(D_expand) debug_printf_indent("prvscheck: signature expired, $pvrs_result unset\n");
@@ -5790,7 +5871,8 @@ while (*s != 0)
       if (*s++ != '}')
         {                                              /*{*/
         expand_string_message = string_sprintf("missing } at end of condition "
-          "or expression inside \"%s\"", name);
+          "or expression inside \"%s\"; could be an unquoted } in the content",
+         name);
         goto EXPAND_FAILED;
         }
 
@@ -6495,7 +6577,7 @@ while (*s != 0)
        }
         continue;
 #else
-       expand_string_message = US"sha3 only supported with GnuTLS 3.5.0 +";
+       expand_string_message = US"sha3 only supported with GnuTLS 3.5.0 + or OpenSSL 1.1.1 +";
        goto EXPAND_FAILED;
 #endif
 
@@ -7478,10 +7560,9 @@ terminating brace. */
 
 if (ket_ends && *s == 0)
   {
-  expand_string_message = malformed_header?
-    US"missing } at end of string - could be header name not terminated by colon"
-    :
-    US"missing } at end of string";
+  expand_string_message = malformed_header
+    ? US"missing } at end of string - could be header name not terminated by colon"
+    : US"missing } at end of string";
   goto EXPAND_FAILED;
   }
 
@@ -7546,7 +7627,7 @@ DEBUG(D_expand)
   if (expand_string_forcedfail)
     debug_printf_indent(UTF8_UP_RIGHT "failure was forced\n");
   }
-if (resetok_p) *resetok_p = resetok;
+if (resetok_p && !resetok) *resetok_p = FALSE;
 expand_level--;
 return NULL;
 }
@@ -7560,28 +7641,35 @@ Returns:  the expanded string, or NULL if expansion failed; if failure was
           due to a lookup deferring, search_find_defer will be TRUE
 */
 
-uschar *
-expand_string(uschar *string)
+const uschar *
+expand_cstring(const uschar * string)
 {
-search_find_defer = FALSE;
-malformed_header = FALSE;
-return (Ustrpbrk(string, "$\\") == NULL)? string :
-  expand_string_internal(string, FALSE, NULL, FALSE, TRUE, NULL);
+if (Ustrpbrk(string, "$\\") != NULL)
+  {
+  int old_pool = store_pool;
+  uschar * s;
+
+  search_find_defer = FALSE;
+  malformed_header = FALSE;
+  store_pool = POOL_MAIN;
+    s = expand_string_internal(string, FALSE, NULL, FALSE, TRUE, NULL);
+  store_pool = old_pool;
+  return s;
+  }
+return string;
 }
 
 
-
-const uschar *
-expand_cstring(const uschar *string)
+uschar *
+expand_string(uschar * string)
 {
-search_find_defer = FALSE;
-malformed_header = FALSE;
-return (Ustrpbrk(string, "$\\") == NULL)? string :
-  expand_string_internal(string, FALSE, NULL, FALSE, TRUE, NULL);
+return US expand_cstring(CUS string);
 }
 
 
 
+
+
 /*************************************************
 *              Expand and copy                   *
 *************************************************/
@@ -7807,13 +7895,50 @@ return (  (  Ustrstr(s, "failed to expand") != NULL
 }
 
 
+/* Read given named file into big_buffer.  Use for keying material etc.
+The content will have an ascii NUL appended.
+
+Arguments:
+ filename      as it says
+
+Return:  pointer to buffer, or NULL on error.
+*/
+
+uschar *
+expand_file_big_buffer(const uschar * filename)
+{
+int fd, off = 0, len;
+
+if ((fd = open(CS filename, O_RDONLY)) < 0)
+  {
+  log_write(0, LOG_MAIN | LOG_PANIC, "unable to open file for reading: %s",
+            filename);
+  return NULL;
+  }
+
+do
+  {
+  if ((len = read(fd, big_buffer + off, big_buffer_size - 2 - off)) < 0)
+    {
+    (void) close(fd);
+    log_write(0, LOG_MAIN|LOG_PANIC, "unable to read file: %s", filename);
+    return NULL;
+    }
+  off += len;
+  }
+while (len > 0);
+
+(void) close(fd);
+big_buffer[off] = '\0';
+return big_buffer;
+}
+
+
 
 /*************************************************
 * Error-checking for testsuite                   *
 *************************************************/
 typedef struct {
-  const char * filename;
-  int          linenumber;
   uschar *     region_start;
   uschar *     region_end;
   const uschar *var_name;
@@ -7834,7 +7959,8 @@ if (var_data >= e->region_start  &&  var_data < e->region_end)
 void
 assert_no_variables(void * ptr, int len, const char * filename, int linenumber)
 {
-err_ctx e = {filename, linenumber, ptr, US ptr + len, NULL };
+err_ctx e = { .region_start = ptr, .region_end = US ptr + len,
+             .var_name = NULL, .var_data = NULL };
 int i;
 var_entry * v;
 
@@ -7855,10 +7981,16 @@ for (v = var_table; v < var_table + var_table_size; v++)
   if (v->type == vtype_stringptr)
     assert_variable_notin(US v->name, *(USS v->value), &e);
 
+/* check dns and address trees */
+tree_walk(tree_dns_fails,     assert_variable_notin, &e);
+tree_walk(tree_duplicates,    assert_variable_notin, &e);
+tree_walk(tree_nonrecipients, assert_variable_notin, &e);
+tree_walk(tree_unusable,      assert_variable_notin, &e);
+
 if (e.var_name)
   log_write(0, LOG_MAIN|LOG_PANIC_DIE,
     "live variable '%s' destroyed by reset_store at %s:%d\n- value '%.64s'",
-    e.var_name, e.filename, e.linenumber, e.var_data);
+    e.var_name, filename, linenumber, e.var_data);
 }