/*
#ident	"@(#)smail/src/directors:RELEASE-3_2_0_120:aliasfile.c,v 1.44 2004/07/24 16:01:05 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.
 */

/*
 * aliasfile.c:
 *	direct mail using an aliases database stored as key/value pairs
 *	where the key is a local address and the value is a string
 *	which can be run through process_field to extract addr
 *	structures.
 *
 * Specifications for the aliasfile directing driver:
 *
 *	private attribute data:
 *	    file (string):  the name of a file which contains the
 *		key/value association database.
 *
 *	    proto (name):  specifies the protocol used in accessing
 *		the database.  Can be one of:
 *
 *		lsearch - performs a linear serach of an ASCII file.
 *		bsearch - performs a straightforward binary search
 *		    on a sorted file of text lines.
 *		dbm - use V7/BSD DBM library to perform search.
 *
 *	    modemask (number):  specifies bits that are not allowed
 *		to be set.  If some of these bits are set, the
 *		ALIAS_SECURE flag is not set for the resultant
 *		addr structures.
 *
 *	    owners (string):  list of possible owners for the file.
 *		For files owned by others, the ADDR_CAUTION bit is
 *		set in the resultant addr structures.
 *
 *	    owngroups (string):  like the `owners' attribute except
 *		that it applies to groups.
 *
 *	    retries (number):  specifies how many retries should be
 *		attempted in opening the file.  Retries are useful
 *		in for a UN*X system that does not have an atomic
 *		rename system call, where unlink/link must be used
 *		rendering the database file nonexistent for some
 *		small period of time.
 *
 *	    interval (number):  specifies the retry interval.  Sleep
 *		is called with this number between each retry to open
 *		the database.
 *
 *	private attribute flags:
 *	    reopen:  if set, then reopen the database for each call
 *		to the directing driver, and close before each return.
 *		This is necessary for systems that would not otherwise
 *		have a sufficient number of available file descriptors
 *		for all of their routing and directing needs.
 *	    optional:  if set, then if the open fails, assume an empty
 *		alias file.
 *	    tryagain:  if set, then if the open fails, try again on a
 *		later spool directory queue run.
 */

#include "defs.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <pwd.h>
#include <grp.h>
#include <errno.h>
#include <ctype.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 "../alloc.h"
#include "../list.h"
#include "../main.h"
#include "../parse.h"
#include "../addr.h"
#include "../field.h"
#include "../log.h"
#include "../exitcodes.h"
#include "../lookup.h"
#include "../smailstring.h"
#include "../dys.h"
#include "../direct.h"
#include "../transport.h"
#include "../smailconf.h"
#include "aliasfile.h"
#include "../extern.h"
#include "../error.h"
#include "../debug.h"
#include "../smailport.h"

/* functions local to this file */
static int alias_secure __P((struct aliasfile_private *, struct stat *));

static struct attr_table aliasfile_attributes[] = {
    { "file", t_string, 0, NULL, NULL, OFFSET(aliasfile_private, file) },
    { "proto", t_string, 0, NULL, NULL, OFFSET(aliasfile_private, proto) },
    { "modemask", t_mode, 0, NULL, NULL, OFFSET(aliasfile_private, modemask) },
    { "owners", t_string, 0, NULL, NULL, OFFSET(aliasfile_private, owners) },
    { "owngroups", t_string, 0, NULL, NULL,
	  OFFSET(aliasfile_private, owngroups) },
    { "retries", t_int, 0, NULL, NULL, OFFSET(aliasfile_private, retries) },
    { "interval", t_int, 0, NULL, NULL, OFFSET(aliasfile_private, interval) },
    { "reopen", t_boolean, 0, NULL, NULL, ALIAS_REOPEN },
    { "optional", t_boolean, 0, NULL, NULL, ALIAS_OPTIONAL },
    { "tryagain", t_boolean, 0, NULL, NULL, ALIAS_TRYAGAIN },
};
static struct attr_table *end_aliasfile_attributes =
	ENDTABLE(aliasfile_attributes);
