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