/* $Cambridge: exim/exim-src/src/routers/manualroute.c,v 1.4 2006/02/07 11:19:02 ph10 Exp $ */

/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 2006 */
/* See the file NOTICE for conditions of use and distribution. */


#include "../exim.h"
#include "rf_functions.h"
#include "manualroute.h"


/* Options specific to the manualroute router. */

optionlist manualroute_router_options[] = {
  { "host_find_failed", opt_stringptr,
      (void *)(offsetof(manualroute_router_options_block, host_find_failed)) },
  { "hosts_randomize",  opt_bool,
      (void *)(offsetof(manualroute_router_options_block, hosts_randomize)) },
  { "route_data",       opt_stringptr,
      (void *)(offsetof(manualroute_router_options_block, route_data)) },
  { "route_list",       opt_stringptr,
      (void *)(offsetof(manualroute_router_options_block, route_list)) },
  { "same_domain_copy_routing", opt_bool|opt_public,
      (void *)(offsetof(router_instance, same_domain_copy_routing)) }
};

/* Size of the options list. An extern variable has to be used so that its
address can appear in the tables drtables.c. */

int manualroute_router_options_count =
  sizeof(manualroute_router_options)/sizeof(optionlist);

/* Default private options block for the manualroute router. */

manualroute_router_options_block manualroute_router_option_defaults = {
  hff_freeze,   /* host_find_failed code */
  FALSE,        /* hosts_randomize */
  US"freeze",   /* host_find_failed */
  NULL,         /* route_data */
  NULL          /* route_list */
};





/*************************************************
*          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. */

void
manualroute_router_init(router_instance *rblock)
{
manualroute_router_options_block *ob =
  (manualroute_router_options_block *)(rblock->options_block);

/* Host_find_failed must be a recognized word */

if      (Ustrcmp(ob->host_find_failed, "freeze") == 0)
  ob->hff_code = hff_freeze;
else if (Ustrcmp(ob->host_find_failed, "decline") == 0)
  ob->hff_code = hff_decline;
else if (Ustrcmp(ob->host_find_failed, "defer") == 0)
  ob->hff_code = hff_defer;
else if (Ustrcmp(ob->host_find_failed, "pass") == 0)
  ob->hff_code = hff_pass;
else if (Ustrcmp(ob->host_find_failed, "fail") == 0)
  ob->hff_code = hff_fail;
else
  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n  "
    "unrecognized setting for host_find_failed option", rblock->name);

/* One of route_list or route_data must be specified */

if ((ob->route_list == NULL && ob->route_data == NULL) ||
    (ob->route_list != NULL && ob->route_data != NULL))
  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n  "
    "route_list or route_data (but not both) must be specified",
    rblock->name);
}




/*************************************************
*               Parse a route item               *
*************************************************/

/* The format of a route list item is:

  <domain> [<host[list]> [<options>]]

if obtained from route_list. The domain is absent if the string came from
route_data, in which case domain==NULL. The domain and the host list may be
enclosed in quotes.

Arguments:
  s         pointer to route list item
  domain    if not NULL, where to put the domain pointer
  hostlist  where to put the host[list] pointer
  options   where to put the options pointer

Returns:    FALSE if domain expected and string is empty;
            TRUE otherwise
*/

static BOOL
parse_route_item(uschar *s, uschar **domain, uschar **hostlist,
  uschar **options)
{
while (*s != 0 && isspace(*s)) s++;

if (domain != NULL)
  {
  if (*s == 0) return FALSE;            /* missing data */
  *domain = string_dequote(&s);
  while (*s != 0 && isspace(*s)) s++;
  }

*hostlist = string_dequote(&s);
while (*s != 0 && isspace(*s)) s++;
*options = s;
return TRUE;
}



/*************************************************
*              Main entry point                  *
*************************************************/

/* The manualroute router provides a manual routing facility (surprise,
surprise). The data that defines the routing can either be set in route_data
(which means it can be found by, for example, looking up the domain in a file),
or a list of domain patterns and their corresponding data can be provided in
route_list. */

