1/*	$NetBSD: transport.c,v 1.4 2022/10/08 16:12:50 christos Exp $	*/
2
3/*++
4/* NAME
5/*	transport 3
6/* SUMMARY
7/*	transport mapping
8/* SYNOPSIS
9/*	#include "transport.h"
10/*
11/*	TRANSPORT_INFO *transport_pre_init(maps_name, maps)
12/*	const char *maps_name;
13/*	const char *maps;
14/*
15/*	void	transport_post_init(info)
16/*	TRANSPORT_INFO *info;
17/*
18/*	int	transport_lookup(info, address, rcpt_domain, channel, nexthop)
19/*	TRANSPORT_INFO *info;
20/*	const char *address;
21/*	const char *rcpt_domain;
22/*	VSTRING *channel;
23/*	VSTRING *nexthop;
24/*
25/*	void	transport_free(info);
26/*	TRANSPORT_INFO * info;
27/* DESCRIPTION
28/*	This module implements access to the table that maps transport
29/*	user@domain addresses to (channel, nexthop) tuples.
30/*
31/*	transport_pre_init() performs initializations that should be
32/*	done before the process enters the chroot jail, and
33/*	before calling transport_lookup().
34/*
35/*	transport_post_init() can be invoked after entering the chroot
36/*	jail, and must be called before calling transport_lookup().
37/*
38/*	transport_lookup() finds the channel and nexthop for the given
39/*	domain, and returns 1 if something was found.	Otherwise, 0
40/*	is returned.
41/* DIAGNOSTICS
42/*	info->transport_path->error is non-zero when the lookup
43/*	should be tried again.
44/* SEE ALSO
45/*	maps(3), multi-dictionary search
46/*	strip_addr(3), strip extension from address
47/*	transport(5), format of transport map
48/* CONFIGURATION PARAMETERS
49/*	transport_maps, names of maps to be searched.
50/* LICENSE
51/* .ad
52/* .fi
53/*	The Secure Mailer license must be distributed with this software.
54/* AUTHOR(S)
55/*	Wietse Venema
56/*	IBM T.J. Watson Research
57/*	P.O. Box 704
58/*	Yorktown Heights, NY 10598, USA
59/*
60/*	Wietse Venema
61/*	Google, Inc.
62/*	111 8th Avenue
63/*	New York, NY 10011, USA
64/*--*/
65
66/* System library. */
67
68#include <sys_defs.h>
69#include <string.h>
70
71/* Utility library. */
72
73#include <msg.h>
74#include <stringops.h>
75#include <mymalloc.h>
76#include <vstring.h>
77#include <split_at.h>
78#include <dict.h>
79#include <events.h>
80
81/* Global library. */
82
83#include <strip_addr.h>
84#include <mail_params.h>
85#include <mail_addr_find.h>
86#include <match_parent_style.h>
87#include <mail_proto.h>
88
89/* Application-specific. */
90
91#include "transport.h"
92
93static int transport_match_parent_style;
94
95#define STR(x)	vstring_str(x)
96
97static void transport_wildcard_init(TRANSPORT_INFO *);
98
99/* transport_pre_init - pre-jail initialization */
100
101TRANSPORT_INFO *transport_pre_init(const char *transport_maps_name,
102				           const char *transport_maps)
103{
104    TRANSPORT_INFO *tp;
105
106    tp = (TRANSPORT_INFO *) mymalloc(sizeof(*tp));
107    tp->transport_path = maps_create(transport_maps_name, transport_maps,
108				     DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
109				     | DICT_FLAG_NO_REGSUB
110				     | DICT_FLAG_UTF8_REQUEST);
111    tp->wildcard_channel = tp->wildcard_nexthop = 0;
112    tp->wildcard_errno = 0;
113    tp->expire = 0;
114    return (tp);
115}
116
117/* transport_post_init - post-jail initialization */
118
119void    transport_post_init(TRANSPORT_INFO *tp)
120{
121    transport_match_parent_style = match_parent_style(VAR_TRANSPORT_MAPS);
122    transport_wildcard_init(tp);
123}
124
125/* transport_free - destroy transport info */
126
127void    transport_free(TRANSPORT_INFO *tp)
128{
129    if (tp->transport_path)
130	maps_free(tp->transport_path);
131    if (tp->wildcard_channel)
132	vstring_free(tp->wildcard_channel);
133    if (tp->wildcard_nexthop)
134	vstring_free(tp->wildcard_nexthop);
135    myfree((void *) tp);
136}
137
138/* update_entry - update from transport table entry */
139
140static void update_entry(const char *new_channel, const char *new_nexthop,
141			         const char *rcpt_domain, VSTRING *channel,
142			         VSTRING *nexthop)
143{
144
145    /*
146     * :[nexthop] means don't change the channel, and don't change the
147     * nexthop unless a non-default nexthop is specified. Thus, a right-hand
148     * side of ":" is the transport table equivalent of a NOOP.
149     */
150    if (*new_channel == 0) {			/* :[nexthop] */
151	if (*new_nexthop != 0)
152	    vstring_strcpy(nexthop, new_nexthop);
153    }
154
155    /*
156     * transport[:[nexthop]] means change the channel, and reset the nexthop
157     * to the default unless a non-default nexthop is specified.
158     */
159    else {
160	vstring_strcpy(channel, new_channel);
161	if (*new_nexthop != 0)
162	    vstring_strcpy(nexthop, new_nexthop);
163	else if (strcmp(STR(channel), MAIL_SERVICE_ERROR) != 0
164		 && strcmp(STR(channel), MAIL_SERVICE_RETRY) != 0)
165	    vstring_strcpy(nexthop, rcpt_domain);
166	else
167	    vstring_strcpy(nexthop, "Address is undeliverable");
168    }
169}
170
171/* parse_transport_entry - parse transport table entry */
172
173static void parse_transport_entry(const char *value, const char *rcpt_domain,
174				          VSTRING *channel, VSTRING *nexthop)
175{
176    char   *saved_value;
177    const char *host;
178
179#define FOUND		1
180#define NOTFOUND	0
181
182    /*
183     * It would be great if we could specify a recipient address in the
184     * lookup result. Unfortunately, we cannot simply run the result through
185     * a parser that recognizes "transport:user@domain" because the lookup
186     * result can have arbitrary content (especially in the case of the error
187     * mailer).
188     */
189    saved_value = mystrdup(value);
190    host = split_at(saved_value, ':');
191    update_entry(saved_value, host ? host : "", rcpt_domain, channel, nexthop);
192    myfree(saved_value);
193}
194
195/* transport_wildcard_init - (re) initialize wild-card lookup result */
196
197static void transport_wildcard_init(TRANSPORT_INFO *tp)
198{
199    VSTRING *channel = vstring_alloc(10);
200    VSTRING *nexthop = vstring_alloc(10);
201    const char *value;
202
203    /*
204     * Both channel and nexthop may be zero-length strings. Therefore we must
205     * use something else to represent "wild-card does not exist". We use
206     * null VSTRING pointers, for historical reasons.
207     */
208    if (tp->wildcard_channel)
209	vstring_free(tp->wildcard_channel);
210    if (tp->wildcard_nexthop)
211	vstring_free(tp->wildcard_nexthop);
212
213    /*
214     * Technically, the wildcard lookup pattern is redundant. A static map
215     * (keys always match, result is fixed string) could achieve the same:
216     *
217     * transport_maps = hash:/etc/postfix/transport static:xxx:yyy
218     *
219     * But the user interface of such an approach would be less intuitive. We
220     * tolerate the continued existence of wildcard lookup patterns because
221     * of human interface considerations.
222     */
223#define WILDCARD	"*"
224#define FULL		0
225#define PARTIAL		DICT_FLAG_FIXED
226
227    if ((value = maps_find(tp->transport_path, WILDCARD, FULL)) != 0) {
228	parse_transport_entry(value, "", channel, nexthop);
229	tp->wildcard_errno = 0;
230	tp->wildcard_channel = channel;
231	tp->wildcard_nexthop = nexthop;
232	if (msg_verbose)
233	    msg_info("wildcard_{chan:hop}={%s:%s}",
234		     vstring_str(channel), vstring_str(nexthop));
235    } else {
236	tp->wildcard_errno = tp->transport_path->error;
237	vstring_free(channel);
238	vstring_free(nexthop);
239	tp->wildcard_channel = 0;
240	tp->wildcard_nexthop = 0;
241    }
242    tp->expire = event_time() + 30;		/* XXX make configurable */
243}
244
245/* transport_lookup - map a transport domain */
246
247int     transport_lookup(TRANSPORT_INFO *tp, const char *addr,
248			         const char *rcpt_domain,
249			         VSTRING *channel, VSTRING *nexthop)
250{
251    char   *ratsign = 0;
252    const char *value;
253
254#define STREQ(x,y)	(strcmp((x), (y)) == 0)
255#define DISCARD_EXTENSION ((char **) 0)
256
257    /*
258     * The null recipient is rewritten to the local mailer daemon address.
259     */
260    if (*addr == 0) {
261	msg_warn("transport_lookup: null address - skipping table lookup");
262	return (NOTFOUND);
263    }
264
265    /*
266     * Look up the full and extension-stripped address, then match the domain
267     * and subdomains. Try the external form before the backwards-compatible
268     * internal form.
269     */
270#define LOOKUP_STRATEGY \
271	(MA_FIND_FULL | MA_FIND_NOEXT | MA_FIND_DOMAIN | \
272	(transport_match_parent_style == MATCH_FLAG_PARENT ? \
273		MA_FIND_PDMS : MA_FIND_PDDMDS))
274
275    if ((ratsign = strrchr(addr, '@')) == 0 || ratsign[1] == 0)
276	msg_panic("transport_lookup: bad address: \"%s\"", addr);
277
278    if ((value = mail_addr_find_strategy(tp->transport_path, addr, (char **) 0,
279					 LOOKUP_STRATEGY)) != 0) {
280	parse_transport_entry(value, rcpt_domain, channel, nexthop);
281	return (FOUND);
282    }
283    if (tp->transport_path->error != 0)
284	return (NOTFOUND);
285
286    /*
287     * Fall back to the wild-card entry.
288     */
289    if (tp->wildcard_errno || event_time() > tp->expire)
290	transport_wildcard_init(tp);
291    if (tp->wildcard_errno) {
292	tp->transport_path->error = tp->wildcard_errno;
293	return (NOTFOUND);
294    } else if (tp->wildcard_channel) {
295	update_entry(STR(tp->wildcard_channel), STR(tp->wildcard_nexthop),
296		     rcpt_domain, channel, nexthop);
297	return (FOUND);
298    }
299
300    /*
301     * We really did not find it.
302     */
303    return (NOTFOUND);
304}
305
306#ifdef TEST
307
308 /*
309  * Proof-of-concept test program. Read an address from stdin, and spit out
310  * the lookup result.
311  */
312
313#include <string.h>
314
315#include <mail_conf.h>
316#include <vstream.h>
317#include <vstring_vstream.h>
318
319static NORETURN usage(const char *progname)
320{
321    msg_fatal("usage: %s [-v] database", progname);
322}
323
324int     main(int argc, char **argv)
325{
326    VSTRING *buffer = vstring_alloc(100);
327    VSTRING *channel = vstring_alloc(100);
328    VSTRING *nexthop = vstring_alloc(100);
329    TRANSPORT_INFO *tp;
330    char   *bp;
331    char   *addr_field;
332    char   *rcpt_domain;
333    char   *expect_channel;
334    char   *expect_nexthop;
335    int     status;
336    int     ch;
337    int     errs = 0;
338
339    /*
340     * Parse JCL.
341     */
342    while ((ch = GETOPT(argc, argv, "v")) > 0) {
343	switch (ch) {
344	case 'v':
345	    msg_verbose++;
346	    break;
347	default:
348	    usage(argv[0]);
349	}
350    }
351    if (argc != optind + 1)
352	usage(argv[0]);
353
354    /*
355     * Initialize.
356     */
357#define UPDATE(var, val) do { myfree(var); var = mystrdup(val); } while (0)
358
359    mail_conf_read();				/* XXX eliminate dependency. */
360    UPDATE(var_rcpt_delim, "+");
361    UPDATE(var_mydomain, "localdomain");
362    UPDATE(var_myorigin, "localhost.localdomain");
363    UPDATE(var_mydest, "localhost.localdomain");
364
365    tp = transport_pre_init("transport map", argv[optind]);
366    transport_post_init(tp);
367
368    while (vstring_fgets_nonl(buffer, VSTREAM_IN)) {
369	bp = STR(buffer);
370
371	/*
372	 * Parse the input and expectations. XXX We can't expect empty
373	 * fields, so require '-' instead.
374	 */
375	if ((addr_field = mystrtok(&bp, ":")) == 0)
376	    msg_fatal("no address field");
377	if ((rcpt_domain = strrchr(addr_field, '@')) == 0)
378	    msg_fatal("no recipient domain");
379	rcpt_domain += 1;
380	expect_channel = mystrtok(&bp, ":");
381	expect_nexthop = mystrtok(&bp, ":");
382	if ((expect_channel != 0) != (expect_nexthop != 0))
383	    msg_fatal("specify both channel and nexthop, or specify neither");
384	if (expect_channel) {
385	    if (strcmp(expect_channel, "-") == 0)
386		*expect_channel = 0;
387	    if (strcmp(expect_nexthop, "-") == 0)
388		*expect_nexthop = 0;
389	    vstring_strcpy(channel, "DEFAULT");
390	    vstring_strcpy(nexthop, rcpt_domain);
391	}
392	if (mystrtok(&bp, ":") != 0)
393	    msg_fatal("garbage after nexthop field");
394
395	/*
396	 * Lookups.
397	 */
398	status = transport_lookup(tp, addr_field, rcpt_domain,
399				  channel, nexthop);
400
401	/*
402	 * Enforce expectations.
403	 */
404	if (expect_nexthop && status) {
405	    vstream_printf("%s:%s -> %s:%s \n",
406			   addr_field, rcpt_domain,
407			   STR(channel), STR(nexthop));
408	    vstream_fflush(VSTREAM_OUT);
409	    if (strcmp(expect_channel, STR(channel)) != 0) {
410		msg_warn("expect channel '%s' but got '%s'",
411			 expect_channel, STR(channel));
412		errs = 1;
413	    }
414	    if (strcmp(expect_nexthop, STR(nexthop)) != 0) {
415		msg_warn("expect nexthop '%s' but got '%s'",
416			 expect_nexthop, STR(nexthop));
417		errs = 1;
418	    }
419	} else if (expect_nexthop && !status) {
420	    vstream_printf("%s:%s -> %s\n", addr_field, rcpt_domain,
421			   tp->transport_path->error ?
422			   "(try again)" : "(not found)");
423	    vstream_fflush(VSTREAM_OUT);
424	    msg_warn("expect channel '%s' but got none", expect_channel);
425	    msg_warn("expect nexthop '%s' but got none", expect_nexthop);
426	    errs = 1;
427	} else if (!status) {
428	    vstream_printf("%s:%s -> %s\n", addr_field, rcpt_domain,
429			   tp->transport_path->error ?
430			   "(try again)" : "(not found)");
431	}
432    }
433    transport_free(tp);
434    vstring_free(nexthop);
435    vstring_free(channel);
436    vstring_free(buffer);
437    exit(errs != 0);
438}
439
440#endif
441