1 /*************************************************
2 * Exim - an Internet mail transport agent *
3 *************************************************/
6 Copyright (c) The Exim Maintainers 2015 - 2024
7 Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 - 2014
9 SPDX-License-Identifier: GPL-2.0-or-later
12 /* Code for calling spf checks via libspf-alt. Called from acl.c. */
17 /* must be kept in numeric order */
18 static spf_result_id spf_result_id_list[] = {
26 { US"temperror", 6 }, /* RFC 4408 defined */
27 { US"permerror", 7 } /* RFC 4408 defined */
30 SPF_server_t *spf_server = NULL;
31 SPF_request_t *spf_request = NULL;
32 SPF_response_t *spf_response = NULL;
33 SPF_response_t *spf_response_2mx = NULL;
35 SPF_dns_rr_t * spf_nxdomain = NULL;
37 uschar * spf_guess = US"v=spf1 a/24 mx/24 ptr ?all";
38 uschar * spf_header_comment = NULL;
39 uschar * spf_received = NULL;
40 uschar * spf_result = NULL;
41 uschar * spf_smtp_comment = NULL;
42 uschar * spf_smtp_comment_template
43 /* Used to be: "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}" */
44 = US"Please%_see%_http://www.open-spf.org/Why";
45 BOOL spf_result_guessed = FALSE;
51 spf_lib_version_report(gstring * g)
55 SPF_get_lib_version(&maj, &min, &patch);
56 g = string_fmt_append(g, "Library version: spf2: Compile: %d.%d.%d\n",
57 SPF_LIB_VERSION_MAJOR, SPF_LIB_VERSION_MINOR, SPF_LIB_VERSION_PATCH);
58 g = string_fmt_append(g, " Runtime: %d.%d.%d\n",
66 SPF_dns_exim_lookup(SPF_dns_server_t *spf_dns_server,
67 const char *domain, ns_type rr_type, int should_cache)
69 dns_answer * dnsa = store_get_dns_answer();
75 .domain = CS domain, /* query information */
79 .rr_buf_len = 0, /* answer information */
80 .rr_buf_num = 0, /* no free of s */
83 .hook = NULL, /* misc information */
84 .source = spf_dns_server
87 DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", domain);
89 /* Shortcircuit SPF RR lookups by returning NO_DATA. They were obsoleted by
90 RFC 6686/7208 years ago. see bug #1294 */
94 HDEBUG(D_host_lookup) debug_printf("faking NO_DATA for SPF RR(99) lookup\n");
96 SPF_dns_rr_dup(&spfrr, &srr);
97 store_free_dns_answer(dnsa);
101 switch (dns_lookup(dnsa, US domain, rr_type, NULL))
103 case DNS_AGAIN: srr.herrno = TRY_AGAIN; break;
104 case DNS_NOMATCH: srr.herrno = HOST_NOT_FOUND; break;
105 case DNS_NODATA: srr.herrno = NO_DATA; break;
107 default: srr.herrno = NO_RECOVERY; break;
109 srr.herrno = NETDB_SUCCESS;
110 for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
111 rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
112 /* Need to alloc space for all records, so no early-out */
113 if (rr->type == rr_type) found++;
119 SPF_dns_rr_dup(&spfrr, &srr);
120 store_free_dns_answer(dnsa);
124 srr.rr = store_malloc(sizeof(SPF_dns_rr_data_t) * found);
127 for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
128 rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
129 if (rr->type == rr_type)
131 const uschar * s = rr->data;
137 if (rr->size < 2) continue;
138 s += 2; /* skip the MX precedence field */
141 uschar * buf = store_malloc(256);
142 (void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
143 (DN_EXPAND_ARG4_TYPE)buf, 256);
153 if (rr->size < 1+6) continue; /* min for version str */
154 if (strncmpic(rr->data+1, US SPF_VER_STR, 6) != 0)
156 HDEBUG(D_host_lookup) debug_printf("not an spf record: %.*s\n",
161 /* require 1 byte for the chunk_len */
162 for (int off = 0; off < rr->size - 1; off += chunk_len)
164 if ( !(chunk_len = s[off++])
165 || rr->size < off + chunk_len /* ignore bogus size chunks */
167 g = string_catn(g, s+off, chunk_len);
171 gstring_release_unused(g);
172 s = string_copy_malloc(string_from_gstring(g));
173 DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", s);
181 uschar * buf = store_malloc(dnsa->answerlen + 1);
182 s = memcpy(buf, s, dnsa->answerlen + 1);
186 srr.rr[found++] = (void *) s;
189 /* Did we filter out all TXT RRs? Return NO_DATA instead of SUCCESS with
190 empty ANSWER section. */
192 if (!(srr.num_rr = found))
193 srr.herrno = NO_DATA;
195 /* spfrr->rr must have been malloc()d for this */
196 SPF_dns_rr_dup(&spfrr, &srr);
197 store_free_dns_answer(dnsa);
203 static SPF_dns_server_t *
204 SPF_dns_exim_new(int debug)
206 SPF_dns_server_t * spf_dns_server = store_malloc(sizeof(SPF_dns_server_t));
208 /* DEBUG(D_receive) debug_printf("SPF_dns_exim_new\n"); */
210 memset(spf_dns_server, 0, sizeof(SPF_dns_server_t));
211 spf_dns_server->destroy = NULL;
212 spf_dns_server->lookup = SPF_dns_exim_lookup;
213 spf_dns_server->get_spf = NULL;
214 spf_dns_server->get_exp = NULL;
215 spf_dns_server->add_cache = NULL;
216 spf_dns_server->layer_below = NULL;
217 spf_dns_server->name = "exim";
218 spf_dns_server->debug = debug;
220 /* XXX This might have to return NO_DATA sometimes. */
222 spf_nxdomain = SPF_dns_rr_new_init(spf_dns_server,
223 "", ns_t_any, 24 * 60 * 60, HOST_NOT_FOUND);
226 store_free(spf_dns_server);
230 return spf_dns_server;
236 /* Construct the SPF library stack.
237 Return: Boolean success.
241 spf_init(void * dummy_ctx)
243 SPF_dns_server_t * dc;
247 DEBUG(D_receive) debug = 1;
249 /* We insert our own DNS access layer rather than letting the spf library
250 do it, so that our dns access path is used for debug tracing and for the
253 if (!(dc = SPF_dns_exim_new(debug)))
255 DEBUG(D_receive) debug_printf("spf: SPF_dns_exim_new() failed\n");
258 if (!(dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
260 DEBUG(D_receive) debug_printf("spf: SPF_dns_cache_new() failed\n");
263 if (!(spf_server = SPF_server_new_dns(dc, debug)))
265 DEBUG(D_receive) debug_printf("spf: SPF_server_new() failed.\n");
269 /* Override the outdated explanation URL.
270 See https://www.mail-archive.com/mailop@mailop.org/msg08019.html
271 Used to work as "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}",
272 but is broken now (May 18th, 2020) */
274 GET_OPTION("spf_smtp_comment_template");
275 if (!(s = expand_string(spf_smtp_comment_template)))
276 log_write(0, LOG_MAIN|LOG_PANIC_DIE, "expansion of spf_smtp_comment_template failed");
278 SPF_server_set_explanation(spf_server, CCS s, &spf_response);
279 if (SPF_response_errcode(spf_response) != SPF_E_SUCCESS)
280 log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", SPF_strerror(SPF_response_errcode(spf_response)));
286 /* Set up a context that can be re-used for several
287 messages on the same SMTP connection (that come from the
288 same host with the same HELO string).
294 spf_conn_init(const uschar * spf_helo_domain, const uschar * spf_remote_addr)
297 debug_printf("spf_conn_init: %s %s\n", spf_helo_domain, spf_remote_addr);
299 if (!spf_server && !spf_init(NULL))
302 if (SPF_server_set_rec_dom(spf_server, CS primary_hostname))
304 DEBUG(D_receive) debug_printf("spf: SPF_server_set_rec_dom(\"%s\") failed.\n",
310 spf_request = SPF_request_new(spf_server);
312 if ( SPF_request_set_ipv4_str(spf_request, CCS spf_remote_addr)
313 && SPF_request_set_ipv6_str(spf_request, CCS spf_remote_addr)
317 debug_printf("spf: SPF_request_set_ipv4_str() and "
318 "SPF_request_set_ipv6_str() failed [%s]\n", spf_remote_addr);
324 if (SPF_request_set_helo_dom(spf_request, CCS spf_helo_domain))
326 DEBUG(D_receive) debug_printf("spf: SPF_set_helo_dom(\"%s\") failed.\n",
339 spf_header_comment = spf_received = spf_result = spf_smtp_comment = NULL;
340 spf_result_guessed = FALSE;
345 spf_response_debug(SPF_response_t * spf_response)
347 if (SPF_response_messages(spf_response) == 0)
348 debug_printf(" (no errors)\n");
349 else for (int i = 0; i < SPF_response_messages(spf_response); i++)
351 SPF_error_t * err = SPF_response_message(spf_response, i);
352 debug_printf( "%s_msg = (%d) %s\n",
353 (SPF_error_errorp(err) ? "warn" : "err"),
355 SPF_error_message(err));
360 /* spf_process adds the envelope sender address to the existing
361 context (if any), retrieves the result, sets up expansion
362 strings and evaluates the condition outcome.
367 spf_process(const uschar ** listptr, const uschar * spf_envelope_sender,
371 const uschar *list = *listptr;
372 uschar *spf_result_id;
373 int rc = SPF_RESULT_PERMERROR;
375 DEBUG(D_receive) debug_printf("spf_process\n");
377 if (!(spf_server && spf_request))
378 /* no global context, assume temp error and skip to evaluation */
379 rc = SPF_RESULT_PERMERROR;
381 else if (SPF_request_set_env_from(spf_request, CS spf_envelope_sender))
382 /* Invalid sender address. This should be a real rare occurrence */
383 rc = SPF_RESULT_PERMERROR;
388 if (action == SPF_PROCESS_FALLBACK)
390 SPF_request_query_fallback(spf_request, &spf_response, CS spf_guess);
391 spf_result_guessed = TRUE;
394 SPF_request_query_mailfrom(spf_request, &spf_response);
396 /* set up expansion items */
397 spf_header_comment = US SPF_response_get_header_comment(spf_response);
398 spf_received = US SPF_response_get_received_spf(spf_response);
399 spf_result = US SPF_strresult(SPF_response_result(spf_response));
400 spf_smtp_comment = US SPF_response_get_smtp_comment(spf_response);
402 rc = SPF_response_result(spf_response);
404 DEBUG(D_acl) spf_response_debug(spf_response);
407 /* We got a result. Now see if we should return OK or FAIL for it */
408 DEBUG(D_acl) debug_printf("SPF result is %s (%d)\n", SPF_strresult(rc), rc);
410 if (action == SPF_PROCESS_GUESS && (!strcmp (SPF_strresult(rc), "none")))
411 return spf_process(listptr, spf_envelope_sender, SPF_PROCESS_FALLBACK);
413 while ((spf_result_id = string_nextinlist(&list, &sep, NULL, 0)))
417 if ((negate = spf_result_id[0] == '!'))
420 result = Ustrcmp(spf_result_id, spf_result_id_list[rc].name) == 0;
421 if (negate != result) return OK;
431 authres_spf(gstring * g)
436 int start = 0; /* Compiler quietening */
437 DEBUG(D_acl) start = gstring_length(g);
439 g = string_append(g, 2, US";\n\tspf=", spf_result);
440 if (spf_result_guessed)
441 g = string_cat(g, US" (best guess record for domain)");
443 s = expand_string(US"$sender_address_domain");
445 g = string_append(g, 2, US" smtp.mailfrom=", s);
448 s = sender_helo_name;
450 ? string_append(g, 2, US" smtp.helo=", s)
451 : string_cat(g, US" smtp.mailfrom=<>");
453 DEBUG(D_acl) debug_printf("SPF:\tauthres '%.*s'\n",
454 gstring_length(g) - start - 3, g->s + start + 3);
457 DEBUG(D_acl) debug_printf("SPF:\tno authres\n");
462 /* Ugly; used only by dmarc (peeking into our data!)
463 Exposure of values as $variables might be better? */
465 static SPF_response_t *
466 spf_get_response(void)
471 /******************************************************************************/
475 spf_lookup_open(const uschar * filename, uschar ** errmsg)
477 SPF_dns_server_t * dc;
478 SPF_server_t * spf_server = NULL;
481 DEBUG(D_lookup) debug = 1;
483 if ((dc = SPF_dns_exim_new(debug)))
484 if ((dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
485 spf_server = SPF_server_new_dns(dc, debug);
489 *errmsg = US"SPF_dns_exim_nnew() failed";
492 return (void *) spf_server;
496 spf_lookup_close(void * handle)
498 SPF_server_t * spf_server = handle;
499 if (spf_server) SPF_server_free(spf_server);
503 spf_lookup_find(void * handle, const uschar * filename,
504 const uschar * keystring, int key_len, uschar ** result, uschar ** errmsg,
505 uint * do_cache, const uschar * opts)
507 SPF_server_t *spf_server = handle;
508 SPF_request_t *spf_request;
509 SPF_response_t *spf_response = NULL;
511 if (!(spf_request = SPF_request_new(spf_server)))
513 *errmsg = US"SPF_request_new() failed";
518 switch (string_is_ip_address(filename, NULL))
524 if (!SPF_request_set_ipv4_str(spf_request, CS filename))
526 *errmsg = string_sprintf("invalid IPv4 address '%s'", filename);
531 if (!SPF_request_set_ipv6_str(spf_request, CS filename))
533 *errmsg = string_sprintf("invalid IPv6 address '%s'", filename);
537 *errmsg = string_sprintf("invalid IP address '%s'", filename);
542 if (SPF_request_set_env_from(spf_request, CS keystring))
544 *errmsg = string_sprintf("invalid envelope from address '%s'", keystring);
548 SPF_request_query_mailfrom(spf_request, &spf_response);
549 *result = string_copy(US SPF_strresult(SPF_response_result(spf_response)));
551 DEBUG(D_lookup) spf_response_debug(spf_response);
553 SPF_response_free(spf_response);
554 SPF_request_free(spf_request);
559 /******************************************************************************/
562 static optionlist spf_options[] = {
563 { "spf_guess", opt_stringptr, {&spf_guess} },
564 { "spf_smtp_comment_template",opt_stringptr, {&spf_smtp_comment_template} },
567 static void * spf_functions[] = {
568 [SPF_PROCESS] = spf_process,
569 [SPF_AUTHRES] = authres_spf,
570 [SPF_GET_RESPONSE] = spf_get_response, /* ugly; for dmarc */
572 [SPF_OPEN] = spf_lookup_open,
573 [SPF_CLOSE] = spf_lookup_close,
574 [SPF_FIND] = spf_lookup_find,
577 static var_entry spf_variables[] = {
578 { "spf_guess", vtype_stringptr, &spf_guess },
579 { "spf_header_comment", vtype_stringptr, &spf_header_comment },
580 { "spf_received", vtype_stringptr, &spf_received },
581 { "spf_result", vtype_stringptr, &spf_result },
582 { "spf_result_guessed", vtype_bool, &spf_result_guessed },
583 { "spf_smtp_comment", vtype_stringptr, &spf_smtp_comment },
586 misc_module_info spf_module_info =
590 .dyn_magic = MISC_MODULE_MAGIC,
593 .lib_vers_report = spf_lib_version_report,
594 .conn_init = spf_conn_init,
595 .smtp_reset = spf_smtp_reset,
597 .options = spf_options,
598 .options_count = nelem(spf_options),
600 .functions = spf_functions,
601 .functions_count = nelem(spf_functions),
603 .variables = spf_variables,
604 .variables_count = nelem(spf_variables),
607 #endif /* almost all the file */