/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 1999 */
/* See the file NOTICE for conditions of use and distribution. */


#include "../exim.h"
#include "aliasfile.h"



/* Options specific to the aliasfile director. */

optionlist aliasfile_director_options[] = {
  { "*expand_directory2_transport",  opt_stringptr|opt_hidden,
      (void *)offsetof(aliasfile_director_options_block, expand_directory2_transport) },
  { "*expand_directory_transport",  opt_stringptr|opt_hidden,
      (void *)offsetof(aliasfile_director_options_block, expand_directory_transport) },
  { "*expand_file_transport",  opt_stringptr|opt_hidden,
      (void *)offsetof(aliasfile_director_options_block, expand_file_transport) },
  { "*expand_pipe_transport",  opt_stringptr|opt_hidden,
      (void *)offsetof(aliasfile_director_options_block, expand_pipe_transport) },
  { "*partial_match", opt_int | opt_hidden,
      (void *)(offsetof(aliasfile_director_options_block, partial_match)) },
  { "check_ancestor",     opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, check_ancestor)) },
  { "current_directory",     opt_stringptr,
      (void *)(offsetof(aliasfile_director_options_block, current_dir)) },
  { "directory",          opt_stringptr | opt_hidden,
      (void *)(offsetof(aliasfile_director_options_block, home_dir)) },
  { "directory2_transport",opt_transportptr,
      (void *)(offsetof(aliasfile_director_options_block, directory2_transport)) },
  { "directory_transport",opt_transportptr,
      (void *)(offsetof(aliasfile_director_options_block, directory_transport)) },
  { "expand",             opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, expand)) },
  { "file",               opt_stringptr,
      (void *)(offsetof(aliasfile_director_options_block, file)) },
  { "file_transport",     opt_transportptr,
      (void *)(offsetof(aliasfile_director_options_block, file_transport)) },
  { "forbid_file",        opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, forbid_file)) },
  { "forbid_pipe",        opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, forbid_pipe)) },
  { "freeze_missing_include", opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, freeze_missing_include)) },
  { "home_directory",     opt_stringptr,
      (void *)(offsetof(aliasfile_director_options_block, home_dir)) },
  { "include_domain",     opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, include_domain)) },
  { "modemask",           opt_octint,
      (void *)(offsetof(aliasfile_director_options_block, modemask)) },
  { "one_time",           opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, one_time)) },
  { "optional",           opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, optional)) },
  { "owners",             opt_uidlist,
      (void *)(offsetof(aliasfile_director_options_block, owners)) },
  { "owngroups",          opt_gidlist,
      (void *)(offsetof(aliasfile_director_options_block, owngroups)) },
  { "pipe_transport",     opt_transportptr,
      (void *)(offsetof(aliasfile_director_options_block, pipe_transport)) },
  { "qualify_preserve_domain", opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, qualify_preserve_domain)) },
  { "queries",            opt_stringptr,
      (void *)(offsetof(aliasfile_director_options_block, queries)) },
  { "query",              opt_stringptr,
      (void *)(offsetof(aliasfile_director_options_block, query)) },
  { "rewrite",            opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, rewrite)) },
  { "search_type",        opt_searchtype,
      (void *)(offsetof(aliasfile_director_options_block, search_type)) },
  { "skip_syntax_errors", opt_bool,
      (void *)(offsetof(aliasfile_director_options_block, skip_syntax_errors)) },
  { "syntax_errors_text", opt_stringptr,
      (void *)(offsetof(aliasfile_director_options_block, syntax_errors_text)) },
  { "syntax_errors_to",   opt_stringptr,
      (void *)(offsetof(aliasfile_director_options_block, syntax_errors_to)) }
};

/* Size of the options list. An extern variable has to be used so that its
address can appear in the tables drtables.c. */

int aliasfile_director_options_count =
  sizeof(aliasfile_director_options)/sizeof(optionlist);

/* Default private options block for the aliasfile director. */

