1 /* $NetBSD: mail_addr_map.c,v 1.3 2020/03/18 19:05:16 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@example.com=bb@example.com }", 278 DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER, 279 MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, 280 "aa@example.com", 281 {"bb@example.com"}, 1, 282 }, 283 { 284 "2 external -external-> external, extension, propagation", 285 "inline:{ aa@example.com=bb@example.com }", 286 DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER, 287 MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, 288 "aa+ext@example.com", 289 {"bb+ext@example.com"}, 1, 290 }, 291 { 292 "3 external -external-> external, extension, no propagation, no match", 293 "inline:{ aa@example.com=bb@example.com }", 294 DONT_PROPAGATE_UNMATCHED_EXTENSION, NO_RECIPIENT_DELIMITER, 295 MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, 296 "aa+ext@example.com", 297 {0}, 0, 298 }, 299 { 300 "4 external -external-> external, extension, full match", 301 "inline:{{cc+ext@example.com=dd@example.com,ee@example.com}}", 302 DO_PROPAGATE_UNMATCHED_EXTENSION, PLUS_RECIPIENT_DELIMITER, 303 MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, 304 "cc+ext@example.com", 305 {"dd@example.com", "ee@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@example.com", 329 {"b b@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@example.com", 337 {"b b+ext@example.com"}, 1, 338 }, 339 { 340 "9 internal -external-> internal, no extension, propagation, embedded space", 341 "inline:{ {a_a@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@example.com", 345 {"b b@example.com"}, 1, 346 }, 347 { 348 "10 internal -external-> internal, extension, propagation, embedded space", 349 "inline:{ {a_a@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@example.com", 353 {"b b+ext@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@example.com", 361 {"\"a@a\"@example.net"}, 1, 362 }, 363 { 364 "12 external -external-> external, extension, propagation", 365 "inline:{ aa@example.com=bb@example.com }", 366 DO_PROPAGATE_UNMATCHED_EXTENSION, DOT_RECIPIENT_DELIMITER, 367 MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, MA_FORM_EXTERNAL, 368 "aa.ext@example.com", 369 {"bb.ext@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