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