From e90fcc54809db2591dc083f43ef54c6ec8c60847 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 18:16:13 +0200 Subject: Adding upstream version 4.96. Signed-off-by: Daniel Baumann --- src/route.c | 2072 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2072 insertions(+) create mode 100644 src/route.c (limited to 'src/route.c') diff --git a/src/route.c b/src/route.c new file mode 100644 index 0000000..fa69b8b --- /dev/null +++ b/src/route.c @@ -0,0 +1,2072 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Functions concerned with routing, and the list of generic router options. */ + + +#include "exim.h" + + + +/* Generic options for routers, all of which live inside router_instance +data blocks and which therefore have the opt_public flag set. */ +#define LOFF(field) OPT_OFF(router_instance, field) + +optionlist optionlist_routers[] = { + { "*expand_group", opt_stringptr | opt_hidden | opt_public, + LOFF(expand_gid) }, + { "*expand_more", opt_stringptr | opt_hidden | opt_public, + LOFF(expand_more) }, + { "*expand_unseen", opt_stringptr | opt_hidden | opt_public, + LOFF(expand_unseen) }, + { "*expand_user", opt_stringptr | opt_hidden | opt_public, + LOFF(expand_uid) }, + { "*set_group", opt_bool | opt_hidden | opt_public, + LOFF(gid_set) }, + { "*set_user", opt_bool | opt_hidden | opt_public, + LOFF(uid_set) }, + { "address_data", opt_stringptr|opt_public, + LOFF(address_data) }, + { "address_test", opt_bool|opt_public, + LOFF(address_test) }, +#ifdef EXPERIMENTAL_BRIGHTMAIL + { "bmi_deliver_alternate", opt_bool | opt_public, + LOFF(bmi_deliver_alternate) }, + { "bmi_deliver_default", opt_bool | opt_public, + LOFF(bmi_deliver_default) }, + { "bmi_dont_deliver", opt_bool | opt_public, + LOFF(bmi_dont_deliver) }, + { "bmi_rule", opt_stringptr|opt_public, + LOFF(bmi_rule) }, +#endif + { "cannot_route_message", opt_stringptr | opt_public, + LOFF(cannot_route_message) }, + { "caseful_local_part", opt_bool | opt_public, + LOFF(caseful_local_part) }, + { "check_local_user", opt_bool | opt_public, + LOFF(check_local_user) }, + { "condition", opt_stringptr|opt_public|opt_rep_con, + LOFF(condition) }, + { "debug_print", opt_stringptr | opt_public, + LOFF(debug_string) }, + { "disable_logging", opt_bool | opt_public, + LOFF(disable_logging) }, + { "dnssec_request_domains", opt_stringptr|opt_public, + LOFF(dnssec.request) }, + { "dnssec_require_domains", opt_stringptr|opt_public, + LOFF(dnssec.require) }, + { "domains", opt_stringptr|opt_public, + LOFF(domains) }, + { "driver", opt_stringptr|opt_public, + LOFF(driver_name) }, + { "dsn_lasthop", opt_bool|opt_public, + LOFF(dsn_lasthop) }, + { "errors_to", opt_stringptr|opt_public, + LOFF(errors_to) }, + { "expn", opt_bool|opt_public, + LOFF(expn) }, + { "fail_verify", opt_bool_verify|opt_hidden|opt_public, + LOFF(fail_verify_sender) }, + { "fail_verify_recipient", opt_bool|opt_public, + LOFF(fail_verify_recipient) }, + { "fail_verify_sender", opt_bool|opt_public, + LOFF(fail_verify_sender) }, + { "fallback_hosts", opt_stringptr|opt_public, + LOFF(fallback_hosts) }, + { "group", opt_expand_gid | opt_public, + LOFF(gid) }, + { "headers_add", opt_stringptr|opt_public|opt_rep_str, + LOFF(extra_headers) }, + { "headers_remove", opt_stringptr|opt_public|opt_rep_str, + LOFF(remove_headers) }, + { "ignore_target_hosts",opt_stringptr|opt_public, + LOFF(ignore_target_hosts) }, + { "initgroups", opt_bool | opt_public, + LOFF(initgroups) }, + { "local_part_prefix", opt_stringptr|opt_public, + LOFF(prefix) }, + { "local_part_prefix_optional",opt_bool|opt_public, + LOFF(prefix_optional) }, + { "local_part_suffix", opt_stringptr|opt_public, + LOFF(suffix) }, + { "local_part_suffix_optional",opt_bool|opt_public, + LOFF(suffix_optional) }, + { "local_parts", opt_stringptr|opt_public, + LOFF(local_parts) }, + { "log_as_local", opt_bool|opt_public, + LOFF(log_as_local) }, + { "more", opt_expand_bool|opt_public, + LOFF(more) }, + { "pass_on_timeout", opt_bool|opt_public, + LOFF(pass_on_timeout) }, + { "pass_router", opt_stringptr|opt_public, + LOFF(pass_router_name) }, + { "redirect_router", opt_stringptr|opt_public, + LOFF(redirect_router_name) }, + { "require_files", opt_stringptr|opt_public, + LOFF(require_files) }, + { "retry_use_local_part", opt_bool|opt_public, + LOFF(retry_use_local_part) }, + { "router_home_directory", opt_stringptr|opt_public, + LOFF(router_home_directory) }, + { "self", opt_stringptr|opt_public, + LOFF(self) }, + { "senders", opt_stringptr|opt_public, + LOFF(senders) }, + { "set", opt_stringptr|opt_public|opt_rep_str, + LOFF(set) }, + #ifdef SUPPORT_TRANSLATE_IP_ADDRESS + { "translate_ip_address", opt_stringptr|opt_public, + LOFF(translate_ip_address) }, + #endif + { "transport", opt_stringptr|opt_public, + LOFF(transport_name) }, + { "transport_current_directory", opt_stringptr|opt_public, + LOFF(current_directory) }, + { "transport_home_directory", opt_stringptr|opt_public, + LOFF(home_directory) }, + { "unseen", opt_expand_bool|opt_public, + LOFF(unseen) }, + { "user", opt_expand_uid | opt_public, + LOFF(uid) }, + { "verify", opt_bool_verify|opt_hidden|opt_public, + LOFF(verify_sender) }, + { "verify_only", opt_bool|opt_public, + LOFF(verify_only) }, + { "verify_recipient", opt_bool|opt_public, + LOFF(verify_recipient) }, + { "verify_sender", opt_bool|opt_public, + LOFF(verify_sender) } +}; + +int optionlist_routers_size = nelem(optionlist_routers); + + +#ifdef MACRO_PREDEF + +# include "macro_predef.h" + +void +options_routers(void) +{ +uschar buf[64]; + +options_from_list(optionlist_routers, nelem(optionlist_routers), US"ROUTERS", NULL); + +for (router_info * ri = routers_available; ri->driver_name[0]; ri++) + { + spf(buf, sizeof(buf), US"_DRIVER_ROUTER_%T", ri->driver_name); + builtin_macro_create(buf); + options_from_list(ri->options, (unsigned)*ri->options_count, US"ROUTER", ri->driver_name); + } +} + +#else /*!MACRO_PREDEF*/ + +/************************************************* +* Set router pointer from name * +*************************************************/ + +/* This function is used for the redirect_router and pass_router options and +called from route_init() below. + +Arguments: + r the current router + name new router name + ptr where to put the pointer + after TRUE if router must follow this one + +Returns: nothing. +*/ + +static void +set_router(router_instance *r, uschar *name, router_instance **ptr, BOOL after) +{ +BOOL afterthis = FALSE; +router_instance *rr; + +for (rr = routers; rr; rr = rr->next) + { + if (Ustrcmp(name, rr->name) == 0) + { + *ptr = rr; + break; + } + if (rr == r) afterthis = TRUE; + } + +if (!rr) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "new_router \"%s\" not found for \"%s\" router", name, r->name); + +if (after && !afterthis) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "new_router \"%s\" does not follow \"%s\" router", name, r->name); +} + + + +/************************************************* +* Initialize router list * +*************************************************/ + +/* Read the routers section of the configuration file, and set up a chain of +router instances according to its contents. Each router has generic options and +may also have its own private options. This function is only ever called when +routers == NULL. We use generic code in readconf to do the work. It will set +values from the configuration file, and then call the driver's initialization +function. */ + +void +route_init(void) +{ +readconf_driver_init(US"router", + (driver_instance **)(&routers), /* chain anchor */ + (driver_info *)routers_available, /* available drivers */ + sizeof(router_info), /* size of info blocks */ + &router_defaults, /* default values for generic options */ + sizeof(router_instance), /* size of instance block */ + optionlist_routers, /* generic options */ + optionlist_routers_size); + +for (router_instance * r = routers; r; r = r->next) + { + uschar *s = r->self; + + /* If log_as_local is unset, its overall default is FALSE. (The accept + router defaults it to TRUE.) */ + + if (r->log_as_local == TRUE_UNSET) r->log_as_local = FALSE; + + /* Check for transport or no transport on certain routers */ + + if ( (r->info->ri_flags & ri_yestransport) + && !r->transport_name && !r->verify_only) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, "%s router:\n " + "a transport is required for this router", r->name); + + if ((r->info->ri_flags & ri_notransport) && r->transport_name) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, "%s router:\n " + "a transport must not be defined for this router", r->name); + + /* The "self" option needs to be decoded into a code value and possibly a + new domain string and a rewrite boolean. */ + + if (Ustrcmp(s, "freeze") == 0) r->self_code = self_freeze; + else if (Ustrcmp(s, "defer") == 0) r->self_code = self_defer; + else if (Ustrcmp(s, "send") == 0) r->self_code = self_send; + else if (Ustrcmp(s, "pass") == 0) r->self_code = self_pass; + else if (Ustrcmp(s, "fail") == 0) r->self_code = self_fail; + else if (Ustrncmp(s, "reroute:", 8) == 0) + { + s += 8; + while (isspace(*s)) s++; + if (Ustrncmp(s, "rewrite:", 8) == 0) + { + r->self_rewrite = TRUE; + s += 8; + while (isspace(*s)) s++; + } + r->self = s; + r->self_code = self_reroute; + } + + else log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n " + "%s is not valid for the self option", r->name, s); + + /* If any router has check_local_user set, default retry_use_local_part + TRUE; otherwise its default is FALSE. */ + + if (r->retry_use_local_part == TRUE_UNSET) + r->retry_use_local_part = + r->check_local_user || r->local_parts || r->condition || r->prefix || r->suffix || r->senders || r->require_files; + + /* Build a host list if fallback hosts is set. */ + + { + int old_pool = store_pool; + store_pool = POOL_PERM; + host_build_hostlist(&r->fallback_hostlist, r->fallback_hosts, FALSE); + store_pool = old_pool; + } + + /* Check redirect_router and pass_router are valid */ + + if (r->redirect_router_name) + set_router(r, r->redirect_router_name, &(r->redirect_router), FALSE); + + if (r->pass_router_name) + set_router(r, r->pass_router_name, &(r->pass_router), TRUE); + +#ifdef notdef + DEBUG(D_route) debug_printf("DSN: %s %s\n", r->name, + r->dsn_lasthop ? "lasthop set" : "propagating DSN"); +#endif + } +} + + + +/************************************************* +* Tidy up after routing * +*************************************************/ + +/* Routers are entitled to keep hold of certain resources in their instance +blocks so as to save setting them up each time. An example is an open file. +Such routers must provide a tidyup entry point which is called when all routing +is finished, via this function. */ + +void +route_tidyup(void) +{ +for (router_instance * r = routers; r; r = r->next) + if (r->info->tidyup) (r->info->tidyup)(r); +} + + + +/************************************************* +* Check local part for prefix * +*************************************************/ + +/* This function is handed a local part and a list of possible prefixes; if any +one matches, return the prefix length. A prefix beginning with '*' is a +wildcard. + +Arguments: + local_part the local part to check + prefixes the list of prefixes + vp if set, pointer to place for size of wildcard portion + +Returns: length of matching prefix or zero +*/ + +int +route_check_prefix(const uschar * local_part, const uschar * prefixes, + unsigned * vp) +{ +int sep = 0; +uschar *prefix; +const uschar *listptr = prefixes; + +while ((prefix = string_nextinlist(&listptr, &sep, NULL, 0))) + { + int plen = Ustrlen(prefix); + if (prefix[0] == '*') + { + prefix++; + for (const uschar * p = local_part + Ustrlen(local_part) - (--plen); + p >= local_part; p--) + if (strncmpic(prefix, p, plen) == 0) + { + unsigned vlen = p - local_part; + if (vp) *vp = vlen; + return plen + vlen; + } + } + else + if (strncmpic(prefix, local_part, plen) == 0) + { + if (vp) *vp = 0; + return plen; + } + } + +return 0; +} + + + +/************************************************* +* Check local part for suffix * +*************************************************/ + +/* This function is handed a local part and a list of possible suffixes; +if any one matches, return the suffix length. A suffix ending with '*' +is a wildcard. + +Arguments: + local_part the local part to check + suffixes the list of suffixes + vp if set, pointer to place for size of wildcard portion + +Returns: length of matching suffix or zero +*/ + +int +route_check_suffix(const uschar * local_part, const uschar * suffixes, + unsigned * vp) +{ +int sep = 0; +int alen = Ustrlen(local_part); +uschar *suffix; +const uschar *listptr = suffixes; + +while ((suffix = string_nextinlist(&listptr, &sep, NULL, 0))) + { + int slen = Ustrlen(suffix); + if (suffix[slen-1] == '*') + { + const uschar * pend = local_part + alen - (--slen) + 1; + for (const uschar * p = local_part; p < pend; p++) + if (strncmpic(suffix, p, slen) == 0) + { + int tlen = alen - (p - local_part); + if (vp) *vp = tlen - slen; + return tlen; + } + } + else + if (alen > slen && strncmpic(suffix, local_part + alen - slen, slen) == 0) + { + if (vp) *vp = 0; + return slen; + } + } + +return 0; +} + + + + +/************************************************* +* Check local part, domain, or sender * +*************************************************/ + +/* The checks in check_router_conditions() require similar code, so we use +this function to save repetition. + +Arguments: + rname router name for error messages + type type of check, for error message + list domains, local_parts, or senders list + anchorptr -> tree for possibly cached items (domains) + cache_bits cached bits pointer + listtype MCL_DOMAIN for domain check + MCL_LOCALPART for local part check + MCL_ADDRESS for sender check + domloc current domain, current local part, or NULL for sender check + ldata where to put lookup data + caseless passed on to match_isinlist() + perror where to put an error message + +Returns: OK item is in list + SKIP item is not in list, router is to be skipped + DEFER lookup or other defer +*/ + +static int +route_check_dls(uschar *rname, uschar *type, const uschar *list, + tree_node **anchorptr, unsigned int *cache_bits, int listtype, + const uschar *domloc, const uschar **ldata, BOOL caseless, uschar **perror) +{ +if (!list) return OK; /* Empty list always succeeds */ + +DEBUG(D_route) debug_printf("checking %s\n", type); + +/* The domain and local part use the same matching function, whereas sender +has its own code. */ + +switch(domloc + ? match_isinlist(domloc, &list, 0, anchorptr, cache_bits, listtype, + caseless, ldata) + : match_address_list(sender_address ? sender_address : US"", + TRUE, TRUE, &list, cache_bits, -1, 0, CUSS &sender_data) + ) + { + case OK: + return OK; + + case FAIL: + *perror = string_sprintf("%s router skipped: %s mismatch", rname, type); + DEBUG(D_route) debug_printf("%s\n", *perror); + return SKIP; + + default: /* Paranoia, and keeps compilers happy */ + case DEFER: + *perror = string_sprintf("%s check lookup or other defer", type); + DEBUG(D_route) debug_printf("%s\n", *perror); + return DEFER; + } +} + + + +/************************************************* +* Check access by a given uid/gid * +*************************************************/ + +/* This function checks whether a given uid/gid has access to a given file or +directory. It is called only from check_files() below. This is hopefully a +cheapish check that does the job most of the time. Exim does *not* rely on this +test when actually accessing any file. The test is used when routing to make it +possible to take actions such as "if user x can access file y then run this +router". + +During routing, Exim is normally running as root, and so the test will work +except for NFS non-root mounts. When verifying during message reception, Exim +is running as "exim", so the test may not work. This is a limitation of the +Exim design. + +Code in check_files() below detects the case when it cannot stat() the file (as +root), and in that situation it uses a setuid subprocess in which to run this +test. + +Arguments: + path the path to check + uid the user + gid the group + bits the bits required in the final component + +Returns: TRUE + FALSE errno=EACCES or ENOENT (or others from realpath or stat) +*/ + +static BOOL +route_check_access(uschar *path, uid_t uid, gid_t gid, int bits) +{ +struct stat statbuf; +uschar *slash; +uschar *rp = US realpath(CS path, CS big_buffer); +uschar *sp = rp + 1; + +DEBUG(D_route) debug_printf("route_check_access(%s,%d,%d,%o)\n", path, + (int)uid, (int)gid, bits); + +if (!rp) return FALSE; + +while ((slash = Ustrchr(sp, '/'))) + { + *slash = 0; + DEBUG(D_route) debug_printf("stat %s\n", rp); + if (Ustat(rp, &statbuf) < 0) return FALSE; + if ((statbuf.st_mode & + ((statbuf.st_uid == uid)? 0100 : (statbuf.st_gid == gid)? 0010 : 001) + ) == 0) + { + errno = EACCES; + return FALSE; + } + *slash = '/'; + sp = slash + 1; + } + +/* Down to the final component */ + +DEBUG(D_route) debug_printf("stat %s\n", rp); + +if (Ustat(rp, &statbuf) < 0) return FALSE; + +if (statbuf.st_uid == uid) bits = bits << 6; + else if (statbuf.st_gid == gid) bits = bits << 3; +if ((statbuf.st_mode & bits) != bits) + { + errno = EACCES; + return FALSE; + } + +DEBUG(D_route) debug_printf("route_check_access() succeeded\n"); +return TRUE; +} + + + +/************************************************* +* Do file existence tests * +*************************************************/ + +/* This function is given a colon-separated list of file tests, each of which +is expanded before use. A test consists of a file name, optionally preceded by +! (require non-existence) and/or + for handling permission denied (+ means +treat as non-existing). + +An item that contains no slashes is interpreted as a username or id, with an +optional group id, for checking access to the file. This cannot be done +"perfectly", but it is good enough for a number of applications. + +Arguments: + s a colon-separated list of file tests or NULL + perror a pointer to an anchor for an error text in the case of a DEFER + +Returns: OK if s == NULL or all tests are as required + DEFER if the existence of at least one of the files is + unclear (an error other than non-existence occurred); + DEFER if an expansion failed + DEFER if a name is not absolute + DEFER if problems with user/group + SKIP otherwise +*/ + +static int +check_files(const uschar *s, uschar **perror) +{ +int sep = 0; /* List has default separators */ +uid_t uid = 0; /* For picky compilers */ +gid_t gid = 0; /* For picky compilers */ +BOOL ugid_set = FALSE; +const uschar *listptr; +uschar *check; + +if (!s) return OK; + +DEBUG(D_route) debug_printf("checking require_files\n"); + +listptr = s; +while ((check = string_nextinlist(&listptr, &sep, NULL, 0))) + { + int rc; + int eacces_code = 0; + BOOL invert = FALSE; + struct stat statbuf; + uschar *ss = expand_string(check); + + if (!ss) + { + if (f.expand_string_forcedfail) continue; + *perror = string_sprintf("failed to expand \"%s\" for require_files: %s", + check, expand_string_message); + goto RETURN_DEFER; + } + + /* Empty items are just skipped */ + + if (*ss == 0) continue; + + /* If there are no slashes in the string, we have a user name or uid, with + optional group/gid. */ + + if (Ustrchr(ss, '/') == NULL) + { + BOOL ok; + struct passwd *pw; + uschar *comma = Ustrchr(ss, ','); + + /* If there's a comma, temporarily terminate the user name/number + at that point. Then set the uid. */ + + if (comma != NULL) *comma = 0; + ok = route_finduser(ss, &pw, &uid); + if (comma != NULL) *comma = ','; + + if (!ok) + { + *perror = string_sprintf("user \"%s\" for require_files not found", ss); + goto RETURN_DEFER; + } + + /* If there was no comma, the gid is that associated with the user. */ + + if (comma == NULL) + { + if (pw != NULL) gid = pw->pw_gid; else + { + *perror = string_sprintf("group missing after numerical uid %d for " + "require_files", (int)uid); + goto RETURN_DEFER; + } + } + else + { + if (!route_findgroup(comma + 1, &gid)) + { + *perror = string_sprintf("group \"%s\" for require_files not found\n", + comma + 1); + goto RETURN_DEFER; + } + } + + /* Note that we have values set, and proceed to next item */ + + DEBUG(D_route) + debug_printf("check subsequent files for access by %s\n", ss); + ugid_set = TRUE; + continue; + } + + /* Path, possibly preceded by + and ! */ + + if (*ss == '+') + { + eacces_code = 1; + while (isspace((*(++ss)))); + } + + if (*ss == '!') + { + invert = TRUE; + while (isspace((*(++ss)))); + } + + if (*ss != '/') + { + *perror = string_sprintf("require_files: \"%s\" is not absolute", ss); + goto RETURN_DEFER; + } + + /* Stat the file, either as root (while routing) or as exim (while verifying + during message reception). */ + + rc = Ustat(ss, &statbuf); + + DEBUG(D_route) + { + debug_printf("file check: %s\n", check); + if (ss != check) debug_printf("expanded file: %s\n", ss); + debug_printf("stat() yielded %d\n", rc); + } + + /* If permission is denied, and we are running as root (i.e. routing for + delivery rather than verifying), and the requirement is to test for access by + a particular uid/gid, it must mean that the file is on a non-root-mounted NFS + system. In this case, we have to use a subprocess that runs as the relevant + uid in order to do the test. */ + + if (rc != 0 && errno == EACCES && ugid_set && getuid() == root_uid) + { + int status; + pid_t pid; + void (*oldsignal)(int); + + DEBUG(D_route) debug_printf("root is denied access: forking to check " + "in subprocess\n"); + + /* Before forking, ensure that SIGCHLD is set to SIG_DFL before forking, so + that the child process can be waited for, just in case get here with it set + otherwise. Save the old state for resetting on the wait. */ + + oldsignal = signal(SIGCHLD, SIG_DFL); + pid = exim_fork(US"require-files"); + + /* If fork() fails, reinstate the original error and behave as if + this block of code were not present. This is the same behaviour as happens + when Exim is not running as root at this point. */ + + if (pid < 0) + { + DEBUG(D_route) + debug_printf("require_files: fork failed: %s\n", strerror(errno)); + errno = EACCES; + goto HANDLE_ERROR; + } + + /* In the child process, change uid and gid, and then do the check using + the route_check_access() function. This does more than just stat the file; + it tests permissions as well. Return 0 for OK and 1 for failure. */ + + if (pid == 0) + { + exim_setugid(uid, gid, TRUE, + string_sprintf("require_files check, file=%s", ss)); + if (route_check_access(ss, uid, gid, 4)) + exim_underbar_exit(EXIT_SUCCESS); + DEBUG(D_route) debug_printf("route_check_access() failed\n"); + exim_underbar_exit(EXIT_FAILURE); + } + + /* In the parent, wait for the child to finish */ + + while (waitpid(pid, &status, 0) < 0) + if (errno != EINTR) /* unexpected error, interpret as failure */ + { + status = 1; + break; + } + + signal(SIGCHLD, oldsignal); /* restore */ + if ((status == 0) == invert) return SKIP; + continue; /* to test the next file */ + } + + /* Control reaches here if the initial stat() succeeds, or fails with an + error other than EACCES, or no uid/gid is set, or we are not running as root. + If we know the file exists and uid/gid are set, try to check read access for + that uid/gid as best we can. */ + + if (rc == 0 && ugid_set && !route_check_access(ss, uid, gid, 4)) + { + DEBUG(D_route) debug_printf("route_check_access() failed\n"); + rc = -1; + } + + /* Handle error returns from stat() or route_check_access(). The EACCES error + is handled specially. At present, we can force it to be treated as + non-existence. Write the code so that it will be easy to add forcing for + existence if required later. */ + + HANDLE_ERROR: + if (rc < 0) + { + DEBUG(D_route) debug_printf("errno = %d\n", errno); + if (errno == EACCES) + { + if (eacces_code == 1) + { + DEBUG(D_route) debug_printf("EACCES => ENOENT\n"); + errno = ENOENT; /* Treat as non-existent */ + } + } + if (errno != ENOENT) + { + *perror = string_sprintf("require_files: error for %s: %s", ss, + strerror(errno)); + goto RETURN_DEFER; + } + } + + /* At this point, rc < 0 => non-existence; rc >= 0 => existence */ + + if ((rc >= 0) == invert) return SKIP; + } + +return OK; + +/* Come here on any of the errors that return DEFER. */ + +RETURN_DEFER: +DEBUG(D_route) debug_printf("%s\n", *perror); +return DEFER; +} + + + + + +/************************************************* +* Check for router skipping * +*************************************************/ + +/* This function performs various checks to see whether a router should be +skipped. The order in which they are performed is important. + +Arguments: + r pointer to router instance block + addr address that is being handled + verify the verification type + pw ptr to ptr to passwd structure for local user + perror for lookup errors + +Returns: OK if all the tests succeed + SKIP if router is to be skipped + DEFER for a lookup defer + FAIL for address to be failed +*/ + +static BOOL +check_router_conditions(router_instance *r, address_item *addr, int verify, + struct passwd **pw, uschar **perror) +{ +int rc; +uschar *check_local_part; +unsigned int *localpart_cache; + +/* Reset variables to hold a home directory and data from lookup of a domain or +local part, and ensure search_find_defer is unset, in case there aren't any +actual lookups. */ + +deliver_home = NULL; +deliver_domain_data = NULL; +deliver_localpart_data = NULL; +sender_data = NULL; +local_user_gid = (gid_t)(-1); +local_user_uid = (uid_t)(-1); +f.search_find_defer = FALSE; + +/* Skip this router if not verifying and it has verify_only set */ + +if ((verify == v_none || verify == v_expn) && r->verify_only) + { + DEBUG(D_route) debug_printf("%s router skipped: verify_only set\n", r->name); + return SKIP; + } + +/* Skip this router if testing an address (-bt) and address_test is not set */ + +if (f.address_test_mode && !r->address_test) + { + DEBUG(D_route) debug_printf("%s router skipped: address_test is unset\n", + r->name); + return SKIP; + } + +/* Skip this router if verifying and it hasn't got the appropriate verify flag +set. */ + +if ((verify == v_sender && !r->verify_sender) || + (verify == v_recipient && !r->verify_recipient)) + { + DEBUG(D_route) debug_printf("%s router skipped: verify %d %d %d\n", + r->name, verify, r->verify_sender, r->verify_recipient); + return SKIP; + } + +/* Skip this router if processing EXPN and it doesn't have expn set */ + +if (verify == v_expn && !r->expn) + { + DEBUG(D_route) debug_printf("%s router skipped: no_expn set\n", r->name); + return SKIP; + } + +/* Skip this router if there's a domain mismatch. */ + +if ((rc = route_check_dls(r->name, US"domains", r->domains, &domainlist_anchor, + addr->domain_cache, TRUE, addr->domain, CUSS &deliver_domain_data, + MCL_DOMAIN, perror)) != OK) + return rc; + +/* Skip this router if there's a local part mismatch. We want to pass over the +caseful local part, so that +caseful can restore it, even if this router is +handling local parts caselessly. However, we can't just pass cc_local_part, +because that doesn't have the prefix or suffix stripped. A bit of massaging is +required. Also, we only use the match cache for local parts that have not had +a prefix or suffix stripped. */ + +if (!addr->prefix && !addr->suffix) + { + localpart_cache = addr->localpart_cache; + check_local_part = addr->cc_local_part; + } +else + { + localpart_cache = NULL; + check_local_part = string_copy(addr->cc_local_part); + if (addr->prefix) + check_local_part += Ustrlen(addr->prefix); + if (addr->suffix) + check_local_part[Ustrlen(check_local_part) - Ustrlen(addr->suffix)] = 0; + } + +if ((rc = route_check_dls(r->name, US"local_parts", r->local_parts, + &localpartlist_anchor, localpart_cache, MCL_LOCALPART, + check_local_part, CUSS &deliver_localpart_data, + !r->caseful_local_part, perror)) != OK) + return rc; + +/* If the check_local_user option is set, check that the local_part is the +login of a local user. Note: the third argument to route_finduser() must be +NULL here, to prevent a numeric string being taken as a numeric uid. If the +user is found, set deliver_home to the home directory, and also set +local_user_{uid,gid} and local_part_data. */ + +if (r->check_local_user) + { + DEBUG(D_route) debug_printf("checking for local user\n"); + if (!route_finduser(addr->local_part, pw, NULL)) + { + DEBUG(D_route) debug_printf("%s router skipped: %s is not a local user\n", + r->name, addr->local_part); + return SKIP; + } + addr->prop.localpart_data = + deliver_localpart_data = string_copy(US (*pw)->pw_name); + deliver_home = string_copy(US (*pw)->pw_dir); + local_user_gid = (*pw)->pw_gid; + local_user_uid = (*pw)->pw_uid; + } + +/* Set (or override in the case of check_local_user) the home directory if +router_home_directory is set. This is done here so that it overrides $home from +check_local_user before any subsequent expansions are done. Otherwise, $home +could mean different things for different options, which would be extremely +confusing. */ + +if (r->router_home_directory) + { + uschar * router_home = expand_string(r->router_home_directory); + if (router_home) + { + setflag(addr, af_home_expanded); /* Note set from router_home_directory */ + deliver_home = router_home; + } + else if (!f.expand_string_forcedfail) + { + *perror = string_sprintf("failed to expand \"%s\" for " + "router_home_directory: %s", r->router_home_directory, + expand_string_message); + return DEFER; + } + } + +/* Skip if the sender condition is not met. We leave this one till after the +local user check so that $home is set - enabling the possibility of letting +individual recipients specify lists of acceptable/unacceptable senders. */ + +if ((rc = route_check_dls(r->name, US"senders", r->senders, NULL, + sender_address_cache, MCL_ADDRESS, NULL, NULL, FALSE, perror)) != OK) + return rc; + +/* This is the point at which we print out the router's debugging string if it +is set. We wait till here so as to have $home available for local users (and +anyway, we don't want too much stuff for skipped routers). */ + +debug_print_string(r->debug_string); + +/* Perform file existence tests. */ + +if ((rc = check_files(r->require_files, perror)) != OK) + { + DEBUG(D_route) debug_printf("%s router %s: file check\n", r->name, + (rc == SKIP)? "skipped" : "deferred"); + return rc; + } + +/* Now the general condition test. */ + +if (r->condition) + { + DEBUG(D_route) debug_printf("checking \"condition\" \"%.80s\"...\n", r->condition); + if (!expand_check_condition(r->condition, r->name, US"router")) + { + if (f.search_find_defer) + { + *perror = US"condition check lookup defer"; + DEBUG(D_route) debug_printf("%s\n", *perror); + return DEFER; + } + DEBUG(D_route) + debug_printf("%s router skipped: condition failure\n", r->name); + return SKIP; + } + } + +#ifdef EXPERIMENTAL_BRIGHTMAIL +/* check if a specific Brightmail AntiSpam rule fired on the message */ +if (r->bmi_rule) + { + DEBUG(D_route) debug_printf("checking bmi_rule\n"); + if (bmi_check_rule(bmi_base64_verdict, r->bmi_rule) == 0) + { /* none of the rules fired */ + DEBUG(D_route) + debug_printf("%s router skipped: none of bmi_rule rules fired\n", r->name); + return SKIP; + } + } + +/* check if message should not be delivered */ +if (r->bmi_dont_deliver && bmi_deliver == 1) + { + DEBUG(D_route) + debug_printf("%s router skipped: bmi_dont_deliver is FALSE\n", r->name); + return SKIP; + } + +/* check if message should go to an alternate location */ +if ( r->bmi_deliver_alternate + && (bmi_deliver == 0 || !bmi_alt_location) + ) + { + DEBUG(D_route) + debug_printf("%s router skipped: bmi_deliver_alternate is FALSE\n", r->name); + return SKIP; + } + +/* check if message should go to default location */ +if ( r->bmi_deliver_default + && (bmi_deliver == 0 || bmi_alt_location) + ) + { + DEBUG(D_route) + debug_printf("%s router skipped: bmi_deliver_default is FALSE\n", r->name); + return SKIP; + } +#endif + +/* All the checks passed. */ + +return OK; +} + + + + +/************************************************* +* Find a local user * +*************************************************/ + +/* Try several times (if configured) to find a local user, in case delays in +NIS or NFS whatever cause an incorrect refusal. It's a pity that getpwnam() +doesn't have some kind of indication as to why it has failed. If the string +given consists entirely of digits, and the third argument is not NULL, assume +the string is the numerical value of the uid. Otherwise it is looked up using +getpwnam(). The uid is passed back via return_uid, if not NULL, and the +pointer to a passwd structure, if found, is passed back via pw, if not NULL. + +Because this may be called several times in succession for the same user for +different routers, cache the result of the previous getpwnam call so that it +can be re-used. Note that we can't just copy the structure, as the store it +points to can get trashed. + +Arguments: + s the login name or textual form of the numerical uid of the user + pw if not NULL, return the result of getpwnam here, or set NULL + if no call to getpwnam is made (s numeric, return_uid != NULL) + return_uid if not NULL, return the uid via this address + +Returns: TRUE if s is numerical or was looked up successfully + +*/ + +static struct passwd pwcopy; +static struct passwd *lastpw = NULL; +static uschar lastname[48] = { 0 }; +static uschar lastdir[128]; +static uschar lastgecos[128]; +static uschar lastshell[128]; + +BOOL +route_finduser(const uschar *s, struct passwd **pw, uid_t *return_uid) +{ +BOOL cache_set = (Ustrcmp(lastname, s) == 0); + +DEBUG(D_uid) debug_printf("seeking password data for user \"%s\": %s\n", s, + cache_set ? "using cached result" : "cache not available"); + +if (!cache_set) + { + int i = 0; + + if (return_uid && (isdigit(*s) || *s == '-') && + s[Ustrspn(s+1, "0123456789")+1] == 0) + { + *return_uid = (uid_t)Uatoi(s); + if (pw) *pw = NULL; + return TRUE; + } + + string_format_nt(lastname, sizeof(lastname), "%s", s); + + /* Force failure if string length is greater than given maximum */ + + if (max_username_length > 0 && Ustrlen(lastname) > max_username_length) + { + DEBUG(D_uid) debug_printf("forced failure of finduser(): string " + "length of %s is greater than %d\n", lastname, max_username_length); + lastpw = NULL; + } + + /* Try a few times if so configured; this handles delays in NIS etc. */ + + else for (;;) + { + errno = 0; + if ((lastpw = getpwnam(CS s))) break; + if (++i > finduser_retries) break; + sleep(1); + } + + if (lastpw) + { + pwcopy.pw_uid = lastpw->pw_uid; + pwcopy.pw_gid = lastpw->pw_gid; + (void)string_format(lastdir, sizeof(lastdir), "%s", lastpw->pw_dir); + (void)string_format(lastgecos, sizeof(lastgecos), "%s", lastpw->pw_gecos); + (void)string_format(lastshell, sizeof(lastshell), "%s", lastpw->pw_shell); + pwcopy.pw_name = CS lastname; + pwcopy.pw_dir = CS lastdir; + pwcopy.pw_gecos = CS lastgecos; + pwcopy.pw_shell = CS lastshell; + lastpw = &pwcopy; + } + + else DEBUG(D_uid) if (errno != 0) + debug_printf("getpwnam(%s) failed: %s\n", s, strerror(errno)); + } + +if (!lastpw) + { + DEBUG(D_uid) debug_printf("getpwnam() returned NULL (user not found)\n"); + return FALSE; + } + +DEBUG(D_uid) debug_printf("getpwnam() succeeded uid=%d gid=%d\n", + lastpw->pw_uid, lastpw->pw_gid); + +if (return_uid) *return_uid = lastpw->pw_uid; +if (pw) *pw = lastpw; + +return TRUE; +} + + + + +/************************************************* +* Find a local group * +*************************************************/ + +/* Try several times (if configured) to find a local group, in case delays in +NIS or NFS whatever cause an incorrect refusal. It's a pity that getgrnam() +doesn't have some kind of indication as to why it has failed. + +Arguments: + s the group name or textual form of the numerical gid + return_gid return the gid via this address + +Returns: TRUE if the group was found; FALSE otherwise + +*/ + +BOOL +route_findgroup(uschar *s, gid_t *return_gid) +{ +int i = 0; +struct group *gr; + +if ((isdigit(*s) || *s == '-') && s[Ustrspn(s+1, "0123456789")+1] == 0) + { + *return_gid = (gid_t)Uatoi(s); + return TRUE; + } + +for (;;) + { + if ((gr = getgrnam(CS s))) + { + *return_gid = gr->gr_gid; + return TRUE; + } + if (++i > finduser_retries) break; + sleep(1); + } + +return FALSE; +} + + + + +/************************************************* +* Find user by expanding string * +*************************************************/ + +/* Expands a string, and then looks up the result in the passwd file. + +Arguments: + string the string to be expanded, yielding a login name or a numerical + uid value (to be passed to route_finduser()) + driver_name caller name for panic error message (only) + driver_type caller type for panic error message (only) + pw return passwd entry via this pointer + uid return uid via this pointer + errmsg where to point a message on failure + +Returns: TRUE if user found, FALSE otherwise +*/ + +BOOL +route_find_expanded_user(uschar *string, uschar *driver_name, + uschar *driver_type, struct passwd **pw, uid_t *uid, uschar **errmsg) +{ +uschar *user = expand_string(string); + +if (!user) + { + *errmsg = string_sprintf("Failed to expand user string \"%s\" for the " + "%s %s: %s", string, driver_name, driver_type, expand_string_message); + log_write(0, LOG_MAIN|LOG_PANIC, "%s", *errmsg); + return FALSE; + } + +if (route_finduser(user, pw, uid)) return TRUE; + +*errmsg = string_sprintf("Failed to find user \"%s\" from expanded string " + "\"%s\" for the %s %s", user, string, driver_name, driver_type); +log_write(0, LOG_MAIN|LOG_PANIC, "%s", *errmsg); +return FALSE; +} + + + +/************************************************* +* Find group by expanding string * +*************************************************/ + +/* Expands a string and then looks up the result in the group file. + +Arguments: + string the string to be expanded, yielding a group name or a numerical + gid value (to be passed to route_findgroup()) + driver_name caller name for panic error message (only) + driver_type caller type for panic error message (only) + gid return gid via this pointer + errmsg return error message via this pointer + +Returns: TRUE if found group, FALSE otherwise +*/ + +BOOL +route_find_expanded_group(uschar *string, uschar *driver_name, uschar *driver_type, + gid_t *gid, uschar **errmsg) +{ +BOOL yield = TRUE; +uschar *group = expand_string(string); + +if (!group) + { + *errmsg = string_sprintf("Failed to expand group string \"%s\" for the " + "%s %s: %s", string, driver_name, driver_type, expand_string_message); + log_write(0, LOG_MAIN|LOG_PANIC, "%s", *errmsg); + return FALSE; + } + +if (!route_findgroup(group, gid)) + { + *errmsg = string_sprintf("Failed to find group \"%s\" from expanded string " + "\"%s\" for the %s %s", group, string, driver_name, driver_type); + log_write(0, LOG_MAIN|LOG_PANIC, "%s", *errmsg); + yield = FALSE; + } + +return yield; +} + + + +/************************************************* +* Handle an unseen routing * +*************************************************/ + +/* This function is called when an address is routed by a router with "unseen" +set. It must make a clone of the address, for handling by subsequent drivers. +The clone is set to start routing at the next router. + +The original address must be replaced by an invented "parent" which has the +routed address plus the clone as its children. This is necessary in case the +address is at the top level - we don't want to mark it complete until both +deliveries have been done. + +A new unique field must be made, so that the record of the delivery isn't a +record of the original address, and checking for already delivered has +therefore to be done here. If the delivery has happened, then take the base +address off whichever delivery queue it is on - it will always be the top item. + +Arguments: + name router name + addr address that was routed + paddr_local chain of local-delivery addresses + paddr_remote chain of remote-delivery addresses + addr_new chain for newly created addresses + +Returns: nothing +*/ + +static void +route_unseen(uschar *name, address_item *addr, address_item **paddr_local, + address_item **paddr_remote, address_item **addr_new) +{ +address_item *parent = deliver_make_addr(addr->address, TRUE); +address_item *new = deliver_make_addr(addr->address, TRUE); + +/* The invented parent is a copy that replaces the original; note that +this copies its parent pointer. It has two children, and its errors_address is +from the original address' parent, if present, otherwise unset. */ + +*parent = *addr; +parent->child_count = 2; +parent->prop.errors_address = + addr->parent ? addr->parent->prop.errors_address : NULL; + +/* The routed address gets a new parent. */ + +addr->parent = parent; + +/* The clone has this parent too. Set its errors address from the parent. This +was set from the original parent (or to NULL) - see above. We do NOT want to +take the errors address from the unseen router. */ + +new->parent = parent; +new->prop.errors_address = parent->prop.errors_address; + +/* Copy the propagated flags and address_data from the original. */ + +new->prop.ignore_error = addr->prop.ignore_error; +new->prop.address_data = addr->prop.address_data; +new->prop.variables = NULL; +tree_dup((tree_node **)&new->prop.variables, addr->prop.variables); +new->dsn_flags = addr->dsn_flags; +new->dsn_orcpt = addr->dsn_orcpt; + + +/* As it has turned out, we haven't set headers_add or headers_remove for the + * clone. Thinking about it, it isn't entirely clear whether they should be + * copied from the original parent, like errors_address, or taken from the + * unseen router, like address_data and the flags. Until somebody brings this + * up, I propose to leave the code as it is. + */ + + +/* Set the cloned address to start at the next router, and put it onto the +chain of new addresses. */ + +new->start_router = addr->router->next; +new->next = *addr_new; +*addr_new = new; + +DEBUG(D_route) debug_printf("\"unseen\" set: replicated %s\n", addr->address); + +/* Make a new unique field, to distinguish from the normal one. */ + +addr->unique = string_sprintf("%s/%s", addr->unique, name); + +/* If the address has been routed to a transport, see if it was previously +delivered. If so, we take it off the relevant queue so that it isn't delivered +again. Otherwise, it was an alias or something, and the addresses it generated +are handled in the normal way. */ + +if (addr->transport && tree_search(tree_nonrecipients, addr->unique)) + { + DEBUG(D_route) + debug_printf("\"unseen\" delivery previously done - discarded\n"); + parent->child_count--; + if (*paddr_remote == addr) *paddr_remote = addr->next; + if (*paddr_local == addr) *paddr_local = addr->next; + } +} + + + +/************************************************/ +/* Add router-assigned variables +Return OK/DEFER/FAIL/PASS */ + +static int +set_router_vars(address_item * addr, const router_instance * r) +{ +const uschar * varlist = r->set; +tree_node ** root = (tree_node **) &addr->prop.variables; +int sep = ';'; + +if (!varlist) return OK; + +/* Walk the varlist, creating variables */ + +for (uschar * ele; (ele = string_nextinlist(&varlist, &sep, NULL, 0)); ) + { + const uschar * assignment = ele; + int esep = '='; + uschar * name = string_nextinlist(&assignment, &esep, NULL, 0); + uschar * val; + tree_node * node; + + /* Variable name must exist and start "r_". */ + + if (!name || name[0] != 'r' || name[1] != '_' || !name[2]) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "bad router variable name '%s' in router '%s'\n", name, r->name); + return FAIL; + } + name += 2; + + while (isspace(*assignment)) assignment++; + + if (!(val = expand_string(US assignment))) + if (f.expand_string_forcedfail) + { + int yield; + BOOL more; + DEBUG(D_route) debug_printf("forced failure in expansion of \"%s\" " + "(router variable): decline action taken\n", ele); + + /* Expand "more" if necessary; DEFER => an expansion failed */ + + yield = exp_bool(addr, US"router", r->name, D_route, + US"more", r->more, r->expand_more, &more); + if (yield != OK) return yield; + + if (!more) + { + DEBUG(D_route) + debug_printf("\"more\"=false: skipping remaining routers\n"); + router_name = NULL; + r = NULL; + return FAIL; + } + return PASS; + } + else + { + addr->message = string_sprintf("expansion of \"%s\" failed " + "in %s router: %s", ele, r->name, expand_string_message); + return DEFER; + } + + if (!(node = tree_search(*root, name))) + { /* name should never be tainted */ + node = store_get(sizeof(tree_node) + Ustrlen(name), GET_UNTAINTED); + Ustrcpy(node->name, name); + (void)tree_insertnode(root, node); + } + node->data.ptr = US val; + DEBUG(D_route) debug_printf("set r_%s%s = '%s'%s\n", + name, is_tainted(name)?" (tainted)":"", + val, is_tainted(val)?" (tainted)":""); + + /* All expansions after this point need visibility of that variable */ + router_var = *root; + } +return OK; +} + + +/************************************************* +* Route one address * +*************************************************/ + +/* This function is passed in one address item, for processing by the routers. +The verify flag is set if this is being called for verification rather than +delivery. If the router doesn't have its "verify" flag set, it is skipped. + +Arguments: + addr address to route + paddr_local chain of local-delivery addresses + paddr_remote chain of remote-delivery addresses + addr_new chain for newly created addresses + addr_succeed chain for completed addresses + verify v_none if not verifying + v_sender if verifying a sender address + v_recipient if verifying a recipient address + v_expn if processing an EXPN address + +Returns: OK => address successfully routed + DISCARD => address was discarded + FAIL => address could not be routed + DEFER => some temporary problem + ERROR => some major internal or configuration failure +*/ + +int +route_address(address_item *addr, address_item **paddr_local, + address_item **paddr_remote, address_item **addr_new, + address_item **addr_succeed, int verify) +{ +int yield = OK; +BOOL unseen; +router_instance *r, *nextr; +const uschar *old_domain = addr->domain; + +HDEBUG(D_route) + { + debug_printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + debug_printf("routing %s\n", addr->address); + } + +/* Loop through all router instances until a router succeeds, fails, defers, or +encounters an error. If the address has start_router set, we begin from there +instead of at the first router. */ + +for (r = addr->start_router ? addr->start_router : routers; r; r = nextr) + { + uschar *error; + struct passwd *pw = NULL; + struct passwd pwcopy; + BOOL loop_detected = FALSE; + BOOL more; + int loopcount = 0; + int rc; + + DEBUG(D_route) debug_printf("--------> %s router <--------\n", r->name); + + /* Reset any search error message from the previous router. */ + + search_error_message = NULL; + + /* There are some weird cases where logging is disabled */ + + f.disable_logging = r->disable_logging; + + /* Record the last router to handle the address, and set the default + next router. */ + + addr->router = r; + nextr = r->next; + + /* Loop protection: If this address has an ancestor with the same address, + and that ancestor was routed by this router, we skip this router. This + prevents a variety of looping states when a new address is created by + redirection or by the use of "unseen" on a router. + + If no_repeat_use is set on the router, we skip if _any_ ancestor was routed + by this router, even if it was different to the current address. + + Just in case someone does put it into a loop (possible with redirection + continually adding to an address, for example), put a long stop counter on + the number of parents. */ + + for (address_item * parent = addr->parent; parent; parent = parent->parent) + { + if (parent->router == r) + { + BOOL break_loop = !r->repeat_use; + + /* When repeat_use is set, first check the active addresses caselessly. + If they match, we have to do a further caseful check of the local parts + when caseful_local_part is set. This is assumed to be rare, which is why + the code is written this way. */ + + if (!break_loop) + { + break_loop = strcmpic(parent->address, addr->address) == 0; + if (break_loop && r->caseful_local_part) + break_loop = Ustrncmp(parent->address, addr->address, + Ustrrchr(addr->address, '@') - addr->address) == 0; + } + + if (break_loop) + { + DEBUG(D_route) debug_printf("%s router skipped: previously routed %s\n", + r->name, parent->address); + loop_detected = TRUE; + break; + } + } + + /* Continue with parents, limiting the size of the dynasty. */ + + if (loopcount++ > 100) + { + log_write(0, LOG_MAIN|LOG_PANIC, "routing loop for %s", addr->address); + yield = DEFER; + goto ROUTE_EXIT; + } + } + + if (loop_detected) continue; + + /* Default no affixes and select whether to use a caseful or caseless local + part in this router. */ + + addr->prefix = addr->prefix_v = addr->suffix = addr->suffix_v = NULL; + addr->local_part = r->caseful_local_part + ? addr->cc_local_part : addr->lc_local_part; + + DEBUG(D_route) debug_printf("local_part=%s domain=%s\n", addr->local_part, + addr->domain); + + /* Handle any configured prefix by replacing the local_part address, + and setting the prefix. Skip the router if the prefix doesn't match, + unless the prefix is optional. */ + + if (r->prefix) + { + unsigned vlen; + int plen = route_check_prefix(addr->local_part, r->prefix, &vlen); + if (plen > 0) + { + /* If the variable-part is zero-length then the prefix was not + wildcarded and we can detaint-copy it since it matches the + (non-expandable) router option. Otherwise copy the (likely) tainted match + and the variable-part of the match from the local_part. */ + + if (vlen) + { + addr->prefix = string_copyn(addr->local_part, plen); + addr->prefix_v = string_copyn(addr->local_part, vlen); + } + else + addr->prefix = string_copyn_taint(addr->local_part, plen, GET_UNTAINTED); + addr->local_part += plen; + DEBUG(D_route) debug_printf("stripped prefix %s\n", addr->prefix); + } + else if (!r->prefix_optional) + { + DEBUG(D_route) debug_printf("%s router skipped: prefix mismatch\n", + r->name); + continue; + } + } + + /* Handle any configured suffix likewise. */ + + if (r->suffix) + { + unsigned vlen; + int slen = route_check_suffix(addr->local_part, r->suffix, &vlen); + if (slen > 0) + { + int lplen = Ustrlen(addr->local_part) - slen; + addr->suffix = vlen + ? addr->local_part + lplen + : string_copy_taint(addr->local_part + lplen, GET_UNTAINTED); + addr->suffix_v = addr->suffix + Ustrlen(addr->suffix) - vlen; + addr->local_part = string_copyn(addr->local_part, lplen); + DEBUG(D_route) debug_printf("stripped suffix %s\n", addr->suffix); + } + else if (!r->suffix_optional) + { + DEBUG(D_route) debug_printf("%s router skipped: suffix mismatch\n", + r->name); + continue; + } + } + + /* Set the expansion variables now that we have the affixes and the case of + the local part sorted. */ + + router_name = r->name; + driver_srcfile = r->srcfile; + driver_srcline = r->srcline; + deliver_set_expansions(addr); + + /* For convenience, the pre-router checks are in a separate function, which + returns OK, SKIP, FAIL, or DEFER. */ + + if ((rc = check_router_conditions(r, addr, verify, &pw, &error)) != OK) + { + driver_srcfile = router_name = NULL; driver_srcline = 0; + if (rc == SKIP) continue; + addr->message = error; + yield = rc; + goto ROUTE_EXIT; + } + + /* All pre-conditions have been met. Reset any search error message from + pre-condition tests. These can arise in negated tests where the failure of + the lookup leads to a TRUE pre-condition. */ + + search_error_message = NULL; + + /* Add any variable-settings that are on the router, to the set on the + addr. Expansion is done here and not later when the addr is used. There may + be multiple settings, gathered during readconf; this code gathers them during + router traversal. On the addr string they are held as a variable tree, so + as to maintain the post-expansion taints separate. */ + + switch (set_router_vars(addr, r)) + { + case OK: break; + case PASS: continue; /* with next router */ + default: goto ROUTE_EXIT; + } + + /* Finally, expand the address_data field in the router. Forced failure + behaves as if the router declined. Any other failure is more serious. On + success, the string is attached to the address for all subsequent processing. + */ + + if (r->address_data) + { + DEBUG(D_route) debug_printf("processing address_data\n"); + if (!(deliver_address_data = expand_string(r->address_data))) + { + if (f.expand_string_forcedfail) + { + DEBUG(D_route) debug_printf("forced failure in expansion of \"%s\" " + "(address_data): decline action taken\n", r->address_data); + + /* Expand "more" if necessary; DEFER => an expansion failed */ + + yield = exp_bool(addr, US"router", r->name, D_route, + US"more", r->more, r->expand_more, &more); + if (yield != OK) goto ROUTE_EXIT; + + if (!more) + { + DEBUG(D_route) + debug_printf("\"more\"=false: skipping remaining routers\n"); + driver_srcfile = router_name = NULL; driver_srcline = 0; + r = NULL; + break; + } + else continue; /* With next router */ + } + + else + { + addr->message = string_sprintf("expansion of \"%s\" failed " + "in %s router: %s", r->address_data, r->name, expand_string_message); + yield = DEFER; + goto ROUTE_EXIT; + } + } + addr->prop.address_data = deliver_address_data; + } + + /* We are finally cleared for take-off with this router. Clear the the flag + that records that a local host was removed from a routed host list. Make a + copy of relevant fields in the password information from check_local_user, + because it will be overwritten if check_local_user is invoked again while + verifying an errors_address setting. */ + + clearflag(addr, af_local_host_removed); + + if (pw) + { + pwcopy.pw_name = CS string_copy(US pw->pw_name); + pwcopy.pw_uid = pw->pw_uid; + pwcopy.pw_gid = pw->pw_gid; + pwcopy.pw_gecos = CS string_copy(US pw->pw_gecos); + pwcopy.pw_dir = CS string_copy(US pw->pw_dir); + pwcopy.pw_shell = CS string_copy(US pw->pw_shell); + pw = &pwcopy; + } + + /* If this should be the last hop for DSN flag the addr. */ + + if (r->dsn_lasthop && !(addr->dsn_flags & rf_dsnlasthop)) + { + addr->dsn_flags |= rf_dsnlasthop; + HDEBUG(D_route) debug_printf("DSN: last hop for %s\n", addr->address); + } + + /* Run the router, and handle the consequences. */ + + HDEBUG(D_route) debug_printf("calling %s router\n", r->name); + + yield = (r->info->code)(r, addr, pw, verify, paddr_local, paddr_remote, + addr_new, addr_succeed); + + driver_srcfile = router_name = NULL; driver_srcline = 0; + + if (yield == FAIL) + { + HDEBUG(D_route) debug_printf("%s router forced address failure\n", r->name); + goto ROUTE_EXIT; + } + + /* If succeeded while verifying but fail_verify is set, convert into + a failure, and take it off the local or remote delivery list. */ + + if ( ( verify == v_sender && r->fail_verify_sender + || verify == v_recipient && r->fail_verify_recipient + ) + && (yield == OK || yield == PASS)) + { + addr->message = string_sprintf("%s router forced verify failure", r->name); + if (*paddr_remote == addr) *paddr_remote = addr->next; + if (*paddr_local == addr) *paddr_local = addr->next; + yield = FAIL; + goto ROUTE_EXIT; + } + + /* PASS and DECLINE are the only two cases where the loop continues. For all + other returns, we break the loop and handle the result below. */ + + if (yield != PASS && yield != DECLINE) break; + + HDEBUG(D_route) + { + debug_printf("%s router %s for %s\n", r->name, + yield == PASS ? "passed" : "declined", addr->address); + if (Ustrcmp(old_domain, addr->domain) != 0) + debug_printf("domain %s rewritten\n", old_domain); + } + + /* PASS always continues to another router; DECLINE does so if "more" + is true. Initialization insists that pass_router is always a following + router. Otherwise, break the loop as if at the end of the routers. */ + + if (yield == PASS) + { + if (r->pass_router != NULL) nextr = r->pass_router; + } + else + { + /* Expand "more" if necessary */ + + yield = exp_bool(addr, US"router", r->name, D_route, + US"more", r->more, r->expand_more, &more); + if (yield != OK) goto ROUTE_EXIT; + + if (!more) + { + HDEBUG(D_route) + debug_printf("\"more\" is false: skipping remaining routers\n"); + r = NULL; + break; + } + } + } /* Loop for all routers */ + +/* On exit from the routers loop, if r == NULL we have run out of routers, +either genuinely, or as a result of no_more. Otherwise, the loop ended +prematurely, either because a router succeeded, or because of some special +router response. Note that FAIL errors and errors detected before actually +running a router go direct to ROUTE_EXIT from code above. */ + +if (!r) + { + HDEBUG(D_route) debug_printf("no more routers\n"); + if (!addr->message) + { + uschar *message = US"Unrouteable address"; + if (addr->router && addr->router->cannot_route_message) + { + uschar *expmessage = expand_string(addr->router->cannot_route_message); + if (!expmessage) + { + if (!f.expand_string_forcedfail) + log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand " + "cannot_route_message in %s router: %s", addr->router->name, + expand_string_message); + } + else message = expmessage; + } + addr->user_message = addr->message = message; + } + addr->router = NULL; /* For logging */ + yield = FAIL; + goto ROUTE_EXIT; + } + +if (yield == DEFER) + { + HDEBUG(D_route) debug_printf("%s router: defer for %s\n message: %s\n", + r->name, addr->address, addr->message ? addr->message : US""); + goto ROUTE_EXIT; + } + +if (yield == DISCARD) goto ROUTE_EXIT; + +/* The yield must be either OK or REROUTED. */ + +if (yield != OK && yield != REROUTED) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s router returned unknown value %d", + r->name, yield); + +/* If the yield was REROUTED, the router put a child address on the new chain +as a result of a domain change of some sort (widening, typically). */ + +if (yield == REROUTED) + { + HDEBUG(D_route) debug_printf("re-routed to %s\n", addr->address); + yield = OK; + goto ROUTE_EXIT; + } + +/* The only remaining possibility is that the router succeeded. If the +translate_ip_address options is set and host addresses were associated with the +address, run them through the translation. This feature is for weird and +wonderful situations (the amateur packet radio people need it) or very broken +networking, so it is included in the binary only if requested. */ + +#ifdef SUPPORT_TRANSLATE_IP_ADDRESS + +if (r->translate_ip_address) + { + int rc; + int old_pool = store_pool; + for (host_item * h = addr->host_list; h; h = h->next) + { + uschar *newaddress; + uschar *oldaddress, *oldname; + + if (!h->address) continue; + + deliver_host_address = h->address; + newaddress = expand_string(r->translate_ip_address); + deliver_host_address = NULL; + + if (!newaddress) + { + if (f.expand_string_forcedfail) continue; + addr->basic_errno = ERRNO_EXPANDFAIL; + addr->message = string_sprintf("translate_ip_address expansion " + "failed: %s", expand_string_message); + yield = DEFER; + goto ROUTE_EXIT; + } + + DEBUG(D_route) debug_printf("%s [%s] translated to %s\n", + h->name, h->address, newaddress); + if (string_is_ip_address(newaddress, NULL) != 0) + { + h->address = newaddress; + continue; + } + + oldname = h->name; + oldaddress = h->address; + h->name = newaddress; + h->address = NULL; + h->mx = MX_NONE; + + store_pool = POOL_PERM; + rc = host_find_byname(h, NULL, HOST_FIND_QUALIFY_SINGLE, NULL, TRUE); + store_pool = old_pool; + + if (rc == HOST_FIND_FAILED || rc == HOST_FIND_AGAIN) + { + addr->basic_errno = ERRNO_UNKNOWNHOST; + addr->message = string_sprintf("host %s not found when " + "translating %s [%s]", h->name, oldname, oldaddress); + yield = DEFER; + goto ROUTE_EXIT; + } + } + } +#endif /* SUPPORT_TRANSLATE_IP_ADDRESS */ + +/* See if this is an unseen routing; first expand the option if necessary. +DEFER can be given if the expansion fails */ + +yield = exp_bool(addr, US"router", r->name, D_route, + US"unseen", r->unseen, r->expand_unseen, &unseen); +if (yield != OK) goto ROUTE_EXIT; + +/* Debugging output recording a successful routing */ + +HDEBUG(D_route) debug_printf("routed by %s router%s\n", r->name, + unseen? " (unseen)" : ""); + +DEBUG(D_route) + { + debug_printf(" envelope to: %s\n", addr->address); + debug_printf(" transport: %s\n", addr->transport + ? addr->transport->name : US""); + + if (addr->prop.errors_address) + debug_printf(" errors to %s\n", addr->prop.errors_address); + + for (host_item * h = addr->host_list; h; h = h->next) + { + debug_printf(" host %s", h->name); + if (h->address) debug_printf(" [%s]", h->address); + if (h->mx >= 0) debug_printf(" MX=%d", h->mx); + else if (h->mx != MX_NONE) debug_printf(" rgroup=%d", h->mx); + if (h->port != PORT_NONE) debug_printf(" port=%d", h->port); + if (h->dnssec != DS_UNK) debug_printf(" dnssec=%s", h->dnssec==DS_YES ? "yes" : "no"); + debug_printf("\n"); + } + } + +/* Clear any temporary error message set by a router that declined, and handle +the "unseen" option (ignore if there are no further routers). */ + +addr->message = NULL; +if (unseen && r->next) + route_unseen(r->name, addr, paddr_local, paddr_remote, addr_new); + +/* Unset the address expansions, and return the final result. */ + +ROUTE_EXIT: +if (yield == DEFER && addr->message) + addr->message = expand_hide_passwords(addr->message); + +deliver_set_expansions(NULL); +driver_srcfile = router_name = NULL; driver_srcline = 0; +f.disable_logging = FALSE; +return yield; +} + + + +/* For error messages, a string describing the config location associated +with current processing. NULL if we are not in a router. */ +/* Name only, for now */ + +uschar * +router_current_name(void) +{ +if (!router_name) return NULL; +return string_sprintf(" (router %s, %s %d)", router_name, driver_srcfile, driver_srcline); +} + +#endif /*!MACRO_PREDEF*/ +/* End of route.c */ -- cgit v1.2.3