d977d29feec5d71019838e1d1c2aad994bf6b7df
[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 g = arc_dmarc_hist_append(g);
364 # else
365 g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n",
366                       ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
367 # endif
368 #endif
369
370 /* Write the contents to the history file */
371 DEBUG(D_receive)
372   {
373   debug_printf_indent("DMARC logging history data for opendmarc reporting%s\n",
374              host_checking ? " (not really)" : "");
375   debug_printf_indent("DMARC history data for debugging:\n");
376   expand_level++;
377     debug_printf_indent("%Y", g);
378   expand_level--;
379   }
380
381 if (!host_checking)
382   {
383   written_len = write_to_fd_buf(history_file_fd,
384                                 g->s,
385                                 gstring_length(g));
386   if (written_len == 0)
387     {
388     log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s",
389                            dmarc_history_file);
390     return DMARC_HIST_WRITE_ERR;
391     }
392   (void)close(history_file_fd);
393   }
394 return DMARC_HIST_OK;
395 }
396
397
398 /* dmarc_process adds the envelope sender address to the existing
399 context (if any), retrieves the result, sets up expansion
400 strings and evaluates the condition outcome.
401 Called for the first ACL dmarc= condition. */
402
403 static int
404 dmarc_process(void)
405 {
406 int sr, origin;             /* used in SPF section */
407 int dmarc_spf_result  = 0;  /* stores spf into dmarc conn ctx */
408 int tmp_ans, c;
409 uschar * rr;
410 BOOL has_dmarc_record = TRUE;
411 u_char ** ruf; /* forensic report addressees, if called for */
412
413 /* ACLs have "control=dmarc_disable_verify" */
414 if (f.dmarc_disable_verify)
415   return OK;
416
417 /* Store the header From: sender domain for this part of DMARC.
418 If there is no from_header struct, then it's likely this message
419 is locally generated and relying on fixups to add it.  Just skip
420 the entire DMARC system if we can't find a From: header....or if
421 there was a previous error.  */
422
423 if (!from_header)
424   {
425   DEBUG(D_receive) debug_printf_indent("DMARC: no From: header\n");
426   dmarc_abort = TRUE;
427   }
428 else if (!dmarc_abort)
429   {
430   uschar * errormsg;
431   int dummy, domain;
432   uschar * p;
433   uschar saveend;
434
435   f.parse_allow_group = TRUE;
436   p = parse_find_address_end(from_header->text, FALSE);
437   saveend = *p; *p = '\0';
438   if ((header_from_sender = parse_extract_address(from_header->text, &errormsg,
439                               &dummy, &dummy, &domain, FALSE)))
440     header_from_sender += domain;
441   *p = saveend;
442
443   /* The opendmarc library extracts the domain from the email address, but
444   only try to store it if it's not empty.  Otherwise, skip out of DMARC. */
445
446   if (!header_from_sender || (strcmp( CCS header_from_sender, "") == 0))
447     dmarc_abort = TRUE;
448   libdm_status = dmarc_abort
449     ? DMARC_PARSE_OKAY
450     : opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender);
451   if (libdm_status != DMARC_PARSE_OKAY)
452     {
453     log_write(0, LOG_MAIN|LOG_PANIC,
454               "failure to store header From: in DMARC: %s, header was '%s'",
455               opendmarc_policy_status_to_str(libdm_status), from_header->text);
456     dmarc_abort = TRUE;
457     }
458   }
459
460 /* Skip DMARC if connection is SMTP Auth. Temporarily, admin should
461 instead do this in the ACLs.  */
462
463 if (!dmarc_abort && !sender_host_authenticated)
464   {
465   uschar * dmarc_domain;
466   gstring * dkim_history_buffer = NULL;
467   typedef const pdkim_signature * (*sigs_fn_t)(void);
468
469   /* Use the envelope sender domain for this part of DMARC */
470
471   spf_sender_domain = expand_string(US"$sender_address_domain");
472
473     {
474     typedef SPF_response_t * (*fn_t)(void);
475     if (dmarc_spf_mod_info)
476       /*XXX ugly use of a pointer */
477       spf_response_p = ((fn_t *) dmarc_spf_mod_info->functions)[SPF_GET_RESPONSE]();
478     }
479
480   if (!spf_response_p)
481     {
482     /* No spf data means null envelope sender so generate a domain name
483     from the sender_helo_name  */
484
485     if (!spf_sender_domain)
486       {
487       spf_sender_domain = sender_helo_name;
488       log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n",
489                              spf_sender_domain);
490       DEBUG(D_receive)
491         debug_printf_indent("DMARC using synthesized SPF sender domain = %s\n",
492           spf_sender_domain);
493       }
494     dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE;
495     dmarc_spf_ares_result = ARES_RESULT_UNKNOWN;
496     origin = DMARC_POLICY_SPF_ORIGIN_HELO;
497     spf_human_readable = US"";
498     }
499   else
500     {
501     sr = spf_response_p->result;
502     dmarc_spf_result = sr == SPF_RESULT_NEUTRAL  ? DMARC_POLICY_SPF_OUTCOME_NONE :
503                        sr == SPF_RESULT_PASS     ? DMARC_POLICY_SPF_OUTCOME_PASS :
504                        sr == SPF_RESULT_FAIL     ? DMARC_POLICY_SPF_OUTCOME_FAIL :
505                        sr == SPF_RESULT_SOFTFAIL ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL :
506                        DMARC_POLICY_SPF_OUTCOME_NONE;
507     dmarc_spf_ares_result = sr == SPF_RESULT_NEUTRAL   ? ARES_RESULT_NEUTRAL :
508                             sr == SPF_RESULT_PASS      ? ARES_RESULT_PASS :
509                             sr == SPF_RESULT_FAIL      ? ARES_RESULT_FAIL :
510                             sr == SPF_RESULT_SOFTFAIL  ? ARES_RESULT_SOFTFAIL :
511                             sr == SPF_RESULT_NONE      ? ARES_RESULT_NONE :
512                             sr == SPF_RESULT_TEMPERROR ? ARES_RESULT_TEMPERROR :
513                             sr == SPF_RESULT_PERMERROR ? ARES_RESULT_PERMERROR :
514                             ARES_RESULT_UNKNOWN;
515     origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM;
516     spf_human_readable = US spf_response_p->header_comment;
517     DEBUG(D_receive)
518       debug_printf_indent("DMARC using SPF sender domain = %s\n", spf_sender_domain);
519     }
520   if (strcmp( CCS spf_sender_domain, "") == 0)
521     dmarc_abort = TRUE;
522   if (!dmarc_abort)
523     {
524     libdm_status = opendmarc_policy_store_spf(dmarc_pctx, spf_sender_domain,
525                                 dmarc_spf_result, origin, spf_human_readable);
526     if (libdm_status != DMARC_PARSE_OKAY)
527       log_write(0, LOG_MAIN|LOG_PANIC, "failure to store spf for DMARC: %s",
528                            opendmarc_policy_status_to_str(libdm_status));
529     }
530
531   /* Now we cycle through the dkim signature results and put into
532   the opendmarc context, further building the DMARC reply. */
533
534   for(const pdkim_signature * sig =
535               (((sigs_fn_t *)dmarc_dkim_mod_info->functions)[DKIM_SIGS_LIST])();
536       sig; sig = sig->next)
537     {
538     int dkim_result, dkim_ares_result, vs, ves;
539
540     vs  = sig->verify_status & ~PDKIM_VERIFY_POLICY;
541     ves = sig->verify_ext_status;
542     dkim_result = vs == PDKIM_VERIFY_PASS ? DMARC_POLICY_DKIM_OUTCOME_PASS :
543                   vs == PDKIM_VERIFY_FAIL ? DMARC_POLICY_DKIM_OUTCOME_FAIL :
544                   vs == PDKIM_VERIFY_INVALID ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL :
545                   DMARC_POLICY_DKIM_OUTCOME_NONE;
546     libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, US sig->domain,
547
548 /* The opendmarc project broke its API in a way we can't detect easily.
549 The EDITME provides a DMARC_API variable */
550 #if DMARC_API >= 100400
551                                                sig->selector,
552 #endif
553                                                dkim_result, US"");
554     DEBUG(D_receive)
555       debug_printf_indent("DMARC adding DKIM sender domain = %s\n", sig->domain);
556     if (libdm_status != DMARC_PARSE_OKAY)
557       log_write(0, LOG_MAIN|LOG_PANIC,
558                 "failure to store dkim (%s) for DMARC: %s",
559                 sig->domain, opendmarc_policy_status_to_str(libdm_status));
560
561     dkim_ares_result =
562       vs == PDKIM_VERIFY_PASS    ? ARES_RESULT_PASS :
563       vs == PDKIM_VERIFY_FAIL    ? ARES_RESULT_FAIL :
564       vs == PDKIM_VERIFY_NONE    ? ARES_RESULT_NONE :
565       vs == PDKIM_VERIFY_INVALID ?
566        ves == PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE ? ARES_RESULT_PERMERROR :
567        ves == PDKIM_VERIFY_INVALID_BUFFER_SIZE        ? ARES_RESULT_PERMERROR :
568        ves == PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD   ? ARES_RESULT_PERMERROR :
569        ves == PDKIM_VERIFY_INVALID_PUBKEY_IMPORT      ? ARES_RESULT_PERMERROR :
570        ARES_RESULT_UNKNOWN :
571       ARES_RESULT_UNKNOWN;
572 #if DMARC_API >= 100400
573     dkim_history_buffer = string_fmt_append(dkim_history_buffer,
574       "dkim %s %s %d\n", sig->domain, sig->selector, dkim_ares_result);
575 #else
576     dkim_history_buffer = string_fmt_append(dkim_history_buffer,
577       "dkim %s %d\n", sig->domain, dkim_ares_result);
578 #endif
579     }
580
581   /* Look up DMARC policy record in DNS.  We do this explicitly, rather than
582   letting the dmarc library do it with opendmarc_policy_query_dmarc(), so that
583   our dns access path is used for debug tracing and for the testsuite
584   diversion. */
585
586   libdm_status = (rr = dmarc_dns_lookup(header_from_sender))
587     ? opendmarc_policy_store_dmarc(dmarc_pctx, rr, header_from_sender, NULL)
588     : DMARC_DNS_ERROR_NO_RECORD;
589   switch (libdm_status)
590     {
591     case DMARC_DNS_ERROR_NXDOMAIN:
592     case DMARC_DNS_ERROR_NO_RECORD:
593       DEBUG(D_receive)
594         debug_printf_indent("DMARC no record found for %s\n", header_from_sender);
595       has_dmarc_record = FALSE;
596       break;
597     case DMARC_PARSE_OKAY:
598       DEBUG(D_receive)
599         debug_printf_indent("DMARC record found for %s\n", header_from_sender);
600       break;
601     case DMARC_PARSE_ERROR_BAD_VALUE:
602       DEBUG(D_receive)
603         debug_printf_indent("DMARC record parse error for %s\n", header_from_sender);
604       has_dmarc_record = FALSE;
605       break;
606     default:
607       /* everything else, skip dmarc */
608       DEBUG(D_receive)
609         debug_printf_indent("DMARC skipping (%s), unsure what to do with %s",
610                       opendmarc_policy_status_to_str(libdm_status),
611                       from_header->text);
612       has_dmarc_record = FALSE;
613       break;
614     }
615
616   /* Store the policy string in an expandable variable. */
617
618   libdm_status = opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
619   for (c = 0; dmarc_policy_description[c].name; c++)
620     if (tmp_ans == dmarc_policy_description[c].value)
621       {
622       dmarc_domain_policy = string_sprintf("%s",dmarc_policy_description[c].name);
623       break;
624       }
625
626   /* Can't use exim's string manipulation functions so allocate memory
627   for libopendmarc using its max hostname length definition. */
628
629   dmarc_domain = store_get(DMARC_MAXHOSTNAMELEN, GET_TAINTED);
630   libdm_status = opendmarc_policy_fetch_utilized_domain(dmarc_pctx,
631     dmarc_domain, DMARC_MAXHOSTNAMELEN-1);
632   store_release_above(dmarc_domain + Ustrlen(dmarc_domain)+1);
633   dmarc_used_domain = dmarc_domain;
634
635   if (libdm_status != DMARC_PARSE_OKAY)
636     log_write(0, LOG_MAIN|LOG_PANIC,
637       "failure to read domainname used for DMARC lookup: %s",
638       opendmarc_policy_status_to_str(libdm_status));
639
640   dmarc_policy = libdm_status = opendmarc_get_policy_to_enforce(dmarc_pctx);
641   switch(libdm_status)
642     {
643     case DMARC_POLICY_ABSENT:     /* No DMARC record found */
644       dmarc_status = US"norecord";
645       dmarc_pass_fail = US"none";
646       dmarc_status_text = US"No DMARC record";
647       action = DMARC_RESULT_ACCEPT;
648       break;
649     case DMARC_FROM_DOMAIN_ABSENT:    /* No From: domain */
650       dmarc_status = US"nofrom";
651       dmarc_pass_fail = US"temperror";
652       dmarc_status_text = US"No From: domain found";
653       action = DMARC_RESULT_ACCEPT;
654       break;
655     case DMARC_POLICY_NONE:       /* Accept and report */
656       dmarc_status = US"none";
657       dmarc_pass_fail = US"none";
658       dmarc_status_text = US"None, Accept";
659       action = DMARC_RESULT_ACCEPT;
660       break;
661     case DMARC_POLICY_PASS:       /* Explicit accept */
662       dmarc_status = US"accept";
663       dmarc_pass_fail = US"pass";
664       dmarc_status_text = US"Accept";
665       action = DMARC_RESULT_ACCEPT;
666       break;
667     case DMARC_POLICY_REJECT:       /* Explicit reject */
668       dmarc_status = US"reject";
669       dmarc_pass_fail = US"fail";
670       dmarc_status_text = US"Reject";
671       action = DMARC_RESULT_REJECT;
672       break;
673     case DMARC_POLICY_QUARANTINE:       /* Explicit quarantine */
674       dmarc_status = US"quarantine";
675       dmarc_pass_fail = US"fail";
676       dmarc_status_text = US"Quarantine";
677       action = DMARC_RESULT_QUARANTINE;
678       break;
679     default:
680       dmarc_status = US"temperror";
681       dmarc_pass_fail = US"temperror";
682       dmarc_status_text = US"Internal Policy Error";
683       action = DMARC_RESULT_TEMPFAIL;
684       break;
685     }
686
687   libdm_status = opendmarc_policy_fetch_alignment(dmarc_pctx, &da, &sa);
688   if (libdm_status != DMARC_PARSE_OKAY)
689     log_write(0, LOG_MAIN|LOG_PANIC, "failure to read DMARC alignment: %s",
690                              opendmarc_policy_status_to_str(libdm_status));
691
692   if (has_dmarc_record)
693     {
694     log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
695                            "spf_align=%s dkim_align=%s enforcement='%s'",
696                            spf_sender_domain, dmarc_used_domain,
697                            sa==DMARC_POLICY_SPF_ALIGNMENT_PASS  ?"yes":"no",
698                            da==DMARC_POLICY_DKIM_ALIGNMENT_PASS ?"yes":"no",
699                            dmarc_status_text);
700     history_file_status = dmarc_write_history_file(dkim_history_buffer);
701     /* Now get the forensic reporting addresses, if any */
702     ruf = opendmarc_policy_fetch_ruf(dmarc_pctx, NULL, 0, 1);
703     dmarc_send_forensic_report(ruf);
704     }
705   }
706
707 /* shut down libopendmarc */
708 if (dmarc_pctx)
709   (void) opendmarc_policy_connect_shutdown(dmarc_pctx);
710 if (!f.dmarc_disable_verify)
711   (void) opendmarc_policy_library_shutdown(&dmarc_ctx);
712
713 return OK;
714 }
715
716 static uschar *
717 dmarc_exim_expand_defaults(int what)
718 {
719 if (what == DMARC_VERIFY_STATUS)
720   return f.dmarc_disable_verify ?  US"off" : US"none";
721 return US"";
722 }
723
724 static uschar *
725 dmarc_exim_expand_query(int what)
726 {
727 if (f.dmarc_disable_verify || !dmarc_pctx)
728   return dmarc_exim_expand_defaults(what);
729
730 if (what == DMARC_VERIFY_STATUS)
731   return dmarc_status;
732 return US"";
733 }
734
735
736 static gstring *
737 authres_dmarc(gstring * g)
738 {
739 if (f.dmarc_has_been_checked)
740   {
741   int start = 0;                /* Compiler quietening */
742   DEBUG(D_acl) start = gstring_length(g);
743   g = string_append(g, 2, US";\n\tdmarc=", dmarc_pass_fail);
744   if (header_from_sender)
745     g = string_append(g, 2, US" header.from=", header_from_sender);
746   DEBUG(D_acl) debug_printf("DMARC:\tauthres '%.*s'\n",
747                   gstring_length(g) - start - 3, g->s + start + 3);
748   }
749 else
750   DEBUG(D_acl) debug_printf("DMARC:\tno authres\n");
751 return g;
752 }
753
754 /******************************************************************************/
755 /* Module API */
756
757 static optionlist dmarc_options[] = {
758   { "dmarc_forensic_sender",    opt_stringptr,      {&dmarc_forensic_sender} },
759   { "dmarc_history_file",       opt_stringptr,      {&dmarc_history_file} },
760   { "dmarc_tld_file",           opt_stringptr,      {&dmarc_tld_file} },
761 };
762
763 static void * dmarc_functions[] = {
764   [DMARC_PROCESS] =     dmarc_process,
765   [DMARC_EXPAND_QUERY] = dmarc_exim_expand_query,
766   [DMARC_STORE_DATA] =  dmarc_store_data,
767 };
768
769 /* dmarc_forensic_sender is provided for visibility of the the option setting
770 by moan_send_message. We do not document it as a config-visible $variable.
771 We could provide it via a function but there's little advantage. */
772
773 static var_entry dmarc_variables[] = {
774   { "dmarc_domain_policy", vtype_stringptr,   &dmarc_domain_policy },
775   { "dmarc_forensic_sender", vtype_stringptr, &dmarc_forensic_sender },
776   { "dmarc_status",        vtype_stringptr,   &dmarc_status },
777   { "dmarc_status_text",   vtype_stringptr,   &dmarc_status_text },
778   { "dmarc_used_domain",   vtype_stringptr,   &dmarc_used_domain },
779 };
780
781 misc_module_info dmarc_module_info =
782 {
783   .name =               US"dmarc",
784 # if SUPPORT_SPF==2
785   .dyn_magic =          MISC_MODULE_MAGIC,
786 # endif
787   .init =               dmarc_init,
788   .lib_vers_report =    dmarc_version_report,
789   .smtp_reset =         dmarc_smtp_reset,
790   .msg_init =           dmarc_msg_init,
791   .authres =            authres_dmarc,
792
793   .options =            dmarc_options,
794   .options_count =      nelem(dmarc_options),
795
796   .functions =          dmarc_functions,
797   .functions_count =    nelem(dmarc_functions),
798
799   .variables =          dmarc_variables,
800   .variables_count =    nelem(dmarc_variables),
801 };
802
803 # endif /* SUPPORT_SPF */
804 #endif /* SUPPORT_DMARC */
805 /* vi: aw ai sw=2
806  */