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