static struct aliasfile_private aliasfile_template = {
    NULL,				/* file */
    "bsearch",				/* proto */
    000,				/* modemask */
    NULL,				/* owners */
    NULL,				/* owngroups */
#ifdef	HAVE_RENAME
    0,					/* retries */
#else  /* not HAVE_RENAME */
    1,					/* retries */
#endif /* not HAVE_RENAME */
    2,					/* interval */
    0,					/* flags_set -- for internal use */
    NULL,				/* database -- for internal use */
    NULL,				/* error_text -- for internal use */
};


/*
 * dtd_aliasfile - direct using aliases database
 */
/*ARGSUSED*/
struct addr *
dtd_aliasfile(dp, in, out, new, defer, fail)
    struct director *dp;		/* director entry */
    struct addr *in;			/* input local-form addrs */
    struct addr **out;			/* output resolved addrs */
    struct addr **new;			/* output new addrs to resolve */
    struct addr **defer;		/* addrs to defer to a later time */
    struct addr **fail;			/* unresolvable addrs */
{
    register struct addr *cur;		/* temp for processing input */
    struct addr *pass = NULL;		/* addrs to pass to next director */
    struct addr *next;			/* next value for cur */
    struct aliasfile_private *priv;	/* private data area */
    char *error = NULL;			/* error message */
    char *owner = NULL;                 /* address owner */
    char *preparsed_owner = NULL;       /* preparsed version */

    if (! do_aliasing) {
	return in;
    }

    if (!(priv = (struct aliasfile_private *)dp->private)) {
	priv = &aliasfile_template;
    }

    DEBUG(DBG_DRIVER_HI, "dtd_aliasfile called\n");

    if (priv->database == NULL) {
	struct stat statbuf;		/* get stat of database */
	int ret = FILE_AGAIN;		/* safest default to quiet compiler? */

	if (dp->flags & ALIAS_OPENFAIL) {
	    ret = FILE_FAIL;
	} else if (dp->flags & ALIAS_OPENAGAIN) {
	    ret = FILE_AGAIN;
	} else {
	    if (priv->error_text) {
		xfree(priv->error_text);
	    }
	    if (!priv->file) {
		ret = FILE_FAIL;
		error = "Database file name not specified";
	    } else {
		ret = open_database(priv->file, priv->proto,
				    priv->retries, priv->interval,
				    &statbuf, &priv->database, &error);
	    }
	}
	if (ret != FILE_SUCCEED) {
	    struct error *err;

	    if (ret == FILE_FAIL) {
		dp->flags |= ALIAS_OPENFAIL;
	    }
	    if (ret == FILE_NOMATCH) {
		dp->flags |= ALIAS_OPENFAIL;
		error = "Database not found";
	    }
	    if (ret == FILE_AGAIN) {
		dp->flags |= ALIAS_OPENAGAIN;
	    }

	    if (ret != FILE_AGAIN && (dp->flags & ALIAS_OPTIONAL)) {
		/* optional and not found or not openable */
		return in;
	    }

	    if (! priv->error_text) {
		priv->error_text = COPY_STRING(error);
	    }

	    /*
	     * ERR_112 - failed to open alias database
	     *
	     * DESCRIPTION
	     *      open_database() failed to open an alias database.  The
	     *      error encountered should be stored in errno.
	     *
	     * ACTIONS
	     *      Fail all of the input addresses as configuration errors
	     *      unless `tryagain' is set or open_database returned
	     *      FILE_AGAIN.
	     *
	     * RESOLUTION
	     *      The postmaster should check the director entry against
	     *      the database he wishes to use.
	     */
	    error = xprintf("director %v: alias database %v: %s",
			    dp->name,
			    priv->file,
			    priv->error_text);
	    err = note_error((((dp->flags & ALIAS_TRYAGAIN) || ret == FILE_AGAIN) ? 0 : ERR_CONFERR) | ERR_112,
			     error);
	    insert_addr_list(in, defer, err);

	    return NULL;
	}
	if (alias_secure(priv, &statbuf)) {
	    /* check succeeded */
	    priv->flags_set = ADDR_ALIASTYPE;
	} else {
	    /* security check failed, don't secure secure flag */
	    priv->flags_set = ADDR_ALIASTYPE | ADDR_CAUTION;
	}
    }

