SPF: enhance A-R result
[exim.git] / src / src / spf.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* SPF support.
6    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 - 2014
7    License: GPL
8    Copyright (c) The Exim Maintainers 2015 - 2020
9 */
10
11 /* Code for calling spf checks via libspf-alt. Called from acl.c. */
12
13 #include "exim.h"
14 #ifdef SUPPORT_SPF
15
16 /* must be kept in numeric order */
17 static spf_result_id spf_result_id_list[] = {
18   /* name               value */
19   { US"invalid",        0},
20   { US"neutral",        1 },
21   { US"pass",           2 },
22   { US"fail",           3 },
23   { US"softfail",       4 },
24   { US"none",           5 },
25   { US"temperror",      6 }, /* RFC 4408 defined */
26   { US"permerror",      7 }  /* RFC 4408 defined */
27 };
28
29 SPF_server_t    *spf_server = NULL;
30 SPF_request_t   *spf_request = NULL;
31 SPF_response_t  *spf_response = NULL;
32 SPF_response_t  *spf_response_2mx = NULL;
33
34 SPF_dns_rr_t  * spf_nxdomain = NULL;
35
36
37 void
38 spf_lib_version_report(FILE * fp)
39 {
40 int maj, min, patch;
41 SPF_get_lib_version(&maj, &min, &patch);
42 fprintf(fp, "Library version: spf2: Compile: %d.%d.%d\n",
43         SPF_LIB_VERSION_MAJOR, SPF_LIB_VERSION_MINOR, SPF_LIB_VERSION_PATCH);
44 fprintf(fp, "                       Runtime: %d.%d.%d\n",
45          maj, min, patch);
46 }
47
48
49
50 static SPF_dns_rr_t *
51 SPF_dns_exim_lookup(SPF_dns_server_t *spf_dns_server,
52   const char *domain, ns_type rr_type, int should_cache)
53 {
54 dns_answer * dnsa = store_get_dns_answer();
55 dns_scan dnss;
56 SPF_dns_rr_t * spfrr;
57 unsigned found = 0;
58
59 SPF_dns_rr_t srr = {
60   .domain = CS domain,                  /* query information */
61   .domain_buf_len = 0,
62   .rr_type = rr_type,
63
64   .rr_buf_len = 0,                      /* answer information */
65   .rr_buf_num = 0, /* no free of s */
66   .utc_ttl = 0,
67
68   .hook = NULL,                         /* misc information */
69   .source = spf_dns_server
70 };
71 int dns_rc;
72
73 DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", domain);
74
75 /* Shortcircuit SPF RR lookups by returning NO_DATA.  They were obsoleted by
76 RFC 6686/7208 years ago. see bug #1294 */
77
78 if (rr_type == T_SPF)
79   {
80   HDEBUG(D_host_lookup) debug_printf("faking NO_DATA for SPF RR(99) lookup\n");
81   srr.herrno = NO_DATA;
82   SPF_dns_rr_dup(&spfrr, &srr);
83   return spfrr;
84   }
85
86 switch (dns_rc = dns_lookup(dnsa, US domain, rr_type, NULL))
87   {
88   case DNS_SUCCEED:     srr.herrno = NETDB_SUCCESS;     break;
89   case DNS_AGAIN:       srr.herrno = TRY_AGAIN;         break;
90   case DNS_NOMATCH:     srr.herrno = HOST_NOT_FOUND;    break;
91   case DNS_NODATA:      srr.herrno = NO_DATA;           break;
92   case DNS_FAIL:
93   default:              srr.herrno = NO_RECOVERY;       break;
94   }
95
96 for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
97      rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
98   if (rr->type == rr_type) found++;
99
100 if (found == 0)
101   {
102   SPF_dns_rr_dup(&spfrr, &srr);
103   return spfrr;
104   }
105
106 srr.rr = store_malloc(sizeof(SPF_dns_rr_data_t) * found);
107
108 found = 0;
109 for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
110    rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
111   if (rr->type == rr_type)
112     {
113     const uschar * s = rr->data;
114
115     srr.ttl = rr->ttl;
116     switch(rr_type)
117       {
118       case T_MX:
119         s += 2; /* skip the MX precedence field */
120       case T_PTR:
121         {
122         uschar * buf = store_malloc(256);
123         (void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
124           (DN_EXPAND_ARG4_TYPE)buf, 256);
125         s = buf;
126         break;
127         }
128
129       case T_TXT:
130         {
131         gstring * g = NULL;
132         uschar chunk_len;
133
134         if (strncmpic(rr->data+1, US SPF_VER_STR, 6) != 0)
135           {
136           HDEBUG(D_host_lookup) debug_printf("not an spf record: %.*s\n",
137             (int) s[0], s+1);
138           continue;
139           }
140
141         for (int off = 0; off < rr->size; off += chunk_len)
142           {
143           if (!(chunk_len = s[off++])) break;
144           g = string_catn(g, s+off, chunk_len);
145           }
146         if (!g)
147           continue;
148         gstring_release_unused(g);
149         s = string_copy_malloc(string_from_gstring(g));
150         DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", s);
151         break;
152         }
153
154       case T_A:
155       case T_AAAA:
156       default:
157         {
158         uschar * buf = store_malloc(dnsa->answerlen + 1);
159         s = memcpy(buf, s, dnsa->answerlen + 1);
160         break;
161         }
162       }
163     srr.rr[found++] = (void *) s;
164     }
165
166 /* Did we filter out all TXT RRs? Return NO_DATA instead of SUCCESS with
167 empty ANSWER section. */
168
169 if (!(srr.num_rr = found))
170   srr.herrno = NO_DATA;
171
172 /* spfrr->rr must have been malloc()d for this */
173 SPF_dns_rr_dup(&spfrr, &srr);
174 return spfrr;
175 }
176
177
178
179 SPF_dns_server_t *
180 SPF_dns_exim_new(int debug)
181 {
182 SPF_dns_server_t * spf_dns_server = store_malloc(sizeof(SPF_dns_server_t));
183
184 DEBUG(D_receive) debug_printf("SPF_dns_exim_new\n");
185
186 memset(spf_dns_server, 0, sizeof(SPF_dns_server_t));
187 spf_dns_server->destroy      = NULL;
188 spf_dns_server->lookup       = SPF_dns_exim_lookup;
189 spf_dns_server->get_spf      = NULL;
190 spf_dns_server->get_exp      = NULL;
191 spf_dns_server->add_cache    = NULL;
192 spf_dns_server->layer_below  = NULL;
193 spf_dns_server->name         = "exim";
194 spf_dns_server->debug        = debug;
195
196 /* XXX This might have to return NO_DATA sometimes. */
197
198 spf_nxdomain = SPF_dns_rr_new_init(spf_dns_server,
199   "", ns_t_any, 24 * 60 * 60, HOST_NOT_FOUND);
200 if (!spf_nxdomain)
201   {
202   free(spf_dns_server);
203   return NULL;
204   }
205
206 return spf_dns_server;
207 }
208
209
210
211
212 /* Construct the SPF library stack.
213    Return: Boolean success.
214 */
215
216 BOOL
217 spf_init(void)
218 {
219 SPF_dns_server_t * dc;
220 int debug = 0;
221 const uschar *s;
222
223 DEBUG(D_receive) debug = 1;
224
225 /* We insert our own DNS access layer rather than letting the spf library
226 do it, so that our dns access path is used for debug tracing and for the
227 testsuite. */
228
229 if (!(dc = SPF_dns_exim_new(debug)))
230   {
231   DEBUG(D_receive) debug_printf("spf: SPF_dns_exim_new() failed\n");
232   return FALSE;
233   }
234 if (!(dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
235   {
236   DEBUG(D_receive) debug_printf("spf: SPF_dns_cache_new() failed\n");
237   return FALSE;
238   }
239 if (!(spf_server = SPF_server_new_dns(dc, debug)))
240   {
241   DEBUG(D_receive) debug_printf("spf: SPF_server_new() failed.\n");
242   return FALSE;
243   }
244   /* Override the outdated explanation URL.
245   See https://www.mail-archive.com/mailop@mailop.org/msg08019.html
246   Used to work as "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}",
247   but is broken now (May 18th, 2020) */
248 if (!(s = expand_string(spf_smtp_comment_template)))
249   log_write(0, LOG_MAIN|LOG_PANIC_DIE, "expansion of spf_smtp_comment_template failed");
250
251 SPF_server_set_explanation(spf_server, CCS s, &spf_response);
252 if (SPF_response_errcode(spf_response) != SPF_E_SUCCESS)
253   log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", SPF_strerror(SPF_response_errcode(spf_response)));
254
255 return TRUE;
256 }
257
258
259 /* Set up a context that can be re-used for several
260    messages on the same SMTP connection (that come from the
261    same host with the same HELO string).
262
263 Return: Boolean success
264 */
265
266 BOOL
267 spf_conn_init(uschar * spf_helo_domain, uschar * spf_remote_addr)
268 {
269 DEBUG(D_receive)
270   debug_printf("spf_conn_init: %s %s\n", spf_helo_domain, spf_remote_addr);
271
272 if (!spf_server && !spf_init()) return FALSE;
273
274 if (SPF_server_set_rec_dom(spf_server, CS primary_hostname))
275   {
276   DEBUG(D_receive) debug_printf("spf: SPF_server_set_rec_dom(\"%s\") failed.\n",
277     primary_hostname);
278   spf_server = NULL;
279   return FALSE;
280   }
281
282 spf_request = SPF_request_new(spf_server);
283
284 if (  SPF_request_set_ipv4_str(spf_request, CS spf_remote_addr)
285    && SPF_request_set_ipv6_str(spf_request, CS spf_remote_addr)
286    )
287   {
288   DEBUG(D_receive)
289     debug_printf("spf: SPF_request_set_ipv4_str() and "
290       "SPF_request_set_ipv6_str() failed [%s]\n", spf_remote_addr);
291   spf_server = NULL;
292   spf_request = NULL;
293   return FALSE;
294   }
295
296 if (SPF_request_set_helo_dom(spf_request, CS spf_helo_domain))
297   {
298   DEBUG(D_receive) debug_printf("spf: SPF_set_helo_dom(\"%s\") failed.\n",
299     spf_helo_domain);
300   spf_server = NULL;
301   spf_request = NULL;
302   return FALSE;
303   }
304
305 return TRUE;
306 }
307
308
309 void
310 spf_response_debug(SPF_response_t * spf_response)
311 {
312 if (SPF_response_messages(spf_response) == 0)
313   debug_printf(" (no errors)\n");
314 else for (int i = 0; i < SPF_response_messages(spf_response); i++)
315   {
316   SPF_error_t * err = SPF_response_message(spf_response, i);
317   debug_printf( "%s_msg = (%d) %s\n",
318                   (SPF_error_errorp(err) ? "warn" : "err"),
319                   SPF_error_code(err),
320                   SPF_error_message(err));
321   }
322 }
323
324
325 /* spf_process adds the envelope sender address to the existing
326    context (if any), retrieves the result, sets up expansion
327    strings and evaluates the condition outcome.
328
329 Return: OK/FAIL  */
330
331 int
332 spf_process(const uschar **listptr, uschar *spf_envelope_sender, int action)
333 {
334 int sep = 0;
335 const uschar *list = *listptr;
336 uschar *spf_result_id;
337 int rc = SPF_RESULT_PERMERROR;
338
339 DEBUG(D_receive) debug_printf("spf_process\n");
340
341 if (!(spf_server && spf_request))
342   /* no global context, assume temp error and skip to evaluation */
343   rc = SPF_RESULT_PERMERROR;
344
345 else if (SPF_request_set_env_from(spf_request, CS spf_envelope_sender))
346   /* Invalid sender address. This should be a real rare occurrence */
347   rc = SPF_RESULT_PERMERROR;
348
349 else
350   {
351   /* get SPF result */
352   if (action == SPF_PROCESS_FALLBACK)
353     {
354     SPF_request_query_fallback(spf_request, &spf_response, CS spf_guess);
355     spf_result_guessed = TRUE;
356     }
357   else
358     SPF_request_query_mailfrom(spf_request, &spf_response);
359
360   /* set up expansion items */
361   spf_header_comment     = US SPF_response_get_header_comment(spf_response);
362   spf_received           = US SPF_response_get_received_spf(spf_response);
363   spf_result             = US SPF_strresult(SPF_response_result(spf_response));
364   spf_smtp_comment       = US SPF_response_get_smtp_comment(spf_response);
365
366   rc = SPF_response_result(spf_response);
367
368   DEBUG(D_acl) spf_response_debug(spf_response);
369   }
370
371 /* We got a result. Now see if we should return OK or FAIL for it */
372 DEBUG(D_acl) debug_printf("SPF result is %s (%d)\n", SPF_strresult(rc), rc);
373
374 if (action == SPF_PROCESS_GUESS && (!strcmp (SPF_strresult(rc), "none")))
375   return spf_process(listptr, spf_envelope_sender, SPF_PROCESS_FALLBACK);
376
377 while ((spf_result_id = string_nextinlist(&list, &sep, NULL, 0)))
378   {
379   BOOL negate, result;
380
381   if ((negate = spf_result_id[0] == '!'))
382     spf_result_id++;
383
384   result = Ustrcmp(spf_result_id, spf_result_id_list[rc].name) == 0;
385   if (negate != result) return OK;
386   }
387
388 /* no match */
389 return FAIL;
390 }
391
392
393
394 gstring *
395 authres_spf(gstring * g)
396 {
397 uschar * s;
398 if (!spf_result) return g;
399
400 g = string_append(g, 2, US";\n\tspf=", spf_result);
401 if (spf_result_guessed)
402   g = string_cat(g, US" (best guess record for domain)");
403
404 s = expand_string(US"$sender_address_domain");
405 if (s && *s)
406   return string_append(g, 2, US" smtp.mailfrom=", s);
407
408 s = sender_helo_name;
409 return s && *s
410   ? string_append(g, 2, US" smtp.helo=", s)
411   : string_cat(g, US" smtp.mailfrom=<>");
412 }
413
414
415 #endif