/* See local README for interface details. This router returns:

DECLINE
  . no pattern in route_list matched (route_data not set)
  . route_data was an empty string (route_list not set)
  . forced expansion failure in route_data (rf_expand_data)
  . forced expansion of host list
  . host_find_failed = decline

DEFER
  . transport not defined when needed
  . lookup defer in route_list when matching domain pattern
  . non-forced expansion failure in route_data
  . non-forced expansion failure in host list
  . unknown routing option
  . host list missing for remote transport (not verifying)
  . timeout etc on host lookup (pass_on_timeout not set)
  . verifying the errors address caused a deferment or a big disaster such
      as an expansion failure (rf_get_errors_address)
  . expanding a headers_{add,remove} string caused a deferment or another
      expansion error (rf_get_munge_headers)
  . a problem in rf_get_transport: no transport when one is needed;
      failed to expand dynamic transport; failed to find dynamic transport
  . failure to expand or find a uid/gid (rf_get_ugid via rf_queue_add)
  . host_find_failed = freeze or defer
  . self = freeze or defer

PASS
  . timeout etc on host lookup (pass_on_timeout set)
  . host_find_failed = pass
  . self = pass

REROUTED
  . self = reroute

FAIL
  . host_find_failed = fail
  . self = fail

OK
  . added address to addr_local or addr_remote, as appropriate for the
    type of transport; this includes the self="send" case.
*/

