/*
#ident	"@(#)smail/src:RELEASE-3_2_0_120:resolve.c,v 1.43 2004/08/27 06:53:30 woods Exp"
 */

/*
 *    Copyright (C) 1987, 1988 Ronald S. Karr and Landon Curt Noll
 *    Copyright (C) 1992  Ronald S. Karr
 * 
 * See the file COPYING, distributed with smail, for restriction
 * and warranty information.
 */

/*
 * resolve.c:
 *	resolve addresses to completed addr structures with transports.
 *
 *	external functions: resolve_addr_list, islocalhost
 */

#include "defs.h"

#include <sys/types.h>
#include <stdio.h>

#ifdef STDC_HEADERS
# include <stdlib.h>
# include <stddef.h>
#else
# ifdef HAVE_STDLIB_H
#  include <stdlib.h>
# endif
#endif

#ifdef HAVE_STRING_H
# if !defined(STDC_HEADERS) && defined(HAVE_MEMORY_H)
#  include <memory.h>
# endif
# include <string.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif

#ifdef __STDC__
# include <stdarg.h>
#else
# include <varargs.h>
#endif

#include "smail.h"
#include "smailsock.h"
#include "main.h"
#include "parse.h"
#include "addr.h"
#include "hash.h"
#include "direct.h"
#include "route.h"
#include "alloc.h"
#include "list.h"
#include "smailstring.h"
#include "dys.h"
#include "log.h"
#include "transport.h"
#include "exitcodes.h"
#include "extern.h"
#include "debug.h"
#include "error.h"
#include "smailport.h"

/* exported variables */
struct hash_table *hit_table = NULL;	/* table to recognize address hits */
struct block *hit_table_block = NULL;	/* block() for bmalloc() et al */


/*
 * resolve_addr_list - resolve addresses to transports and next hosts
 *
 * given a list of user-supplied addresses on input, produce resolved
 * and unresolvable addresses on output.
 *
 * inputs:
 *	in	- the list of input address structures
 *
 *	hash_addrs - set to TRUE to ignore duplicate addresses
 *
 * outputs:
 *	out	- the list of completely resolved address structures.
 *		  transport, next_host and next_addr will be properly
 *		  filled in for all of these structures.
 *	defer	- a list of temporarily unresolvable address structures.
 *		  an error structure is stored in the error element.
 *		  These addresses should be retried at a later time.  If
 *		  ERR_CONFERR is set in the error->info element, the
 *		  problem is a configuration error.
 *	fail	- a list of unresolvable address structures.  An error
 *		  structure is stored in the error element.  If
 *		  ERR_NSENDER is set, a note is returned to the sender.
 *		  If ERR_NPOSTMASTER is set, then a note is mailed to
 *		  the postmaster.  If ERR_NSOWNER is set then a note is
 *		  sent to an owner for an address, or to the sender if
 *		  the address has no owner.  If ERR_NPOWNER is set then
 *		  a note is sent to an owner or to the postmaster if the
 *		  address has no owner.
 */
