9741a3a45f0956d743ae18f9ee6f42486bd39717
[exim.git] / src / src / miscmods / dmarc.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4 /* DMARC support.
5    Copyright (c) The Exim Maintainers 2019 - 2024
6    Copyright (c) Todd Lyons <tlyons@exim.org> 2012 - 2014
7    License: GPL */
8 /* SPDX-License-Identifier: GPL-2.0-or-later */
9
10 /* Portions Copyright (c) 2012, 2013, The Trusted Domain Project;
11    All rights reserved, licensed for use per LICENSE.opendmarc. */
12
13 /* Code for calling dmarc checks via libopendmarc. Called from acl.c. */
14
15 #include "../exim.h"
16 #ifdef SUPPORT_DMARC
17 # if !defined SUPPORT_SPF
18 #  error SPF must also be enabled for DMARC
19 # elif defined DISABLE_DKIM
20 #  error DKIM must also be enabled for DMARC
21 # else
22
23 #  include "../functions.h"
24 #  include "dmarc.h"
25 #  include "pdkim.h"
26
27 OPENDMARC_LIB_T     dmarc_ctx;
28 DMARC_POLICY_T     *dmarc_pctx = NULL;
29 OPENDMARC_STATUS_T  libdm_status, action, dmarc_policy;
30 OPENDMARC_STATUS_T  da, sa, action;
31 BOOL dmarc_abort  = FALSE;
32 uschar *dmarc_pass_fail = US"skipped";
33 header_line *from_header   = NULL;
34
35 static misc_module_info * dmarc_dkim_mod_info;
36 static misc_module_info * dmarc_spf_mod_info;
37 SPF_response_t   *spf_response_p;
38 int dmarc_spf_ares_result  = 0;
39 uschar *spf_sender_domain  = NULL;
40 uschar *spf_human_readable = NULL;
41 u_char *header_from_sender = NULL;
42 int history_file_status    = DMARC_HIST_OK;
43
44 typedef struct dmarc_exim_p {
45   uschar *name;
46   int    value;
47 } dmarc_exim_p;
48
49 static dmarc_exim_p dmarc_policy_description[] = {
50   /* name               value */
51   { US"",           DMARC_RECORD_P_UNSPECIFIED },
52   { US"none",       DMARC_RECORD_P_NONE },
53   { US"quarantine", DMARC_RECORD_P_QUARANTINE },
54   { US"reject",     DMARC_RECORD_P_REJECT },
55   { NULL,           0 }
56 };
57
58
59 /* $variables */
60 uschar * dmarc_domain_policy     = NULL; /* Declared policy of used domain */
61 uschar * dmarc_status            = NULL; /* One word value */
62 uschar * dmarc_status_text       = NULL; /* Human readable value */
63 uschar * dmarc_used_domain       = NULL; /* Domain libopendmarc chose for DMARC policy lookup */
64
65 /* options */
66 uschar * dmarc_forensic_sender   = NULL; /* Set sender address for forensic reports */
67 uschar * dmarc_history_file      = NULL; /* File to store dmarc results */
68 uschar * dmarc_tld_file          = NULL; /* Mozilla TLDs text file */
69
70
71 /* One-time initialisation for dmarc.  Ensure the spf module is available. */
72
73 static BOOL
74 dmarc_init(void * dummy)
75 {
76 uschar * errstr;
77 if (!(dmarc_spf_mod_info = misc_mod_find(US"spf", &errstr)))
78   {
79   log_write(0, LOG_MAIN|LOG_PANIC, "dmarc: %s", errstr);
80   return FALSE;
81   }
82
83 if (!(dmarc_dkim_mod_info = misc_mod_find(US"dkim", &errstr)))
84   {
85   log_write(0, LOG_MAIN|LOG_PANIC, "dmarc: %s", errstr);
86   return FALSE;
87   }
88
89 return TRUE;
90 }
91
92 static gstring *
93 dmarc_version_report(gstring * g)
94 {
95 return string_fmt_append(g, "Library version: dmarc: Compile: %d.%d.%d.%d\n",
96     (OPENDMARC_LIB_VERSION & 0xff000000) >> 24,
97     (OPENDMARC_LIB_VERSION & 0x00ff0000) >> 16,
98     (OPENDMARC_LIB_VERSION & 0x0000ff00) >> 8,
99     (OPENDMARC_LIB_VERSION & 0x000000ff));
100 }
101
102
103 /* Accept an error_block struct, initialize if empty, parse to the
104 end, and append the two strings passed to it.  Used for adding
105 variable amounts of value:pair data to the forensic emails. */
106
107 static error_block *
108 add_to_eblock(error_block *eblock, uschar *t1, uschar *t2)
109 {
110 error_block *eb = store_malloc(sizeof(error_block));
111 if (!eblock)
112   eblock = eb;
113 else
114   {
115   /* Find the end of the eblock struct and point it at eb */
116   error_block *tmp = eblock;
117   while(tmp->next)
118     tmp = tmp->next;
119   tmp->next = eb;
120   }
121 eb->text1 = t1;
122 eb->text2 = t2;
123 eb->next  = NULL;
124 return eblock;
125 }
126
127 /* dmarc_msg_init sets up a context that can be re-used for several
128 messages on the same SMTP connection (that come from the
129 same host with the same HELO string).
130 However, we seem to only use it for one; we destroy some sort of context
131 at the tail end of dmarc_process(). */
132
133 static int
134 dmarc_msg_init()
135 {
136 int *netmask   = NULL;   /* Ignored */
137 int is_ipv6    = 0;
138
139 /* Set some sane defaults.  Also clears previous results when
140 multiple messages in one connection. */
141
142 dmarc_pctx         = NULL;
143 dmarc_status       = US"none";
144 dmarc_abort        = FALSE;
145 dmarc_pass_fail    = US"skipped";
146 dmarc_used_domain  = US"";
147 f.dmarc_has_been_checked = FALSE;
148 header_from_sender = NULL;
149 spf_response_p     = NULL;
150 spf_sender_domain  = NULL;
151 spf_human_readable = NULL;
152
153 /* ACLs have "control=dmarc_disable_verify" */
154 if (f.dmarc_disable_verify)
155   return OK;
156
157 (void) memset(&dmarc_ctx, '\0', sizeof dmarc_ctx);
158 dmarc_ctx.nscount = 0;
159 libdm_status = opendmarc_policy_library_init(&dmarc_ctx);
160 if (libdm_status != DMARC_PARSE_OKAY)
161   {
162   log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to init library: %s",
163                        opendmarc_policy_status_to_str(libdm_status));
164   dmarc_abort = TRUE;
165   }
166 if (!dmarc_tld_file || !*dmarc_tld_file)
167   {
168   DEBUG(D_receive) debug_printf_indent("DMARC: no dmarc_tld_file\n");
169   dmarc_abort = TRUE;
170   }
171 else if (opendmarc_tld_read_file(CS dmarc_tld_file, NULL, NULL, NULL))
172   {
173   log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to load tld list '%s': %s",
174                        dmarc_tld_file, strerror(errno));
175   dmarc_abort = TRUE;
176   }
177 if (!sender_host_address)
178   {
179   DEBUG(D_receive) debug_printf_indent("DMARC: no sender_host_address\n");
180   dmarc_abort = TRUE;
181   }
182 /* This catches locally originated email and startup errors above. */
183 if (!dmarc_abort)
184   {
185   is_ipv6 = string_is_ip_address(sender_host_address, netmask) == 6;
186   if (!(dmarc_pctx = opendmarc_policy_connect_init(sender_host_address, is_ipv6)))
187     {
188     log_write(0, LOG_MAIN|LOG_PANIC,
189       "DMARC failure creating policy context: ip=%s", sender_host_address);
190     dmarc_abort = TRUE;
191     }
192   }
193
194 return OK;
195 }
196
197
198 static void
199 dmarc_smtp_reset(void)
200 {
201 f.dmarc_has_been_checked = f.dmarc_disable_verify =
202 f.dmarc_enable_forensic = FALSE;
203 dmarc_domain_policy = dmarc_status = dmarc_status_text =
204 dmarc_used_domain = NULL;
205 }
206
207
208 /* dmarc_store_data stores the header data so that subsequent dmarc_process can
209 access the data.
210 Called after the entire message has been received, with the From: header. */
211
212 static int
213 dmarc_store_data(header_line * hdr)
214 {
215 /* No debug output because would change every test debug output */
216 if (!f.dmarc_disable_verify)
217   from_header = hdr;
218 return OK;
219 }
220
221
222 static void
223 dmarc_send_forensic_report(u_char ** ruf)
224 {
225 uschar *recipient, *save_sender;
226 BOOL  send_status = FALSE;
227 error_block *eblock = NULL;
228 FILE *message_file = NULL;
229
230 /* Earlier ACL does not have *required* control=dmarc_enable_forensic */
231 if (!f.dmarc_enable_forensic)
232   return;
233
234 if (  dmarc_policy == DMARC_POLICY_REJECT     && action == DMARC_RESULT_REJECT
235    || dmarc_policy == DMARC_POLICY_QUARANTINE && action == DMARC_RESULT_QUARANTINE
236    || dmarc_policy == DMARC_POLICY_NONE       && action == DMARC_RESULT_REJECT
237    || dmarc_policy == DMARC_POLICY_NONE       && action == DMARC_RESULT_QUARANTINE
238    )
239   if (ruf)
240     {
241     eblock = add_to_eblock(eblock, US"Sender Domain", dmarc_used_domain);
242     eblock = add_to_eblock(eblock, US"Sender IP Address", sender_host_address);
243     eblock = add_to_eblock(eblock, US"Received Date", tod_stamp(tod_full));
244     eblock = add_to_eblock(eblock, US"SPF Alignment",
245                      sa == DMARC_POLICY_SPF_ALIGNMENT_PASS ? US"yes" : US"no");
246     eblock = add_to_eblock(eblock, US"DKIM Alignment",
247                      da == DMARC_POLICY_DKIM_ALIGNMENT_PASS ? US"yes" : US"no");
248     eblock = add_to_eblock(eblock, US"DMARC Results", dmarc_status_text);
249
250     for (int c = 0; ruf[c]; c++)
251       {
252       recipient = string_copylc(ruf[c]);
253       if (Ustrncmp(recipient, "mailto:",7))
254         continue;
255       /* Move to first character past the colon */
256       recipient += 7;
257       DEBUG(D_receive)
258         debug_printf_indent("DMARC forensic report to %s%s\n", recipient,
259              (host_checking || f.running_in_test_harness) ? " (not really)" : "");
260       if (host_checking || f.running_in_test_harness)
261         continue;
262
263       if (!moan_send_message(recipient, ERRMESS_DMARC_FORENSIC, eblock,
264                             header_list, message_file, NULL))
265         log_write(0, LOG_MAIN|LOG_PANIC,
266           "failure to send DMARC forensic report to %s", recipient);
267       }
268     }
269 }
270
271
272 /* Look up a DNS dmarc record for the given domain.  Return it or NULL */
273
274 static uschar *
275 dmarc_dns_lookup(uschar * dom)
276 {
277 dns_answer * dnsa = store_get_dns_answer();
278 dns_scan dnss;
279 int rc = dns_lookup(dnsa, string_sprintf("_dmarc.%s", dom), T_TXT, NULL);
280
281 if (rc == DNS_SUCCEED)
282   for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
283        rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
284     if (rr->type == T_TXT && rr->size > 3)
285       {
286       uschar *record = string_copyn_taint(US rr->data, rr->size, GET_TAINTED);
287       store_free_dns_answer(dnsa);
288       return record;
289       }
290 store_free_dns_answer(dnsa);
291 return NULL;
292 }
293
294
295 static int
296 dmarc_write_history_file(const gstring * dkim_history_buffer)
297 {
298 int history_file_fd = 0;
299 ssize_t written_len;
300 int tmp_ans;
301 u_char ** rua; /* aggregate report addressees */
302 gstring * g;
303
304 if (!dmarc_history_file)
305   {
306   DEBUG(D_receive) debug_printf_indent("DMARC history file not set\n");
307   return DMARC_HIST_DISABLED;
308   }
309 if (!host_checking)
310   {
311   uschar * s = string_copy(dmarc_history_file);         /* need a writeable copy */
312   if ((history_file_fd = log_open_as_exim(s)) < 0)
313     {
314     log_write(0, LOG_MAIN|LOG_PANIC,
315               "failure to create DMARC history file: %s: %s",
316               s, strerror(errno));
317     return DMARC_HIST_FILE_ERR;
318     }
319   }
320
321 /* Generate the contents of the history file entry */
322
323 g = string_fmt_append(NULL,
324   "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n",
325   message_id, primary_hostname, time(NULL), sender_host_address,
326   header_from_sender, expand_string(US"$sender_address_domain"));
327
328 if (spf_response_p)
329   g = string_fmt_append(g, "spf %d\n", dmarc_spf_ares_result);
330
331 if (dkim_history_buffer)
332   g = string_fmt_append(g, "%Y", dkim_history_buffer);
333
334 g = string_fmt_append(g, "pdomain %s\npolicy %d\n",
335   dmarc_used_domain, dmarc_policy);
336
337 if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1)))
338   for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++)
339     g = string_fmt_append(g, "rua %s\n", rua[tmp_ans]);
340 else
341   g = string_catn(g, US"rua -\n", 6);
342
343 opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans);
344 g = string_fmt_append(g, "pct %d\n", tmp_ans);
345
346 opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans);
347 g = string_fmt_append(g, "adkim %d\n", tmp_ans);
348
349 opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans);
350 g = string_fmt_append(g, "aspf %d\n", tmp_ans);
351
352 opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
353 g = string_fmt_append(g, "p %d\n", tmp_ans);
354
355 opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans);
356 g = string_fmt_append(g, "sp %d\n", tmp_ans);
357
358 g = string_fmt_append(g, "align_dkim %d\nalign_spf %d\naction %d\n",
359   da, sa, action);
360
361 #if DMARC_API >= 100400
362 # ifdef EXPERIMENTAL_ARC
363   {
364   const misc_module_info * mi = misc_mod_findonly(US"arc");
365   const uschar * s;
366   gstring * g2 = NULL;
367   typedef const uschar * (*fn_t)(gstring **);
368
369   if (mi && (s = (((fn_t *) mi->functions)[ARC_ARCSET_INFO]) (&g2)))
370     {
371     int i = Ustrcmp(s, "pass") == 0 ? ARES_RESULT_PASS
372             : Ustrcmp(s, "fail") == 0 ? ARES_RESULT_FAIL
373             : ARES_RESULT_UNKNOWN;
374
375     g = string_fmt_append(g, "arc %d\n"
376                              "arc_policy %d json[%#Y ]\n",
377                           i,
378                           i == ARES_RESULT_PASS ? DMARC_ARC_POLICY_RESULT_PASS
379                           : i == ARES_RESULT_FAIL ? DMARC_ARC_POLICY_RESULT_FAIL
380                           : DMARC_ARC_POLICY_RESULT_UNUSED,
381                           g2
382                           );
383     }
384   else
385     string_fmt_append(g, "arc %d\narc_policy %d json:[]\n",
386                         ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
387   }
388
389 # else
390 g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n",
391                       ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
392 # endif
393 #endif
394
395 /* Write the contents to the history file */
396 DEBUG(D_receive)
397   {
398   debug_printf_indent("DMARC logging history data for opendmarc reporting%s\n",
399              host_checking ? " (not really)" : "");
400   debug_printf_indent("DMARC history data for debugging:\n");
401   expand_level++;
402     debug_printf_indent("%Y", g);
403   expand_level--;
404   }
405
406 if (!host_checking)
407   {
408   written_len = write_to_fd_buf(history_file_fd,
409                                 g->s,
410                                 gstring_length(g));
411   if (written_len == 0)
412     {
413     log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s",
414                            dmarc_history_file);
415     return DMARC_HIST_WRITE_ERR;
416     }
417   (void)close(history_file_fd);
418   }
419 return DMARC_HIST_OK;
420 }
421
422
423 /* dmarc_process adds the envelope sender address to the existing
424 context (if any), retrieves the result, sets up expansion
425 strings and evaluates the condition outcome.
426 Called for the first ACL dmarc= condition. */
427
428 static int
429 dmarc_process(void)
430 {
431 int sr, origin;             /* used in SPF section */
432 int dmarc_spf_result  = 0;  /* stores spf into dmarc conn ctx */
433 int tmp_ans, c;
434 uschar * rr;
435 BOOL has_dmarc_record = TRUE;
436 u_char ** ruf; /* forensic report addressees, if called for */
437
438 /* ACLs have "control=dmarc_disable_verify" */
439 if (f.dmarc_disable_verify)
440   return OK;
441
442 /* Store the header From: sender domain for this part of DMARC.
443 If there is no from_header struct, then it's likely this message
444 is locally generated and relying on fixups to add it.  Just skip
445 the entire DMARC system if we can't find a From: header....or if
446 there was a previous error.  */
447
448 if (!from_header)
449   {
450   DEBUG(D_receive) debug_printf_indent("DMARC: no From: header\n");
451   dmarc_abort = TRUE;
452   }
453 else if (!dmarc_abort)
454   {
455   uschar * errormsg;
456   int dummy, domain;
457   uschar * p;
458   uschar saveend;
459
460   f.parse_allow_group = TRUE;
461   p = parse_find_address_end(from_header->text, FALSE);
462   saveend = *p; *p = '\0';
463   if ((header_from_sender = parse_extract_address(from_header->text, &errormsg,
464                               &dummy, &dummy, &domain, FALSE)))
465     header_from_sender += domain;
466   *p = saveend;
467
468   /* The opendmarc library extracts the domain from the email address, but
469   only try to store it if it's not empty.  Otherwise, skip out of DMARC. */
470
471   if (!header_from_sender || (strcmp( CCS header_from_sender, "") == 0))
472     dmarc_abort = TRUE;
473   libdm_status = dmarc_abort
474     ? DMARC_PARSE_OKAY
475     : opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender);
476   if (libdm_status != DMARC_PARSE_OKAY)
477     {
478     log_write(0, LOG_MAIN|LOG_PANIC,
479               "failure to store header From: in DMARC: %s, header was '%s'",
480               opendmarc_policy_status_to_str(libdm_status), from_header->text);
481     dmarc_abort = TRUE;
482     }
483   }
484
485 /* Skip DMARC if connection is SMTP Auth. Temporarily, admin should
486 instead do this in the ACLs.  */
487
488 if (!dmarc_abort && !sender_host_authenticated)
489   {
490   uschar * dmarc_domain;
491   gstring * dkim_history_buffer = NULL;
492   typedef const pdkim_signature * (*sigs_fn_t)(void);
493
494   /* Use the envelope sender domain for this part of DMARC */
495
496   spf_sender_domain = expand_string(US"$sender_address_domain");
497
498     {
499     typedef SPF_response_t * (*fn_t)(void);
500     if (dmarc_spf_mod_info)
501       /*XXX ugly use of a pointer */
502       spf_response_p = ((fn_t *) dmarc_spf_mod_info->functions)[SPF_GET_RESPONSE]();
503     }
504
505   if (!spf_response_p)
506     {
507     /* No spf data means null envelope sender so generate a domain name
508     from the sender_helo_name  */
509
510     if (!spf_sender_domain)
511       {
512       spf_sender_domain = sender_helo_name;
513       log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n",
514                              spf_sender_domain);
515       DEBUG(D_receive)
516         debug_printf_indent("DMARC using synthesized SPF sender domain = %s\n",
517           spf_sender_domain);
518       }
519     dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE;
520     dmarc_spf_ares_result = ARES_RESULT_UNKNOWN;
521     origin = DMARC_POLICY_SPF_ORIGIN_HELO;
522     spf_human_readable = US"";
523     }
524   else
525     {
526     sr = spf_response_p->result;
527     dmarc_spf_result = sr == SPF_RESULT_NEUTRAL  ? DMARC_POLICY_SPF_OUTCOME_NONE :
528                        sr == SPF_RESULT_PASS     ? DMARC_POLICY_SPF_OUTCOME_PASS :
529                        sr == SPF_RESULT_FAIL     ? DMARC_POLICY_SPF_OUTCOME_FAIL :
530                        sr == SPF_RESULT_SOFTFAIL ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL :
531                        DMARC_POLICY_SPF_OUTCOME_NONE;
532     dmarc_spf_ares_result = sr == SPF_RESULT_NEUTRAL   ? ARES_RESULT_NEUTRAL :
533                             sr == SPF_RESULT_PASS      ? ARES_RESULT_PASS :
534                             sr == SPF_RESULT_FAIL      ? ARES_RESULT_FAIL :
535                             sr == SPF_RESULT_SOFTFAIL  ? ARES_RESULT_SOFTFAIL :
536                             sr == SPF_RESULT_NONE      ? ARES_RESULT_NONE :
537                             sr == SPF_RESULT_TEMPERROR ? ARES_RESULT_TEMPERROR :
538                             sr == SPF_RESULT_PERMERROR ? ARES_RESULT_PERMERROR :
539                             ARES_RESULT_UNKNOWN;
540     origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM;
541     spf_human_readable = US spf_response_p->header_comment;
542     DEBUG(D_receive)
543       debug_printf_indent("DMARC using SPF sender domain = %s\n", spf_sender_domain);
544     }
545   if (strcmp( CCS spf_sender_domain, "") == 0)
546     dmarc_abort = TRUE;
547   if (!dmarc_abort)
548     {
549     libdm_status = opendmarc_policy_store_spf(dmarc_pctx, spf_sender_domain,
550                                 dmarc_spf_result, origin, spf_human_readable);
551     if (libdm_status != DMARC_PARSE_OKAY)
552       log_write(0, LOG_MAIN|LOG_PANIC, "failure to store spf for DMARC: %s",
553                            opendmarc_policy_status_to_str(libdm_status));
554     }
555
556   /* Now we cycle through the dkim signature results and put into
557   the opendmarc context, further building the DMARC reply. */
558
559   for(const pdkim_signature * sig =
560               (((sigs_fn_t *)dmarc_dkim_mod_info->functions)[DKIM_SIGS_LIST])();
561       sig; sig = sig->next)
562     {
563     int dkim_result, dkim_ares_result, vs, ves;
564
565     vs  = sig->verify_status & ~PDKIM_VERIFY_POLICY;
566     ves = sig->verify_ext_status;
567     dkim_result = vs == PDKIM_VERIFY_PASS ? DMARC_POLICY_DKIM_OUTCOME_PASS :
568                   vs == PDKIM_VERIFY_FAIL ? DMARC_POLICY_DKIM_OUTCOME_FAIL :
569                   vs == PDKIM_VERIFY_INVALID ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL :
570                   DMARC_POLICY_DKIM_OUTCOME_NONE;
571     libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, US sig->domain,
572
573 /* The opendmarc project broke its API in a way we can't detect easily.
574 The EDITME provides a DMARC_API variable */
575 #if DMARC_API >= 100400
576                                                sig->selector,
577 #endif
578                                                dkim_result, US"");
579     DEBUG(D_receive)
580       debug_printf_indent("DMARC adding DKIM sender domain = %s\n", sig->domain);
581     if (libdm_status != DMARC_PARSE_OKAY)
582       log_write(0, LOG_MAIN|LOG_PANIC,
583                 "failure to store dkim (%s) for DMARC: %s",
584                 sig->domain, opendmarc_policy_status_to_str(libdm_status));
585
586     dkim_ares_result =
587       vs == PDKIM_VERIFY_PASS    ? ARES_RESULT_PASS :
588       vs == PDKIM_VERIFY_FAIL    ? ARES_RESULT_FAIL :
589       vs == PDKIM_VERIFY_NONE    ? ARES_RESULT_NONE :
590       vs == PDKIM_VERIFY_INVALID ?
591        ves == PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE ? ARES_RESULT_PERMERROR :
592        ves == PDKIM_VERIFY_INVALID_BUFFER_SIZE        ? ARES_RESULT_PERMERROR :
593        ves == PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD   ? ARES_RESULT_PERMERROR :
594        ves == PDKIM_VERIFY_INVALID_PUBKEY_IMPORT      ? ARES_RESULT_PERMERROR :
595        ARES_RESULT_UNKNOWN :
596       ARES_RESULT_UNKNOWN;
597 #if DMARC_API >= 100400
598     dkim_history_buffer = string_fmt_append(dkim_history_buffer,
599       "dkim %s %s %d\n", sig->domain, sig->selector, dkim_ares_result);
600 #else
601     dkim_history_buffer = string_fmt_append(dkim_history_buffer,
602       "dkim %s %d\n", sig->domain, dkim_ares_result);
603 #endif
604     }
605
606   /* Look up DMARC policy record in DNS.  We do this explicitly, rather than
607   letting the dmarc library do it with opendmarc_policy_query_dmarc(), so that
608   our dns access path is used for debug tracing and for the testsuite
609   diversion. */
610
611   libdm_status = (rr = dmarc_dns_lookup(header_from_sender))
612     ? opendmarc_policy_store_dmarc(dmarc_pctx, rr, header_from_sender, NULL)
613     : DMARC_DNS_ERROR_NO_RECORD;
614   switch (libdm_status)
615     {
616     case DMARC_DNS_ERROR_NXDOMAIN:
617     case DMARC_DNS_ERROR_NO_RECORD:
618       DEBUG(D_receive)
619         debug_printf_indent("DMARC no record found for %s\n", header_from_sender);
620       has_dmarc_record = FALSE;
621       break;
622     case DMARC_PARSE_OKAY:
623       DEBUG(D_receive)
624         debug_printf_indent("DMARC record found for %s\n", header_from_sender);
625       break;
626     case DMARC_PARSE_ERROR_BAD_VALUE:
627       DEBUG(D_receive)
628         debug_printf_indent("DMARC record parse error for %s\n", header_from_sender);
629       has_dmarc_record = FALSE;
630       break;
631     default:
632       /* everything else, skip dmarc */
633       DEBUG(D_receive)
634         debug_printf_indent("DMARC skipping (%s), unsure what to do with %s",
635                       opendmarc_policy_status_to_str(libdm_status),
636                       from_header->text);
637       has_dmarc_record = FALSE;
638       break;
639     }
640
641   /* Store the policy string in an expandable variable. */
642
643   libdm_status = opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
644   for (c = 0; dmarc_policy_description[c].name; c++)
645     if (tmp_ans == dmarc_policy_description[c].value)
646       {
647       dmarc_domain_policy = string_sprintf("%s",dmarc_policy_description[c].name);
648       break;
649       }
650
651   /* Can't use exim's string manipulation functions so allocate memory
652   for libopendmarc using its max hostname length definition. */
653
654   dmarc_domain = store_get(DMARC_MAXHOSTNAMELEN, GET_TAINTED);
655   libdm_status = opendmarc_policy_fetch_utilized_domain(dmarc_pctx,
656     dmarc_domain, DMARC_MAXHOSTNAMELEN-1);
657   store_release_above(dmarc_domain + Ustrlen(dmarc_domain)+1);
658   dmarc_used_domain = dmarc_domain;
659
660   if (libdm_status != DMARC_PARSE_OKAY)
661     log_write(0, LOG_MAIN|LOG_PANIC,
662       "failure to read domainname used for DMARC lookup: %s",
663       opendmarc_policy_status_to_str(libdm_status));
664
665   dmarc_policy = libdm_status = opendmarc_get_policy_to_enforce(dmarc_pctx);
666   switch(libdm_status)
667     {
668     case DMARC_POLICY_ABSENT:     /* No DMARC record found */
669       dmarc_status = US"norecord";
670       dmarc_pass_fail = US"none";
671       dmarc_status_text = US"No DMARC record";
672       action = DMARC_RESULT_ACCEPT;
673       break;
674     case DMARC_FROM_DOMAIN_ABSENT:    /* No From: domain */
675       dmarc_status = US"nofrom";
676       dmarc_pass_fail = US"temperror";
677       dmarc_status_text = US"No From: domain found";
678       action = DMARC_RESULT_ACCEPT;
679       break;
680     case DMARC_POLICY_NONE:       /* Accept and report */
681       dmarc_status = US"none";
682       dmarc_pass_fail = US"none";
683       dmarc_status_text = US"None, Accept";
684       action = DMARC_RESULT_ACCEPT;
685       break;
686     case DMARC_POLICY_PASS:       /* Explicit accept */
687       dmarc_status = US"accept";
688       dmarc_pass_fail = US"pass";
689       dmarc_status_text = US"Accept";
690       action = DMARC_RESULT_ACCEPT;
691       break;
692     case DMARC_POLICY_REJECT:       /* Explicit reject */
693       dmarc_status = US"reject";
694       dmarc_pass_fail = US"fail";
695       dmarc_status_text = US"Reject";
696       action = DMARC_RESULT_REJECT;
697       break;
698     case DMARC_POLICY_QUARANTINE:       /* Explicit quarantine */
699       dmarc_status = US"quarantine";
700       dmarc_pass_fail = US"fail";
701       dmarc_status_text = US"Quarantine";
702       action = DMARC_RESULT_QUARANTINE;
703       break;
704     default:
705       dmarc_status = US"temperror";
706       dmarc_pass_fail = US"temperror";
707       dmarc_status_text = US"Internal Policy Error";
708       action = DMARC_RESULT_TEMPFAIL;
709       break;
710     }
711
712   libdm_status = opendmarc_policy_fetch_alignment(dmarc_pctx, &da, &sa);
713   if (libdm_status != DMARC_PARSE_OKAY)
714     log_write(0, LOG_MAIN|LOG_PANIC, "failure to read DMARC alignment: %s",
715                              opendmarc_policy_status_to_str(libdm_status));
716
717   if (has_dmarc_record)
718     {
719     log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
720                            "spf_align=%s dkim_align=%s enforcement='%s'",
721                            spf_sender_domain, dmarc_used_domain,
722                            sa==DMARC_POLICY_SPF_ALIGNMENT_PASS  ?"yes":"no",
723                            da==DMARC_POLICY_DKIM_ALIGNMENT_PASS ?"yes":"no",
724                            dmarc_status_text);
725     history_file_status = dmarc_write_history_file(dkim_history_buffer);
726     /* Now get the forensic reporting addresses, if any */
727     ruf = opendmarc_policy_fetch_ruf(dmarc_pctx, NULL, 0, 1);
728     dmarc_send_forensic_report(ruf);
729     }
730   }
731
732 /* shut down libopendmarc */
733 if (dmarc_pctx)
734   (void) opendmarc_policy_connect_shutdown(dmarc_pctx);
735 if (!f.dmarc_disable_verify)
736   (void) opendmarc_policy_library_shutdown(&dmarc_ctx);
737
738 return OK;
739 }
740
741 static uschar *
742 dmarc_exim_expand_defaults(int what)
743 {
744 if (what == DMARC_VERIFY_STATUS)
745   return f.dmarc_disable_verify ?  US"off" : US"none";
746 return US"";
747 }
748
749 static uschar *
750 dmarc_exim_expand_query(int what)
751 {
752 if (f.dmarc_disable_verify || !dmarc_pctx)
753   return dmarc_exim_expand_defaults(what);
754
755 if (what == DMARC_VERIFY_STATUS)
756   return dmarc_status;
757 return US"";
758 }
759
760
761 static gstring *
762 authres_dmarc(gstring * g)
763 {
764 if (f.dmarc_has_been_checked)
765   {
766   int start = 0;                /* Compiler quietening */
767   DEBUG(D_acl) start = gstring_length(g);
768   g = string_append(g, 2, US";\n\tdmarc=", dmarc_pass_fail);
769   if (header_from_sender)
770     g = string_append(g, 2, US" header.from=", header_from_sender);
771   DEBUG(D_acl) debug_printf("DMARC:\tauthres '%.*s'\n",
772                   gstring_length(g) - start - 3, g->s + start + 3);
773   }
774 else
775   DEBUG(D_acl) debug_printf("DMARC:\tno authres\n");
776 return g;
777 }
778
779 /******************************************************************************/
780 /* Module API */
781
782 static optionlist dmarc_options[] = {
783   { "dmarc_forensic_sender",    opt_stringptr,      {&dmarc_forensic_sender} },
784   { "dmarc_history_file",       opt_stringptr,      {&dmarc_history_file} },
785   { "dmarc_tld_file",           opt_stringptr,      {&dmarc_tld_file} },
786 };
787
788 static void * dmarc_functions[] = {
789   [DMARC_PROCESS] =     (void *) dmarc_process,
790   [DMARC_EXPAND_QUERY] = (void *) dmarc_exim_expand_query,
791   [DMARC_STORE_DATA] =  (void *) dmarc_store_data,
792 };
793
794 /* dmarc_forensic_sender is provided for visibility of the the option setting
795 by moan_send_message. We do not document it as a config-visible $variable.
796 We could provide it via a function but there's little advantage. */
797
798 static var_entry dmarc_variables[] = {
799   { "dmarc_domain_policy", vtype_stringptr,   &dmarc_domain_policy },
800   { "dmarc_forensic_sender", vtype_stringptr, &dmarc_forensic_sender },
801   { "dmarc_status",        vtype_stringptr,   &dmarc_status },
802   { "dmarc_status_text",   vtype_stringptr,   &dmarc_status_text },
803   { "dmarc_used_domain",   vtype_stringptr,   &dmarc_used_domain },
804 };
805
806 misc_module_info dmarc_module_info =
807 {
808   .name =               US"dmarc",
809 # ifdef DYNLOOKUP
810   .dyn_magic =          MISC_MODULE_MAGIC,
811 # endif
812   .init =               dmarc_init,
813   .lib_vers_report =    dmarc_version_report,
814   .smtp_reset =         dmarc_smtp_reset,
815   .msg_init =           dmarc_msg_init,
816   .authres =            authres_dmarc,
817
818   .options =            dmarc_options,
819   .options_count =      nelem(dmarc_options),
820
821   .functions =          dmarc_functions,
822   .functions_count =    nelem(dmarc_functions),
823
824   .variables =          dmarc_variables,
825   .variables_count =    nelem(dmarc_variables),
826 };
827
828 # endif /* SUPPORT_SPF */
829 #endif /* SUPPORT_DMARC */
830 /* vi: aw ai sw=2
831  */