xref: /netbsd-src/external/ibm-public/postfix/dist/src/global/mail_addr_find.c (revision 67b9b338a7386232ac596b5fd0cd5a9cc8a03c71)
1 /*	$NetBSD: mail_addr_find.c,v 1.4 2022/10/08 16:12:45 christos Exp $	*/
2 
3 /*++
4 /* NAME
5 /*	mail_addr_find 3
6 /* SUMMARY
7 /*	generic address-based lookup
8 /* SYNOPSIS
9 /*	#include <mail_addr_find.h>
10 /*
11 /*	const char *mail_addr_find_int_to_ext(maps, address, extension)
12 /*	MAPS	*maps;
13 /*	const char *address;
14 /*	char	**extension;
15 /*
16 /*	const char *mail_addr_find_opt(maps, address, extension, in_form,
17 /*					query_form, out_form, strategy)
18 /*	MAPS	*maps;
19 /*	const char *address;
20 /*	char	**extension;
21 /*	int	in_form;
22 /*	int	in_form;
23 /*	int	out_form;
24 /*	int	strategy;
25 /* LEGACY SUPPORT
26 /*	const char *mail_addr_find(maps, address, extension)
27 /*	MAPS	*maps;
28 /*	const char *address;
29 /*	char	**extension;
30 /*
31 /*	const char *mail_addr_find_to_internal(maps, address, extension)
32 /*	MAPS	*maps;
33 /*	const char *address;
34 /*	char	**extension;
35 /*
36 /*	const char *mail_addr_find_strategy(maps, address, extension)
37 /*	MAPS	*maps;
38 /*	const char *address;
39 /*	char	**extension;
40 /*	int	strategy;
41 /* DESCRIPTION
42 /*	mail_addr_find*() searches the specified maps for an entry with as
43 /*	key the specified address, and derivations from that address.
44 /*	It is up to the caller to specify its case sensitivity
45 /*	preferences when it opens the maps.
46 /*	The result is overwritten upon each call.
47 /*
48 /*	In the lookup table, the key is expected to be in external
49 /*	form (as produced with the postmap command) and the value is
50 /*	expected to be in external (quoted) form if it is an email
51 /*	address. Override these assumptions with the query_form
52 /*	and out_form arguments.
53 /*
54 /*	With mail_addr_find_int_to_ext(), the specified address is in
55 /*	internal (unquoted) form, the query is made in external (quoted)
56 /*	form, and the result is in the form found in the table (it is
57 /*	not necessarily an email address). This version minimizes
58 /*	internal/external (unquoted/quoted) conversions of the input,
59 /*	query, extension, or result.
60 /*
61 /*	mail_addr_find_opt() gives more control, at the cost of
62 /*	additional conversions between internal and external forms.
63 /*	In particular, output conversion to internal form assumes
64 /*	that the lookup result is an email address.
65 /*
66 /*	mail_addr_find() is used by legacy code that historically searched
67 /*	with internal-form queries. The input is in internal form. It
68 /*	searches with external-form queries first, and falls back to
69 /*	internal-form queries if no result was found and the external
70 /*	and internal forms differ. The result is external form (i.e. no
71 /*	conversion).
72 /*
73 /*	mail_addr_find_to_internal() is like mail_addr_find() but assumes
74 /*	that the lookup result is one external-form email address,
75 /*	and converts it to internal form.
76 /*
77 /*	mail_addr_find_strategy() is like mail_addr_find() but overrides
78 /*	the default search strategy for full and partial addresses.
79 /*
80 /*	Arguments:
81 /* .IP maps
82 /*	Dictionary search path (see maps(3)).
83 /* .IP address
84 /*	The address to be looked up.
85 /* .IP extension
86 /*	A null pointer, or the address of a pointer that is set to
87 /*	the address of a dynamic memory copy of the address extension
88 /*	that had to be chopped off in order to match the lookup tables.
89 /*	The copy includes the recipient address delimiter.
90 /*	The copy is in internal (unquoted) form.
91 /*	The caller is expected to pass the copy to myfree().
92 /* .IP query_form
93 /*	The address form to use for database queries: one of
94 /*	MA_FORM_INTERNAL (unquoted form), MA_FORM_EXTERNAL (quoted form),
95 /*	MA_FORM_EXTERNAL_FIRST (external form, then internal form if the
96 /*	external and internal forms differ), or MA_FORM_INTERNAL_FIRST
97 /*	(internal form, then external form if the internal and external
98 /*	forms differ).
99 /* .IP in_form
100 /* .IP out_form
101 /*	Input and output address forms, one of MA_FORM_INTERNAL (unquoted
102 /*	form), or MA_FORM_EXTERNAL (quoted form).
103 /* .IP strategy
104 /*	The lookup strategy for full and partial addresses, specified
105 /*	as the binary OR of one or more of the following. These lookups
106 /*	are implemented in the order as listed below.
107 /* .RS
108 /* .IP MA_FIND_DEFAULT
109 /*	A convenience alias for (MA_FIND_FULL |
110 /*	MA_FIND_NOEXT | MA_FIND_LOCALPART_IF_LOCAL |
111 /*	MA_FIND_AT_DOMAIN).
112 /* .IP MA_FIND_FULL
113 /*	Look up the full email address.
114 /* .IP MA_FIND_NOEXT
115 /*	If no match was found, and the address has a localpart extension,
116 /*	look up the address after removing the extension.
117 /* .IP MA_FIND_LOCALPART_IF_LOCAL
118 /*	If no match was found, and the domain matches myorigin,
119 /*	mydestination, or any inet_interfaces or proxy_interfaces IP
120 /*	address, look up the localpart.  If no match was found, and the
121 /*	address has a localpart extension, repeat the same query after
122 /*	removing the extension unless MA_FIND_NOEXT is specified.
123 /* .IP MA_FIND_LOCALPART_AT_IF_LOCAL
124 /*	As above, but using the localpart@ instead.
125 /* .IP MA_FIND_AT_DOMAIN
126 /*	If no match was found, look up the @domain without localpart.
127 /* .IP MA_FIND_DOMAIN
128 /*	If no match was found, look up the domain without localpart.
129 /* .IP MA_FIND_PDMS
130 /*	When used with MA_FIND_DOMAIN, the domain also matches subdomains.
131 /* .IP MA_FIND_PDDMDS
132 /*	When used with MA_FIND_DOMAIN, dot-domain also matches
133 /*	dot-subdomains.
134 /* .IP MA_FIND_LOCALPART_AT
135 /*	If no match was found, look up the localpart@, regardless of
136 /*	the domain content.
137 /* .RE
138 /* DIAGNOSTICS
139 /*	The maps->error value is non-zero when the lookup failed due to
140 /*	a non-permanent error.
141 /* SEE ALSO
142 /*	maps(3), multi-dictionary search resolve_local(3), recognize
143 /*	local system
144 /* LICENSE
145 /* .ad
146 /* .fi
147 /*	The Secure Mailer license must be distributed with this software.
148 /* AUTHOR(S)
149 /*	Wietse Venema
150 /*	IBM T.J. Watson Research
151 /*	P.O. Box 704
152 /*	Yorktown Heights, NY 10598, USA
153 /*
154 /*	Wietse Venema
155 /*	Google, Inc.
156 /*	111 8th Avenue
157 /*	New York, NY 10011, USA
158 /*--*/
159 
160 /* System library. */
161 
162 #include <sys_defs.h>
163 #include <string.h>
164 
165 /* Utility library. */
166 
167 #include <msg.h>
168 #include <name_mask.h>
169 #include <dict.h>
170 #include <stringops.h>
171 #include <mymalloc.h>
172 #include <vstring.h>
173 
174 /* Global library. */
175 
176 #include <mail_params.h>
177 #include <strip_addr.h>
178 #include <mail_addr_find.h>
179 #include <resolve_local.h>
180 #include <quote_822_local.h>
181 
182 /* Application-specific. */
183 
184 #define STR	vstring_str
185 
186 #ifdef TEST
187 
188 static const NAME_MASK strategy_table[] = {
189     "full", MA_FIND_FULL,
190     "noext", MA_FIND_NOEXT,
191     "localpart_if_local", MA_FIND_LOCALPART_IF_LOCAL,
192     "localpart_at_if_local", MA_FIND_LOCALPART_AT_IF_LOCAL,
193     "at_domain", MA_FIND_AT_DOMAIN,
194     "domain", MA_FIND_DOMAIN,
195     "pdms", MA_FIND_PDMS,
196     "pddms", MA_FIND_PDDMDS,
197     "localpart_at", MA_FIND_LOCALPART_AT,
198     "default", MA_FIND_DEFAULT,
199     0, -1,
200 };
201 
202 /* strategy_from_string - symbolic strategy flags to internal form */
203 
strategy_from_string(const char * strategy_string)204 static int strategy_from_string(const char *strategy_string)
205 {
206     return (name_mask_delim_opt("strategy_from_string", strategy_table,
207 				strategy_string, "|",
208 				NAME_MASK_WARN | NAME_MASK_ANY_CASE));
209 }
210 
211 /* strategy_to_string - internal form to symbolic strategy flags */
212 
strategy_to_string(VSTRING * res_buf,int strategy_mask)213 static const char *strategy_to_string(VSTRING *res_buf, int strategy_mask)
214 {
215     static VSTRING *my_buf;
216 
217     if (res_buf == 0 && (res_buf = my_buf) == 0)
218 	res_buf = my_buf = vstring_alloc(20);
219     return (str_name_mask_opt(res_buf, "strategy_to_string",
220 			      strategy_table, strategy_mask,
221 			      NAME_MASK_WARN | NAME_MASK_PIPE));
222 }
223 
224 #endif
225 
226  /*
227   * Specify what keys are partial or full, to avoid matching partial
228   * addresses with regular expressions.
229   */
230 #define FULL	0
231 #define PARTIAL	DICT_FLAG_FIXED
232 
233 /* find_addr - helper to search maps with the right query form */
234 
find_addr(MAPS * path,const char * address,int flags,int with_domain,int query_form,VSTRING * ext_addr_buf)235 static const char *find_addr(MAPS *path, const char *address, int flags,
236 	             int with_domain, int query_form, VSTRING *ext_addr_buf)
237 {
238     const char *result;
239 
240 #define SANS_DOMAIN	0
241 #define WITH_DOMAIN	1
242 
243     switch (query_form) {
244 
245 	/*
246 	 * Query with external-form (quoted) address. The code looks a bit
247 	 * unusual to emphasize the symmetry with the other cases.
248 	 */
249     case MA_FORM_EXTERNAL:
250     case MA_FORM_EXTERNAL_FIRST:
251 	quote_822_local_flags(ext_addr_buf, address,
252 			      with_domain ? QUOTE_FLAG_DEFAULT :
253 			    QUOTE_FLAG_DEFAULT | QUOTE_FLAG_BARE_LOCALPART);
254 	result = maps_find(path, STR(ext_addr_buf), flags);
255 	if (result != 0 || path->error != 0
256 	    || query_form != MA_FORM_EXTERNAL_FIRST
257 	    || strcmp(address, STR(ext_addr_buf)) == 0)
258 	    break;
259 	result = maps_find(path, address, flags);
260 	break;
261 
262 	/*
263 	 * Query with internal-form (unquoted) address. The code looks a bit
264 	 * unusual to emphasize the symmetry with the other cases.
265 	 */
266     case MA_FORM_INTERNAL:
267     case MA_FORM_INTERNAL_FIRST:
268 	result = maps_find(path, address, flags);
269 	if (result != 0 || path->error != 0
270 	    || query_form != MA_FORM_INTERNAL_FIRST)
271 	    break;
272 	quote_822_local_flags(ext_addr_buf, address,
273 			      with_domain ? QUOTE_FLAG_DEFAULT :
274 			    QUOTE_FLAG_DEFAULT | QUOTE_FLAG_BARE_LOCALPART);
275 	if (strcmp(address, STR(ext_addr_buf)) == 0)
276 	    break;
277 	result = maps_find(path, STR(ext_addr_buf), flags);
278 	break;
279 
280 	/*
281 	 * Can't happen.
282 	 */
283     default:
284 	msg_panic("mail_addr_find: bad query_form: %d", query_form);
285     }
286     return (result);
287 }
288 
289 /* find_local - search on localpart info */
290 
find_local(MAPS * path,char * ratsign,int rats_offs,char * int_full_key,char * int_bare_key,int query_form,char ** extp,char ** saved_ext,VSTRING * ext_addr_buf)291 static const char *find_local(MAPS *path, char *ratsign, int rats_offs,
292 			              char *int_full_key, char *int_bare_key,
293 		              int query_form, char **extp, char **saved_ext,
294 			              VSTRING *ext_addr_buf)
295 {
296     const char *myname = "mail_addr_find";
297     const char *result;
298     int     with_domain;
299     int     saved_ch;
300 
301     /*
302      * This code was ripped from the middle of a function so that it can be
303      * reused multiple times, that's why the interface makes little sense.
304      */
305     with_domain = rats_offs ? WITH_DOMAIN : SANS_DOMAIN;
306 
307     saved_ch = *(unsigned char *) (ratsign + rats_offs);
308     *(ratsign + rats_offs) = 0;
309     result = find_addr(path, int_full_key, PARTIAL, with_domain,
310 		       query_form, ext_addr_buf);
311     *(ratsign + rats_offs) = saved_ch;
312     if (result == 0 && path->error == 0 && int_bare_key != 0) {
313 	if ((ratsign = strrchr(int_bare_key, '@')) == 0)
314 	    msg_panic("%s: bare key botch", myname);
315 	saved_ch = *(unsigned char *) (ratsign + rats_offs);
316 	*(ratsign + rats_offs) = 0;
317 	if ((result = find_addr(path, int_bare_key, PARTIAL, with_domain,
318 				query_form, ext_addr_buf)) != 0
319 	    && extp != 0) {
320 	    *extp = *saved_ext;
321 	    *saved_ext = 0;
322 	}
323 	*(ratsign + rats_offs) = saved_ch;
324     }
325     return result;
326 }
327 
328 /* mail_addr_find_opt - map a canonical address */
329 
mail_addr_find_opt(MAPS * path,const char * address,char ** extp,int in_form,int query_form,int out_form,int strategy)330 const char *mail_addr_find_opt(MAPS *path, const char *address, char **extp,
331 			               int in_form, int query_form,
332 			               int out_form, int strategy)
333 {
334     const char *myname = "mail_addr_find";
335     VSTRING *ext_addr_buf = 0;
336     VSTRING *int_addr_buf = 0;
337     const char *int_addr;
338     static VSTRING *int_result = 0;
339     const char *result;
340     char   *ratsign = 0;
341     char   *int_full_key;
342     char   *int_bare_key;
343     char   *saved_ext;
344     int     rc = 0;
345 
346     /*
347      * Optionally convert the address from external form.
348      */
349     if (in_form == MA_FORM_EXTERNAL) {
350 	int_addr_buf = vstring_alloc(100);
351 	unquote_822_local(int_addr_buf, address);
352 	int_addr = STR(int_addr_buf);
353     } else {
354 	int_addr = address;
355     }
356     if (query_form == MA_FORM_EXTERNAL_FIRST
357 	|| query_form == MA_FORM_EXTERNAL)
358 	ext_addr_buf = vstring_alloc(100);
359 
360     /*
361      * Initialize.
362      */
363     int_full_key = mystrdup(int_addr);
364     if (*var_rcpt_delim == 0 || (strategy & MA_FIND_NOEXT) == 0) {
365 	int_bare_key = saved_ext = 0;
366     } else {
367 	/* XXX This could be done after user+foo@domain fails. */
368 	int_bare_key =
369 	    strip_addr_internal(int_full_key, &saved_ext, var_rcpt_delim);
370     }
371 
372     /*
373      * Try user+foo@domain and user@domain.
374      */
375     if ((strategy & MA_FIND_FULL) != 0) {
376 	result = find_addr(path, int_full_key, FULL, WITH_DOMAIN,
377 			   query_form, ext_addr_buf);
378     } else {
379 	result = 0;
380 	path->error = 0;
381     }
382 
383     if (result == 0 && path->error == 0 && int_bare_key != 0
384 	&& (result = find_addr(path, int_bare_key, PARTIAL, WITH_DOMAIN,
385 			       query_form, ext_addr_buf)) != 0
386 	&& extp != 0) {
387 	*extp = saved_ext;
388 	saved_ext = 0;
389     }
390 
391     /*
392      * Try user+foo if the domain matches user+foo@$myorigin,
393      * user+foo@$mydestination or user+foo@[${proxy,inet}_interfaces]. Then
394      * try with +foo stripped off.
395      */
396     if (result == 0 && path->error == 0
397 	&& (ratsign = strrchr(int_full_key, '@')) != 0
398 	&& (strategy & (MA_FIND_LOCALPART_IF_LOCAL
399 			| MA_FIND_LOCALPART_AT_IF_LOCAL)) != 0) {
400 	if (strcasecmp_utf8(ratsign + 1, var_myorigin) == 0
401 	    || (rc = resolve_local(ratsign + 1)) > 0) {
402 	    if ((strategy & MA_FIND_LOCALPART_IF_LOCAL) != 0)
403 		result = find_local(path, ratsign, 0, int_full_key,
404 				 int_bare_key, query_form, extp, &saved_ext,
405 				    ext_addr_buf);
406 	    if (result == 0 && path->error == 0
407 		&& (strategy & MA_FIND_LOCALPART_AT_IF_LOCAL) != 0)
408 		result = find_local(path, ratsign, 1, int_full_key,
409 				 int_bare_key, query_form, extp, &saved_ext,
410 				    ext_addr_buf);
411 	} else if (rc < 0)
412 	    path->error = rc;
413     }
414 
415     /*
416      * Try @domain.
417      */
418     if (result == 0 && path->error == 0 && ratsign != 0
419 	&& (strategy & MA_FIND_AT_DOMAIN) != 0)
420 	result = maps_find(path, ratsign, PARTIAL);
421 
422     /*
423      * Try domain (optionally, subdomains).
424      */
425     if (result == 0 && path->error == 0 && ratsign != 0
426 	&& (strategy & MA_FIND_DOMAIN) != 0) {
427 	const char *name;
428 	const char *next;
429 
430 	if ((strategy & MA_FIND_PDMS) && (strategy & MA_FIND_PDDMDS))
431 	    msg_warn("mail_addr_find_opt: do not specify both "
432 		     "MA_FIND_PDMS and MA_FIND_PDDMDS");
433 	for (name = ratsign + 1; *name != 0; name = next) {
434 	    if ((result = maps_find(path, name, PARTIAL)) != 0
435 		|| path->error != 0
436 		|| (strategy & (MA_FIND_PDMS | MA_FIND_PDDMDS)) == 0
437 		|| (next = strchr(name + 1, '.')) == 0)
438 		break;
439 	    if ((strategy & MA_FIND_PDDMDS) == 0)
440 		next++;
441 	}
442     }
443 
444     /*
445      * Try localpart@ even if the domain is not local.
446      */
447     if ((strategy & MA_FIND_LOCALPART_AT) != 0 \
448 	&&result == 0 && path->error == 0)
449 	result = find_local(path, ratsign, 1, int_full_key,
450 			    int_bare_key, query_form, extp, &saved_ext,
451 			    ext_addr_buf);
452 
453     /*
454      * Optionally convert the result to internal form. The lookup result is
455      * supposed to be one external-form email address.
456      */
457     if (result != 0 && out_form == MA_FORM_INTERNAL) {
458 	if (int_result == 0)
459 	    int_result = vstring_alloc(100);
460 	unquote_822_local(int_result, result);
461 	result = STR(int_result);
462     }
463 
464     /*
465      * Clean up.
466      */
467     if (msg_verbose)
468 	msg_info("%s: %s -> %s", myname, address,
469 		 result ? result :
470 		 path->error ? "(try again)" :
471 		 "(not found)");
472     myfree(int_full_key);
473     if (int_bare_key)
474 	myfree(int_bare_key);
475     if (saved_ext)
476 	myfree(saved_ext);
477     if (int_addr_buf)
478 	vstring_free(int_addr_buf);
479     if (ext_addr_buf)
480 	vstring_free(ext_addr_buf);
481     return (result);
482 }
483 
484 #ifdef TEST
485 
486  /*
487   * Proof-of-concept test program. Read an address and expected results from
488   * stdin, and warn about any discrepancies.
489   */
490 #include <ctype.h>
491 #include <stdlib.h>
492 
493 #include <vstream.h>
494 #include <vstring_vstream.h>
495 #include <mail_params.h>
496 
usage(const char * progname)497 static NORETURN usage(const char *progname)
498 {
499     msg_fatal("usage: %s [-v]", progname);
500 }
501 
main(int argc,char ** argv)502 int     main(int argc, char **argv)
503 {
504     VSTRING *buffer = vstring_alloc(100);
505     char   *bp;
506     MAPS   *path = 0;
507     const char *result;
508     char   *extent;
509     char   *cmd;
510     char   *in_field;
511     char   *query_field;
512     char   *out_field;
513     char   *strategy_field;
514     char   *key_field;
515     char   *expect_res;
516     char   *expect_ext;
517     int     in_form;
518     int     query_form;
519     int     out_form;
520     int     strategy_flags;
521     int     ch;
522     int     errs = 0;
523 
524     /*
525      * Parse JCL.
526      */
527     while ((ch = GETOPT(argc, argv, "v")) > 0) {
528 	switch (ch) {
529 	case 'v':
530 	    msg_verbose++;
531 	    break;
532 	default:
533 	    usage(argv[0]);
534 	}
535     }
536     if (argc != optind)
537 	usage(argv[0]);
538 
539     /*
540      * Initialize.
541      */
542 #define UPDATE(var, val) do { myfree(var); var = mystrdup(val); } while (0)
543 
544     mail_params_init();
545 
546     /*
547      * TODO: move these assignments into the read/eval loop.
548      */
549     UPDATE(var_rcpt_delim, "+");
550     UPDATE(var_mydomain, "localdomain");
551     UPDATE(var_myorigin, "localdomain");
552     UPDATE(var_mydest, "localhost.localdomain");
553     while (vstring_fgets_nonl(buffer, VSTREAM_IN)) {
554 	bp = STR(buffer);
555 	if (msg_verbose)
556 	    msg_info("> %s", bp);
557 	if ((cmd = mystrtok(&bp, CHARS_SPACE)) == 0 || *cmd == '#')
558 	    continue;
559 	while (ISSPACE(*bp))
560 	    bp++;
561 
562 	/*
563 	 * Visible comment.
564 	 */
565 	if (strcmp(cmd, "echo") == 0) {
566 	    vstream_printf("%s\n", bp);
567 	}
568 
569 	/*
570 	 * Open maps.
571 	 */
572 	else if (strcmp(cmd, "maps") == 0) {
573 	    if (path)
574 		maps_free(path);
575 	    path = maps_create(argv[0], bp, DICT_FLAG_LOCK
576 			     | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST);
577 	    vstream_printf("%s\n", bp);
578 	    continue;
579 	}
580 
581 	/*
582 	 * Lookup and verify.
583 	 */
584 	else if (path && strcmp(cmd, "test") == 0) {
585 
586 	    /*
587 	     * Parse the input and expectations.
588 	     */
589 	    /* internal, external. */
590 	    if ((in_field = mystrtok(&bp, ":")) == 0)
591 		msg_fatal("no input form");
592 	    if ((in_form = mail_addr_form_from_string(in_field)) < 0)
593 		msg_fatal("bad input form: '%s'", in_field);
594 	    if ((query_field = mystrtok(&bp, ":")) == 0)
595 		msg_fatal("no query form");
596 	    /* internal, external, external-first. */
597 	    if ((query_form = mail_addr_form_from_string(query_field)) < 0)
598 		msg_fatal("bad query form: '%s'", query_field);
599 	    if ((out_field = mystrtok(&bp, ":")) == 0)
600 		msg_fatal("no output form");
601 	    /* internal, external. */
602 	    if ((out_form = mail_addr_form_from_string(out_field)) < 0)
603 		msg_fatal("bad output form: '%s'", out_field);
604 	    if ((strategy_field = mystrtok(&bp, ":")) == 0)
605 		msg_fatal("no strategy field");
606 	    if ((strategy_flags = strategy_from_string(strategy_field)) < 0)
607 		msg_fatal("bad strategy field: '%s'", strategy_field);
608 	    if ((key_field = mystrtok(&bp, ":")) == 0)
609 		msg_fatal("no search key");
610 	    expect_res = mystrtok(&bp, ":");
611 	    expect_ext = mystrtok(&bp, ":");
612 	    if (mystrtok(&bp, ":") != 0)
613 		msg_fatal("garbage after extension field");
614 
615 	    /*
616 	     * Lookups.
617 	     */
618 	    extent = 0;
619 	    result = mail_addr_find_opt(path, key_field, &extent,
620 					in_form, query_form, out_form,
621 					strategy_flags);
622 	    vstream_printf("%s:%s -%s-> %s:%s (%s)\n",
623 	      in_field, key_field, query_field, out_field, result ? result :
624 			   path->error ? "(try again)" :
625 			 "(not found)", extent ? extent : "null extension");
626 	    vstream_fflush(VSTREAM_OUT);
627 
628 	    /*
629 	     * Enforce expectations.
630 	     */
631 	    if (expect_res && result) {
632 		if (strcmp(expect_res, result) != 0) {
633 		    msg_warn("expect result '%s' but got '%s'", expect_res, result);
634 		    errs = 1;
635 		    if (expect_ext && extent) {
636 			if (strcmp(expect_ext, extent) != 0)
637 			    msg_warn("expect extension '%s' but got '%s'",
638 				     expect_ext, extent);
639 			errs = 1;
640 		    } else if (expect_ext && !extent) {
641 			msg_warn("expect extension '%s' but got none", expect_ext);
642 			errs = 1;
643 		    } else if (!expect_ext && extent) {
644 			msg_warn("expect no extension but got '%s'", extent);
645 			errs = 1;
646 		    }
647 		}
648 	    } else if (expect_res && !result) {
649 		msg_warn("expect result '%s' but got none", expect_res);
650 		errs = 1;
651 	    } else if (!expect_res && result) {
652 		msg_warn("expected no result but got '%s'", result);
653 		errs = 1;
654 	    }
655 	    vstream_fflush(VSTREAM_OUT);
656 	    if (extent)
657 		myfree(extent);
658 	}
659 
660 	/*
661 	 * Unknown request.
662 	 */
663 	else {
664 	    msg_warn("bad request: %s", cmd);
665 	}
666     }
667     vstring_free(buffer);
668 
669     maps_free(path);
670     return (errs != 0);
671 }
672 
673 #endif
674