void
resolve_addr_list(in, out, defer, fail, hash_addrs)
    struct addr *in;			/* the address list to resolve */
    struct addr **out;			/* produced addr list w/transports */
    struct addr **defer;		/* addrs to defer to a later time */
    struct addr **fail;			/* unresolvable addrs */
    int hash_addrs;			/* TRUE to prevent duplicate addrs */
{
    struct addr *cur;			/* current address being processed */
    struct addr *local;			/* addrs that parsed local */
    struct addr *remote;		/* addrs that parsed remote */
    struct addr *next;			/* next value for cur */

    remote = NULL;
    local = NULL;

    DEBUG1(DBG_RESOLVE_HI, "resolve_addr_list(): called [will%s check for dups]\n", hash_addrs ? "" : " NOT");

    /*
     * Resolve all addresses in our input queue.
     *
     * Each step through the loop advances the progress
     * in resolving all addresses to a transport.
     *
     * The loop is done when nothing remains to be processed.
     *
     * As an optimization, remote form processing is not
     * done unless/until no local processing was required.
     */
    while (in || remote) {
	/*
	 * split the input list into local and remote forms.
	 */
	for (cur = in, in = NULL; cur; cur = next) {
	    int form;			/* address form from parse_address() */

	    DEBUG1(DBG_RESOLVE_HI, "resolve_addr_list working on %s.\n", cur->work_addr);
	    next = cur->succ;

	    /* First, if allowed, we try adding the current address to the
	     * master hash table to ensure we don't do extra work checking
	     * obviously identical addresses.  Note that since we don't
	     * re-parse anything that looks anything like a remote address we
	     * don't protect from multiple deliveries to different addresses
	     * that really do resolve to the same adress.  We leave final
	     * duplicate stripping to a final scan of the out list.
	     */
	    if (cur->flags & ADDR_DONTHASH) {
		DEBUG1(DBG_RESOLVE_MID, "%s: not checking for dups because of ADDR_DONTHASH.\n", cur->work_addr);
	    } else if (hash_addrs && add_to_hash(cur->work_addr, (char *) NULL, (size_t) 0, hit_table) == ALREADY_HASHED) {
		DEBUG1(DBG_RESOLVE_LO, "%s: has already been seen -- skipping.\n", cur->work_addr);
		continue;
	    }

	    form = parse_address(cur->work_addr, &cur->target,
				 &cur->remainder, &cur->parseflags);
	    switch (form) {
	    case FAIL:
		/*
		 * ERR_111 - address parse error
		 *
		 * DESCRIPTION
		 *      parse_address() encountered an error while parsing
		 *      the work_addr for this address.  The error is stored
		 *      in cur->remainder.
		 *
		 * ACTIONS
		 *      A message about the parse error should be returned
		 *      to the owner of the address or to the sender.
		 *
		 * RESOLUTION
		 *      The owner or sender should correct the address and
		 *      resubmit the message.
		 */
		cur->error = note_error(ERR_NSOWNER|ERR_111, cur->remainder);
		cur->flags &= ~ADDR_FORM_MASK;
		cur->succ = *fail;
		*fail = cur;
		continue;

	    case LOCAL:
		(void) strip(cur->remainder); /* strip any quotes, etc. XXX necessary? */
		if (!cur->local_name) {
		    cur->local_name = visible_name;
		}
		cur->succ = local;
		local = cur;
		DEBUG1(DBG_RESOLVE_HI, "moved local form %s to be directed.\n", cur->remainder);
		break;

	    default:			/* anything else is a remote-form address */
		/* determine if the target host is actually a local host */
		if (islocalhost(cur->target)) {
		    DEBUG2(DBG_RESOLVE_LO, "target domain %s is a local host, will reparse remainder (%s).\n", cur->target, cur->remainder);
		    /* remember the local name */
		    cur->local_name = COPY_STRING(cur->target);/* XXX will leak? */
		    /* it is a local host, but save the name for virtual host processing */
		    cur->work_addr = COPY_STRING(cur->remainder);/* XXX will leak? */
		    next = cur;		/* erni: must parse again, remainder could be remote */
		    continue;
		}
		cur->flags &= ~(ADDR_FORM_MASK);
		cur->flags |= form;
		cur->succ = remote;
		remote = cur;
		DEBUG1(DBG_RESOLVE_HI, "moved target %s to be routed.\n", cur->target);
		break;
	    }
	}

	/*
	 * either process local or remote addresses.
	 */
	if (local) {
	    direct_local_addrs(local, out, &in, defer, fail);
	    local = NULL;
	} else {
	    route_remote_addrs(remote, out, &in, defer, fail);
	    remote = NULL;
	}
    }
    if (hash_addrs) {
	struct addr *prev;
	struct hash_table *hit_out_table; /* table to recognize address hits */
	struct block *hit_out_table_block; /* block for bmalloc() et al */
	char *hashval;

	hit_out_table_block = malloc_block();
	hit_out_table = new_hash_table(hit_table_len,
				       hit_out_table_block,
				       HASH_DEFAULT);
	/*
	 * Re-scan the out list for duplicates.  Note that a duplicate is
	 * one that has the same destination *and* the same transport.
	 */
	for (cur = *out, prev = NULL; cur; cur = next) {
	    next = cur->succ;
	    hashval = xprintf("%s at %s via %s",
			      cur->next_addr,
			      cur->next_host ? cur->next_host : "(localhost)",
			      cur->transport->name);
	    if (!(cur->flags & ADDR_DONTHASH) && add_to_hash(hashval, (char *) NULL, (size_t) 0, hit_out_table) == ALREADY_HASHED) {
		DEBUG1(DBG_RESOLVE_LO, "%s: has already been seen -- skipping during rescan.\n", hashval);
		/* we always get around the loop at least once without entering
		 * this side of the 'if', so 'prev' will never be NULL
		 */
		prev->succ = next;
		cur->succ = NULL;
		/* XXX should probably garbage collect the duplicate */
	    } else {
		prev = cur;
	    }
	    xfree(hashval);
	}
	/* free everything */
	free_block(hit_out_table_block);
    }

    return;
}

