1 /* $NetBSD: mail_addr_find.c,v 1.4 2022/10/08 16:12:45 christos Exp $ */ 2 3 /*++ 4 /* NAME 5 /* mail_addr_find 3 6 /* SUMMARY 7 /* generic address-based lookup 8 /* SYNOPSIS 9 /* #include <mail_addr_find.h> 10 /* 11 /* const char *mail_addr_find_int_to_ext(maps, address, extension) 12 /* MAPS *maps; 13 /* const char *address; 14 /* char **extension; 15 /* 16 /* const char *mail_addr_find_opt(maps, address, extension, in_form, 17 /* query_form, out_form, strategy) 18 /* MAPS *maps; 19 /* const char *address; 20 /* char **extension; 21 /* int in_form; 22 /* int in_form; 23 /* int out_form; 24 /* int strategy; 25 /* LEGACY SUPPORT 26 /* const char *mail_addr_find(maps, address, extension) 27 /* MAPS *maps; 28 /* const char *address; 29 /* char **extension; 30 /* 31 /* const char *mail_addr_find_to_internal(maps, address, extension) 32 /* MAPS *maps; 33 /* const char *address; 34 /* char **extension; 35 /* 36 /* const char *mail_addr_find_strategy(maps, address, extension) 37 /* MAPS *maps; 38 /* const char *address; 39 /* char **extension; 40 /* int strategy; 41 /* DESCRIPTION 42 /* mail_addr_find*() searches the specified maps for an entry with as 43 /* key the specified address, and derivations from that address. 44 /* It is up to the caller to specify its case sensitivity 45 /* preferences when it opens the maps. 46 /* The result is overwritten upon each call. 47 /* 48 /* In the lookup table, the key is expected to be in external 49 /* form (as produced with the postmap command) and the value is 50 /* expected to be in external (quoted) form if it is an email 51 /* address. Override these assumptions with the query_form 52 /* and out_form arguments. 53 /* 54 /* With mail_addr_find_int_to_ext(), the specified address is in 55 /* internal (unquoted) form, the query is made in external (quoted) 56 /* form, and the result is in the form found in the table (it is 57 /* not necessarily an email address). This version minimizes 58 /* internal/external (unquoted/quoted) conversions of the input, 59 /* query, extension, or result. 60 /* 61 /* mail_addr_find_opt() gives more control, at the cost of 62 /* additional conversions between internal and external forms. 63 /* In particular, output conversion to internal form assumes 64 /* that the lookup result is an email address. 65 /* 66 /* mail_addr_find() is used by legacy code that historically searched 67 /* with internal-form queries. The input is in internal form. It 68 /* searches with external-form queries first, and falls back to 69 /* internal-form queries if no result was found and the external 70 /* and internal forms differ. The result is external form (i.e. no 71 /* conversion). 72 /* 73 /* mail_addr_find_to_internal() is like mail_addr_find() but assumes 74 /* that the lookup result is one external-form email address, 75 /* and converts it to internal form. 76 /* 77 /* mail_addr_find_strategy() is like mail_addr_find() but overrides 78 /* the default search strategy for full and partial addresses. 79 /* 80 /* Arguments: 81 /* .IP maps 82 /* Dictionary search path (see maps(3)). 83 /* .IP address 84 /* The address to be looked up. 85 /* .IP extension 86 /* A null pointer, or the address of a pointer that is set to 87 /* the address of a dynamic memory copy of the address extension 88 /* that had to be chopped off in order to match the lookup tables. 89 /* The copy includes the recipient address delimiter. 90 /* The copy is in internal (unquoted) form. 91 /* The caller is expected to pass the copy to myfree(). 92 /* .IP query_form 93 /* The address form to use for database queries: one of 94 /* MA_FORM_INTERNAL (unquoted form), MA_FORM_EXTERNAL (quoted form), 95 /* MA_FORM_EXTERNAL_FIRST (external form, then internal form if the 96 /* external and internal forms differ), or MA_FORM_INTERNAL_FIRST 97 /* (internal form, then external form if the internal and external 98 /* forms differ). 99 /* .IP in_form 100 /* .IP out_form 101 /* Input and output address forms, one of MA_FORM_INTERNAL (unquoted 102 /* form), or MA_FORM_EXTERNAL (quoted form). 103 /* .IP strategy 104 /* The lookup strategy for full and partial addresses, specified 105 /* as the binary OR of one or more of the following. These lookups 106 /* are implemented in the order as listed below. 107 /* .RS 108 /* .IP MA_FIND_DEFAULT 109 /* A convenience alias for (MA_FIND_FULL | 110 /* MA_FIND_NOEXT | MA_FIND_LOCALPART_IF_LOCAL | 111 /* MA_FIND_AT_DOMAIN). 112 /* .IP MA_FIND_FULL 113 /* Look up the full email address. 114 /* .IP MA_FIND_NOEXT 115 /* If no match was found, and the address has a localpart extension, 116 /* look up the address after removing the extension. 117 /* .IP MA_FIND_LOCALPART_IF_LOCAL 118 /* If no match was found, and the domain matches myorigin, 119 /* mydestination, or any inet_interfaces or proxy_interfaces IP 120 /* address, look up the localpart. If no match was found, and the 121 /* address has a localpart extension, repeat the same query after 122 /* removing the extension unless MA_FIND_NOEXT is specified. 123 /* .IP MA_FIND_LOCALPART_AT_IF_LOCAL 124 /* As above, but using the localpart@ instead. 125 /* .IP MA_FIND_AT_DOMAIN 126 /* If no match was found, look up the @domain without localpart. 127 /* .IP MA_FIND_DOMAIN 128 /* If no match was found, look up the domain without localpart. 129 /* .IP MA_FIND_PDMS 130 /* When used with MA_FIND_DOMAIN, the domain also matches subdomains. 131 /* .IP MA_FIND_PDDMDS 132 /* When used with MA_FIND_DOMAIN, dot-domain also matches 133 /* dot-subdomains. 134 /* .IP MA_FIND_LOCALPART_AT 135 /* If no match was found, look up the localpart@, regardless of 136 /* the domain content. 137 /* .RE 138 /* DIAGNOSTICS 139 /* The maps->error value is non-zero when the lookup failed due to 140 /* a non-permanent error. 141 /* SEE ALSO 142 /* maps(3), multi-dictionary search resolve_local(3), recognize 143 /* local system 144 /* LICENSE 145 /* .ad 146 /* .fi 147 /* The Secure Mailer license must be distributed with this software. 148 /* AUTHOR(S) 149 /* Wietse Venema 150 /* IBM T.J. Watson Research 151 /* P.O. Box 704 152 /* Yorktown Heights, NY 10598, USA 153 /* 154 /* Wietse Venema 155 /* Google, Inc. 156 /* 111 8th Avenue 157 /* New York, NY 10011, USA 158 /*--*/ 159 160 /* System library. */ 161 162 #include <sys_defs.h> 163 #include <string.h> 164 165 /* Utility library. */ 166 167 #include <msg.h> 168 #include <name_mask.h> 169 #include <dict.h> 170 #include <stringops.h> 171 #include <mymalloc.h> 172 #include <vstring.h> 173 174 /* Global library. */ 175 176 #include <mail_params.h> 177 #include <strip_addr.h> 178 #include <mail_addr_find.h> 179 #include <resolve_local.h> 180 #include <quote_822_local.h> 181 182 /* Application-specific. */ 183 184 #define STR vstring_str 185 186 #ifdef TEST 187 188 static const NAME_MASK strategy_table[] = { 189 "full", MA_FIND_FULL, 190 "noext", MA_FIND_NOEXT, 191 "localpart_if_local", MA_FIND_LOCALPART_IF_LOCAL, 192 "localpart_at_if_local", MA_FIND_LOCALPART_AT_IF_LOCAL, 193 "at_domain", MA_FIND_AT_DOMAIN, 194 "domain", MA_FIND_DOMAIN, 195 "pdms", MA_FIND_PDMS, 196 "pddms", MA_FIND_PDDMDS, 197 "localpart_at", MA_FIND_LOCALPART_AT, 198 "default", MA_FIND_DEFAULT, 199 0, -1, 200 }; 201 202 /* strategy_from_string - symbolic strategy flags to internal form */ 203 204 static int strategy_from_string(const char *strategy_string) 205 { 206 return (name_mask_delim_opt("strategy_from_string", strategy_table, 207 strategy_string, "|", 208 NAME_MASK_WARN | NAME_MASK_ANY_CASE)); 209 } 210 211 /* strategy_to_string - internal form to symbolic strategy flags */ 212 213 static const char *strategy_to_string(VSTRING *res_buf, int strategy_mask) 214 { 215 static VSTRING *my_buf; 216 217 if (res_buf == 0 && (res_buf = my_buf) == 0) 218 res_buf = my_buf = vstring_alloc(20); 219 return (str_name_mask_opt(res_buf, "strategy_to_string", 220 strategy_table, strategy_mask, 221 NAME_MASK_WARN | NAME_MASK_PIPE)); 222 } 223 224 #endif 225 226 /* 227 * Specify what keys are partial or full, to avoid matching partial 228 * addresses with regular expressions. 229 */ 230 #define FULL 0 231 #define PARTIAL DICT_FLAG_FIXED 232 233 /* find_addr - helper to search maps with the right query form */ 234 235 static const char *find_addr(MAPS *path, const char *address, int flags, 236 int with_domain, int query_form, VSTRING *ext_addr_buf) 237 { 238 const char *result; 239 240 #define SANS_DOMAIN 0 241 #define WITH_DOMAIN 1 242 243 switch (query_form) { 244 245 /* 246 * Query with external-form (quoted) address. The code looks a bit 247 * unusual to emphasize the symmetry with the other cases. 248 */ 249 case MA_FORM_EXTERNAL: 250 case MA_FORM_EXTERNAL_FIRST: 251 quote_822_local_flags(ext_addr_buf, address, 252 with_domain ? QUOTE_FLAG_DEFAULT : 253 QUOTE_FLAG_DEFAULT | QUOTE_FLAG_BARE_LOCALPART); 254 result = maps_find(path, STR(ext_addr_buf), flags); 255 if (result != 0 || path->error != 0 256 || query_form != MA_FORM_EXTERNAL_FIRST 257 || strcmp(address, STR(ext_addr_buf)) == 0) 258 break; 259 result = maps_find(path, address, flags); 260 break; 261 262 /* 263 * Query with internal-form (unquoted) address. The code looks a bit 264 * unusual to emphasize the symmetry with the other cases. 265 */ 266 case MA_FORM_INTERNAL: 267 case MA_FORM_INTERNAL_FIRST: 268 result = maps_find(path, address, flags); 269 if (result != 0 || path->error != 0 270 || query_form != MA_FORM_INTERNAL_FIRST) 271 break; 272 quote_822_local_flags(ext_addr_buf, address, 273 with_domain ? QUOTE_FLAG_DEFAULT : 274 QUOTE_FLAG_DEFAULT | QUOTE_FLAG_BARE_LOCALPART); 275 if (strcmp(address, STR(ext_addr_buf)) == 0) 276 break; 277 result = maps_find(path, STR(ext_addr_buf), flags); 278 break; 279 280 /* 281 * Can't happen. 282 */ 283 default: 284 msg_panic("mail_addr_find: bad query_form: %d", query_form); 285 } 286 return (result); 287 } 288 289 /* find_local - search on localpart info */ 290 291 static const char *find_local(MAPS *path, char *ratsign, int rats_offs, 292 char *int_full_key, char *int_bare_key, 293 int query_form, char **extp, char **saved_ext, 294 VSTRING *ext_addr_buf) 295 { 296 const char *myname = "mail_addr_find"; 297 const char *result; 298 int with_domain; 299 int saved_ch; 300 301 /* 302 * This code was ripped from the middle of a function so that it can be 303 * reused multiple times, that's why the interface makes little sense. 304 */ 305 with_domain = rats_offs ? WITH_DOMAIN : SANS_DOMAIN; 306 307 saved_ch = *(unsigned char *) (ratsign + rats_offs); 308 *(ratsign + rats_offs) = 0; 309 result = find_addr(path, int_full_key, PARTIAL, with_domain, 310 query_form, ext_addr_buf); 311 *(ratsign + rats_offs) = saved_ch; 312 if (result == 0 && path->error == 0 && int_bare_key != 0) { 313 if ((ratsign = strrchr(int_bare_key, '@')) == 0) 314 msg_panic("%s: bare key botch", myname); 315 saved_ch = *(unsigned char *) (ratsign + rats_offs); 316 *(ratsign + rats_offs) = 0; 317 if ((result = find_addr(path, int_bare_key, PARTIAL, with_domain, 318 query_form, ext_addr_buf)) != 0 319 && extp != 0) { 320 *extp = *saved_ext; 321 *saved_ext = 0; 322 } 323 *(ratsign + rats_offs) = saved_ch; 324 } 325 return result; 326 } 327 328 /* mail_addr_find_opt - map a canonical address */ 329 330 const char *mail_addr_find_opt(MAPS *path, const char *address, char **extp, 331 int in_form, int query_form, 332 int out_form, int strategy) 333 { 334 const char *myname = "mail_addr_find"; 335 VSTRING *ext_addr_buf = 0; 336 VSTRING *int_addr_buf = 0; 337 const char *int_addr; 338 static VSTRING *int_result = 0; 339 const char *result; 340 char *ratsign = 0; 341 char *int_full_key; 342 char *int_bare_key; 343 char *saved_ext; 344 int rc = 0; 345 346 /* 347 * Optionally convert the address from external form. 348 */ 349 if (in_form == MA_FORM_EXTERNAL) { 350 int_addr_buf = vstring_alloc(100); 351 unquote_822_local(int_addr_buf, address); 352 int_addr = STR(int_addr_buf); 353 } else { 354 int_addr = address; 355 } 356 if (query_form == MA_FORM_EXTERNAL_FIRST 357 || query_form == MA_FORM_EXTERNAL) 358 ext_addr_buf = vstring_alloc(100); 359 360 /* 361 * Initialize. 362 */ 363 int_full_key = mystrdup(int_addr); 364 if (*var_rcpt_delim == 0 || (strategy & MA_FIND_NOEXT) == 0) { 365 int_bare_key = saved_ext = 0; 366 } else { 367 /* XXX This could be done after user+foo@domain fails. */ 368 int_bare_key = 369 strip_addr_internal(int_full_key, &saved_ext, var_rcpt_delim); 370 } 371 372 /* 373 * Try user+foo@domain and user@domain. 374 */ 375 if ((strategy & MA_FIND_FULL) != 0) { 376 result = find_addr(path, int_full_key, FULL, WITH_DOMAIN, 377 query_form, ext_addr_buf); 378 } else { 379 result = 0; 380 path->error = 0; 381 } 382 383 if (result == 0 && path->error == 0 && int_bare_key != 0 384 && (result = find_addr(path, int_bare_key, PARTIAL, WITH_DOMAIN, 385 query_form, ext_addr_buf)) != 0 386 && extp != 0) { 387 *extp = saved_ext; 388 saved_ext = 0; 389 } 390 391 /* 392 * Try user+foo if the domain matches user+foo@$myorigin, 393 * user+foo@$mydestination or user+foo@[${proxy,inet}_interfaces]. Then 394 * try with +foo stripped off. 395 */ 396 if (result == 0 && path->error == 0 397 && (ratsign = strrchr(int_full_key, '@')) != 0 398 && (strategy & (MA_FIND_LOCALPART_IF_LOCAL 399 | MA_FIND_LOCALPART_AT_IF_LOCAL)) != 0) { 400 if (strcasecmp_utf8(ratsign + 1, var_myorigin) == 0 401 || (rc = resolve_local(ratsign + 1)) > 0) { 402 if ((strategy & MA_FIND_LOCALPART_IF_LOCAL) != 0) 403 result = find_local(path, ratsign, 0, int_full_key, 404 int_bare_key, query_form, extp, &saved_ext, 405 ext_addr_buf); 406 if (result == 0 && path->error == 0 407 && (strategy & MA_FIND_LOCALPART_AT_IF_LOCAL) != 0) 408 result = find_local(path, ratsign, 1, int_full_key, 409 int_bare_key, query_form, extp, &saved_ext, 410 ext_addr_buf); 411 } else if (rc < 0) 412 path->error = rc; 413 } 414 415 /* 416 * Try @domain. 417 */ 418 if (result == 0 && path->error == 0 && ratsign != 0 419 && (strategy & MA_FIND_AT_DOMAIN) != 0) 420 result = maps_find(path, ratsign, PARTIAL); 421 422 /* 423 * Try domain (optionally, subdomains). 424 */ 425 if (result == 0 && path->error == 0 && ratsign != 0 426 && (strategy & MA_FIND_DOMAIN) != 0) { 427 const char *name; 428 const char *next; 429 430 if ((strategy & MA_FIND_PDMS) && (strategy & MA_FIND_PDDMDS)) 431 msg_warn("mail_addr_find_opt: do not specify both " 432 "MA_FIND_PDMS and MA_FIND_PDDMDS"); 433 for (name = ratsign + 1; *name != 0; name = next) { 434 if ((result = maps_find(path, name, PARTIAL)) != 0 435 || path->error != 0 436 || (strategy & (MA_FIND_PDMS | MA_FIND_PDDMDS)) == 0 437 || (next = strchr(name + 1, '.')) == 0) 438 break; 439 if ((strategy & MA_FIND_PDDMDS) == 0) 440 next++; 441 } 442 } 443 444 /* 445 * Try localpart@ even if the domain is not local. 446 */ 447 if ((strategy & MA_FIND_LOCALPART_AT) != 0 \ 448 &&result == 0 && path->error == 0) 449 result = find_local(path, ratsign, 1, int_full_key, 450 int_bare_key, query_form, extp, &saved_ext, 451 ext_addr_buf); 452 453 /* 454 * Optionally convert the result to internal form. The lookup result is 455 * supposed to be one external-form email address. 456 */ 457 if (result != 0 && out_form == MA_FORM_INTERNAL) { 458 if (int_result == 0) 459 int_result = vstring_alloc(100); 460 unquote_822_local(int_result, result); 461 result = STR(int_result); 462 } 463 464 /* 465 * Clean up. 466 */ 467 if (msg_verbose) 468 msg_info("%s: %s -> %s", myname, address, 469 result ? result : 470 path->error ? "(try again)" : 471 "(not found)"); 472 myfree(int_full_key); 473 if (int_bare_key) 474 myfree(int_bare_key); 475 if (saved_ext) 476 myfree(saved_ext); 477 if (int_addr_buf) 478 vstring_free(int_addr_buf); 479 if (ext_addr_buf) 480 vstring_free(ext_addr_buf); 481 return (result); 482 } 483 484 #ifdef TEST 485 486 /* 487 * Proof-of-concept test program. Read an address and expected results from 488 * stdin, and warn about any discrepancies. 489 */ 490 #include <ctype.h> 491 #include <stdlib.h> 492 493 #include <vstream.h> 494 #include <vstring_vstream.h> 495 #include <mail_params.h> 496 497 static NORETURN usage(const char *progname) 498 { 499 msg_fatal("usage: %s [-v]", progname); 500 } 501 502 int main(int argc, char **argv) 503 { 504 VSTRING *buffer = vstring_alloc(100); 505 char *bp; 506 MAPS *path = 0; 507 const char *result; 508 char *extent; 509 char *cmd; 510 char *in_field; 511 char *query_field; 512 char *out_field; 513 char *strategy_field; 514 char *key_field; 515 char *expect_res; 516 char *expect_ext; 517 int in_form; 518 int query_form; 519 int out_form; 520 int strategy_flags; 521 int ch; 522 int errs = 0; 523 524 /* 525 * Parse JCL. 526 */ 527 while ((ch = GETOPT(argc, argv, "v")) > 0) { 528 switch (ch) { 529 case 'v': 530 msg_verbose++; 531 break; 532 default: 533 usage(argv[0]); 534 } 535 } 536 if (argc != optind) 537 usage(argv[0]); 538 539 /* 540 * Initialize. 541 */ 542 #define UPDATE(var, val) do { myfree(var); var = mystrdup(val); } while (0) 543 544 mail_params_init(); 545 546 /* 547 * TODO: move these assignments into the read/eval loop. 548 */ 549 UPDATE(var_rcpt_delim, "+"); 550 UPDATE(var_mydomain, "localdomain"); 551 UPDATE(var_myorigin, "localdomain"); 552 UPDATE(var_mydest, "localhost.localdomain"); 553 while (vstring_fgets_nonl(buffer, VSTREAM_IN)) { 554 bp = STR(buffer); 555 if (msg_verbose) 556 msg_info("> %s", bp); 557 if ((cmd = mystrtok(&bp, CHARS_SPACE)) == 0 || *cmd == '#') 558 continue; 559 while (ISSPACE(*bp)) 560 bp++; 561 562 /* 563 * Visible comment. 564 */ 565 if (strcmp(cmd, "echo") == 0) { 566 vstream_printf("%s\n", bp); 567 } 568 569 /* 570 * Open maps. 571 */ 572 else if (strcmp(cmd, "maps") == 0) { 573 if (path) 574 maps_free(path); 575 path = maps_create(argv[0], bp, DICT_FLAG_LOCK 576 | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); 577 vstream_printf("%s\n", bp); 578 continue; 579 } 580 581 /* 582 * Lookup and verify. 583 */ 584 else if (path && strcmp(cmd, "test") == 0) { 585 586 /* 587 * Parse the input and expectations. 588 */ 589 /* internal, external. */ 590 if ((in_field = mystrtok(&bp, ":")) == 0) 591 msg_fatal("no input form"); 592 if ((in_form = mail_addr_form_from_string(in_field)) < 0) 593 msg_fatal("bad input form: '%s'", in_field); 594 if ((query_field = mystrtok(&bp, ":")) == 0) 595 msg_fatal("no query form"); 596 /* internal, external, external-first. */ 597 if ((query_form = mail_addr_form_from_string(query_field)) < 0) 598 msg_fatal("bad query form: '%s'", query_field); 599 if ((out_field = mystrtok(&bp, ":")) == 0) 600 msg_fatal("no output form"); 601 /* internal, external. */ 602 if ((out_form = mail_addr_form_from_string(out_field)) < 0) 603 msg_fatal("bad output form: '%s'", out_field); 604 if ((strategy_field = mystrtok(&bp, ":")) == 0) 605 msg_fatal("no strategy field"); 606 if ((strategy_flags = strategy_from_string(strategy_field)) < 0) 607 msg_fatal("bad strategy field: '%s'", strategy_field); 608 if ((key_field = mystrtok(&bp, ":")) == 0) 609 msg_fatal("no search key"); 610 expect_res = mystrtok(&bp, ":"); 611 expect_ext = mystrtok(&bp, ":"); 612 if (mystrtok(&bp, ":") != 0) 613 msg_fatal("garbage after extension field"); 614 615 /* 616 * Lookups. 617 */ 618 extent = 0; 619 result = mail_addr_find_opt(path, key_field, &extent, 620 in_form, query_form, out_form, 621 strategy_flags); 622 vstream_printf("%s:%s -%s-> %s:%s (%s)\n", 623 in_field, key_field, query_field, out_field, result ? result : 624 path->error ? "(try again)" : 625 "(not found)", extent ? extent : "null extension"); 626 vstream_fflush(VSTREAM_OUT); 627 628 /* 629 * Enforce expectations. 630 */ 631 if (expect_res && result) { 632 if (strcmp(expect_res, result) != 0) { 633 msg_warn("expect result '%s' but got '%s'", expect_res, result); 634 errs = 1; 635 if (expect_ext && extent) { 636 if (strcmp(expect_ext, extent) != 0) 637 msg_warn("expect extension '%s' but got '%s'", 638 expect_ext, extent); 639 errs = 1; 640 } else if (expect_ext && !extent) { 641 msg_warn("expect extension '%s' but got none", expect_ext); 642 errs = 1; 643 } else if (!expect_ext && extent) { 644 msg_warn("expect no extension but got '%s'", extent); 645 errs = 1; 646 } 647 } 648 } else if (expect_res && !result) { 649 msg_warn("expect result '%s' but got none", expect_res); 650 errs = 1; 651 } else if (!expect_res && result) { 652 msg_warn("expected no result but got '%s'", result); 653 errs = 1; 654 } 655 vstream_fflush(VSTREAM_OUT); 656 if (extent) 657 myfree(extent); 658 } 659 660 /* 661 * Unknown request. 662 */ 663 else { 664 msg_warn("bad request: %s", cmd); 665 } 666 } 667 vstring_free(buffer); 668 669 maps_free(path); 670 return (errs != 0); 671 } 672 673 #endif 674