Home | History | Annotate | Line # | Download | only in trivial-rewrite
      1 /*	$NetBSD: resolve.c,v 1.6 2026/05/09 18:49:21 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	resolve 3
      6 /* SUMMARY
      7 /*	mail address resolver
      8 /* SYNOPSIS
      9 /*	#include "trivial-rewrite.h"
     10 /*
     11 /*	void	resolve_init(void)
     12 /*
     13 /*	int	resolve_class(domain)
     14 /*	const char *domain;
     15 /*
     16 /*	void	resolve_proto(context, stream)
     17 /*	RES_CONTEXT *context;
     18 /*	VSTREAM	*stream;
     19 /* DESCRIPTION
     20 /*	This module implements the trivial address resolving engine.
     21 /*	It distinguishes between local and remote mail, and optionally
     22 /*	consults one or more transport tables that map a destination
     23 /*	to a transport, nexthop pair.
     24 /*
     25 /*	resolve_init() initializes data structures that are private
     26 /*	to this module. It should be called once before using the
     27 /*	actual resolver routines.
     28 /*
     29 /*	resolve_class() returns the address class for the specified
     30 /*	domain, or -1 in case of error.
     31 /*
     32 /*	resolve_proto() implements the client-server protocol:
     33 /*	read one address in FQDN form, reply with a (transport,
     34 /*	nexthop, internalized recipient) triple.
     35 /* STANDARDS
     36 /* DIAGNOSTICS
     37 /*	Problems and transactions are logged to \fBsyslogd\fR(8)
     38 /*	or \fBpostlogd\fR(8).
     39 /* BUGS
     40 /* SEE ALSO
     41 /* LICENSE
     42 /* .ad
     43 /* .fi
     44 /*	The Secure Mailer license must be distributed with this software.
     45 /* AUTHOR(S)
     46 /*	Wietse Venema
     47 /*	IBM T.J. Watson Research
     48 /*	P.O. Box 704
     49 /*	Yorktown Heights, NY 10598, USA
     50 /*
     51 /*	Wietse Venema
     52 /*	Google, Inc.
     53 /*	111 8th Avenue
     54 /*	New York, NY 10011, USA
     55 /*--*/
     56 
     57 /* System library. */
     58 
     59 #include <sys_defs.h>
     60 #include <stdlib.h>
     61 #include <string.h>
     62 
     63 #ifdef STRCASECMP_IN_STRINGS_H
     64 #include <strings.h>
     65 #endif
     66 
     67 /* Utility library. */
     68 
     69 #include <msg.h>
     70 #include <vstring.h>
     71 #include <vstream.h>
     72 #include <vstring_vstream.h>
     73 #include <split_at.h>
     74 #include <valid_utf8_hostname.h>
     75 #include <stringops.h>
     76 #include <mymalloc.h>
     77 
     78 /* Global library. */
     79 
     80 #include <mail_params.h>
     81 #include <mail_proto.h>
     82 #include <resolve_local.h>
     83 #include <mail_conf.h>
     84 #include <quote_822_local.h>
     85 #include <tok822.h>
     86 #include <domain_list.h>
     87 #include <string_list.h>
     88 #include <match_parent_style.h>
     89 #include <maps.h>
     90 #include <mail_addr_find.h>
     91 #include <valid_mailhost_addr.h>
     92 #include <dsn_util.h>
     93 
     94 /* Application-specific. */
     95 
     96 #include "trivial-rewrite.h"
     97 #include "transport.h"
     98 
     99  /*
    100   * The job of the address resolver is to map one recipient address to a
    101   * triple of (channel, nexthop, recipient). The channel is the name of the
    102   * delivery service specified in master.cf, the nexthop is (usually) a
    103   * description of the next host to deliver to, and recipient is the final
    104   * recipient address. The latter may differ from the input address as the
    105   * result of stripping multiple layers of sender-specified routing.
    106   *
    107   * Addresses are resolved by their domain name. Known domain names are
    108   * categorized into classes: local, virtual alias, virtual mailbox, relay,
    109   * and everything else. Finding the address domain class is a matter of
    110   * table lookups.
    111   *
    112   * Different address domain classes generally use different delivery channels,
    113   * and may use class dependent ways to arrive at the corresponding nexthop
    114   * information. With classes that do final delivery, the nexthop is
    115   * typically the local machine hostname.
    116   *
    117   * The transport lookup table provides a means to override the domain class
    118   * channel and/or nexhop information for specific recipients or for entire
    119   * domain hierarchies.
    120   *
    121   * This works well in the general case. The only bug in this approach is that
    122   * the structure of the nexthop information is transport dependent.
    123   * Typically, the nexthop specifies a hostname, hostname + TCP Port, or the
    124   * pathname of a UNIX-domain socket. However, with the error transport the
    125   * nexthop field contains free text with the reason for non-delivery.
    126   *
    127   * Therefore, a transport map entry that overrides the channel but not the
    128   * nexthop information (or vice versa) may produce surprising results. In
    129   * particular, the free text nexthop information for the error transport is
    130   * likely to confuse regular delivery agents; and conversely, a hostname or
    131   * socket pathname is not an adequate text as reason for non-delivery.
    132   *
    133   * In the code below, rcpt_domain specifies the domain name that we will use
    134   * when the transport table specifies a non-default channel but no nexthop
    135   * information (we use a generic text when that non-default channel is the
    136   * error transport).
    137   */
    138 
    139 #define STR	vstring_str
    140 #define LEN	VSTRING_LEN
    141 
    142  /*
    143   * Some of the lists that define the address domain classes.
    144   */
    145 static DOMAIN_LIST *relay_domains;
    146 static STRING_LIST *virt_alias_doms;
    147 static STRING_LIST *virt_mailbox_doms;
    148 
    149 static MAPS *relocated_maps;
    150 
    151 /* resolve_class - determine domain address class */
    152 
    153 int     resolve_class(const char *domain)
    154 {
    155     int     ret;
    156 
    157     /*
    158      * Same order as in resolve_addr().
    159      */
    160     if ((ret = resolve_local(domain)) != 0)
    161 	return (ret > 0 ? RESOLVE_CLASS_LOCAL : -1);
    162     if (virt_alias_doms) {
    163 	if (string_list_match(virt_alias_doms, domain))
    164 	    return (RESOLVE_CLASS_ALIAS);
    165 	if (virt_alias_doms->error)
    166 	    return (-1);
    167     }
    168     if (virt_mailbox_doms) {
    169 	if (string_list_match(virt_mailbox_doms, domain))
    170 	    return (RESOLVE_CLASS_VIRTUAL);
    171 	if (virt_mailbox_doms->error)
    172 	    return (-1);
    173     }
    174     if (relay_domains) {
    175 	if (string_list_match(relay_domains, domain))
    176 	    return (RESOLVE_CLASS_RELAY);
    177 	if (relay_domains->error)
    178 	    return (-1);
    179     }
    180     return (RESOLVE_CLASS_DEFAULT);
    181 }
    182 
    183 /* resolve_addr - resolve address according to rule set */
    184 
    185 static void resolve_addr(RES_CONTEXT *rp, char *sender, char *addr,
    186 			         VSTRING *channel, VSTRING *nexthop,
    187 			         VSTRING *nextrcpt, int *flags)
    188 {
    189     const char *myname = "resolve_addr";
    190     VSTRING *addr_buf = vstring_alloc(100);
    191     TOK822 *tree = 0;
    192     TOK822 *saved_domain = 0;
    193     TOK822 *domain = 0;
    194     char   *destination;
    195     const char *blame = 0;
    196     const char *rcpt_domain;
    197     ssize_t addr_len;
    198     ssize_t loop_count;
    199     ssize_t loop_max;
    200     char   *local;
    201     char   *oper;
    202     char   *junk;
    203     const char *relay;
    204     const char *xport;
    205     const char *sender_key;
    206     int     rc;
    207 
    208     *flags = 0;
    209     vstring_strcpy(channel, "CHANNEL NOT UPDATED");
    210     vstring_strcpy(nexthop, "NEXTHOP NOT UPDATED");
    211     vstring_strcpy(nextrcpt, "NEXTRCPT NOT UPDATED");
    212 
    213     /*
    214      * The address is in internalized (unquoted) form.
    215      *
    216      * In an ideal world we would parse the externalized address form as given
    217      * to us by the sender.
    218      *
    219      * However, in the real world we have to look for routing characters like
    220      * %@! in the address local-part, even when that information is quoted
    221      * due to the presence of special characters or whitespace. Although
    222      * technically incorrect, this is needed to stop user@domain@domain relay
    223      * attempts when forwarding mail to a Sendmail MX host.
    224      *
    225      * This suggests that we parse the address in internalized (unquoted) form.
    226      * Unfortunately, if we do that, the unparser generates incorrect white
    227      * space between adjacent non-operator tokens. Example: ``first last''
    228      * needs white space, but ``stuff[stuff]'' does not. This is not a
    229      * problem when unparsing the result from parsing externalized forms,
    230      * because the parser/unparser were designed for valid externalized forms
    231      * where ``stuff[stuff]'' does not happen.
    232      *
    233      * As a workaround we start with the quoted form and then dequote the
    234      * local-part only where needed. This will do the right thing in most
    235      * (but not all) cases.
    236      */
    237     addr_len = strlen(addr);
    238     quote_822_local(addr_buf, addr);
    239     tree = tok822_scan_addr(vstring_str(addr_buf));
    240 
    241     /*
    242      * The optimizer will eliminate tests that always fail, and will replace
    243      * multiple expansions of this macro by a GOTO to a single instance.
    244      */
    245 #define FREE_MEMORY_AND_RETURN { \
    246 	if (saved_domain) \
    247 	    tok822_free_tree(saved_domain); \
    248 	if(tree) \
    249 	    tok822_free_tree(tree); \
    250 	if (addr_buf) \
    251 	    vstring_free(addr_buf); \
    252 	return; \
    253     }
    254 
    255     /*
    256      * Preliminary resolver: strip off all instances of the local domain.
    257      * Terminate when no destination domain is left over, or when the
    258      * destination domain is remote.
    259      *
    260      * XXX To whom it may concern. If you change the resolver loop below, or
    261      * quote_822_local.c, or tok822_parse.c, be sure to re-run the tests
    262      * under "make resolve_clnt_test" in the global directory.
    263      */
    264 #define RESOLVE_LOCAL(domain) \
    265     resolve_local(STR(tok822_internalize(addr_buf, domain, TOK822_STR_DEFL)))
    266 
    267     for (loop_count = 0, loop_max = addr_len + 100; /* void */ ; loop_count++) {
    268 
    269 	/*
    270 	 * XXX Should never happen, but if this happens with some
    271 	 * pathological address, then that is not sufficient reason to
    272 	 * disrupt the operation of an MTA.
    273 	 */
    274 	if (loop_count > loop_max) {
    275 	    msg_warn("resolve_addr: <%s>: giving up after %ld iterations",
    276 		     addr, (long) loop_count);
    277 	    *flags |= RESOLVE_FLAG_FAIL;
    278 	    FREE_MEMORY_AND_RETURN;
    279 	    break;
    280 	}
    281 
    282 	/*
    283 	 * Strip trailing dot at end of domain, but not dot-dot or at-dot.
    284 	 * This merely makes diagnostics more accurate by leaving bogus
    285 	 * addresses alone.
    286 	 */
    287 	if (tree->tail
    288 	    && tree->tail->type == '.'
    289 	    && tok822_rfind_type(tree->tail, '@') != 0
    290 	    && tree->tail->prev->type != '.'
    291 	    && tree->tail->prev->type != '@')
    292 	    tok822_free_tree(tok822_sub_keep_before(tree, tree->tail));
    293 
    294 	/*
    295 	 * Strip trailing @.
    296 	 */
    297 	if (var_resolve_nulldom
    298 	    && tree->tail
    299 	    && tree->tail->type == '@')
    300 	    tok822_free_tree(tok822_sub_keep_before(tree, tree->tail));
    301 
    302 	/*
    303 	 * Strip (and save) @domain if local.
    304 	 *
    305 	 * Grr. resolve_local() table lookups may fail. It may be OK for local
    306 	 * file lookup code to abort upon failure, but with network-based
    307 	 * tables it is preferable to return an error indication to the
    308 	 * requestor.
    309 	 */
    310 	if ((domain = tok822_rfind_type(tree->tail, '@')) != 0) {
    311 	    if (domain->next && (rc = RESOLVE_LOCAL(domain->next)) <= 0) {
    312 		if (rc < 0) {
    313 		    *flags |= RESOLVE_FLAG_FAIL;
    314 		    FREE_MEMORY_AND_RETURN;
    315 		}
    316 		break;
    317 	    }
    318 	    tok822_sub_keep_before(tree, domain);
    319 	    if (saved_domain)
    320 		tok822_free_tree(saved_domain);
    321 	    saved_domain = domain;
    322 	    domain = 0;				/* safety for future change */
    323 	}
    324 
    325 	/*
    326 	 * After stripping the local domain, if any, replace foo%bar by
    327 	 * foo@bar, site!user by user@site, rewrite to canonical form, and
    328 	 * retry.
    329 	 */
    330 	if (tok822_rfind_type(tree->tail, '@')
    331 	    || (var_swap_bangpath && tok822_rfind_type(tree->tail, '!'))
    332 	    || (var_percent_hack && tok822_rfind_type(tree->tail, '%'))) {
    333 	    rewrite_tree(&local_context, tree);
    334 	    continue;
    335 	}
    336 
    337 	/*
    338 	 * If the local-part is a quoted string, crack it open when we're
    339 	 * permitted to do so and look for routing operators. This is
    340 	 * technically incorrect, but is needed to stop relaying problems.
    341 	 *
    342 	 * XXX Do another feeble attempt to keep local-part info quoted.
    343 	 */
    344 	if (var_resolve_dequoted
    345 	    && tree->head && tree->head == tree->tail
    346 	    && tree->head->type == TOK822_QSTRING
    347 	    && ((oper = strrchr(local = STR(tree->head->vstr), '@')) != 0
    348 		|| (var_percent_hack && (oper = strrchr(local, '%')) != 0)
    349 	     || (var_swap_bangpath && (oper = strrchr(local, '!')) != 0))) {
    350 	    if (*oper == '%')
    351 		*oper = '@';
    352 	    tok822_internalize(addr_buf, tree->head, TOK822_STR_DEFL);
    353 	    if (*oper == '@') {
    354 		junk = mystrdup(STR(addr_buf));
    355 		quote_822_local(addr_buf, junk);
    356 		myfree(junk);
    357 	    }
    358 	    tok822_free(tree->head);
    359 	    tree->head = tok822_scan(STR(addr_buf), &tree->tail);
    360 	    rewrite_tree(&local_context, tree);
    361 	    continue;
    362 	}
    363 
    364 	/*
    365 	 * An empty local-part or an empty quoted string local-part becomes
    366 	 * the local MAILER-DAEMON, for consistency with our own From:
    367 	 * message headers.
    368 	 */
    369 	if (tree->head && tree->head == tree->tail
    370 	    && tree->head->type == TOK822_QSTRING
    371 	    && VSTRING_LEN(tree->head->vstr) == 0) {
    372 	    tok822_free(tree->head);
    373 	    tree->head = 0;
    374 	}
    375 	/* XXX Re-resolve the surrogate, in case already in user@domain form. */
    376 	if (tree->head == 0) {
    377 	    tree->head = tok822_scan(var_empty_addr, &tree->tail);
    378 	    continue;
    379 	}
    380 	/* XXX Re-resolve with @$myhostname for backwards compatibility. */
    381 	if (domain == 0 && saved_domain == 0) {
    382 	    tok822_sub_append(tree, tok822_alloc('@', (char *) 0));
    383 	    tok822_sub_append(tree, tok822_scan(var_myhostname, (TOK822 **) 0));
    384 	    continue;
    385 	}
    386 
    387 	/*
    388 	 * We're done. There are no domains left to strip off the address,
    389 	 * and all null local-part information is sanitized.
    390 	 */
    391 	domain = 0;
    392 	break;
    393     }
    394 
    395     vstring_free(addr_buf);
    396     addr_buf = 0;
    397 
    398     /*
    399      * Make sure the resolved envelope recipient has the user@domain form. If
    400      * no domain was specified in the address, assume the local machine. See
    401      * above for what happens with an empty address.
    402      */
    403     if (domain == 0) {
    404 	if (saved_domain) {
    405 	    tok822_sub_append(tree, saved_domain);
    406 	    saved_domain = 0;
    407 	} else {
    408 	    tok822_sub_append(tree, tok822_alloc('@', (char *) 0));
    409 	    tok822_sub_append(tree, tok822_scan(var_myhostname, (TOK822 **) 0));
    410 	}
    411     }
    412 
    413     /*
    414      * Transform the recipient address back to internal form.
    415      *
    416      * XXX This may produce incorrect results if we cracked open a quoted
    417      * local-part with routing operators; see discussion above at the top of
    418      * the big loop.
    419      *
    420      * XXX We explicitly disallow domain names in bare network address form. A
    421      * network address destination should be formatted according to RFC 2821:
    422      * it should be enclosed in [], and an IPv6 address should have an IPv6:
    423      * prefix.
    424      */
    425     tok822_internalize(nextrcpt, tree, TOK822_STR_DEFL);
    426     rcpt_domain = strrchr(STR(nextrcpt), '@') + 1;
    427     if (rcpt_domain == (char *) 1)
    428 	msg_panic("no @ in address: \"%s\"", STR(nextrcpt));
    429     if (*rcpt_domain == '[') {
    430 	if (!valid_mailhost_literal(rcpt_domain, DONT_GRIPE))
    431 	    *flags |= RESOLVE_FLAG_ERROR;
    432     } else if (var_smtputf8_enable
    433 	       && valid_utf8_stringz(STR(nextrcpt)) == 0) {
    434 	*flags |= RESOLVE_FLAG_ERROR;
    435     } else if (!valid_utf8_hostname(var_smtputf8_enable, rcpt_domain,
    436 				    DONT_GRIPE)) {
    437 	if (var_resolve_num_dom && valid_hostaddr(rcpt_domain, DONT_GRIPE)) {
    438 	    vstring_insert(nextrcpt, rcpt_domain - STR(nextrcpt), "[", 1);
    439 	    vstring_strcat(nextrcpt, "]");
    440 	    rcpt_domain = strrchr(STR(nextrcpt), '@') + 1;
    441 	    if ((rc = resolve_local(rcpt_domain)) > 0)	/* XXX */
    442 		domain = 0;
    443 	    else if (rc < 0) {
    444 		*flags |= RESOLVE_FLAG_FAIL;
    445 		FREE_MEMORY_AND_RETURN;
    446 	    }
    447 	} else {
    448 	    *flags |= RESOLVE_FLAG_ERROR;
    449 	}
    450     }
    451     tok822_free_tree(tree);
    452     tree = 0;
    453 
    454     /*
    455      * XXX Short-cut invalid address forms.
    456      */
    457     if (*flags & RESOLVE_FLAG_ERROR) {
    458 	*flags |= RESOLVE_CLASS_DEFAULT;
    459 	FREE_MEMORY_AND_RETURN;
    460     }
    461 
    462     /*
    463      * Recognize routing operators in the local-part, even when we do not
    464      * recognize ! or % as valid routing operators locally. This is needed to
    465      * prevent backup MX hosts from relaying third-party destinations through
    466      * primary MX hosts, otherwise the backup host could end up on black
    467      * lists. Ignore local swap_bangpath and percent_hack settings because we
    468      * can't know how the next MX host is set up.
    469      */
    470     if (strcmp(STR(nextrcpt) + strcspn(STR(nextrcpt), "@!%") + 1, rcpt_domain))
    471 	*flags |= RESOLVE_FLAG_ROUTED;
    472 
    473     /*
    474      * With local, virtual, relay, or other non-local destinations, give the
    475      * highest precedence to transport associated nexthop information.
    476      *
    477      * Otherwise, with relay or other non-local destinations, the relayhost
    478      * setting overrides the recipient domain name, and the sender-dependent
    479      * relayhost overrides both.
    480      *
    481      * XXX Nag if the recipient domain is listed in multiple domain lists. The
    482      * result is implementation defined, and may break when internals change.
    483      *
    484      * For now, we distinguish only a fixed number of address classes.
    485      * Eventually this may become extensible, so that new classes can be
    486      * configured with their own domain list, delivery transport, and
    487      * recipient table.
    488      */
    489 #define STREQ(x,y) (strcmp((x), (y)) == 0)
    490 
    491     if (domain != 0) {
    492 
    493 	/*
    494 	 * Virtual alias domain.
    495 	 */
    496 	if (virt_alias_doms
    497 	    && string_list_match(virt_alias_doms, rcpt_domain)) {
    498 	    if (var_helpful_warnings) {
    499 		if (virt_mailbox_doms
    500 		    && string_list_match(virt_mailbox_doms, rcpt_domain))
    501 		    msg_warn("do not list domain %s in BOTH %s and %s",
    502 			     rcpt_domain, VAR_VIRT_ALIAS_DOMS,
    503 			     VAR_VIRT_MAILBOX_DOMS);
    504 		if (relay_domains
    505 		    && domain_list_match(relay_domains, rcpt_domain))
    506 		    msg_warn("do not list domain %s in BOTH %s and %s",
    507 			     rcpt_domain, VAR_VIRT_ALIAS_DOMS,
    508 			     VAR_RELAY_DOMAINS);
    509 #if 0
    510 		if (strcasecmp_utf8(rcpt_domain, var_myorigin) == 0)
    511 		    msg_warn("do not list $%s (%s) in %s",
    512 			   VAR_MYORIGIN, var_myorigin, VAR_VIRT_ALIAS_DOMS);
    513 #endif
    514 	    }
    515 	    vstring_strcpy(channel, MAIL_SERVICE_ERROR);
    516 	    vstring_sprintf(nexthop, "5.1.1 User unknown%s",
    517 			    var_show_unk_rcpt_table ?
    518 			    " in virtual alias table" : "");
    519 	    *flags |= RESOLVE_CLASS_ALIAS;
    520 	} else if (virt_alias_doms && virt_alias_doms->error != 0) {
    521 	    msg_warn("%s lookup failure", VAR_VIRT_ALIAS_DOMS);
    522 	    *flags |= RESOLVE_FLAG_FAIL;
    523 	    FREE_MEMORY_AND_RETURN;
    524 	}
    525 
    526 	/*
    527 	 * Virtual mailbox domain.
    528 	 */
    529 	else if (virt_mailbox_doms
    530 		 && string_list_match(virt_mailbox_doms, rcpt_domain)) {
    531 	    if (var_helpful_warnings) {
    532 		if (relay_domains
    533 		    && domain_list_match(relay_domains, rcpt_domain))
    534 		    msg_warn("do not list domain %s in BOTH %s and %s",
    535 			     rcpt_domain, VAR_VIRT_MAILBOX_DOMS,
    536 			     VAR_RELAY_DOMAINS);
    537 	    }
    538 	    vstring_strcpy(channel, RES_PARAM_VALUE(rp->virt_transport));
    539 	    vstring_strcpy(nexthop, rcpt_domain);
    540 	    blame = rp->virt_transport_name;
    541 	    *flags |= RESOLVE_CLASS_VIRTUAL;
    542 	} else if (virt_mailbox_doms && virt_mailbox_doms->error != 0) {
    543 	    msg_warn("%s lookup failure", VAR_VIRT_MAILBOX_DOMS);
    544 	    *flags |= RESOLVE_FLAG_FAIL;
    545 	    FREE_MEMORY_AND_RETURN;
    546 	} else {
    547 
    548 	    /*
    549 	     * Off-host relay destination.
    550 	     */
    551 	    if (relay_domains
    552 		&& domain_list_match(relay_domains, rcpt_domain)) {
    553 		vstring_strcpy(channel, RES_PARAM_VALUE(rp->relay_transport));
    554 		blame = rp->relay_transport_name;
    555 		*flags |= RESOLVE_CLASS_RELAY;
    556 	    } else if (relay_domains && relay_domains->error != 0) {
    557 		msg_warn("%s lookup failure", VAR_RELAY_DOMAINS);
    558 		*flags |= RESOLVE_FLAG_FAIL;
    559 		FREE_MEMORY_AND_RETURN;
    560 	    }
    561 
    562 	    /*
    563 	     * Other off-host destination.
    564 	     */
    565 	    else {
    566 		if (rp->snd_def_xp_info
    567 		    && (xport = mail_addr_find(rp->snd_def_xp_info,
    568 					    sender_key = (*sender ? sender :
    569 					       var_null_def_xport_maps_key),
    570 					       (char **) 0)) != 0) {
    571 		    if (*xport == 0) {
    572 			msg_warn("%s: ignoring null lookup result for %s",
    573 				 rp->snd_def_xp_maps_name, sender_key);
    574 			xport = "DUNNO";
    575 		    }
    576 		    vstring_strcpy(channel, strcasecmp(xport, "DUNNO") == 0 ?
    577 				RES_PARAM_VALUE(rp->def_transport) : xport);
    578 		    blame = rp->snd_def_xp_maps_name;
    579 		} else if (rp->snd_def_xp_info
    580 			   && rp->snd_def_xp_info->error != 0) {
    581 		    msg_warn("%s lookup failure", rp->snd_def_xp_maps_name);
    582 		    *flags |= RESOLVE_FLAG_FAIL;
    583 		    FREE_MEMORY_AND_RETURN;
    584 		} else {
    585 		    vstring_strcpy(channel, RES_PARAM_VALUE(rp->def_transport));
    586 		    blame = rp->def_transport_name;
    587 		}
    588 		*flags |= RESOLVE_CLASS_DEFAULT;
    589 	    }
    590 
    591 	    /*
    592 	     * With off-host delivery, sender-dependent or global relayhost
    593 	     * override the recipient domain.
    594 	     */
    595 	    if (rp->snd_relay_info
    596 		&& (relay = mail_addr_find(rp->snd_relay_info,
    597 					   sender_key = (*sender ? sender :
    598 						   var_null_relay_maps_key),
    599 					   (char **) 0)) != 0) {
    600 		if (*relay == 0) {
    601 		    msg_warn("%s: ignoring null lookup result for %s",
    602 			     rp->snd_relay_maps_name, sender_key);
    603 		    relay = 0;
    604 		} else if (strcasecmp_utf8(relay, "DUNNO") == 0)
    605 		    relay = 0;
    606 	    } else if (rp->snd_relay_info
    607 		       && rp->snd_relay_info->error != 0) {
    608 		msg_warn("%s lookup failure", rp->snd_relay_maps_name);
    609 		*flags |= RESOLVE_FLAG_FAIL;
    610 		FREE_MEMORY_AND_RETURN;
    611 	    } else {
    612 		relay = 0;
    613 	    }
    614 	    /* Enforce all the relayhost precedences in one place. */
    615 	    if (relay != 0) {
    616 		vstring_strcpy(nexthop, relay);
    617 	    } else if (*RES_PARAM_VALUE(rp->relayhost))
    618 		vstring_strcpy(nexthop, RES_PARAM_VALUE(rp->relayhost));
    619 	    else
    620 		vstring_strcpy(nexthop, rcpt_domain);
    621 	}
    622     }
    623 
    624     /*
    625      * Local delivery.
    626      *
    627      * XXX Nag if the domain is listed in multiple domain lists. The effect is
    628      * implementation defined, and may break when internals change.
    629      */
    630     else {
    631 	if (var_helpful_warnings) {
    632 	    if (virt_alias_doms
    633 		&& string_list_match(virt_alias_doms, rcpt_domain))
    634 		msg_warn("do not list domain %s in BOTH %s and %s",
    635 			 rcpt_domain, VAR_MYDEST, VAR_VIRT_ALIAS_DOMS);
    636 	    if (virt_mailbox_doms
    637 		&& string_list_match(virt_mailbox_doms, rcpt_domain))
    638 		msg_warn("do not list domain %s in BOTH %s and %s",
    639 			 rcpt_domain, VAR_MYDEST, VAR_VIRT_MAILBOX_DOMS);
    640 	}
    641 	vstring_strcpy(channel, RES_PARAM_VALUE(rp->local_transport));
    642 	vstring_strcpy(nexthop, rcpt_domain);
    643 	blame = rp->local_transport_name;
    644 	*flags |= RESOLVE_CLASS_LOCAL;
    645     }
    646 
    647     /*
    648      * An explicit main.cf transport:nexthop setting overrides the nexthop.
    649      *
    650      * XXX We depend on this mechanism to enforce per-recipient concurrencies
    651      * for local recipients. With "local_transport = local:$myhostname" we
    652      * force mail for any domain in $mydestination/${proxy,inet}_interfaces
    653      * to share the same queue.
    654      */
    655     if ((destination = split_at(STR(channel), ':')) != 0 && *destination)
    656 	vstring_strcpy(nexthop, destination);
    657 
    658     /*
    659      * Sanity checks.
    660      */
    661     if (*STR(channel) == 0) {
    662 	if (blame == 0)
    663 	    msg_panic("%s: null blame", myname);
    664 	msg_warn("file %s/%s: parameter %s: null transport is not allowed",
    665 		 var_config_dir, MAIN_CONF_FILE, blame);
    666 	*flags |= RESOLVE_FLAG_FAIL;
    667 	FREE_MEMORY_AND_RETURN;
    668     }
    669     if (*STR(nexthop) == 0)
    670 	msg_panic("%s: null nexthop", myname);
    671 
    672     /*
    673      * The transport map can selectively override any transport and/or
    674      * nexthop host info that is set up above. Unfortunately, the syntax for
    675      * nexthop information is transport specific. We therefore need sane and
    676      * intuitive semantics for transport map entries that specify a channel
    677      * but no nexthop.
    678      *
    679      * With non-error transports, the initial nexthop information is the
    680      * recipient domain. However, specific main.cf transport definitions may
    681      * specify a transport-specific destination, such as a host + TCP socket,
    682      * or the pathname of a UNIX-domain socket. With less precedence than
    683      * main.cf transport definitions, a main.cf relayhost definition may also
    684      * override nexthop information for off-host deliveries.
    685      *
    686      * With the error transport, the nexthop information is free text that
    687      * specifies the reason for non-delivery.
    688      *
    689      * Because nexthop syntax is transport specific we reset the nexthop
    690      * information to the recipient domain when the transport table specifies
    691      * a transport without also specifying the nexthop information.
    692      *
    693      * Subtle note: reset nexthop even when the transport table does not change
    694      * the transport. Otherwise it is hard to get rid of main.cf specified
    695      * nexthop information.
    696      *
    697      * XXX Don't override the virtual alias class (error:User unknown) result.
    698      */
    699     if (rp->transport_info && !(*flags & RESOLVE_CLASS_ALIAS)) {
    700 	if (transport_lookup(rp->transport_info, STR(nextrcpt),
    701 			     rcpt_domain, channel, nexthop) == 0
    702 	    && rp->transport_info->transport_path->error != 0) {
    703 	    msg_warn("%s lookup failure", rp->transport_maps_name);
    704 	    *flags |= RESOLVE_FLAG_FAIL;
    705 	    FREE_MEMORY_AND_RETURN;
    706 	}
    707     }
    708 
    709     /*
    710      * Bounce recipients that have moved, regardless of domain address class.
    711      * We do this last, in anticipation of transport maps that can override
    712      * the recipient address.
    713      *
    714      * The downside of not doing this in delivery agents is that this table has
    715      * no effect on local alias expansion results. Such mail will have to
    716      * make almost an entire iteration through the mail system.
    717      */
    718 #define IGNORE_ADDR_EXTENSION   ((char **) 0)
    719 
    720     if (relocated_maps != 0) {
    721 	const char *reply;
    722 	DSN_SPLIT dp;
    723 
    724 	if ((reply = mail_addr_find(relocated_maps, STR(nextrcpt),
    725 				    IGNORE_ADDR_EXTENSION)) != 0) {
    726 	    vstring_strcpy(channel, MAIL_SERVICE_ERROR);
    727 	    if (var_enb_relocated_pfx) {
    728 		/* 5.1.6 is the closest match, but not perfect. */
    729 		vstring_sprintf(nexthop, "5.1.6 User has moved to %s", reply);
    730 	    } else if (!dsn_valid(reply)
    731 		       || dsn_split(&dp, "5.2.0", reply)->text[0] == 0) {
    732 		msg_warn("%s result must contain RFC 3463 status and text: '%.100s'",
    733 			 VAR_RELOCATED_MAPS, reply);
    734 		vstring_sprintf(nexthop, "5.2.0 Mailbox is unavailable");
    735 	    } else {
    736 		vstring_sprintf(nexthop, "%s %s", DSN_STATUS(dp.dsn), dp.text);
    737 	    }
    738 	} else if (relocated_maps->error != 0) {
    739 	    msg_warn("%s lookup failure", VAR_RELOCATED_MAPS);
    740 	    *flags |= RESOLVE_FLAG_FAIL;
    741 	    FREE_MEMORY_AND_RETURN;
    742 	}
    743     }
    744 
    745     /*
    746      * Bounce recipient addresses that start with `-'. External commands may
    747      * misinterpret such addresses as command-line options.
    748      *
    749      * In theory I could say people should always carefully set up their
    750      * master.cf pipe mailer entries with `--' before the first non-option
    751      * argument, but mistakes will happen regardless.
    752      *
    753      * Therefore the protection is put in place here, where it cannot be
    754      * bypassed.
    755      */
    756     if (var_allow_min_user == 0 && STR(nextrcpt)[0] == '-') {
    757 	*flags |= RESOLVE_FLAG_ERROR;
    758 	FREE_MEMORY_AND_RETURN;
    759     }
    760 
    761     /*
    762      * Clean up.
    763      */
    764     FREE_MEMORY_AND_RETURN;
    765 }
    766 
    767 /* Static, so they can be used by the network protocol interface only. */
    768 
    769 static VSTRING *channel;
    770 static VSTRING *nexthop;
    771 static VSTRING *nextrcpt;
    772 static VSTRING *query;
    773 static VSTRING *sender;
    774 
    775 /* resolve_proto - read request and send reply */
    776 
    777 int     resolve_proto(RES_CONTEXT *context, VSTREAM *stream)
    778 {
    779     int     flags;
    780 
    781     if (attr_scan(stream, ATTR_FLAG_STRICT,
    782 		  RECV_ATTR_STR(MAIL_ATTR_SENDER, sender),
    783 		  RECV_ATTR_STR(MAIL_ATTR_ADDR, query),
    784 		  ATTR_TYPE_END) != 2)
    785 	return (-1);
    786 
    787     resolve_addr(context, STR(sender), STR(query),
    788 		 channel, nexthop, nextrcpt, &flags);
    789 
    790     if (msg_verbose)
    791 	msg_info("`%s' -> `%s' -> (`%s' `%s' `%s' `%d')",
    792 		 STR(sender), STR(query), STR(channel),
    793 		 STR(nexthop), STR(nextrcpt), flags);
    794 
    795     attr_print(stream, ATTR_FLAG_NONE,
    796 	       SEND_ATTR_INT(MAIL_ATTR_FLAGS, server_flags),
    797 	       SEND_ATTR_STR(MAIL_ATTR_TRANSPORT, STR(channel)),
    798 	       SEND_ATTR_STR(MAIL_ATTR_NEXTHOP, STR(nexthop)),
    799 	       SEND_ATTR_STR(MAIL_ATTR_RECIP, STR(nextrcpt)),
    800 	       SEND_ATTR_INT(MAIL_ATTR_FLAGS, flags),
    801 	       ATTR_TYPE_END);
    802 
    803     if (vstream_fflush(stream) != 0) {
    804 	msg_warn("write resolver reply: %m");
    805 	return (-1);
    806     }
    807     return (0);
    808 }
    809 
    810 /* resolve_init - module initializations */
    811 
    812 void    resolve_init(void)
    813 {
    814     sender = vstring_alloc(100);
    815     query = vstring_alloc(100);
    816     channel = vstring_alloc(100);
    817     nexthop = vstring_alloc(100);
    818     nextrcpt = vstring_alloc(100);
    819 
    820     if (*var_virt_alias_doms)
    821 	virt_alias_doms =
    822 	    string_list_init(VAR_VIRT_ALIAS_DOMS, MATCH_FLAG_RETURN,
    823 			     var_virt_alias_doms);
    824 
    825     if (*var_virt_mailbox_doms)
    826 	virt_mailbox_doms =
    827 	    string_list_init(VAR_VIRT_MAILBOX_DOMS, MATCH_FLAG_RETURN,
    828 			     var_virt_mailbox_doms);
    829 
    830     if (*var_relay_domains)
    831 	relay_domains =
    832 	    domain_list_init(VAR_RELAY_DOMAINS, MATCH_FLAG_RETURN
    833 			     | match_parent_style(VAR_RELAY_DOMAINS),
    834 			     var_relay_domains);
    835 
    836     if (*var_relocated_maps)
    837 	relocated_maps =
    838 	    maps_create(VAR_RELOCATED_MAPS, var_relocated_maps,
    839 			DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
    840 			| DICT_FLAG_UTF8_REQUEST);
    841 }
    842