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