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