Home | History | Annotate | Line # | Download | only in global
      1 /*	$NetBSD: mail_addr_map.c,v 1.4 2025/02/25 19:15:45 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	mail_addr_map 3
      6 /* SUMMARY
      7 /*	generic address mapping
      8 /* SYNOPSIS
      9 /*	#include <mail_addr_map.h>
     10 /*
     11 /*	ARGV	*mail_addr_map_internal(path, address, propagate)
     12 /*	MAPS	*path;
     13 /*	const char *address;
     14 /*	int	propagate;
     15 /*
     16 /*	ARGV	*mail_addr_map_opt(path, address, propagate, in_form,
     17 /*					query_form, out_form)
     18 /*	MAPS	*path;
     19 /*	const char *address;
     20 /*	int	propagate;
     21 /*	int	in_form;
     22 /*	int	query_form;
     23 /*	int	out_form;
     24 /* DESCRIPTION
     25 /*	mail_addr_map_*() returns the translation for the named address,
     26 /*	or a null pointer if none is found.
     27 /*
     28 /*	With mail_addr_map_internal(), the search address and results
     29 /*	are in internal (unquoted) form.
     30 /*
     31 /*	mail_addr_map_opt() gives more control, at the cost of additional
     32 /*	conversions between internal and external forms.
     33 /*
     34 /*	When the \fBpropagate\fR argument is non-zero,
     35 /*	address extensions that aren't explicitly matched in the lookup
     36 /*	table are propagated to the result addresses. The caller is
     37 /*	expected to pass the lookup result to argv_free().
     38 /*
     39 /*	Lookups are performed by mail_addr_find_*(). When the result has the
     40 /*	form \fI@otherdomain\fR, the result is the original user in
     41 /*	\fIotherdomain\fR.
     42 /*
     43 /*	Arguments:
     44 /* .IP path
     45 /*	Dictionary search path (see maps(3)).
     46 /* .IP address
     47 /*	The address to be looked up in external (quoted) form, or
     48 /*	in the form specified with the in_form argument.
     49 /* .IP query_form
     50 /*	Database query address forms, either MA_FORM_INTERNAL (unquoted
     51 /*	form), MA_FORM_EXTERNAL (quoted form), MA_FORM_EXTERNAL_FIRST
     52 /*	(external, then internal if the forms differ), or
     53 /*	MA_FORM_INTERNAL_FIRST (internal, then external if the forms
     54 /*	differ).
     55 /* .IP in_form
     56 /* .IP out_form
     57 /*	Input and output address forms, either MA_FORM_INTERNAL (unquoted
     58 /*	form) or MA_FORM_EXTERNAL (quoted form).
     59 /* DIAGNOSTICS
     60 /*	Warnings: map lookup returns a non-address result.
     61 /*
     62 /*	The path->error value is non-zero when the lookup
     63 /*	failed with a non-permanent error.
     64 /* SEE ALSO
     65 /*	mail_addr_find(3), mail address matching
     66 /*	mail_addr_crunch(3), mail address parsing and rewriting
     67 /* LICENSE
     68 /* .ad
     69 /* .fi
     70 /*	The Secure Mailer license must be distributed with this software.
     71 /* AUTHOR(S)
     72 /*	Wietse Venema
     73 /*	IBM T.J. Watson Research
     74 /*	P.O. Box 704
     75 /*	Yorktown Heights, NY 10598, USA
     76 /*
     77 /*	Wietse Venema
     78 /*	Google, Inc.
     79 /*	111 8th Avenue
     80 /*	New York, NY 10011, USA
     81 /*--*/
     82 
     83 /* System library. */
     84 
     85 #include <sys_defs.h>
     86 #include <string.h>
     87 
     88 /* Utility library. */
     89 
     90 #include <msg.h>
     91 #include <vstring.h>
     92 #include <dict.h>
     93 #include <argv.h>
     94 #include <mymalloc.h>
     95 
     96 /* Global library. */
     97 
     98 #include <quote_822_local.h>
     99 #include <mail_addr_find.h>
    100 #include <mail_addr_crunch.h>
    101 #include <mail_addr_map.h>
    102 
    103 /* Application-specific. */
    104 
    105 #define STR	vstring_str
    106 #define LEN	VSTRING_LEN
    107 
    108 /* mail_addr_map - map a canonical address */
    109 
    110 ARGV   *mail_addr_map_opt(MAPS *path, const char *address, int propagate,
    111 			          int in_form, int query_form, int out_form)
    112 {
    113     VSTRING *buffer = 0;
    114     const char *myname = "mail_addr_map";
    115     const char *string;
    116     char   *ratsign;
    117     char   *extension = 0;
    118     ARGV   *argv = 0;
    119     int     i;
    120     VSTRING *int_address = 0;
    121     VSTRING *ext_address = 0;
    122     const char *int_addr;
    123 
    124     /*
    125      * Optionally convert input from external form. We prefer internal-form
    126      * input to avoid unnecessary input conversion in mail_addr_find_opt().
    127      */
    128     if (in_form == MA_FORM_EXTERNAL) {
    129 	int_address = vstring_alloc(100);
    130 	unquote_822_local(int_address, address);
    131 	int_addr = STR(int_address);
    132 	in_form = MA_FORM_INTERNAL;
    133     } else {
    134 	int_addr = address;
    135     }
    136 
    137     /*
    138      * Look up the full address; if no match is found, look up the address
    139      * with the extension stripped off, and remember the unmatched extension.
    140      */
    141     if ((string = mail_addr_find_opt(path, int_addr, &extension,
    142 				     in_form, query_form,
    143 				     MA_FORM_EXTERNAL,
    144 				     MA_FIND_DEFAULT)) != 0) {
    145 
    146 	/*
    147 	 * Prepend the original user to @otherdomain, but do not propagate
    148 	 * the unmatched address extension. Convert the address to external
    149 	 * form just like the mail_addr_find_opt() output.
    150 	 */
    151 	if (*string == '@') {
    152 	    buffer = vstring_alloc(100);
    153 	    if ((ratsign = strrchr(int_addr, '@')) != 0)
    154 		vstring_strncpy(buffer, int_addr, ratsign - int_addr);
    155 	    else
    156 		vstring_strcpy(buffer, int_addr);
    157 	    if (extension)
    158 		vstring_truncate(buffer, LEN(buffer) - strlen(extension));
    159 	    vstring_strcat(buffer, string);
    160 	    ext_address = vstring_alloc(2 * LEN(buffer));
    161 	    quote_822_local(ext_address, STR(buffer));
    162 	    string = STR(ext_address);
    163 	}
    164 
    165 	/*
    166 	 * Canonicalize the result, and propagate the unmatched extension to
    167 	 * each address found.
    168 	 */
    169 	argv = mail_addr_crunch_opt(string, propagate ? extension : 0,
    170 				    MA_FORM_EXTERNAL, out_form);
    171 	if (buffer)
    172 	    vstring_free(buffer);
    173 	if (ext_address)
    174 	    vstring_free(ext_address);
    175 	if (msg_verbose)
    176 	    for (i = 0; i < argv->argc; i++)
    177 		msg_info("%s: %s -> %d: %s", myname, address, i, argv->argv[i]);
    178 	if (argv->argc == 0) {
    179 	    msg_warn("%s lookup of %s returns non-address result \"%s\"",
    180 		     path->title, address, string);
    181 	    argv = argv_free(argv);
    182 	    path->error = DICT_ERR_RETRY;
    183 	}
    184     }
    185 
    186     /*
    187      * No match found.
    188      */
    189     else {
    190 	if (msg_verbose)
    191 	    msg_info("%s: %s -> %s", myname, address,
    192 		     path->error ? "(try again)" : "(not found)");
    193     }
    194 
    195     /*
    196      * Cleanup.
    197      */
    198     if (extension)
    199 	myfree(extension);
    200     if (int_address)
    201 	vstring_free(int_address);
    202 
    203     return (argv);
    204 }
    205 
    206 #ifdef TEST
    207 
    208 /*
    209  * SYNOPSIS
    210  *	mail_addr_map pass_tests | fail_tests
    211  * DESCRIPTION
    212  *	mail_addr_map performs the specified set of built-in
    213  *	unit tests. With 'pass_tests', all tests must pass, and
    214  *	with 'fail_tests' all tests must fail.
    215  * DIAGNOSTICS
    216  *	When a unit test fails, the program prints details of the
    217  *	failed test.
    218  *
    219  *	The program terminates with a non-zero exit status when at
    220  *	least one test does not pass with 'pass_tests', or when at
    221  *	least one test does not fail with 'fail_tests'.
    222  */
    223 
    224 /* System library. */
    225 
    226 #include <sys_defs.h>
    227 #include <ctype.h>
    228 #include <stdlib.h>
    229 #include <string.h>
    230 #include <unistd.h>
    231 
    232 /* Utility library. */
    233 
    234 #include <argv.h>
    235 #include <msg.h>
    236 #include <mymalloc.h>
    237 #include <vstring.h>
    238 
    239 /* Global library. */
    240 
    241 #include <canon_addr.h>
    242 #include <mail_addr_map.h>
    243 #include <mail_params.h>
    244 
    245 /* Application-specific. */
    246 
    247 #define STR	vstring_str
    248 
    249 typedef struct {
    250     const char *testname;
    251     const char *database;
    252     int     propagate;
    253     const char *delimiter;
    254     int     in_form;
    255     int     query_form;
    256     int     out_form;
    257     const char *address;
    258     const char *expect_argv[2];
    259     int     expect_argc;
    260 } MAIL_ADDR_MAP_TEST;
    261 
    262 #define DONT_PROPAGATE_UNMATCHED_EXTENSION	0
    263 #define DO_PROPAGATE_UNMATCHED_EXTENSION	1
    264 #define NO_RECIPIENT_DELIMITER			""
    265 #define PLUS_RECIPIENT_DELIMITER		"+"
    266 #define DOT_RECIPIENT_DELIMITER			"."
    267 
    268  /*
    269   * All these tests must pass, so that we know that mail_addr_map_opt() works
    270   * as intended. mail_addr_map() has always been used for maps that expect
    271   * external-form queries, so there are no tests here for internal-form
    272   * queries.
    273   */
    274 static MAIL_ADDR_MAP_TEST pass_tests[] = {
    275     {
    276 	"1 external -external-> external, no extension",
    277 	"inline:{ aa (at) example.com=bb (at) example.com }",
    278 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    279 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    280 	"aa (at) example.com",
    281 	{"bb (at) example.com"}, 1,
    282     },
    283     {
    284 	"2 external -external-> external, extension, propagation",
    285 	"inline:{ aa (at) example.com=bb (at) example.com }",
    286 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    287 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    288 	"aa+ext (at) example.com",
    289 	{"bb+ext (at) example.com"}, 1,
    290     },
    291     {
    292 	"3 external -external-> external, extension, no propagation, no match",
    293 	"inline:{ aa (at) example.com=bb (at) example.com }",
    294 	DONT_PROPAGATE_UNMATCHED_EXTENSION, NO_RECIPIENT_DELIMITER,
    295 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    296 	"aa+ext (at) example.com",
    297 	{0}, 0,
    298     },
    299     {
    300 	"4 external -external-> external, extension, full match",
    301 	"inline:{{cc+ext (at) example.com=dd (at) example.com,ee (at) example.com}}",
    302 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    303 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    304 	"cc+ext (at) example.com",
    305 	{"dd (at) example.com", "ee (at) example.com"}, 2,
    306     },
    307     {
    308 	"5 external -external-> external, no extension, quoted",
    309 	"inline:{ {\"a a\"@example.com=\"b b\"@example.com} }",
    310 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    311 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    312 	"\"a a\"@example.com",
    313 	{"\"b b\"@example.com"}, 1,
    314     },
    315     {
    316 	"6 external -external-> external, extension, propagation, quoted",
    317 	"inline:{ {\"a a\"@example.com=\"b b\"@example.com} }",
    318 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    319 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    320 	"\"a a+ext\"@example.com",
    321 	{"\"b b+ext\"@example.com"}, 1,
    322     },
    323     {
    324 	"7 internal -external-> internal, no extension, propagation, embedded space",
    325 	"inline:{ {\"a a\"@example.com=\"b b\"@example.com} }",
    326 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    327 	MA_FORM_INTERNAL, MA_FORM_EXTERNAL, MA_FORM_INTERNAL,
    328 	"a a (at) example.com",
    329 	{"b b (at) example.com"}, 1,
    330     },
    331     {
    332 	"8 internal -external-> internal, extension, propagation, embedded space",
    333 	"inline:{ {\"a a\"@example.com=\"b b\"@example.com} }",
    334 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    335 	MA_FORM_INTERNAL, MA_FORM_EXTERNAL, MA_FORM_INTERNAL,
    336 	"a a+ext (at) example.com",
    337 	{"b b+ext (at) example.com"}, 1,
    338     },
    339     {
    340 	"9 internal -external-> internal, no extension, propagation, embedded space",
    341 	"inline:{ {a_a (at) example.com=\"b b\"@example.com} }",
    342 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    343 	MA_FORM_INTERNAL, MA_FORM_EXTERNAL, MA_FORM_INTERNAL,
    344 	"a_a (at) example.com",
    345 	{"b b (at) example.com"}, 1,
    346     },
    347     {
    348 	"10 internal -external-> internal, extension, propagation, embedded space",
    349 	"inline:{ {a_a (at) example.com=\"b b\"@example.com} }",
    350 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    351 	MA_FORM_INTERNAL, MA_FORM_EXTERNAL, MA_FORM_INTERNAL,
    352 	"a_a+ext (at) example.com",
    353 	{"b b+ext (at) example.com"}, 1,
    354     },
    355     {
    356 	"11 internal -external-> internal, no extension, @domain",
    357 	"inline:{ {@example.com=@example.net} }",
    358 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    359 	MA_FORM_INTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    360 	"a@a (at) example.com",
    361 	{"\"a@a\"@example.net"}, 1,
    362     },
    363     {
    364 	"12 external -external-> external, extension, propagation",
    365 	"inline:{ aa (at) example.com=bb (at) example.com }",
    366 	DO_PROPAGATE_UNMATCHED_EXTENSION, DOT_RECIPIENT_DELIMITER,
    367 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    368 	"aa.ext (at) example.com",
    369 	{"bb.ext (at) example.com"}, 1,
    370     },
    371     0,
    372 };
    373 
    374  /*
    375   * All these tests must fail, so that we know that the tests work.
    376   */
    377 static MAIL_ADDR_MAP_TEST fail_tests[] = {
    378     {
    379 	"selftest 1 external -external-> external, no extension, quoted",
    380 	"inline:{ {\"a a\"@example.com=\"b b\"@example.com} }",
    381 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    382 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    383 	"\"a a\"@example.com",
    384 	{"\"bXb\"@example.com"}, 1,
    385     },
    386     {
    387 	"selftest 2 external -external-> external, no extension, quoted",
    388 	"inline:{ {\"a a\"@example.com=\"b b\"@example.com} }",
    389 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    390 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    391 	"\"aXa\"@example.com",
    392 	{"\"b b\"@example.com"}, 1,
    393     },
    394     {
    395 	"selftest 3 external -external-> external, no extension, quoted",
    396 	"inline:{ {\"a a\"@example.com=\"b b\"@example.com} }",
    397 	DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER,
    398 	MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL,
    399 	"\"a a\"@example.com",
    400 	{0}, 0,
    401     },
    402     0,
    403 };
    404 
    405 /* canon_addr_external - surrogate to avoid trivial-rewrite dependency */
    406 
    407 VSTRING *canon_addr_external(VSTRING *result, const char *addr)
    408 {
    409     return (vstring_strcpy(result, addr));
    410 }
    411 
    412 static int compare(const char *testname,
    413 		           const char **expect_argv, int expect_argc,
    414 		           char **result_argv, int result_argc)
    415 {
    416     int     n;
    417     int     err = 0;
    418 
    419     if (expect_argc != 0 && result_argc != 0) {
    420 	for (n = 0; n < expect_argc && n < result_argc; n++) {
    421 	    if (strcmp(expect_argv[n], result_argv[n]) != 0) {
    422 		msg_warn("fail test %s: expect[%d]='%s', result[%d]='%s'",
    423 			 testname, n, expect_argv[n], n, result_argv[n]);
    424 		err = 1;
    425 	    }
    426 	}
    427     }
    428     if (expect_argc != result_argc) {
    429 	msg_warn("fail test %s: expects %d results but there were %d",
    430 		 testname, expect_argc, result_argc);
    431 	for (n = expect_argc; n < result_argc; n++)
    432 	    msg_info("no expect to match result[%d]='%s'", n, result_argv[n]);
    433 	for (n = result_argc; n < expect_argc; n++)
    434 	    msg_info("no result to match expect[%d]='%s'", n, expect_argv[n]);
    435 	err = 1;
    436     }
    437     return (err);
    438 }
    439 
    440 static char *progname;
    441 
    442 static NORETURN usage(void)
    443 {
    444     msg_fatal("usage: %s pass_test | fail_test", progname);
    445 }
    446 
    447 int     main(int argc, char **argv)
    448 {
    449     MAIL_ADDR_MAP_TEST *test;
    450     MAIL_ADDR_MAP_TEST *tests;
    451     int     errs = 0;
    452 
    453 #define UPDATE(dst, src) { if (dst) myfree(dst); dst = mystrdup(src); }
    454 
    455     /*
    456      * Parse JCL.
    457      */
    458     progname = argv[0];
    459     if (argc != 2) {
    460 	usage();
    461     } else if (strcmp(argv[1], "pass_tests") == 0) {
    462 	tests = pass_tests;
    463     } else if (strcmp(argv[1], "fail_tests") == 0) {
    464 	tests = fail_tests;
    465     } else {
    466 	usage();
    467     }
    468 
    469     /*
    470      * Initialize.
    471      */
    472     mail_params_init();
    473 
    474     /*
    475      * A read-eval-print loop, because specifying C strings with quotes and
    476      * backslashes is painful.
    477      */
    478     for (test = tests; test->testname; test++) {
    479 	ARGV   *result;
    480 	int     fail = 0;
    481 
    482 	if (mail_addr_form_to_string(test->in_form) == 0) {
    483 	    msg_warn("test %s: bad in_form field: %d",
    484 		     test->testname, test->in_form);
    485 	    fail = 1;
    486 	    continue;
    487 	}
    488 	if (mail_addr_form_to_string(test->query_form) == 0) {
    489 	    msg_warn("test %s: bad query_form field: %d",
    490 		     test->testname, test->query_form);
    491 	    fail = 1;
    492 	    continue;
    493 	}
    494 	if (mail_addr_form_to_string(test->out_form) == 0) {
    495 	    msg_warn("test %s: bad out_form field: %d",
    496 		     test->testname, test->out_form);
    497 	    fail = 1;
    498 	    continue;
    499 	}
    500 	MAPS   *maps = maps_create("test", test->database, DICT_FLAG_LOCK
    501 			     | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST);
    502 
    503 	UPDATE(var_rcpt_delim, test->delimiter);
    504 	result = mail_addr_map_opt(maps, test->address, test->propagate,
    505 			   test->in_form, test->query_form, test->out_form);
    506 	if (compare(test->testname, test->expect_argv, test->expect_argc,
    507 	       result ? result->argv : 0, result ? result->argc : 0) != 0) {
    508 	    msg_info("database = %s", test->database);
    509 	    msg_info("propagate = %d", test->propagate);
    510 	    msg_info("delimiter = '%s'", var_rcpt_delim);
    511 	    msg_info("in_form = %s", mail_addr_form_to_string(test->in_form));
    512 	    msg_info("query_form = %s", mail_addr_form_to_string(test->query_form));
    513 	    msg_info("out_form = %s", mail_addr_form_to_string(test->out_form));
    514 	    msg_info("address = %s", test->address);
    515 	    fail = 1;
    516 	}
    517 	maps_free(maps);
    518 	if (result)
    519 	    argv_free(result);
    520 
    521 	/*
    522 	 * It is an error if a test does not pass or fail as intended.
    523 	 */
    524 	errs += (tests == pass_tests ? fail : !fail);
    525     }
    526     return (errs != 0);
    527 }
    528 
    529 #endif
    530