aliasfile_director_options_block aliasfile_director_option_defaults = {
  NULL,     /* expand_directory_transport */
  NULL,     /* expand_directory2_transport */
  NULL,     /* expand_file_transport */
  NULL,     /* expand_pipe_transport */
  NULL,     /* file */
  NULL,     /* queries */
  NULL,     /* query */
  NULL,     /* home_dir */
  NULL,     /* current_dir */
  NULL,     /* syntax_errors_to */
  NULL,     /* syntax_errors_text */
  022,      /* modemask */
  -1,       /* partial_match */
  -1,       /* search_type */
  NULL,     /* owners */
  NULL,     /* owngroups */
  FALSE,    /* include_domain */
  FALSE,    /* check_ancestor */
  FALSE,    /* expand */
  FALSE,    /* optional */
  FALSE,    /* qualify_preserve_domain */
  FALSE,    /* forbid_file */
  FALSE,    /* forbid_pipe */
  TRUE,     /* freeze_missing_include */
  FALSE,    /* one_time */
  TRUE,     /* rewrite */
  FALSE,    /* skip_syntax_errors */
  NULL,     /* directory_transport */
  NULL,     /* directory2_transport */
  NULL,     /* file_transport */
  NULL      /* pipe_transport */
};



/*************************************************
*          Initialization entry point            *
*************************************************/

/* Called for each instance, after its options have been read, to
enable consistency checks to be done, or anything else that needs
to be set up.

Argument:
  dblock       a pointer to the director instance block

Returns:       nothing
*/

void
aliasfile_director_init(director_instance *dblock)
{
aliasfile_director_options_block *ob =
  (aliasfile_director_options_block *)(dblock->options_block);

/* A search type is mandatory */

if (ob->search_type < 0)
  log_write(0, LOG_PANIC_DIE|LOG_CONFIG2, "%s director:\n  "
    "a search type option is required", dblock->name);

/* For single-key+file search types, a file name is mandatory; for other types
a query is mandatory. An absolute file name is mandatory for lsearch and dbm;
but can't check for absoluteness if the name is being looked up. */

if (!mac_islookup(ob->search_type, lookup_querystyle))
  {
  if (ob->query != NULL)
    log_write(0, LOG_PANIC_DIE|LOG_CONFIG2, "%s director:\n  "
      "\"query\" specified for a single-key search type", dblock->name);

  if (ob->file == NULL)
    log_write(0, LOG_PANIC_DIE|LOG_CONFIG2, "%s director:\n  "
      "no file name specified", dblock->name);

  if (mac_islookup(ob->search_type, lookup_absfile) &&
       ob->file[0] != '/' && ob->file[0] != '$')
    log_write(0, LOG_PANIC_DIE|LOG_CONFIG2, "%s director:\n  "
      "an absolute file path name is required for %s",
        dblock->name, lookup_list[ob->search_type].name);
  }

/* Non-single-key search type; either a single query or a list of queries must
be specified. */

else
  {
  if (ob->file != NULL)
    log_write(0, LOG_PANIC_DIE|LOG_CONFIG2, "%s director:\n  "
      "\"file\" specified for a non-single-key search type", dblock->name);

  if (ob->query == NULL && ob->queries == NULL)
    log_write(0, LOG_PANIC_DIE|LOG_CONFIG2, "%s director:\n  "
      "no query specified for %s search", dblock->name,
      lookup_list[ob->search_type].name);

  if (ob->query != NULL && ob->queries != NULL)
    log_write(0, LOG_PANIC_DIE|LOG_CONFIG2, "%s director:\n  "
      "only one of \"query\" or \"queries\" may be given", dblock->name);
  }

/* Onetime aliases can only be real addresses */

if (ob->one_time && (!ob->forbid_pipe || !ob->forbid_file))
  log_write(0, LOG_PANIC_DIE|LOG_CONFIG2, "%s director:\n  "
    "forbid_pipe and forbid_file must be set when one_time is set",
      dblock->name);
}