/*
 * islocalhost - determine if the given target name is a local one
 *
 * Given the currently known names for the local system, determine if the given
 * target name, or if a literal address target matches the address the message
 * arrived on, or if any name resolved from an IP literal matches one of these
 * known names.
 *
 * return TRUE or FALSE.
 *
 * Note: a target domain which looks like a literal IP address (i.e. is
 * surrounded by square brackets "[]"), but which doesn't match smtp_local_addr
 * and which the inet library claims is not a valid IP address, will be treated
 * as a non-local target domain.  This should be OK because we test IP literals
 * last.
 *
 * WARNING:  If no hostname for a literal IP address (i.e. those found via
 * gethostbyaddr()) matches any local domain name, and assuming the literal
 * addrss was also not the address the connection came in on (i.e. did not
 * match smtp_local_addr) then it will be treated as a non-local target even
 * though smail may be listening on that address on a local interface.  However
 * this should be sufficient for the most critical uses of literal IP
 * addresses.  Going any further (i.e. discovering and remembering all local
 * addresses smail was listening on at the time the message arrived) would be
 * very difficult to do portably.  We make one exception for a literal IP
 * address with the value INADDR_LOOPBACK and accept it as a local target even
 * if the smtp daemon is not accepting connections on the loopback interface.
 */
int
islocalhost(target)
    register char *target;		/* name to match */
{
#ifdef HAVE_BSD_NETWORKING
    int len;
#endif /* HAVE_BSD_NETWORKING */

    if ((uucp_name && EQIC(target, uucp_name)) ||
	(hostnames && is_string_in_list(target, hostnames)) ||
	(more_hostnames && is_string_in_list(target, more_hostnames)))
    {
	DEBUG1(DBG_RESOLVE_HI, "islocalhost: target %s is a local host\n", target);
	return TRUE;
    }
#ifdef HAVE_BSD_NETWORKING
    /* 
     * Check if this looks like a [xxx.xxx.xxx.xxx] address...
     */
    len = strlen(target);
    if (target[0] == '[' && target[len-1] == ']') {
	char *hostip;
	struct in_addr inet_s;
	struct hostent *hostentp;

        hostip = COPY_STRING(target+1); /* make a copy so we don't have to modify target. */
	len = strlen(hostip);
	hostip[len-1] = '\0';
	/*
	 * Check whether or not it appears to be the address we answered the
	 * connection on, which is the most common case for anyone doing
	 * testing via SMTP over TCP, but which may not work for messages
	 * arriving over some other non-IP connection, such as on the
	 * command-line (for those the gethostbyaddr() lookup below will have
	 * to work).  This will also help avoid a DNS lookup for the most
	 * common cases too.
	 */
	if (smtp_local_addr && EQ(hostip, smtp_local_addr)) {
	    DEBUG1(DBG_RESOLVE_HI, "islocalhost: address literal target %s matches smtp_local_addr\n", target);
	    xfree(hostip);
	    return TRUE;
	}
	/*
	 * do a reverse query to get the hostname and then do a recursive check
	 * on the hostname(s)...
	 */
	if (!inet_aton(hostip, &inet_s)) {
	    DEBUG1(DBG_RESOLVE_HI, "islocalhost: address literal target %s is not a valid IP address\n", target);
	    xfree(hostip);
	    return FALSE;		/* not a valid IP number.... */
	}
	xfree(hostip);			/* done with this */
	/* XXX should we really allow non-local clients to address local users by our own loobpack ? */
	if (inet_s.s_addr == htonl(INADDR_LOOPBACK)) {
	    return TRUE;		/* obviously it must be us.... */
	}
	if ((hostentp = gethostbyaddr((char *) &(inet_s.s_addr), (socklen_t) sizeof(inet_s.s_addr), AF_INET))) {
	    DEBUG2(DBG_RESOLVE_HI, "islocalhost: address literal target %s resolves to '%s'\n", target, hostentp->h_name);
	    if (islocalhost(hostentp->h_name)) {
	        return TRUE;
	    } else {
	        char *p;
		int i;
		
		for (i = 0; (p = (hostentp->h_aliases)[i]); i++) {
		    DEBUG2(DBG_RESOLVE_HI, "islocalhost: address literal target %s has alias '%s'\n", target, p);
		    if (islocalhost(p)) {
		        return TRUE;
		    }
		}
	    }
	}
    }
#endif /* HAVE_BSD_NETWORKING */
        
    DEBUG1(DBG_RESOLVE_HI, "islocalhost: target %s is a not local\n", target);

    return FALSE;
}

/* 
 * Local Variables:
 * c-file-style: "smail"
 * End:
 */