    /* Set up the new addresses */

    for (cur = in; cur; cur = next) {
	char *vret;			/* value from lookup_database */
	char *value;			/* copy of vret */
	struct addr *list;		/* list returned by lookup */
	int ret;

	next = cur->succ;

	ret = lookup_database(priv->database, cur->remainder, &vret, &error);

	switch (ret) {
	    struct addr *next_list;

	case DB_NOMATCH:
	    DEBUG2(DBG_DRIVER_MID, "director %v: did not match name <%v>\n",
		   dp->name, cur->remainder);
	    cur->succ = pass;
	    pass = cur;
	    break;

	case DB_AGAIN:
	case DB_FAIL:
	case FILE_AGAIN:
	case FILE_FAIL:
	    /*
	     * ERR_158 - aliasfile lookup error
	     *
	     * DESCRIPTION
	     *      database_lookup() returned an error, rather than
	     *      success or not found.
	     *
	     * ACTIONS
	     *      Defer the address without a configuration error, for
	     *      DB_AGAIN or FILE_AGAIN, or fail (and likely freeze) the
	     *      address with a configuration error, for DB_AGAIN or
	     *      FILE_AGAIN.
	     *
	     * RESOLUTION
	     *      For DB_FAIL or FILE_FAIL, the postmaster will need to look
	     *      into why the database lookup failed.  For DB_AGAIN or
	     *      FILE_AGAIN, a later queue run will hopefully take care of
	     *      the problem.
	     */
	    error = xprintf("director %v: lookup failure on %v: %s",
			    dp->name, cur->remainder, error);
	    DEBUG1(DBG_DRIVER_LO, "dtd_aliasfile: %s\n", error);
	    cur->error = note_error(((ret == DB_AGAIN || ret == FILE_AGAIN) ? 0 : ERR_CONFERR) | ERR_158, error);
	    cur->succ = *defer;
	    *defer = cur;
	    break;

	case DB_SUCCEED:
	    cur->director = dp;		/* set the director which matched */

	    /* get the addr structures */
	    list = NULL;
	    error = NULL;
	    value = COPY_STRING(vret);	/* verifying the owner may call lookup_database() again */

	    (void) process_field((char *)NULL, value, (char *)NULL,
				 (char *)NULL, &list, F_ALIAS, &error);
	    /*
	     * If a director owner exists, compute the owner address for this
	     * alias and add it to each generated address.  This is for the
	     * benefit of outgoing SMTP, which will use it in the envelope for
	     * error returns.
	     */
	    if (dp->owner) {
		if ((owner = expand_string(dp->owner,
					   (struct addr *) NULL,
					   dp->default_home,
					   cur->remainder))) {
		    preparsed_owner = preparse_address(owner, &error);
		}
		if (!preparsed_owner) {
		    DEBUG2(DBG_DRIVER_LO, "dtd_aliasfile: %v not usable as owner address for %v!\n", owner, cur->remainder);
		    owner = NULL;
		}
	    }
	    if (owner) {
		/* 
		 * Verify that this owner is sensible (to avoid things like
		 * xxxx-request-request).  To do this, use the pre-parsed
		 * address.  However, this procedure may change the value of
		 * the address given to it by removing the local domain name.
		 * We don't want this to happen so we make a copy beforehand.
		 */
		owner = COPY_STRING(preparsed_owner); /* copy to use later */

		/*
		 * Don't waste time verifying "postmaster", as it should always
		 * be valid.
		 */
		if (strcmpic(preparsed_owner, "postmaster") != 0) { 
		    struct addr *new2 = alloc_addr();
		    struct addr *defer2 = NULL;
		    struct addr *fail2 = NULL;
		    struct addr *okay = NULL;
		    unsigned long int was_reopen = (dp->flags & ALIAS_REOPEN); /* remember... */
		    int oexitval = exitvalue; /* resolve_addr_list() can clobber this */

		    DEBUG1(DBG_DRIVER_MID, "dtd_aliasfile: owner is not postmaster (is '%v'), will attempt to verify.\n", preparsed_owner);
		    dp->flags &= ~ALIAS_REOPEN;	/* make sure db is not closed! needed again! */

		    new2->in_addr = preparsed_owner;
		    new2->work_addr = preparsed_owner;

		    /* disable use of smartuser driver */
		    new2->flags = ADDR_SMARTUSER | ADDR_VRFY_ONLY;

		    resolve_addr_list(new2, &okay, &defer2, &fail2, FALSE);
		    exitvalue = oexitval;
		    if (!okay) {
			xfree(owner);
			/* XXX some might claim this should be a paniclog entry, but it could be too frequent... */
			DEBUG1(DBG_DRIVER_LO, "dtd_aliasfile: couldn't verify owner %v, using 'postmaster'\n", preparsed_owner);
			owner = COPY_STRING("postmaster");
		    }
		    xfree((char *) new2);
		    if (was_reopen) {
			dp->flags |= ALIAS_REOPEN;
		    }
		}     
	    }
#ifndef NDEBUG
	    if (debug) {
		char *av = COPY_STRING(value);

		/*
		 * this is a cheap hack for a very crude debug message, but it
		 * covers the common cases and for them it's better than
		 * spewing long messages that'll just get repeated again in
		 * further debug output!
		 *
		 * Note though that this may obscure other addresses following
		 * this one thus only a match at the very beginning of the
		 * entry is attempted, and the stripping can be disabled at a
		 * higher debug level.
		 *
		 * Ideally we should show exactly what the lookup returned at a
		 * the _HI level, and then always show each of the individual
		 * parse_field() "list" results in a properly re-constructed
		 * list, stripping the text message for only those entries
		 * matching ":fail:" and so on....
		 */
		if (debug < DBG_DRIVER_MID) {
		    if ((strncmpic(av, ":defer:", sizeof(":defer:")-1) == 0) ||
			(strncmpic(av, ":error:", sizeof(":error:")-1) == 0) ||
			(strncmpic(av, ":fail:", sizeof(":fail:")-1) == 0)) {

			*(strchr(av + 1, ':') + 1) = '\0';
		    }
		}
		if (av[strlen(av) - 1] == '\n') {
		    av[strlen(av) - 1] = '\0';
		}
		DEBUG5(DBG_DRIVER_LO,
		       "director %v: matched %v, aliased to '%v'\n  owner %v%s\n",
		       dp->name,
		       cur->remainder,
		       av,
		       owner ? owner : "(none)",
		       cur->flags & ADDR_VRFY_ONLY ? " (VRFY_ONLY)" : "");

		xfree(av);
	    }
#endif
	    /*
	     * if we are only verifying deliverability then we don't want to
	     * actually transform anything just yet so push this one onto the
	     * output list and go on to the next one....
	     */
	    if (cur->flags & ADDR_VRFY_ONLY) {
		cur->succ = *out;
		*out = cur;
		break;
	    }
	    /*
	     * Now we can check the process_field results...
	     */
	    if (error) {
		/*
		 * ERR_113 - alias parsing error
		 *
		 * DESCRIPTION
		 *      process_field() found an error while parsing an
		 *      entry from an alias database.  The specific error is
		 *      stored in `error'.
		 *
		 * ACTIONS
		 *      Fail the address and send to the owner or to the
		 *      postmaster.
		 *
		 * RESOLUTION
		 *      The postmaster or alias owner should correct the
		 *      alias database entry.
		 */
		cur->error =
		    note_error(ERR_NPOWNER|ERR_113,
			       xprintf("director %v: error parsing alias: %v",
				       dp->name, error));
		cur->succ = *fail;
		*fail = cur;
		break;
	    }
	    for (; list; list = next_list) {
		next_list = list->succ;

                list->owner = owner;
		list->parent = cur;
		list->director = dp;
		list->flags = priv->flags_set;
		list->succ = *new;
		*new = list;
	    }
	    break;
	}
    }