/*************************************************
*              Main entry point                  *
*************************************************/

/* See local README for interface description */

int
aliasfile_director_entry(
  director_instance *dblock,      /* data for this instantiation */
  address_item *addr,             /* address we are working on */
  address_item **addr_local,      /* add it to this if it's local */
  address_item **addr_remote,     /* add it to this if it's remote */
  address_item **addr_new,        /* put new addresses on here */
  address_item **addr_succeed,    /* put the old one here on success */
  BOOL verify)                    /* true if verifying */
{
aliasfile_director_options_block *ob =
  (aliasfile_director_options_block *)(dblock->options_block);
address_item *generated = NULL;
char *save_qualify_domain_recipient = qualify_domain_recipient;
char *aliastext = NULL;
char *errors_to;
char *filename;
char *query;
char *error;
void *handle;
header_line *extra_headers;
char *remove_headers;
error_block *eblock = NULL;
transport_instance *transport;
ugid_block ugid;
void *reset_point;
int yield = OK;
int  extracted, rc;

/* Perform file existence and sender verification checks. */

rc = direct_check_fsc(dblock, addr);
if (rc != OK) return rc;

/* For single-key+file search types, set the required file name and expand it.
If the expansion fails, log the incident and indicate an internal error. The
file name has already been checked for absoluteness, at initialization time,
but only if it did not start with an expansion, so we double check here. */

if (!mac_islookup(ob->search_type, lookup_querystyle))
  {
  query = (ob->include_domain)?
    string_sprintf("%s@%s", addr->local_part, addr->domain) : addr->local_part;
  filename = expand_string(ob->file);
  if (filename == NULL)
    {
    addr->message = string_sprintf("%s director failed to expand \"%s\": %s",
      dblock->name, ob->file, expand_string_message);
    if (search_find_defer) return DEFER;
    addr->special_action = SPECIAL_FREEZE;
    return ERROR;
    }

  else if (mac_islookup(ob->search_type, lookup_absfile) &&
           filename[0] != '/')
    {
    log_write(0, LOG_MAIN|LOG_PANIC, "%s director requires absolute file name "
      "for %s: %s generated from expanding %s", dblock->name,
      lookup_list[ob->search_type].name, filename, ob->file);
    addr->special_action = SPECIAL_FREEZE;
    return ERROR;
    }

  DEBUG(2)
    {
    char *pmstar = "";
    char *pmat = "";
    int pm = ob->partial_match;
    if (pm > 2000)
      {
      pmstar = "*@";
      pm -= 2048;
      }
    if (pm > 1000)
      {
      pmstar = "*";
      pm -= 1024;
      }
    debug_printf("%s director: ", dblock->name);
    if (pm >= 0) debug_printf("partial%d-", pm);
    debug_printf("%s%s%s key=%s\n  file=\"%s\"\n",
      lookup_list[ob->search_type].name, pmstar, pmat, query, filename);
    }
  }

/* For query-style lookups, expand the query and set the filename NULL.
For historical reasons, there may be just one query or a list of queries, but
not both. Initialization ensures that one and only one option is set. */

else
  {
  filename = NULL;
  query = expand_string((ob->query == NULL)? ob->queries : ob->query);
  if (query == NULL)
    {
    addr->message = string_sprintf("%s director failed to expand \"%s\": %s",
      dblock->name, (ob->query == NULL)? ob->queries : ob->query,
      expand_string_message);
    if (search_find_defer) return DEFER;
    addr->special_action = SPECIAL_FREEZE;
    return ERROR;
    }
  DEBUG(2) debug_printf("%s director: %s query=%s\n", dblock->name,
    lookup_list[ob->search_type].name, query);
  }

/* Open the file (or whatever) for searching, according to the search type
that is set. The search functions return a handle identifying the search. For
files this is a FILE * or a DBM *; for other things it is < 0. If the
optional flag is set, failure to open is not an error; we just fail to
direct. */

handle = search_open(filename, ob->search_type, ob->modemask, ob->owners,
  ob->owngroups, &error);

if (handle == NULL)
  {
  if (ob->optional && errno == ENOENT)
    {
    DEBUG(2) debug_printf("%s director skipped: file failed to open and "
      "optional flag set\n", dblock->name);
    return FAIL;
    }
  addr->basic_errno = ERRNO_BADALIAS;
  addr->message = error;

  if (errno == ERRNO_BADMODE)
    error = string_sprintf("%s (check the modemask option)", error);

  log_write(0, LOG_MAIN|LOG_PANIC, "%s director: %s", dblock->name, error);
  return ERROR;
  }

/* Now search the file (or whatever) for the entry we are interested in.
The text is returned in dynamic store. If ob->queries is set, we have
a list of queries to try. */

if (ob->queries != NULL)
  {
  char *onequery;
  for (onequery = string_nextinlist(&query, ':', big_buffer, BIG_BUFFER_SIZE);
       onequery != NULL;
       onequery = string_nextinlist(&query, ':', big_buffer, BIG_BUFFER_SIZE))
    {
    aliastext = search_find(handle, filename, onequery, ob->partial_match,
      NULL, &error);
    if (aliastext != NULL || search_find_defer) break;
    }
  }
else
  aliastext = search_find(handle, filename, query, ob->partial_match, NULL,
    &error);

/* The lookup did not succeed. Defer if it deferred; otherwise this director
fails to handle the address. */

if (aliastext == NULL)
  {
  if (search_find_defer)
    {
    addr->message = string_sprintf("%s director: search_find deferred: %s",
      dblock->name, error);
    return DEFER;
    }

  DEBUG(2) debug_printf("%s director failed for %s: %s\n", dblock->name,
    addr->local_part, error);
  return FAIL;
  }

/* If the expand option is set, pass the text through the string expander. */

if (ob->expand)
  {
  char *newtext = expand_string(aliastext);
  if (newtext == NULL)
    {
    addr->message = string_sprintf("%s director failed to expand \"%s\" "
      "(generated from local part %s): %s", dblock->name, aliastext,
      addr->local_part, expand_string_message);
    if (search_find_defer) return DEFER;
    addr->special_action = SPECIAL_FREEZE;
    return ERROR;
    }
  aliastext = newtext;
  }

/* Sort out the fixed or dynamic uid/gid */

if (!direct_get_ugid(dblock, "director", addr, &ugid)) return ERROR;

/* Set up the errors address for the children of this address. */

rc = direct_get_errors_address(addr, dblock, "director", verify, &errors_to);
if (rc != OK) return rc;

/* Set up the additional and removeable headers for the children of this
address. */

rc = direct_get_munge_headers(addr, dblock, "director", &extra_headers,
  &remove_headers);
if (rc != OK) return rc;

/* Get the transport for this director, if any. It might be expanded, or it
might be unset. */

if (!route_get_transport(dblock->transport, dblock->expand_transport,
  addr, &transport, dblock->name)) return ERROR;

/* If there is a transport specified for the director, then set up this
address to use that transport. Ignore the alias text. */

if (transport != NULL)
  {
  addr->transport = transport;

  addr->errors_address = errors_to;
  addr->director = dblock;
  addr->home_dir = ob->home_dir;
  addr->current_dir = ob->current_dir;
  addr->extra_headers = extra_headers;
  addr->remove_headers = remove_headers;

  direct_set_ugid(addr, &ugid);

  if (transport->info->local)
    {
    addr->next = *addr_local;
    *addr_local = addr;
    }

  else
    {
    addr->next = *addr_remote;
    *addr_remote = addr;
    addr->fallback_hosts = dblock->fallback_hostlist;
    }

  DEBUG(2)
    {
    debug_printf("  queued for %s transport ", transport->name);
    if (addr->uid_set) debug_printf("uid=%d ", (int)(addr->uid));
      else debug_printf("uid=unset ");
    if (addr->gid_set) debug_printf("gid=%d\n", (int)(addr->gid));
      else debug_printf("gid=unset\n");
    }

  return OK;
  }

/* If qualify_preserve_domain is set, we temporarily reset the value of
qualify_domain_recipient to the current domain, so that any unqualified
addresses get qualified with the same domain as the incoming address. */

if (ob->qualify_preserve_domain) qualify_domain_recipient = addr->domain;

/* There is a common function for use by forwarding and aliasing directors that
extracts a list of addresses from a text string. Setting the fourth-last
argument FALSE makes generating no addresses an error. However, setting the
third-last TRUE allows an address that is ":blackhole:" in the alias file to
generate nothing without causing an error. It also permits :defer: and :fail:
to occur. Setting the last argument NULL causes no checks to be made on
:include: files.

It is possible that a fair amount of store might be used, especially if there
is an :include: file. Therefore, switch to the temporary pool for the duration.
Note that addresses built by the extractor are forced into the MAIN pool. */

store_pool = POOL_TEMP;
reset_point = store_get(0);

extracted = parse_extract_addresses(aliastext, addr->domain, &generated,
  &error, FALSE, TRUE, ob->rewrite, NULL,
  ob->skip_syntax_errors? &eblock : NULL);

/* Restore the qualify domain, and reset to the main pool for the generation of
any error texts. */

qualify_domain_recipient = save_qualify_domain_recipient;
store_pool = POOL_MAIN;

/* If extraction failed, return error and freeze, unless it was a missing
include file and no_freeze_missing_include is set, or unless it was an explicit
:defer: or :fail: that was encountered. If verifying, set a flag that causes
the messages from :defer: and :fail: to be passed back to the caller - in
particular, this gets them into the response to an SMTP command. */

if (extracted != 0) switch (extracted)
  {
  case EXTRACTED_DEFER:
  addr->message = string_sprintf("forced defer: %s", error);
  if (verify && *error != 0) verify_forced_errmsg = error;
  yield = DEFER;
  break;

  case EXTRACTED_UNKNOWN:
  DEBUG(2) debug_printf("%s director failed by :unknown:\n", dblock->name);
  yield = FAIL;
  break;

  case EXTRACTED_FAIL:
  addr->message = string_sprintf("forced failure: %s", error);
  if (verify && *error != 0) verify_forced_errmsg = error;
  yield =  FORCEFAIL;
  break;

  default:
  addr->basic_errno = ERRNO_BADALIAS;
  addr->message = (filename != NULL)?
    string_sprintf("<%s> - error in alias file %s: %s", addr->orig, filename,
      error) :
    string_sprintf("<%s> - error in alias lookup: %s", addr->orig, error);
  if (extracted > 0 && !ob->freeze_missing_include) return DEFER;
  addr->special_action = SPECIAL_FREEZE;
  yield = ERROR;
  break;
  }

/* If skip_syntax_errors was set and there were syntax errors in the list,
error messages will be present in eblock. Log them, and sent them off in
a mail message if so configured. A common function is used by aliasfile
and forwardfile. */

if (yield == OK && eblock != NULL &&
    !moan_skipped_syntax_errors(
      dblock->name,                          /* For message content */
      filename,                              /* Ditto */
      eblock,                                /* Ditto */
      (verify || address_test_mode)?
        NULL : ob->syntax_errors_to,         /* Who to mail */
      generated != NULL,                     /* True if not all failed */
      ob->syntax_errors_text))               /* Custom message */
  {
  addr->special_action = SPECIAL_FREEZE;
  yield = ERROR;
  }

/* We are now through with the store in the temporary pool; if there was an
error, we are done. */

store_pool = POOL_TEMP;
store_reset(reset_point);
store_pool = POOL_MAIN;

if (yield != OK) return yield;


/* Add the new addresses to the list of new addresses, copying in the
uid, gid and permission flags for use by pipes and files, setting
the parent, and or-ing its ignore_error flag. Also record the setting
for any starting director.

If the generated address is the same as one of its ancestors, and the
check_ancestor flag is set, do not use this generated address, but replace it
with a copy of the input address. This is to cope with cases where A is aliased
to B and B has a .forward file pointing to A, though it is usually set on the
forwardfile rather than the aliasfile. We can't just pass on the old
address by returning FAIL, because it must act as a general parent for
generated addresses, and only get marked "done" when all its children are
delivered. */

while (generated != NULL)
  {
  address_item *parent;
  address_item *next = generated;

  generated = next->next;
  next->parent = addr;
  next->ignore_error |= addr->ignore_error;
  next->start_director = dblock->new;
  addr->child_count++;

  next->next = *addr_new;
  *addr_new = next;

  if (ob->one_time)
    {
    for (parent = addr; parent->parent != NULL; parent = parent->parent);
    next->onetime_parent = parent->orig;
    }

  if (ob->check_ancestor)
    {
    for (parent = addr; parent != NULL; parent = parent->parent)
      {
      if (strcmp(next->orig, parent->orig) == 0)
        {
        DEBUG(2) debug_printf("generated parent replaced by child\n");
        next->orig = string_copy(addr->orig);
        break;
        }
      }
    }

  next->errors_address = errors_to;
  next->extra_headers = extra_headers;
  next->remove_headers = remove_headers;

  if (next->pfr)
    {
    next->director = dblock;
    next->home_dir = ob->home_dir;
    next->current_dir = ob->current_dir;
    next->allow_pipe = !ob->forbid_pipe;
    next->allow_file = !ob->forbid_file;
    direct_set_ugid(next, &ugid);

    /* Aliasfile can produce only pipes or files; if the special transport
    setting for any of them is NULL, the global setting will get used later.
    If the transport setting fails, the error gets picked up at the outer
    level from the setting of basic_errno in the address. */

    if (next->orig[0] == '|')
      {
      (void)route_get_transport(ob->pipe_transport, ob->expand_pipe_transport,
        next, &(next->transport), dblock->name);
      }
    else
      {
      int len = (int)strlen(next->orig);
      if (next->orig[len-1] == '/')
        {
        if (len > 1 && next->orig[len-2] == '/' &&
            ob->directory2_transport != NULL)
          (void)route_get_transport(ob->directory2_transport,
            ob->expand_directory2_transport,
            next, &(next->transport), dblock->name);
        else
          (void)route_get_transport(ob->directory_transport,
            ob->expand_directory_transport,
            next, &(next->transport), dblock->name);
        }
      else
        {
        (void)route_get_transport(ob->file_transport, ob->expand_file_transport,
          next, &(next->transport), dblock->name);
        }
      }
    }

  DEBUG(2)
    {
    debug_printf("%s director generated %s\n  %serrors_to=%s transport=%s\n",
      dblock->name,
      next->orig,
      next->pfr? "pipe, file, or autoreply\n  " : "",
      errors_to,
      (next->transport == NULL)? "NULL" : next->transport->name);

    if (next->uid_set)
      debug_printf("  uid=%d ", (int)(next->uid));
    else
      debug_printf("  uid=unset ");

    if (next->gid_set)
      debug_printf("gid=%d ", (int)(next->gid));
    else
      debug_printf("gid=unset ");

    debug_printf("home=%s\n", next->home_dir);
    }
  }

/* If no children were generated for this address, but no error was given,
it means that there was just a :blackhole: entry in the alias file. Log
something. */

if (addr->child_count <= 0 && !verify && !address_test_mode)
  log_write(0, LOG_MAIN, "=> :blackhole: <%s> D=%s", addr->orig, dblock->name);

/* Put the original address onto the succeed queue. This ensures that any
retry item that it acquires gets processed. */

addr->next = *addr_succeed;
*addr_succeed = addr;

return OK;
}

/* End of director/aliasfile.c */
