1/*	$NetBSD: login_sender_match.c,v 1.2 2022/10/08 16:12:45 christos Exp $	*/
2
3/*++
4/* NAME
5/*	login_sender_match 3
6/* SUMMARY
7/*	match login and sender against (login, sender) patterns
8/* SYNOPSIS
9/*	#include <login_sender.h>
10/*
11/*	typedef LOGIN_SENDER_MATCH LOGIN_SENDER_MATCH;
12/*
13/*	LOGIN_SENDER_MATCH *login_sender_create(
14/*	const char *title,
15/*	const char *map_names,
16/*	const char *ext_delimiters,
17/*	const char *null_sender,
18/*	const char *wildcard)
19/*
20/*	void	login_sender_free(
21/*	LOGIN_SENDER_MATCH *lsm)
22/*
23/*	int	login_sender_match(
24/*	LOGIN_SENDER_MATCH *lsm,
25/*	const char *login_name,
26/*	const char *sender_addr)
27/* DESCRIPTION
28/*	This module determines if a login name and internal-form
29/*	sender address match a (login name, external-form sender
30/*	patterns) table entry. A login name matches if it matches
31/*	a lookup table key. A sender address matches the corresponding
32/*	table entry if it matches a sender pattern. A wildcard
33/*	sender pattern matches any sender address. A sender pattern
34/*	that starts with '@' matches the '@' and the domain portion
35/*	of a sender address. Otherwise, the matcher ignores the
36/*	extension part of a sender address, and requires a
37/*	case-insensitive match against a sender pattern.
38/*
39/*	login_sender_create() creates a (login name, sender patterns)
40/*	matcher.
41/*
42/*	login_sender_free() destroys the specified (login name,
43/*	sender patterns) matcher.
44/*
45/*	login_sender_match() looks up an entry for the \fBlogin_name\fR
46/*	argument, and determines if the lookup result matches the
47/*	\fBsender_adddr\fR argument.
48/*
49/*	Arguments:
50/* .IP title
51/*	The name of the configuration parameter that specifies the
52/*	map_names value, used for error messages.
53/* .IP map_names
54r*	The lookup table(s) with (login name, sender patterns) entries.
55/* .IP ext_delimiters
56/*	The set of address extension delimiters.
57/* .IP null_sender
58/*	If a sender pattern equals the null_sender pattern, then
59/*	the empty address is matched.
60/* .IP wildcard
61/*	Null pointer, or non-empty string with a wildcard pattern.
62/*	If a sender pattern equals the wildcard pattern, then any
63/*	sender address is matched.
64/* .IP login_name
65/*	The login name (for example, UNIX account, or SASL username)
66/*	that will be used as a search key to locate a list of senders.
67/* .IP sender_addr
68/*	The sender email address (unquoted form) that will be matched
69/*	against a (login name, sender patterns) table entry.
70/* DIAGNOSTICS
71/*	login_sender_match() returns LSM_STAT_FOUND if a
72/*	match was found, LOGIN_STAT_NOTFOUND if no match was found,
73/*	LSM_STAT_RETRY if the table lookup failed, or
74/*	LSM_STAT_CONFIG in case of a configuration error.
75/* LICENSE
76/* .ad
77/* .fi
78/*	The Secure Mailer license must be distributed with this software.
79/* AUTHOR(S)
80/*	Wietse Venema
81/*	Google, Inc.
82/*	111 8th Avenue
83/*	New York, NY 10011, USA
84/*--*/
85
86 /*
87  * System library.
88  */
89#include <sys_defs.h>
90#include <string.h>
91
92 /*
93  * Utility library.
94  */
95#include <msg.h>
96#include <mymalloc.h>
97#include <stringops.h>
98#include <vstring.h>
99
100 /*
101  * Global library.
102  */
103#include <mail_params.h>
104#include <maps.h>
105#include <quote_822_local.h>
106#include <strip_addr.h>
107#include <login_sender_match.h>
108
109 /*
110  * Private data structure.
111  */
112struct LOGIN_SENDER_MATCH {
113    MAPS   *maps;
114    VSTRING *ext_stripped_sender;
115    char   *ext_delimiters;
116    char   *null_sender;
117    char   *wildcard;
118};
119
120 /*
121  * SLMs.
122  */
123#define STR(x)	vstring_str(x)
124
125/* login_sender_create - create (login name, sender patterns) matcher */
126
127LOGIN_SENDER_MATCH *login_sender_create(const char *title,
128					        const char *map_names,
129					        const char *ext_delimiters,
130					        const char *null_sender,
131					        const char *wildcard)
132{
133    LOGIN_SENDER_MATCH *lsm = mymalloc(sizeof *lsm);
134
135    lsm->maps = maps_create(title, map_names, DICT_FLAG_LOCK
136			    | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST);
137    lsm->ext_stripped_sender = vstring_alloc(100);
138    lsm->ext_delimiters = mystrdup(ext_delimiters);
139    if (null_sender == 0 || *null_sender == 0)
140	msg_panic("login_sender_create: null or empty null_sender");
141    lsm->null_sender = mystrdup(null_sender);
142    lsm->wildcard = (wildcard && *wildcard) ? mystrdup(wildcard) : 0;
143    return (lsm);
144}
145
146/* login_sender_free - destroy (login name, sender patterns) matcher */
147
148void    login_sender_free(LOGIN_SENDER_MATCH *lsm)
149{
150    maps_free(lsm->maps);
151    vstring_free(lsm->ext_stripped_sender);
152    myfree(lsm->ext_delimiters);
153    myfree(lsm->null_sender);
154    if (lsm->wildcard)
155	myfree(lsm->wildcard);
156    myfree((void *) lsm);
157}
158
159/* strip_externalize_addr - strip address extension and externalize remainder */
160
161static VSTRING *strip_externalize_addr(VSTRING *ext_addr, const char *int_addr,
162				               const char *delims)
163{
164    char   *int_stripped_addr;
165
166    if ((int_stripped_addr = strip_addr_internal(int_addr,
167					       /* extension= */ (char **) 0,
168						 delims)) != 0) {
169	quote_822_local(ext_addr, int_stripped_addr);
170	myfree(int_stripped_addr);
171	return (ext_addr);
172    } else {
173	return quote_822_local(ext_addr, int_addr);
174    }
175}
176
177/* login_sender_match - match login and sender against (login, senders) table */
178
179int     login_sender_match(LOGIN_SENDER_MATCH *lsm, const char *login_name,
180			           const char *sender_addr)
181{
182    int     found_or_error = LSM_STAT_NOTFOUND;
183
184    /* Sender patterns and derived info */
185    const char *sender_patterns;
186    char   *saved_sender_patterns;
187    char   *cp;
188    char   *sender_pattern;
189
190    /* Actual sender and derived info */
191    const char *ext_stripped_sender = 0;
192    const char *at_sender_domain;
193
194    /*
195     * Match the login.
196     */
197    if ((sender_patterns = maps_find(lsm->maps, login_name,
198				      /* flags= */ 0)) != 0) {
199
200	/*
201	 * Match the sender. Don't break a sender pattern between double
202	 * quotes.
203	 */
204	cp = saved_sender_patterns = mystrdup(sender_patterns);
205	while (found_or_error == LSM_STAT_NOTFOUND
206	       && (sender_pattern = mystrtokdq(&cp, CHARS_COMMA_SP)) != 0) {
207	    /* Special pattern: @domain. */
208	    if (*sender_pattern == '@') {
209		if ((at_sender_domain = strrchr(sender_addr, '@')) != 0
210		  && strcasecmp_utf8(sender_pattern, at_sender_domain) == 0)
211		    found_or_error = LSM_STAT_FOUND;
212	    }
213	    /* Special pattern: wildcard. */
214	    else if (strcasecmp(sender_pattern, lsm->wildcard) == 0) {
215		found_or_error = LSM_STAT_FOUND;
216	    }
217	    /* Special pattern: empty sender. */
218	    else if (strcasecmp(sender_pattern, lsm->null_sender) == 0) {
219		if (*sender_addr == 0)
220		    found_or_error = LSM_STAT_FOUND;
221	    }
222	    /* Literal pattern: match the stripped and externalized sender. */
223	    else {
224		if (ext_stripped_sender == 0)
225		    ext_stripped_sender =
226			STR(strip_externalize_addr(lsm->ext_stripped_sender,
227						   sender_addr,
228						   lsm->ext_delimiters));
229		if (strcasecmp_utf8(sender_pattern, ext_stripped_sender) == 0)
230		    found_or_error = LSM_STAT_FOUND;
231	    }
232	}
233	myfree(saved_sender_patterns);
234    } else {
235	found_or_error = lsm->maps->error;
236    }
237    return (found_or_error);
238}
239
240#ifdef TEST
241
242int     main(int argc, char **argv)
243{
244    struct testcase {
245	const char *title;
246	const char *map_names;
247	const char *ext_delimiters;
248	const char *null_sender;
249	const char *wildcard;
250	const char *login_name;
251	const char *sender_addr;
252	int     exp_return;
253    };
254    struct testcase testcases[] = {
255	{"wildcard works",
256	    "inline:{root=*, {foo = foo,foo@example.com}, bar=<>}",
257	    "+-", "<>", "*", "root", "anything", LSM_STAT_FOUND
258	},
259	{"unknown user",
260	    "inline:{root=*, {foo = foo,foo@example.com}, bar=<>}",
261	    "+-", "<>", "*", "toor", "anything", LSM_STAT_NOTFOUND
262	},
263	{"bare user",
264	    "inline:{root=*, {foo = foo,foo@example.com}, bar=<>}",
265	    "+-", "<>", "*", "foo", "foo", LSM_STAT_FOUND
266	},
267	{"user@domain",
268	    "inline:{root=*, {foo = foo,foo@example.com}, bar=<>}",
269	    "+-", "<>", "*", "foo", "foo@example.com", LSM_STAT_FOUND
270	},
271	{"user+ext@domain",
272	    "inline:{root=*, {foo = foo,foo@example.com}, bar=<>}",
273	    "+-", "<>", "*", "foo", "foo+bar@example.com", LSM_STAT_FOUND
274	},
275	{"wrong sender",
276	    "inline:{root=*, {foo = foo,foo@example.com}, bar=<>}",
277	    "+-", "<>", "*", "foo", "bar@example.com", LSM_STAT_NOTFOUND
278	},
279	{"@domain",
280	    "inline:{root=*, {foo = @example.com}, bar=<>}",
281	    "+-", "<>", "*", "foo", "anyone@example.com", LSM_STAT_FOUND
282	},
283	{"wrong @domain",
284	    "inline:{root=*, {foo = @example.com}, bar=<>}",
285	    "+-", "<>", "*", "foo", "anyone@example.org", LSM_STAT_NOTFOUND
286	},
287	{"null sender",
288	    "inline:{root=*, {foo = foo,foo@example.com}, bar=<>}",
289	    "+-", "<>", "*", "bar", "", LSM_STAT_FOUND
290	},
291	{"wrong null sender",
292	    "inline:{root=*, {foo = foo,foo@example.com}, bar=<>}",
293	    "+-", "<>", "*", "baz", "", LSM_STAT_NOTFOUND
294	},
295	{"error",
296	    "inline:{root=*}, fail:sorry",
297	    "+-", "<>", "*", "baz", "whatever", LSM_STAT_RETRY
298	},
299	{"no error",
300	    "inline:{root=*}, fail:sorry",
301	    "+-", "<>", "*", "root", "whatever", LSM_STAT_FOUND
302	},
303	{"unknown uid:number",
304	    "inline:{root=*, {uid:12345 = foo,foo@example.com}, bar=<>}",
305	    "+-", "<>", "*", "uid:54321", "foo", LSM_STAT_NOTFOUND
306	},
307	{"known uid:number",
308	    "inline:{root=*, {uid:12345 = foo,foo@example.com}, bar=<>}",
309	    "+-", "<>", "*", "uid:12345", "foo", LSM_STAT_FOUND
310	},
311	{"unknown \"other last\"",
312	    "inline:{root=*, {foo = \"first last\",\"first last\"@example.com}, bar=<>}",
313	    "+-", "<>", "*", "foo", "other last", LSM_STAT_NOTFOUND
314	},
315	{"bare \"first last\"",
316	    "inline:{root=*, {foo = \"first last\",\"first last\"@example.com}, bar=<>}",
317	    "+-", "<>", "*", "foo", "first last", LSM_STAT_FOUND
318	},
319	{"\"first last\"@domain",
320	    "inline:{root=*, {foo = \"first last\",\"first last\"@example.com}, bar=<>}",
321	    "+-", "<>", "*", "foo", "first last@example.com", LSM_STAT_FOUND
322	},
323    };
324    struct testcase *tp;
325    int     act_return;
326    int     pass;
327    int     fail;
328    LOGIN_SENDER_MATCH *lsm;
329
330    /*
331     * Fake variable settings.
332     */
333    var_double_bounce_sender = DEF_DOUBLE_BOUNCE;
334    var_ownreq_special = DEF_OWNREQ_SPECIAL;
335
336#define NUM_TESTS	sizeof(testcases)/sizeof(testcases[0])
337
338    for (pass = fail = 0, tp = testcases; tp < testcases + NUM_TESTS; tp++) {
339	msg_info("RUN test case %ld %s", (long) (tp - testcases), tp->title);
340#if 0
341	msg_info("title=%s", tp->title);
342	msg_info("map_names=%s", tp->map_names);
343	msg_info("ext_delimiters=%s", tp->ext_delimiters);
344	msg_info("null_sender=%s", tp->null_sender);
345	msg_info("wildcard=%s", tp->wildcard);
346	msg_info("login_name=%s", tp->login_name);
347	msg_info("sender_addr=%s", tp->sender_addr);
348	msg_info("exp_return=%d", tp->exp_return);
349#endif
350	lsm = login_sender_create("test map", tp->map_names,
351				  tp->ext_delimiters, tp->null_sender,
352				  tp->wildcard);
353	act_return = login_sender_match(lsm, tp->login_name, tp->sender_addr);
354	if (act_return == tp->exp_return) {
355	    msg_info("PASS test %ld", (long) (tp - testcases));
356	    pass++;
357	} else {
358	    msg_info("FAIL test %ld", (long) (tp - testcases));
359	    fail++;
360	}
361	login_sender_free(lsm);
362    }
363    return (fail > 0);
364}
365
366#endif					/* TEST */
367