    if (dp->flags & ALIAS_REOPEN) {
	close_database(priv->database);
	priv->database = NULL;
    }

    return pass;			/* return addrs for next director */
}

/*
 * dtb_aliasfile - read the configuration file attributes
 */
char *
dtb_aliasfile(dp, attrs)
    struct director *dp;		/* director entry being defined */
    struct attribute *attrs;		/* list of per-driver attributes */
{
    char *error;
    struct aliasfile_private *priv;	/* new aliasfile_private structure */
    char *fn = xprintf("aliasfile director: %v", dp->name);

    /* copy the template private data */
    priv = (struct aliasfile_private *)xmalloc(sizeof(*priv));
    (void) memcpy((char *)priv, (char *)&aliasfile_template, sizeof(*priv));

    dp->private = (char *)priv;
    /* fill in the attributes of the private data */
    error = fill_attributes((char *)priv,
			    attrs,
			    &dp->flags,
			    aliasfile_attributes,
			    end_aliasfile_attributes,
			    fn);
    xfree(fn);
    if (error) {
	return error;
    }

    return NULL;
}


/*
 * dtp_aliasfile - dump aliasfile config
 */
void
dtp_aliasfile(f, dp)
     FILE * f;
     struct director *dp;
{
    (void) dump_standard_config(f,
				(dp->private) ? dp->private : (char *)&aliasfile_template,
				dp->name,
				dp->flags,
				aliasfile_attributes,
				end_aliasfile_attributes);
}

