diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:44:07 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:44:07 +0000 |
commit | 39ce00b8d520cbecbd6af87257e8fb11df0ec273 (patch) | |
tree | 4c21a2674c19e5c44be3b3550b476b9e63d8ae3d /src/rda.c | |
parent | Initial commit. (diff) | |
download | exim4-upstream/4.94.2.tar.xz exim4-upstream/4.94.2.zip |
Adding upstream version 4.94.2.upstream/4.94.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/rda.c | 999 |
1 files changed, 999 insertions, 0 deletions
diff --git a/src/rda.c b/src/rda.c new file mode 100644 index 0000000..ce6e7a3 --- /dev/null +++ b/src/rda.c @@ -0,0 +1,999 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This module contains code for extracting addresses from a forwarding list +(from an alias or forward file) or by running the filter interpreter. It may do +this in a sub-process if a uid/gid are supplied. */ + + +#include "exim.h" + +enum { FILE_EXIST, FILE_NOT_EXIST, FILE_EXIST_UNCLEAR }; + +#define REPLY_EXISTS 0x01 +#define REPLY_EXPAND 0x02 +#define REPLY_RETURN 0x04 + + +/************************************************* +* Check string for filter program * +*************************************************/ + +/* This function checks whether a string is actually a filter program. The rule +is that it must start with "# Exim filter ..." (any capitalization, spaces +optional). It is envisaged that in future, other kinds of filter may be +implemented. That's why it is implemented the way it is. The function is global +because it is also called from filter.c when checking filters. + +Argument: the string + +Returns: FILTER_EXIM if it starts with "# Exim filter" + FILTER_SIEVE if it starts with "# Sieve filter" + FILTER_FORWARD otherwise +*/ + +/* This is an auxiliary function for matching a tag. */ + +static BOOL +match_tag(const uschar *s, const uschar *tag) +{ +for (; *tag; s++, tag++) + if (*tag == ' ') + { + while (*s == ' ' || *s == '\t') s++; + s--; + } + else + if (tolower(*s) != tolower(*tag)) break; + +return (*tag == 0); +} + +/* This is the real function. It should be easy to add checking different +tags for other types of filter. */ + +int +rda_is_filter(const uschar *s) +{ +Uskip_whitespace(&s); /* Skips initial blank lines */ +if (match_tag(s, CUS"# exim filter")) return FILTER_EXIM; +else if (match_tag(s, CUS"# sieve filter")) return FILTER_SIEVE; +else return FILTER_FORWARD; +} + + + + +/************************************************* +* Check for existence of file * +*************************************************/ + +/* First of all, we stat the file. If this fails, we try to stat the enclosing +directory, because a file in an unmounted NFS directory will look the same as a +non-existent file. It seems that in Solaris 2.6, statting an entry in an +indirect map that is currently unmounted does not cause the mount to happen. +Instead, dummy data is returned, which defeats the whole point of this test. +However, if a stat() is done on some object inside the directory, such as the +"." back reference to itself, then the mount does occur. If an NFS host is +taken offline, it is possible for the stat() to get stuck until it comes back. +To guard against this, stick a timer round it. If we can't access the "." +inside the directory, try the plain directory, just in case that helps. + +Argument: + filename the file name + error for message on error + +Returns: FILE_EXIST the file exists + FILE_NOT_EXIST the file does not exist + FILE_EXIST_UNCLEAR cannot determine existence +*/ + +static int +rda_exists(uschar *filename, uschar **error) +{ +int rc, saved_errno; +struct stat statbuf; +uschar * s; + +if ((rc = Ustat(filename, &statbuf)) >= 0) return FILE_EXIST; +saved_errno = errno; + +s = string_copy(filename); +sigalrm_seen = FALSE; + +if (saved_errno == ENOENT) + { + uschar * slash = Ustrrchr(s, '/'); + Ustrcpy(slash+1, US"."); + + ALARM(30); + rc = Ustat(s, &statbuf); + if (rc != 0 && errno == EACCES && !sigalrm_seen) + { + *slash = 0; + rc = Ustat(s, &statbuf); + } + saved_errno = errno; + ALARM_CLR(0); + + DEBUG(D_route) debug_printf("stat(%s)=%d\n", s, rc); + } + +if (sigalrm_seen || rc != 0) + { + *error = string_sprintf("failed to stat %s (%s)", s, + sigalrm_seen? "timeout" : strerror(saved_errno)); + return FILE_EXIST_UNCLEAR; + } + +*error = string_sprintf("%s does not exist", filename); +DEBUG(D_route) debug_printf("%s\n", *error); +return FILE_NOT_EXIST; +} + + + +/************************************************* +* Get forwarding list from a file * +*************************************************/ + +/* Open a file and read its entire contents into a block of memory. Certain +opening errors are optionally treated the same as "file does not exist". + +ENOTDIR means that something along the line is not a directory: there are +installations that set home directories to be /dev/null for non-login accounts +but in normal circumstances this indicates some kind of configuration error. + +EACCES means there's a permissions failure. Some users turn off read permission +on a .forward file to suspend forwarding, but this is probably an error in any +kind of mailing list processing. + +The redirect block that contains the file name also contains constraints such +as who may own the file, and mode bits that must not be set. This function is + +Arguments: + rdata rdirect block, containing file name and constraints + options for the RDO_ENOTDIR and RDO_EACCES options + error where to put an error message + yield what to return from rda_interpret on error + +Returns: pointer to string in store; NULL on error +*/ + +static uschar * +rda_get_file_contents(redirect_block *rdata, int options, uschar **error, + int *yield) +{ +FILE *fwd; +uschar *filebuf; +uschar *filename = rdata->string; +BOOL uid_ok = !rdata->check_owner; +BOOL gid_ok = !rdata->check_group; +struct stat statbuf; + +/* Reading a file is a form of expansion; we wish to deny attackers the +capability to specify the file name. */ + +if (is_tainted(filename)) + { + *error = string_sprintf("Tainted name '%s' for file read not permitted\n", + filename); + *yield = FF_ERROR; + return NULL; + } + +/* Attempt to open the file. If it appears not to exist, check up on the +containing directory by statting it. If the directory does not exist, we treat +this situation as an error (which will cause delivery to defer); otherwise we +pass back FF_NONEXIST, which causes the redirect router to decline. + +However, if the ignore_enotdir option is set (to ignore "something on the +path is not a directory" errors), the right behaviour seems to be not to do the +directory test. */ + +if (!(fwd = Ufopen(filename, "rb"))) switch(errno) + { + case ENOENT: /* File does not exist */ + DEBUG(D_route) debug_printf("%s does not exist\n%schecking parent directory\n", + filename, options & RDO_ENOTDIR ? "ignore_enotdir set => skip " : ""); + *yield = + options & RDO_ENOTDIR || rda_exists(filename, error) == FILE_NOT_EXIST + ? FF_NONEXIST : FF_ERROR; + return NULL; + + case ENOTDIR: /* Something on the path isn't a directory */ + if (!(options & RDO_ENOTDIR)) goto DEFAULT_ERROR; + DEBUG(D_route) debug_printf("non-directory on path %s: file assumed not to " + "exist\n", filename); + *yield = FF_NONEXIST; + return NULL; + + case EACCES: /* Permission denied */ + if (!(options & RDO_EACCES)) goto DEFAULT_ERROR; + DEBUG(D_route) debug_printf("permission denied for %s: file assumed not to " + "exist\n", filename); + *yield = FF_NONEXIST; + return NULL; + +DEFAULT_ERROR: + default: + *error = string_open_failed(errno, "%s", filename); + *yield = FF_ERROR; + return NULL; + } + +/* Check that we have a regular file. */ + +if (fstat(fileno(fwd), &statbuf) != 0) + { + *error = string_sprintf("failed to stat %s: %s", filename, strerror(errno)); + goto ERROR_RETURN; + } + +if ((statbuf.st_mode & S_IFMT) != S_IFREG) + { + *error = string_sprintf("%s is not a regular file", filename); + goto ERROR_RETURN; + } + +/* Check for unwanted mode bits */ + +if ((statbuf.st_mode & rdata->modemask) != 0) + { + *error = string_sprintf("bad mode (0%o) for %s: 0%o bit(s) unexpected", + statbuf.st_mode, filename, statbuf.st_mode & rdata->modemask); + goto ERROR_RETURN; + } + +/* Check the file owner and file group if required to do so. */ + +if (!uid_ok) + if (rdata->pw && statbuf.st_uid == rdata->pw->pw_uid) + uid_ok = TRUE; + else if (rdata->owners) + for (int i = 1; i <= (int)(rdata->owners[0]); i++) + if (rdata->owners[i] == statbuf.st_uid) { uid_ok = TRUE; break; } + +if (!gid_ok) + if (rdata->pw && statbuf.st_gid == rdata->pw->pw_gid) + gid_ok = TRUE; + else if (rdata->owngroups) + for (int i = 1; i <= (int)(rdata->owngroups[0]); i++) + if (rdata->owngroups[i] == statbuf.st_gid) { gid_ok = TRUE; break; } + +if (!uid_ok || !gid_ok) + { + *error = string_sprintf("bad %s for %s", uid_ok? "group" : "owner", filename); + goto ERROR_RETURN; + } + +/* Put an upper limit on the size of the file, just to stop silly people +feeding in ridiculously large files, which can easily be created by making +files that have holes in them. */ + +if (statbuf.st_size > MAX_FILTER_SIZE) + { + *error = string_sprintf("%s is too big (max %d)", filename, MAX_FILTER_SIZE); + goto ERROR_RETURN; + } + +/* Read the file in one go in order to minimize the time we have it open. */ + +filebuf = store_get(statbuf.st_size + 1, is_tainted(filename)); + +if (fread(filebuf, 1, statbuf.st_size, fwd) != statbuf.st_size) + { + *error = string_sprintf("error while reading %s: %s", + filename, strerror(errno)); + goto ERROR_RETURN; + } +filebuf[statbuf.st_size] = 0; + +DEBUG(D_route) debug_printf(OFF_T_FMT " %sbytes read from %s\n", + statbuf.st_size, is_tainted(filename) ? "(tainted) " : "", filename); + +(void)fclose(fwd); +return filebuf; + +/* Return an error: the string is already set up. */ + +ERROR_RETURN: +*yield = FF_ERROR; +(void)fclose(fwd); +return NULL; +} + + + +/************************************************* +* Extract info from list or filter * +*************************************************/ + +/* This function calls the appropriate function to extract addresses from a +forwarding list, or to run a filter file and get addresses from there. + +Arguments: + rdata the redirection block + options the options bits + include_directory restrain to this directory + sieve_vacation_directory passed to sieve_interpret + sieve_enotify_mailto_owner passed to sieve_interpret + sieve_useraddress passed to sieve_interpret + sieve_subaddress passed to sieve_interpret + generated where to hang generated addresses + error for error messages + eblockp for details of skipped syntax errors + (NULL => no skip) + filtertype set to the filter type: + FILTER_FORWARD => a traditional .forward file + FILTER_EXIM => an Exim filter file + FILTER_SIEVE => a Sieve filter file + a system filter is always forced to be FILTER_EXIM + +Returns: a suitable return for rda_interpret() +*/ + +static int +rda_extract(redirect_block *rdata, int options, uschar *include_directory, + uschar *sieve_vacation_directory, uschar *sieve_enotify_mailto_owner, + uschar *sieve_useraddress, uschar *sieve_subaddress, + address_item **generated, uschar **error, error_block **eblockp, + int *filtertype) +{ +uschar *data; + +if (rdata->isfile) + { + int yield = 0; + if (!(data = rda_get_file_contents(rdata, options, error, &yield))) + return yield; + } +else data = rdata->string; + +*filtertype = f.system_filtering ? FILTER_EXIM : rda_is_filter(data); + +/* Filter interpretation is done by a general function that is also called from +the filter testing option (-bf). There are two versions: one for Exim filtering +and one for Sieve filtering. Several features of string expansion may be locked +out at sites that don't trust users. This is done by setting flags in +expand_forbid that the expander inspects. */ + +if (*filtertype != FILTER_FORWARD) + { + int frc; + int old_expand_forbid = expand_forbid; + + DEBUG(D_route) debug_printf("data is %s filter program\n", + *filtertype == FILTER_EXIM ? "an Exim" : "a Sieve"); + + /* RDO_FILTER is an "allow" bit */ + + if (!(options & RDO_FILTER)) + { + *error = US"filtering not enabled"; + return FF_ERROR; + } + + expand_forbid = + expand_forbid & ~RDO_FILTER_EXPANSIONS | options & RDO_FILTER_EXPANSIONS; + + /* RDO_{EXIM,SIEVE}_FILTER are forbid bits */ + + if (*filtertype == FILTER_EXIM) + { + if ((options & RDO_EXIM_FILTER) != 0) + { + *error = US"Exim filtering not enabled"; + return FF_ERROR; + } + frc = filter_interpret(data, options, generated, error); + } + else + { + if (options & RDO_SIEVE_FILTER) + { + *error = US"Sieve filtering not enabled"; + return FF_ERROR; + } + frc = sieve_interpret(data, options, sieve_vacation_directory, + sieve_enotify_mailto_owner, sieve_useraddress, sieve_subaddress, + generated, error); + } + + expand_forbid = old_expand_forbid; + return frc; + } + +/* Not a filter script */ + +DEBUG(D_route) debug_printf("file is not a filter file\n"); + +return parse_forward_list(data, + options, /* specials that are allowed */ + generated, /* where to hang them */ + error, /* for errors */ + deliver_domain, /* to qualify \name */ + include_directory, /* restrain to directory */ + eblockp); /* for skipped syntax errors */ +} + + + + +/************************************************* +* Write string down pipe * +*************************************************/ + +/* This function is used for transferring a string down a pipe between +processes. If the pointer is NULL, a length of zero is written. + +Arguments: + fd the pipe + s the string + +Returns: -1 on error, else 0 +*/ + +static int +rda_write_string(int fd, const uschar *s) +{ +int len = (s == NULL)? 0 : Ustrlen(s) + 1; +return ( write(fd, &len, sizeof(int)) != sizeof(int) + || (s != NULL && write(fd, s, len) != len) + ) + ? -1 : 0; +} + + + +/************************************************* +* Read string from pipe * +*************************************************/ + +/* This function is used for receiving a string from a pipe. + +Arguments: + fd the pipe + sp where to put the string + +Returns: FALSE if data missing +*/ + +static BOOL +rda_read_string(int fd, uschar **sp) +{ +int len; + +if (read(fd, &len, sizeof(int)) != sizeof(int)) return FALSE; +if (len == 0) + *sp = NULL; +else + /* We know we have enough memory so disable the error on "len" */ + /* coverity[tainted_data] */ + /* We trust the data source, so untainted */ + if (read(fd, *sp = store_get(len, FALSE), len) != len) return FALSE; +return TRUE; +} + + + +/************************************************* +* Interpret forward list or filter * +*************************************************/ + +/* This function is passed a forward list string (unexpanded) or the name of a +file (unexpanded) whose contents are the forwarding list. The list may in fact +be a filter program if it starts with "#Exim filter" or "#Sieve filter". Other +types of filter, with different initial tag strings, may be introduced in due +course. + +The job of the function is to process the forwarding list or filter. It is +pulled out into this separate function, because it is used for system filter +files as well as from the redirect router. + +If the function is given a uid/gid, it runs a subprocess that passes the +results back via a pipe. This provides security for things like :include:s in +users' .forward files, and "logwrite" calls in users' filter files. A +sub-process is NOT used when: + + . No uid/gid is provided + . The input is a string which is not a filter string, and does not contain + :include: + . The input is a file whose non-existence can be detected in the main + process (which is usually running as root). + +Arguments: + rdata redirect data (file + constraints, or data string) + options options to pass to the extraction functions, + plus ENOTDIR and EACCES handling bits + include_directory restrain :include: to this directory + sieve_vacation_directory directory passed to sieve_interpret + sieve_enotify_mailto_owner passed to sieve_interpret + sieve_useraddress passed to sieve_interpret + sieve_subaddress passed to sieve_interpret + ugid uid/gid to run under - if NULL, no change + generated where to hang generated addresses, initially NULL + error pointer for error message + eblockp for skipped syntax errors; NULL if no skipping + filtertype set to the type of file: + FILTER_FORWARD => traditional .forward file + FILTER_EXIM => an Exim filter file + FILTER_SIEVE => a Sieve filter file + a system filter is always forced to be FILTER_EXIM + rname router name for error messages in the format + "xxx router" or "system filter" + +Returns: values from extraction function, or FF_NONEXIST: + FF_DELIVERED success, a significant action was taken + FF_NOTDELIVERED success, no significant action + FF_BLACKHOLE :blackhole: + FF_DEFER defer requested + FF_FAIL fail requested + FF_INCLUDEFAIL some problem with :include: + FF_FREEZE freeze requested + FF_ERROR there was a problem + FF_NONEXIST the file does not exist +*/ + +int +rda_interpret(redirect_block *rdata, int options, uschar *include_directory, + uschar *sieve_vacation_directory, uschar *sieve_enotify_mailto_owner, + uschar *sieve_useraddress, uschar *sieve_subaddress, ugid_block *ugid, + address_item **generated, uschar **error, error_block **eblockp, + int *filtertype, uschar *rname) +{ +int fd, rc, pfd[2]; +int yield, status; +BOOL had_disaster = FALSE; +pid_t pid; +uschar *data; +uschar *readerror = US""; +void (*oldsignal)(int); + +DEBUG(D_route) debug_printf("rda_interpret (%s): '%s'\n", + rdata->isfile ? "file" : "string", string_printing(rdata->string)); + +/* Do the expansions of the file name or data first, while still privileged. */ + +if (!(data = expand_string(rdata->string))) + { + if (f.expand_string_forcedfail) return FF_NOTDELIVERED; + *error = string_sprintf("failed to expand \"%s\": %s", rdata->string, + expand_string_message); + return FF_ERROR; + } +rdata->string = data; + +DEBUG(D_route) + debug_printf("expanded: '%s'%s\n", data, is_tainted(data) ? " (tainted)":""); + +if (rdata->isfile && data[0] != '/') + { + *error = string_sprintf("\"%s\" is not an absolute path", data); + return FF_ERROR; + } + +/* If no uid/gid are supplied, or if we have a data string which does not start +with #Exim filter or #Sieve filter, and does not contain :include:, do all the +work in this process. Note that for a system filter, we always have a file, so +the work is done in this process only if no user is supplied. */ + +if (!ugid->uid_set || /* Either there's no uid, or */ + (!rdata->isfile && /* We've got the data, and */ + rda_is_filter(data) == FILTER_FORWARD && /* It's not a filter script, */ + Ustrstr(data, ":include:") == NULL)) /* and there's no :include: */ + return rda_extract(rdata, options, include_directory, + sieve_vacation_directory, sieve_enotify_mailto_owner, sieve_useraddress, + sieve_subaddress, generated, error, eblockp, filtertype); + +/* We need to run the processing code in a sub-process. However, if we can +determine the non-existence of a file first, we can decline without having to +create the sub-process. */ + +if (rdata->isfile && rda_exists(data, error) == FILE_NOT_EXIST) + return FF_NONEXIST; + +/* If the file does exist, or we can't tell (non-root mounted NFS directory) +we have to create the subprocess to do everything as the given user. The +results of processing are passed back via a pipe. */ + +if (pipe(pfd) != 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "creation of pipe for filter or " + ":include: failed for %s: %s", rname, strerror(errno)); + +/* Ensure that SIGCHLD is set to SIG_DFL before forking, so that the child +process can be waited for. We sometimes get here with it set otherwise. Save +the old state for resetting on the wait. Ensure that all cached resources are +freed so that the subprocess starts with a clean slate and doesn't interfere +with the parent process. */ + +oldsignal = signal(SIGCHLD, SIG_DFL); +search_tidyup(); + +if ((pid = exim_fork(US"router-interpret")) == 0) + { + header_line *waslast = header_last; /* Save last header */ + int fd_flags = -1; + + fd = pfd[pipe_write]; + (void)close(pfd[pipe_read]); + + if ((fd_flags = fcntl(fd, F_GETFD)) == -1) goto bad; + if (fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC) == -1) goto bad; + + exim_setugid(ugid->uid, ugid->gid, FALSE, rname); + + /* Addresses can get rewritten in filters; if we are not root or the exim + user (and we probably are not), turn off rewrite logging, because we cannot + write to the log now. */ + + if (ugid->uid != root_uid && ugid->uid != exim_uid) + { + DEBUG(D_rewrite) debug_printf("turned off address rewrite logging (not " + "root or exim in this process)\n"); + BIT_CLEAR(log_selector, log_selector_size, Li_address_rewrite); + } + + /* Now do the business */ + + yield = rda_extract(rdata, options, include_directory, + sieve_vacation_directory, sieve_enotify_mailto_owner, sieve_useraddress, + sieve_subaddress, generated, error, eblockp, filtertype); + + /* Pass back whether it was a filter, and the return code and any overall + error text via the pipe. */ + + if ( write(fd, filtertype, sizeof(int)) != sizeof(int) + || write(fd, &yield, sizeof(int)) != sizeof(int) + || rda_write_string(fd, *error) != 0 + ) + goto bad; + + /* Pass back the contents of any syntax error blocks if we have a pointer */ + + if (eblockp) + { + for (error_block * ep = *eblockp; ep; ep = ep->next) + if ( rda_write_string(fd, ep->text1) != 0 + || rda_write_string(fd, ep->text2) != 0 + ) + goto bad; + if (rda_write_string(fd, NULL) != 0) /* Indicates end of eblocks */ + goto bad; + } + + /* If this is a system filter, we have to pass back the numbers of any + original header lines that were removed, and then any header lines that were + added but not subsequently removed. */ + + if (f.system_filtering) + { + int i = 0; + for (header_line * h = header_list; h != waslast->next; i++, h = h->next) + if ( h->type == htype_old + && write(fd, &i, sizeof(i)) != sizeof(i) + ) + goto bad; + + i = -1; + if (write(fd, &i, sizeof(i)) != sizeof(i)) + goto bad; + + while (waslast != header_last) + { + waslast = waslast->next; + if (waslast->type != htype_old) + if ( rda_write_string(fd, waslast->text) != 0 + || write(fd, &(waslast->type), sizeof(waslast->type)) + != sizeof(waslast->type) + ) + goto bad; + } + if (rda_write_string(fd, NULL) != 0) /* Indicates end of added headers */ + goto bad; + } + + /* Write the contents of the $n variables */ + + if (write(fd, filter_n, sizeof(filter_n)) != sizeof(filter_n)) + goto bad; + + /* If the result was DELIVERED or NOTDELIVERED, we pass back the generated + addresses, and their associated information, through the pipe. This is + just tedious, but it seems to be the only safe way. We do this also for + FAIL and FREEZE, because a filter is allowed to set up deliveries that + are honoured before freezing or failing. */ + + if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED || + yield == FF_FAIL || yield == FF_FREEZE) + { + for (address_item * addr = *generated; addr; addr = addr->next) + { + int reply_options = 0; + int ig_err = addr->prop.ignore_error ? 1 : 0; + + if ( rda_write_string(fd, addr->address) != 0 + || write(fd, &addr->mode, sizeof(addr->mode)) != sizeof(addr->mode) + || write(fd, &addr->flags, sizeof(addr->flags)) != sizeof(addr->flags) + || rda_write_string(fd, addr->prop.errors_address) != 0 + || write(fd, &ig_err, sizeof(ig_err)) != sizeof(ig_err) + ) + goto bad; + + if (addr->pipe_expandn) + for (uschar ** pp = addr->pipe_expandn; *pp; pp++) + if (rda_write_string(fd, *pp) != 0) + goto bad; + if (rda_write_string(fd, NULL) != 0) + goto bad; + + if (!addr->reply) + { + if (write(fd, &reply_options, sizeof(int)) != sizeof(int)) /* 0 means no reply */ + goto bad; + } + else + { + reply_options |= REPLY_EXISTS; + if (addr->reply->file_expand) reply_options |= REPLY_EXPAND; + if (addr->reply->return_message) reply_options |= REPLY_RETURN; + if ( write(fd, &reply_options, sizeof(int)) != sizeof(int) + || write(fd, &(addr->reply->expand_forbid), sizeof(int)) + != sizeof(int) + || write(fd, &(addr->reply->once_repeat), sizeof(time_t)) + != sizeof(time_t) + || rda_write_string(fd, addr->reply->to) != 0 + || rda_write_string(fd, addr->reply->cc) != 0 + || rda_write_string(fd, addr->reply->bcc) != 0 + || rda_write_string(fd, addr->reply->from) != 0 + || rda_write_string(fd, addr->reply->reply_to) != 0 + || rda_write_string(fd, addr->reply->subject) != 0 + || rda_write_string(fd, addr->reply->headers) != 0 + || rda_write_string(fd, addr->reply->text) != 0 + || rda_write_string(fd, addr->reply->file) != 0 + || rda_write_string(fd, addr->reply->logfile) != 0 + || rda_write_string(fd, addr->reply->oncelog) != 0 + ) + goto bad; + } + } + + if (rda_write_string(fd, NULL) != 0) /* Marks end of addresses */ + goto bad; + } + + /* OK, this process is now done. Free any cached resources. Must use _exit() + and not exit() !! */ + +out: + (void)close(fd); + search_tidyup(); + exim_underbar_exit(EXIT_SUCCESS); + +bad: + DEBUG(D_rewrite) debug_printf("rda_interpret: failed write to pipe\n"); + goto out; + } + +/* Back in the main process: panic if the fork did not succeed. */ + +if (pid < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "fork failed for %s", rname); + +/* Read the pipe to get the data from the filter/forward. Our copy of the +writing end must be closed first, as otherwise read() won't return zero on an +empty pipe. Afterwards, close the reading end. */ + +(void)close(pfd[pipe_write]); + +/* Read initial data, including yield and contents of *error */ + +fd = pfd[pipe_read]; +if (read(fd, filtertype, sizeof(int)) != sizeof(int) || + read(fd, &yield, sizeof(int)) != sizeof(int) || + !rda_read_string(fd, error)) goto DISASTER; + +/* Read the contents of any syntax error blocks if we have a pointer */ + +if (eblockp) + { + error_block *e; + for (error_block ** p = eblockp; ; p = &e->next) + { + uschar *s; + if (!rda_read_string(fd, &s)) goto DISASTER; + if (!s) break; + e = store_get(sizeof(error_block), FALSE); + e->next = NULL; + e->text1 = s; + if (!rda_read_string(fd, &s)) goto DISASTER; + e->text2 = s; + *p = e; + } + } + +/* If this is a system filter, read the identify of any original header lines +that were removed, and then read data for any new ones that were added. */ + +if (f.system_filtering) + { + int hn = 0; + header_line *h = header_list; + + for (;;) + { + int n; + if (read(fd, &n, sizeof(int)) != sizeof(int)) goto DISASTER; + if (n < 0) break; + while (hn < n) + { + hn++; + if (!(h = h->next)) goto DISASTER_NO_HEADER; + } + h->type = htype_old; + } + + for (;;) + { + uschar *s; + int type; + if (!rda_read_string(fd, &s)) goto DISASTER; + if (!s) break; + if (read(fd, &type, sizeof(type)) != sizeof(type)) goto DISASTER; + header_add(type, "%s", s); + } + } + +/* Read the values of the $n variables */ + +if (read(fd, filter_n, sizeof(filter_n)) != sizeof(filter_n)) goto DISASTER; + +/* If the yield is DELIVERED, NOTDELIVERED, FAIL, or FREEZE there may follow +addresses and data to go with them. Keep them in the same order in the +generated chain. */ + +if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED || + yield == FF_FAIL || yield == FF_FREEZE) + { + address_item **nextp = generated; + + for (;;) + { + int i, reply_options; + address_item *addr; + uschar *recipient; + uschar *expandn[EXPAND_MAXN + 2]; + + /* First string is the address; NULL => end of addresses */ + + if (!rda_read_string(fd, &recipient)) goto DISASTER; + if (!recipient) break; + + /* Hang on the end of the chain */ + + addr = deliver_make_addr(recipient, FALSE); + *nextp = addr; + nextp = &(addr->next); + + /* Next comes the mode and the flags fields */ + + if ( read(fd, &addr->mode, sizeof(addr->mode)) != sizeof(addr->mode) + || read(fd, &addr->flags, sizeof(addr->flags)) != sizeof(addr->flags) + || !rda_read_string(fd, &addr->prop.errors_address) + || read(fd, &i, sizeof(i)) != sizeof(i) + ) + goto DISASTER; + addr->prop.ignore_error = (i != 0); + + /* Next comes a possible setting for $thisaddress and any numerical + variables for pipe expansion, terminated by a NULL string. The maximum + number of numericals is EXPAND_MAXN. Note that we put filter_thisaddress + into the zeroth item in the vector - this is sorted out inside the pipe + transport. */ + + for (i = 0; i < EXPAND_MAXN + 1; i++) + { + uschar *temp; + if (!rda_read_string(fd, &temp)) goto DISASTER; + if (i == 0) filter_thisaddress = temp; /* Just in case */ + expandn[i] = temp; + if (temp == NULL) break; + } + + if (i > 0) + { + addr->pipe_expandn = store_get((i+1) * sizeof(uschar *), FALSE); + addr->pipe_expandn[i] = NULL; + while (--i >= 0) addr->pipe_expandn[i] = expandn[i]; + } + + /* Then an int containing reply options; zero => no reply data. */ + + if (read(fd, &reply_options, sizeof(int)) != sizeof(int)) goto DISASTER; + if ((reply_options & REPLY_EXISTS) != 0) + { + addr->reply = store_get(sizeof(reply_item), FALSE); + + addr->reply->file_expand = (reply_options & REPLY_EXPAND) != 0; + addr->reply->return_message = (reply_options & REPLY_RETURN) != 0; + + if (read(fd,&(addr->reply->expand_forbid),sizeof(int)) != + sizeof(int) || + read(fd,&(addr->reply->once_repeat),sizeof(time_t)) != + sizeof(time_t) || + !rda_read_string(fd, &addr->reply->to) || + !rda_read_string(fd, &addr->reply->cc) || + !rda_read_string(fd, &addr->reply->bcc) || + !rda_read_string(fd, &addr->reply->from) || + !rda_read_string(fd, &addr->reply->reply_to) || + !rda_read_string(fd, &addr->reply->subject) || + !rda_read_string(fd, &addr->reply->headers) || + !rda_read_string(fd, &addr->reply->text) || + !rda_read_string(fd, &addr->reply->file) || + !rda_read_string(fd, &addr->reply->logfile) || + !rda_read_string(fd, &addr->reply->oncelog)) + goto DISASTER; + } + } + } + +/* All data has been transferred from the sub-process. Reap it, close the +reading end of the pipe, and we are done. */ + +WAIT_EXIT: +while ((rc = wait(&status)) != pid) + if (rc < 0 && errno == ECHILD) /* Process has vanished */ + { + log_write(0, LOG_MAIN, "redirection process %d vanished unexpectedly", pid); + goto FINAL_EXIT; + } + +DEBUG(D_route) + debug_printf("rda_interpret: subprocess yield=%d error=%s\n", yield, *error); + +if (had_disaster) + { + *error = string_sprintf("internal problem in %s: failure to transfer " + "data from subprocess: status=%04x%s%s%s", rname, + status, readerror, + (*error == NULL)? US"" : US": error=", + (*error == NULL)? US"" : *error); + log_write(0, LOG_MAIN|LOG_PANIC, "%s", *error); + } +else if (status != 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "internal problem in %s: unexpected status " + "%04x from redirect subprocess (but data correctly received)", rname, + status); + } + +FINAL_EXIT: +(void)close(fd); +signal(SIGCHLD, oldsignal); /* restore */ +return yield; + + +/* Come here if the data indicates removal of a header that we can't find */ + +DISASTER_NO_HEADER: +readerror = US" readerror=bad header identifier"; +had_disaster = TRUE; +yield = FF_ERROR; +goto WAIT_EXIT; + +/* Come here is there's a shambles in transferring the data over the pipe. The +value of errno should still be set. */ + +DISASTER: +readerror = string_sprintf(" readerror='%s'", strerror(errno)); +had_disaster = TRUE; +yield = FF_ERROR; +goto WAIT_EXIT; +} + +/* End of rda.c */ |