int
manualroute_router_entry(
  router_instance *rblock,        /* data for this instantiation */
  address_item *addr,             /* address we are working on */
  struct passwd *pw,              /* passwd entry after check_local_user */
  int verify,                     /* v_none/v_recipient/v_sender/v_expn */
  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 old address here on success */
{
int rc, lookup_type;
uschar *route_item = NULL;
uschar *options = NULL;
uschar *hostlist = NULL;
uschar *domain, *newhostlist, *listptr;
manualroute_router_options_block *ob =
  (manualroute_router_options_block *)(rblock->options_block);
transport_instance *transport = NULL;
BOOL individual_transport_set = FALSE;
BOOL randomize = ob->hosts_randomize;

addr_new = addr_new;           /* Keep picky compilers happy */
addr_succeed = addr_succeed;

DEBUG(D_route) debug_printf("%s router called for %s\n  domain = %s\n",
  rblock->name, addr->address, addr->domain);

/* The initialization check ensures that either route_list or route_data is
set. */

if (ob->route_list != NULL)
  {
  int sep = -(';');             /* Default is semicolon */
  listptr = ob->route_list;

  while ((route_item = string_nextinlist(&listptr, &sep, NULL, 0)) != NULL)
    {
    int rc;

    DEBUG(D_route) debug_printf("route_item = %s\n", route_item);
    if (!parse_route_item(route_item, &domain, &hostlist, &options))
      continue;     /* Ignore blank items */

    /* Check the current domain; if it matches, break the loop */

    if ((rc = match_isinlist(addr->domain, &domain, UCHAR_MAX+1,
           &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, &lookup_value)) == OK)
      break;

    /* If there was a problem doing the check, defer */

    if (rc == DEFER)
      {
      addr->message = US"lookup defer in route_list";
      return DEFER;
      }
    }

  if (route_item == NULL) return DECLINE;  /* No pattern in the list matched */
  }

/* Handle a single routing item in route_data. If it expands to an empty
string, decline. */

else
  {
  route_item = rf_expand_data(addr, ob->route_data, &rc);
  if (route_item == NULL) return rc;
  (void) parse_route_item(route_item, NULL, &hostlist, &options);
  if (hostlist[0] == 0) return DECLINE;
  }

/* Expand the hostlist item. It may then pointing to an empty string, or to a
single host or a list of hosts; options is pointing to the rest of the
routelist item, which is either empty or contains various option words. */

DEBUG(D_route) debug_printf("original list of hosts = \"%s\" options = %s\n",
  hostlist, options);

newhostlist = expand_string_copy(hostlist);
lookup_value = NULL;                        /* Finished with */
expand_nmax = -1;

/* If the expansion was forced to fail, just decline. Otherwise there is a
configuration problem. */

if (newhostlist == NULL)
  {
  if (expand_string_forcedfail) return DECLINE;
  addr->message = string_sprintf("%s router: failed to expand \"%s\": %s",
    rblock->name, hostlist, expand_string_message);
  return DEFER;
  }
else hostlist = newhostlist;

DEBUG(D_route) debug_printf("expanded list of hosts = \"%s\" options = %s\n",
  hostlist, options);

/* Set default lookup type and scan the options */

lookup_type = lk_default;

while (*options != 0)
  {
  int term;
  uschar *s = options;
  while (*options != 0 && !isspace(*options)) options++;
  term = *options;
  *options = 0;

  if (Ustrcmp(s, "randomize") == 0) randomize = TRUE;
  else if (Ustrcmp(s, "no_randomize") == 0) randomize = FALSE;
  else if (Ustrcmp(s, "byname") == 0) lookup_type = lk_byname;
  else if (Ustrcmp(s, "bydns") == 0) lookup_type = lk_bydns;
  else
    {
    transport_instance *t;
    for (t = transports; t != NULL; t = t->next)
      {
      if (Ustrcmp(t->name, s) == 0)
        {
        transport = t;
        individual_transport_set = TRUE;
        break;
        }
      }
    if (t == NULL)
      {
      s = string_sprintf("unknown routing option or transport name \"%s\"", s);
      log_write(0, LOG_MAIN, "Error in %s router: %s", rblock->name, s);
      addr->message = string_sprintf("error in router: %s", s);
      return DEFER;
      }
    }

  if (term != 0)
    {
    options++;
    while (*options != 0 && isspace(*options)) options++;
    }
  }

/* Set up the errors address, if any. */

rc = rf_get_errors_address(addr, rblock, verify, &(addr->p.errors_address));
if (rc != OK) return rc;

/* Set up the additional and removeable headers for this address. */

rc = rf_get_munge_headers(addr, rblock, &(addr->p.extra_headers),
  &(addr->p.remove_headers));
if (rc != OK) return rc;

/* If an individual transport is not set, get the transport for this router, if
any. It might be expanded, or it might be unset if this router has verify_only
set. */

if (!individual_transport_set)
  {
  if (!rf_get_transport(rblock->transport_name, &(rblock->transport), addr,
      rblock->name, NULL))
    return DEFER;
  transport = rblock->transport;
  }

/* Deal with the case of a local transport. The host list is passed over as a
single text string that ends up in $host. */

if (transport != NULL && transport->info->local)
  {
  if (hostlist[0] != 0)
    {
    host_item *h;
    addr->host_list = h = store_get(sizeof(host_item));
    h->name = string_copy(hostlist);
    h->address = NULL;
    h->port = PORT_NONE;
    h->mx = MX_NONE;
    h->status = hstatus_unknown;
    h->why = hwhy_unknown;
    h->last_try = 0;
    h->next = NULL;
    }

  /* There is nothing more to do other than to queue the address for the
  local transport, filling in any uid/gid. This can be done by the common
  rf_queue_add() function. */

  addr->transport = transport;
  return rf_queue_add(addr, addr_local, addr_remote, rblock, pw)?
    OK : DEFER;
  }

/* There is either no transport (verify_only) or a remote transport. A host
list is mandatory in either case, except when verifying, in which case the
address is just accepted. */

if (hostlist[0] == 0)
  {
  if (verify != v_none) goto ROUTED;
  addr->message = string_sprintf("error in %s router: no host(s) specified "
    "for domain %s", rblock->name, domain);
  log_write(0, LOG_MAIN, "%s", addr->message);
  return DEFER;
  }

/* Otherwise we finish the routing here by building a chain of host items
for the list of configured hosts, and then finding their addresses. */

host_build_hostlist(&(addr->host_list), hostlist, randomize);
rc = rf_lookup_hostlist(rblock, addr, rblock->ignore_target_hosts, lookup_type,
  ob->hff_code, addr_new);
if (rc != OK) return rc;

/* Finally, since we have done all the routing here, there must be a transport
defined for these hosts. It will be a remote one, as a local transport is
dealt with above. However, we don't need one if verifying only. */

if (transport == NULL && verify == v_none)
    {
    log_write(0, LOG_MAIN, "Error in %s router: no transport defined",
      rblock->name);
    addr->message = US"error in router: transport missing";
    return DEFER;
    }

/* Fill in the transport, queue for remote delivery. The yield of
rf_queue_add() is always TRUE for a remote transport. */

ROUTED:

addr->transport = transport;
(void)rf_queue_add(addr, addr_local, addr_remote, rblock, NULL);
return OK;
}

/* End of routers/manualroute.c */
