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