/*
 * alias_secure - determine if an alias file is secure
 *
 * return TRUE if an aliasfile is secure, given the stat structure
 * and the constraints in the private structure.
 */
static int
alias_secure(priv, statp)
    struct aliasfile_private *priv;	/* source of constraints */
    struct stat *statp;			/* source of data */
{
    /* first constraint, bits in modemask must not be set in st_mode */
    if (statp->st_mode & priv->modemask) {
	return FALSE;
    }

    /* look through the list of acceptible owners */
    if (priv->owners && priv->owners[0]) {
	char *temp = priv->owners;
	int found = FALSE;

	/* step through all of the owners */
	for (temp = strcolon(temp); temp; temp = strcolon((char *)NULL)) {
	    struct passwd *pw = getpwbyname((smail_bool_t) FALSE, temp);

	    /* ignore names not in the passwd file */
	    if (pw == NULL) {
		continue;
	    }

	    /* otherwise check for a match */
	    if (pw->pw_uid == statp->st_uid) {
		found = TRUE;
		break;
	    }
	}

	if (!found) {
	    return FALSE;
	}
    }

    /* check list of allowable owning groups */
    if (priv->owngroups && priv->owngroups[0]) {
	char *temp = priv->owngroups;
	int found = FALSE;

	/* step through all of the owners */
	for (temp = strcolon(temp); temp; temp = strcolon((char *)NULL)) {
	    struct group *gr = getgrbyname(temp);

	    /* ignore names not in the passwd file */
	    if (gr == NULL) {
		continue;
	    }

	    /* otherwise check for a match */
	    if (gr->gr_gid == statp->st_gid) {
		found = TRUE;
		break;
	    }
	}

	if (!found) {
	    return FALSE;
	}
    }

    return TRUE;			/* checks out, by default */
}

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