summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJeremy Harris <jgh146exb@wizmail.org>2023-08-24 20:22:43 +0100
committerJeremy Harris <jgh146exb@wizmail.org>2023-08-27 21:15:33 +0100
commit6d9b05ae272ca2122b48451c317d601e449af932 (patch)
tree624845f8c9d921dd0c2dd2c38c1443a32ca39abc /src
parent21b172df101c2c52faf0cc56a502395451975be9 (diff)
DMARC: add ARC info to history records
Diffstat (limited to 'src')
-rw-r--r--src/src/arc.c312
-rw-r--r--src/src/dkim.c4
-rw-r--r--src/src/dmarc.c42
-rw-r--r--src/src/dmarc.h4
-rw-r--r--src/src/functions.h3
-rw-r--r--src/src/spf.c35
6 files changed, 281 insertions, 119 deletions
diff --git a/src/src/arc.c b/src/src/arc.c
index ef44672f8..611697021 100644
--- a/src/src/arc.c
+++ b/src/src/arc.c
@@ -18,6 +18,10 @@
# include "pdkim/pdkim.h"
# include "pdkim/signing.h"
+# ifdef SUPPORT_DMARC
+# include "dmarc.h"
+# endif
+
extern pdkim_ctx * dkim_verify_ctx;
extern pdkim_ctx dkim_sign_ctx;
@@ -50,6 +54,7 @@ typedef struct arc_line {
blob s;
blob c;
blob l;
+ blob ip;
/* tag content sub-portions */
blob a_algo;
@@ -89,12 +94,43 @@ typedef struct arc_ctx {
#define HDR_AR US"Authentication-Results:"
#define HDRLEN_AR 23
+typedef enum line_extract {
+ le_instance_only,
+ le_instance_plus_ip,
+ le_all
+} line_extract_t;
+
static time_t now;
static time_t expire;
static hdr_rlist * headers_rlist;
static arc_ctx arc_sign_ctx = { NULL };
static arc_ctx arc_verify_ctx = { NULL };
+/* We build a context for either Sign or Verify.
+
+For Verify, it's a fresh new one for ACL verify=arc - there is no connection
+with the single line handling done during reception via the DKIM feed.
+
+For Verify we do it twice; initially during reception (via the DKIM feed)
+and then later for the full verification.
+
+The former only looks at AMS headers, to discover what hash(es) we need done for
+ARC on the message body; we call back to the DKIM code to set up so that it does
+them for us during reception. That call needs info from many of the AMS tags;
+arc_parse_line() for only the AMS is called asking for all the tag types.
+That context is then discarded.
+
+Later, for Verify, we look at ARC headers again and then grab the hash result
+from the DKIM layer. arc_parse_line() is called for all 3 line types,
+gathering info for only 'i' and 'ip' tags from AAR headers,
+for all tag types from AMS and AS headers.
+
+
+For Sign, while running through the existing headers (before adding any for
+this signing operation, we "take copies" of the headers, we call
+arc_parse_line() gathering only the 'i' tag (instance) information.
+*/
+
/******************************************************************************/
@@ -188,18 +224,23 @@ return NULL;
/* Inspect a header line, noting known tag fields.
-Check for duplicates. */
+Check for duplicate named tags.
+
+See the file block comment for how this is used.
+
+Return: NULL for good, or an error string
+*/
static uschar *
-arc_parse_line(arc_line * al, header_line * h, unsigned off, BOOL instance_only)
+arc_parse_line(arc_line * al, header_line * h, unsigned off, line_extract_t l_ext)
{
uschar * s = h->text + off;
-uschar * r = NULL; /* compiler-quietening */
+uschar * r = NULL;
uschar c;
al->complete = h;
-if (!instance_only)
+if (l_ext == le_all) /* need to grab rawsig_no_b */
{
al->rawsig_no_b_val.data = store_get(h->slen + 1, GET_TAINTED);
memcpy(al->rawsig_no_b_val.data, h->text, off); /* copy the header name blind */
@@ -218,75 +259,77 @@ while ((c = *s))
uschar * bstart = NULL, * bend;
/* tag-spec = [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS] */
+ /*X or just a naked FQDN, in a AAR ! */
- s = skip_fws(s); /* FWS */
+ s = skip_fws(s); /* leading FWS */
if (!*s) break;
-/* debug_printf("%s: consider '%s'\n", __FUNCTION__, s); */
tagchar = *s++;
- s = skip_fws(s); /* FWS */
- if (!*s) break;
+ if (!*(s = skip_fws(s))) break; /* FWS */
- if (!instance_only || tagchar == 'i') switch (tagchar)
+ switch (tagchar)
{
case 'a': /* a= AMS algorithm */
- {
- if (*s != '=') return US"no 'a' value";
- if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup";
-
- /* substructure: algo-hash (eg. rsa-sha256) */
-
- t = al->a_algo.data = al->a.data;
- while (*t != '-')
- if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value";
- al->a_algo.len = i;
- if (*t++ != '-') return US"no '-' in 'a' value";
- al->a_hash.data = t;
- al->a_hash.len = al->a.len - i - 1;
- }
+ if (l_ext == le_all && *s == '=')
+ {
+ if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup";
+
+ /* substructure: algo-hash (eg. rsa-sha256) */
+
+ t = al->a_algo.data = al->a.data;
+ while (*t != '-')
+ if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value";
+ al->a_algo.len = i;
+ if (*t++ != '-') return US"no '-' in 'a' value";
+ al->a_hash.data = t;
+ al->a_hash.len = al->a.len - i - 1;
+ }
break;
case 'b':
- {
- gstring * g = NULL;
-
- switch (*s)
+ if (l_ext == le_all)
{
- case '=': /* b= AMS signature */
- if (al->b.data) return US"already b data";
- bstart = s+1;
-
- /* The signature can have FWS inserted in the content;
- make a stripped copy */
-
- while ((c = *++s) && c != ';')
- if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
- g = string_catn(g, s, 1);
- if (!g) return US"no b= value";
- al->b.len = len_string_from_gstring(g, &al->b.data);
- gstring_release_unused(g);
- bend = s;
- break;
- case 'h': /* bh= AMS body hash */
- s = skip_fws(++s); /* FWS */
- if (*s != '=') return US"no bh value";
- if (al->bh.data) return US"already bh data";
-
- /* The bodyhash can have FWS inserted in the content;
- make a stripped copy */
-
- while ((c = *++s) && c != ';')
- if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
- g = string_catn(g, s, 1);
- if (!g) return US"no bh= value";
- al->bh.len = len_string_from_gstring(g, &al->bh.data);
- gstring_release_unused(g);
- break;
- default:
- return US"b? tag";
+ gstring * g = NULL;
+
+ switch (*s)
+ {
+ case '=': /* b= AMS signature */
+ if (al->b.data) return US"already b data";
+ bstart = s+1;
+
+ /* The signature can have FWS inserted in the content;
+ make a stripped copy */
+
+ while ((c = *++s) && c != ';')
+ if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
+ g = string_catn(g, s, 1);
+ if (!g) return US"no b= value";
+ al->b.len = len_string_from_gstring(g, &al->b.data);
+ gstring_release_unused(g);
+ bend = s;
+ break;
+ case 'h': /* bh= AMS body hash */
+ s = skip_fws(++s); /* FWS */
+ if (*s == '=')
+ {
+ if (al->bh.data) return US"already bh data";
+
+ /* The bodyhash can have FWS inserted in the content;
+ make a stripped copy */
+
+ while ((c = *++s) && c != ';')
+ if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
+ g = string_catn(g, s, 1);
+ if (!g) return US"no bh= value";
+ al->bh.len = len_string_from_gstring(g, &al->bh.data);
+ gstring_release_unused(g);
+ }
+ break;
+ default:
+ return US"b? tag";
+ }
}
- }
break;
case 'c':
- switch (*s)
+ if (l_ext == le_all) switch (*s)
{
case '=': /* c= AMS canonicalisation */
if (arc_insert_tagvalue(al, offsetof(arc_line, c), &s)) return US"c tag dup";
@@ -309,43 +352,62 @@ while ((c = *s))
}
break;
case 'v': /* cv= AS validity */
- if (*++s != '=') return US"cv tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s)) return US"cv tag dup";
+ s = skip_fws(s);
+ if (*++s == '=')
+ if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s))
+ return US"cv tag dup";
break;
- default:
- return US"c? tag";
}
break;
case 'd': /* d= AMS domain */
- if (*s != '=') return US"d tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s)) return US"d tag dup";
+ if (l_ext == le_all && *s == '=')
+ if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s))
+ return US"d tag dup";
break;
case 'h': /* h= AMS headers */
- if (*s != '=') return US"h tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s)) return US"h tag dup";
+ if (*s == '=')
+ if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s))
+ return US"h tag dup";
break;
case 'i': /* i= ARC set instance */
- if (*s != '=') return US"i tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s)) return US"i tag dup";
- if (instance_only) goto done;
+ if (*s == '=')
+ {
+ if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s))
+ return US"i tag dup";
+ if (l_ext == le_instance_only)
+ goto done; /* early-out */
+ }
break;
case 'l': /* l= bodylength */
- if (*s != '=') return US"l tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s)) return US"l tag dup";
+ if (l_ext == le_all && *s == '=')
+ if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s))
+ return US"l tag dup";
break;
- case 's': /* s= AMS selector */
- if (*s != '=') return US"s tag val";
- if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s)) return US"s tag dup";
+ case 's':
+ if (*s == '=' && l_ext == le_all)
+ {
+ if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s))
+ return US"s tag dup";
+ }
+ else if ( l_ext == le_instance_plus_ip
+ && Ustrncmp(s, "mtp.remote-ip", 13) == 0)
+ { /* smtp.remote-ip= AAR reception data */
+ s += 13;
+ s = skip_fws(s);
+ if (*s != '=') return US"smtp.remote_ip tag val";
+ if (arc_insert_tagvalue(al, offsetof(arc_line, ip), &s))
+ return US"ip tag dup";
+ }
break;
}
- while ((c = *s) && c != ';') s++;
+ while ((c = *s) && c != ';') s++; /* end of this tag=value */
if (c) s++; /* ; after tag-spec */
/* for all but the b= tag, copy the field including FWS. For the b=,
drop the tag content. */
- if (!instance_only)
+ if (r)
if (bstart)
{
size_t n = bstart - fieldstart;
@@ -366,7 +428,7 @@ while ((c = *s))
}
}
-if (!instance_only)
+if (r)
*r = '\0';
done:
@@ -381,7 +443,7 @@ adding instances as needed and checking for duplicate lines.
static uschar *
arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff,
- BOOL instance_only, arc_line ** alp_ret)
+ line_extract_t l_ext, arc_line ** alp_ret)
{
unsigned i;
arc_set * as;
@@ -390,10 +452,10 @@ uschar * e;
memset(al, 0, sizeof(arc_line));
-if ((e = arc_parse_line(al, h, off, instance_only)))
+if ((e = arc_parse_line(al, h, off, l_ext)))
{
DEBUG(D_acl) if (e) debug_printf("ARC: %s\n", e);
- return US"line parse";
+ return string_sprintf("line parse: %s", e);
}
if (!(i = arc_instance_from_hdr(al))) return US"instance find";
if (i > 50) return US"overlarge instance number";
@@ -407,9 +469,10 @@ return NULL;
+/* Called for both Sign and Verify */
static const uschar *
-arc_try_header(arc_ctx * ctx, header_line * h, BOOL instance_only)
+arc_try_header(arc_ctx * ctx, header_line * h, BOOL is_signing)
{
const uschar * e;
@@ -425,10 +488,10 @@ if (strncmpic(ARC_HDR_AAR, h->text, ARC_HDRLEN_AAR) == 0)
debug_printf("ARC: found AAR: %.*s\n", len, h->text);
}
if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AAR, offsetof(arc_set, hdr_aar),
- TRUE, NULL)))
+ is_signing ? le_instance_only : le_instance_plus_ip, NULL)))
{
DEBUG(D_acl) debug_printf("inserting AAR: %s\n", e);
- return US"inserting AAR";
+ return string_sprintf("inserting AAR: %s", e);
}
}
else if (strncmpic(ARC_HDR_AMS, h->text, ARC_HDRLEN_AMS) == 0)
@@ -444,10 +507,10 @@ else if (strncmpic(ARC_HDR_AMS, h->text, ARC_HDRLEN_AMS) == 0)
debug_printf("ARC: found AMS: %.*s\n", len, h->text);
}
if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AMS, offsetof(arc_set, hdr_ams),
- instance_only, &ams)))
+ is_signing ? le_instance_only : le_all, &ams)))
{
DEBUG(D_acl) debug_printf("inserting AMS: %s\n", e);
- return US"inserting AMS";
+ return string_sprintf("inserting AMS: %s", e);
}
/* defaults */
@@ -468,10 +531,10 @@ else if (strncmpic(ARC_HDR_AS, h->text, ARC_HDRLEN_AS) == 0)
debug_printf("ARC: found AS: %.*s\n", len, h->text);
}
if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AS, offsetof(arc_set, hdr_as),
- instance_only, NULL)))
+ is_signing ? le_instance_only : le_all, NULL)))
{
DEBUG(D_acl) debug_printf("inserting AS: %s\n", e);
- return US"inserting AS";
+ return string_sprintf("inserting AS: %s", e);
}
}
return NULL;
@@ -481,7 +544,8 @@ return NULL;
/* Gather the chain of arc sets from the headers.
Check for duplicates while that is done. Also build the
-reverse-order headers list;
+reverse-order headers list.
+Called on an ACL verify=arc condition.
Return: ARC state if determined, eg. by lack of any ARC chain.
*/
@@ -1194,7 +1258,8 @@ arc_line * al = (arc_line *)(as+1);
header_line * h = (header_line *)(al+1);
g = string_catn(g, ARC_HDR_AAR, ARC_HDRLEN_AAR);
-g = string_fmt_append(g, " i=%d; %s;\r\n\t", instance, identity);
+g = string_fmt_append(g, " i=%d; %s; smtp.remote-ip=%s;\r\n\t",
+ instance, identity, sender_host_address);
g = string_catn(g, US ar->data, ar->len);
h->slen = g->ptr - aar_off;
@@ -1773,7 +1838,7 @@ DEBUG(D_receive) debug_printf("ARC: spotted AMS header\n");
memset(&al, 0, sizeof(arc_line));
h.next = NULL;
h.slen = len_string_from_gstring(g, &h.text);
-if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, FALSE)))
+if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, le_all)))
{
DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr);
goto badline;
@@ -1887,13 +1952,70 @@ if (arc_state)
}
else if (arc_state_reason)
g = string_append(g, 3, US" (", arc_state_reason, US")");
- DEBUG(D_acl) debug_printf("ARC: authres '%.*s'\n",
+ DEBUG(D_acl) debug_printf("ARC:\tauthres '%.*s'\n",
gstring_length(g) - start - 3, g->s + start + 3);
}
else
- DEBUG(D_acl) debug_printf("ARC: no authres\n");
+ DEBUG(D_acl) debug_printf("ARC:\tno authres\n");
+return g;
+}
+
+
+# ifdef SUPPORT_DMARC
+/* Append a DMARC history record pair for ARC, to the given history set */
+
+gstring *
+arc_dmarc_hist_append(gstring * g)
+{
+if (arc_state)
+ {
+ BOOL first = TRUE;
+ int i = Ustrcmp(arc_state, "pass") == 0 ? ARES_RESULT_PASS
+ : Ustrcmp(arc_state, "fail") == 0 ? ARES_RESULT_FAIL
+ : ARES_RESULT_UNKNOWN;
+ g = string_fmt_append(g, "arc %d\n", i);
+ g = string_fmt_append(g, "arc_policy %d json[",
+ i == ARES_RESULT_PASS ? DMARC_ARC_POLICY_RESULT_PASS
+ : i == ARES_RESULT_FAIL ? DMARC_ARC_POLICY_RESULT_FAIL
+ : DMARC_ARC_POLICY_RESULT_UNUSED);
+ /*XXX would we prefer this backwards? */
+ for (arc_set * as = arc_verify_ctx.arcset_chain; as;
+ as = as->next, first = FALSE)
+ {
+ arc_line * line = as->hdr_as;
+ if (line)
+ {
+ blob * d = &line->d;
+ blob * s = &line->s;
+
+ if (!first)
+ g = string_catn(g, US",", 1);
+
+ g = string_fmt_append(g, " (\"i\":%u," /*)*/
+ " \"d\":\"%.*s\","
+ " \"s\":\"%.*s\"",
+ as->instance,
+ d->data ? (int)d->len : 0, d->data && d->len ? d->data : US"",
+ s->data ? (int)s->len : 0, s->data && s->len ? s->data : US""
+ );
+ if ((line = as->hdr_aar))
+ {
+ blob * ip = &line->ip;
+ if (ip->data && ip->len)
+ g = string_fmt_append(g, ", \"ip\":\"%.*s\"", (int)ip->len, ip->data);
+ }
+
+ g = string_catn(g, US")", 1);
+ }
+ }
+ g = string_catn(g, US" ]\n", 3);
+ }
+else
+ g = string_fmt_append(g, "arc %d\narc_policy $d json:[]\n",
+ ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
return g;
}
+# endif
# endif /* DISABLE_DKIM */
diff --git a/src/src/dkim.c b/src/src/dkim.c
index 068b802e0..a49c8d764 100644
--- a/src/src/dkim.c
+++ b/src/src/dkim.c
@@ -885,9 +885,9 @@ for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next)
DEBUG(D_acl)
if (gstring_length(g) == start)
- debug_printf("DKIM: no authres\n");
+ debug_printf("DKIM:\tno authres\n");
else
- debug_printf("DKIM: authres '%.*s'\n", g->ptr - start - 3, g->s + start + 3);
+ debug_printf("DKIM:\tauthres '%.*s'\n", g->ptr - start - 3, g->s + start + 3);
return g;
}
diff --git a/src/src/dmarc.c b/src/src/dmarc.c
index 070095660..042ebe982 100644
--- a/src/src/dmarc.c
+++ b/src/src/dmarc.c
@@ -97,7 +97,8 @@ int *netmask = NULL; /* Ignored */
int is_ipv6 = 0;
/* Set some sane defaults. Also clears previous results when
- * multiple messages in one connection. */
+multiple messages in one connection. */
+
dmarc_pctx = NULL;
dmarc_status = US"none";
dmarc_abort = FALSE;
@@ -153,11 +154,12 @@ return OK;
}
-/* dmarc_store_data stores the header data so that subsequent
-dmarc_process can access the data */
+/* dmarc_store_data stores the header data so that subsequent dmarc_process can
+access the data.
+Called after the entire message has been received, with the From: header. */
int
-dmarc_store_data(header_line *hdr)
+dmarc_store_data(header_line * hdr)
{
/* No debug output because would change every test debug output */
if (!f.dmarc_disable_verify)
@@ -167,7 +169,7 @@ return OK;
static void
-dmarc_send_forensic_report(u_char **ruf)
+dmarc_send_forensic_report(u_char ** ruf)
{
uschar *recipient, *save_sender;
BOOL send_status = FALSE;
@@ -254,15 +256,19 @@ if (!dmarc_history_file)
return DMARC_HIST_DISABLED;
}
if (!host_checking)
- if ((history_file_fd = log_open_as_exim(dmarc_history_file)) < 0)
+ {
+ uschar * s = string_copy(dmarc_history_file); /* need a writeable copy */
+ if ((history_file_fd = log_open_as_exim(s)) < 0)
{
log_write(0, LOG_MAIN|LOG_PANIC,
"failure to create DMARC history file: %s: %s",
- dmarc_history_file, strerror(errno));
+ s, strerror(errno));
return DMARC_HIST_FILE_ERR;
}
+ }
+
+/* Generate the contents of the history file entry */
-/* Generate the contents of the history file */
g = string_fmt_append(NULL,
"job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n",
message_id, primary_hostname, time(NULL), sender_host_address,
@@ -301,6 +307,15 @@ g = string_fmt_append(g, "sp %d\n", tmp_ans);
g = string_fmt_append(g, "align_dkim %d\nalign_spf %d\naction %d\n",
da, sa, action);
+#if DMARC_API >= 100400
+# ifdef EXPERIMENTAL_ARC
+g = arc_dmarc_hist_append(g);
+# else
+g = string_fmt_append(g, "arc %d\narc_policy $d json:[]\n",
+ ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
+# endif
+#endif
+
/* Write the contents to the history file */
DEBUG(D_receive)
{
@@ -331,7 +346,8 @@ return DMARC_HIST_OK;
/* dmarc_process adds the envelope sender address to the existing
context (if any), retrieves the result, sets up expansion
-strings and evaluates the condition outcome. */
+strings and evaluates the condition outcome.
+Called for the first ACL dmarc= condition. */
int
dmarc_process(void)
@@ -536,7 +552,7 @@ The EDITME provides a DMARC_API variable */
break;
}
-/* Store the policy string in an expandable variable. */
+ /* Store the policy string in an expandable variable. */
libdm_status = opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
for (c = 0; dmarc_policy_description[c].name; c++)
@@ -661,10 +677,16 @@ authres_dmarc(gstring * g)
{
if (f.dmarc_has_been_checked)
{
+ int start = 0; /* Compiler quietening */
+ DEBUG(D_acl) start = gstring_length(g);
g = string_append(g, 2, US";\n\tdmarc=", dmarc_pass_fail);
if (header_from_sender)
g = string_append(g, 2, US" header.from=", header_from_sender);
+ DEBUG(D_acl) debug_printf("DMARC:\tauthres '%.*s'\n",
+ gstring_length(g) - start - 3, g->s + start + 3);
}
+else
+ DEBUG(D_acl) debug_printf("DMARC:\tno authres\n");
return g;
}
diff --git a/src/src/dmarc.h b/src/src/dmarc.h
index 7ce0ca953..fa366dd06 100644
--- a/src/src/dmarc.h
+++ b/src/src/dmarc.h
@@ -58,4 +58,8 @@ uschar *dmarc_exim_expand_defaults(int);
#define ARES_RESULT_UNKNOWN 11
#define ARES_RESULT_DISCARD 12
+#define DMARC_ARC_POLICY_RESULT_PASS 0
+#define DMARC_ARC_POLICY_RESULT_UNUSED 1
+#define DMARC_ARC_POLICY_RESULT_FAIL 2
+
#endif /* SUPPORT_DMARC */
diff --git a/src/src/functions.h b/src/src/functions.h
index 0b030e4fe..5db9bc610 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -109,6 +109,9 @@ extern tree_node *acl_var_create(uschar *);
extern void acl_var_write(uschar *, uschar *, void *);
#ifdef EXPERIMENTAL_ARC
+# ifdef SUPPORT_DMARC
+extern gstring *arc_dmarc_hist_append(gstring *);
+# endif
extern void *arc_ams_setup_sign_bodyhash(void);
extern const uschar *arc_header_feed(gstring *, BOOL);
extern gstring *arc_sign(const uschar *, gstring *, uschar **);
diff --git a/src/src/spf.c b/src/src/spf.c
index 6f0917a9c..3d83f07ba 100644
--- a/src/src/spf.c
+++ b/src/src/spf.c
@@ -401,20 +401,31 @@ gstring *
authres_spf(gstring * g)
{
uschar * s;
-if (!spf_result) return g;
-
-g = string_append(g, 2, US";\n\tspf=", spf_result);
-if (spf_result_guessed)
- g = string_cat(g, US" (best guess record for domain)");
+if (spf_result)
+ {
+ int start = 0; /* Compiler quietening */
+ DEBUG(D_acl) start = gstring_length(g);
-s = expand_string(US"$sender_address_domain");
-if (s && *s)
- return string_append(g, 2, US" smtp.mailfrom=", s);
+ g = string_append(g, 2, US";\n\tspf=", spf_result);
+ if (spf_result_guessed)
+ g = string_cat(g, US" (best guess record for domain)");
-s = sender_helo_name;
-return s && *s
- ? string_append(g, 2, US" smtp.helo=", s)
- : string_cat(g, US" smtp.mailfrom=<>");
+ s = expand_string(US"$sender_address_domain");
+ if (s && *s)
+ g = string_append(g, 2, US" smtp.mailfrom=", s);
+ else
+ {
+ s = sender_helo_name;
+ g = s && *s
+ ? string_append(g, 2, US" smtp.helo=", s)
+ : string_cat(g, US" smtp.mailfrom=<>");
+ }
+ DEBUG(D_acl) debug_printf("SPF:\tauthres '%.*s'\n",
+ gstring_length(g) - start - 3, g->s + start + 3);
+ }
+else
+ DEBUG(D_acl) debug_printf("SPF:\tno authres\n");
+return g;
}