/*
#ident	"@(#)smail/src:RELEASE-3_2_0_120:expand.c,v 1.75 2004/07/24 16:14:00 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.
 */

/*
 * expand.c:
 *	expand filenames used by directors.
 *
 *	external functions:  expand_string, build_cmd_line
 */

#include "defs.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <ctype.h>
#include <grp.h>
#include <pwd.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

#if defined(HAVE_UNISTD_H)
# include <unistd.h>
#endif

#ifdef TIME_WITH_SYS_TIME
# include <sys/time.h>
# include <time.h>
#else
# ifdef HAVE_SYS_TIME_H
#  include <sys/time.h>
# else
#  include <time.h>
# endif
#endif

#include "smail.h"
#include "alloc.h"
#include "list.h"
#include "main.h"
#include "parse.h"
#include "addr.h"
#include "direct.h"
#include "route.h"
#include "lookup.h"
#include "log.h"
#include "smailstring.h"
#include "dys.h"
#include "spool.h"
#include "header.h"
#include "transport.h"
#include "smailconf.h"
#include "exitcodes.h"
#include "version.h"
#include "debug.h"
#include "extern.h"
#include "smailport.h"

/* functions local to this file */
static int expand_string_to __P((struct str *, char *, struct addr *, char *));
static int if_clause __P((struct addr *, char *, struct str *, char *, char *));
static int lookup_clause __P((struct addr *, struct str *, char *, char *));
static int foreach_clause __P((struct addr *, struct str *, char *, char *));
static int test_cond __P((struct addr *, char *, char **));
static char **build_argv __P((char *, int));
static char *substitute __P((struct addr *, char *, char *, char *, size_t));
static char *strip_quotes __P((char *));
static char *uc_fold __P((char *));
static char *lc_fold __P((char *));
#ifdef notyet
static char *phrase_quote __P((char *));
#endif
static char *shell_quote __P((char *));
static char *regex_quote __P((char *));
static char *skip_ident __P((char *, char *));
static int skip_nesting __P((char **));
static int clause_token __P((char **, char **, size_t *));
static void bad_subst __P((char *, size_t, char *));


/*
 * expand_string - expand a string containing parts to be expanded
 *
 * This function does ~user and ~/ expansion and also performs expansions
 * of the form $name or ${name}.  See substitute() for the possible
 * substitutions.
 *
 * If `addr' is NULL, then a dummy addr structure is formed with all
 * items NULL except for home and user, which are taken from the
 * arguments to expand_string().
 *
 * return NULL on error attempting expansion.  The area returned may
 * be reused on subsequent calls.  If the caller wishes to retain the
 * returned data, it should be copied elsewhere.
 */
char *
expand_string(string, addr, home, user)
    register char *string;		/* unexpanded string */
    struct addr *addr;			/* addr structure with values */
    char *home;				/* home directory */
    char *user;				/* user name for $user */
{
    static struct str str;		/* build strings here */
    static int inited = FALSE;		/* TRUE if str inited */
    static struct addr *dummy_addr = NULL; /* dummy addr for home and user */

    if (! inited) {
	STR_INIT(&str);
	inited = TRUE;
    } else {
	STR_CHECK(&str);
	STR_CLEAR(&str);
    }
    if (addr == NULL) {
	/* no addr structure given, setup a dummy one */
	if (dummy_addr == NULL) {
	    dummy_addr = alloc_addr();
	}
	addr = dummy_addr;
	addr->home = home;
	addr->next_addr = user;
    }
#if 0
    DEBUG4(DBG_DRIVER_HI, "expand_string(%v, %v, %v, %v) called\n",
	   string,
	   addr->in_addr ? addr->in_addr : "(none)",
	   home ? home : "(nobody)",
	   user ? user : "(no-one)");
#endif
    /*
     * do the grunt work of expansion, appending to str
     */
    if (expand_string_to(&str, string, addr, (char *) NULL) == FAIL) {
	DEBUG(DBG_DRIVER_MID, "expand_string_to(): failed\n");
	return NULL;
    }
    /*
     * expansion was done, finish up the string and return it
     */
    STR_NEXT(&str, '\0');
    DEBUG1(DBG_DRIVER_HI, "expand_string(): returns '%v'\n", STR(&str));

    return STR(&str);
}

static int
expand_string_to(str, expr, addr, found)
    struct str *str;			/* output string */
    char *expr;				/* unexpanded expression */
    struct addr *addr;			/* addr structure with values */
    char *found;			/* value for $value */
{
    char *string = expr;

    DEBUG3(DBG_DRIVER_HI, "expand_string_to(str, %v, %v, %v) called\n",
	   string,
	   addr->in_addr ? addr->in_addr : "(none)",
	   found ? found : "(no-value)");

    if (string[0] == '~') {	/* do some kind of twiddle expansion */
	if (string[1] == '/') {	    /* ~/ turns into home/ */
	    if (addr->home) {
		string++;
		STR_CAT(str, addr->home);
	    } else {
		/* no home directory, so ~/ is not valid */
		DEBUG(DBG_DRIVER_MID, "expanding ~/ failed: home directory unknown\n");
		return FAIL;
	    }
	} else {     /* ~user turns into home directory for the given user */
	    char *p = string + 1;
	    char *u;
	    struct passwd *pw;

	    while (*string && *string != '/') {
		string++;
	    }
	    u = strncpy(xmalloc((size_t) (string - p + 1)), p, (size_t) (string - p));
	    u[string - p] = '\0';
	    pw = getpwbyname((smail_bool_t) TRUE, p);
	    if (pw == NULL) {
		/* ~user but username isn't valid */
		DEBUG1(DBG_DRIVER_MID, "expanding '%s' failed: user not found\n", u);
		xfree(u);
		/* XXX should we just STR_CAT(str, u) anyway? */
		return FAIL;
	    }
	    STR_CAT(str, pw->pw_dir);
	    xfree(u);
	}
    }

    /*
     * we have the silly ~ business out of the way, now
     * get all of the rest of the silly business out of the way
     */
    while (*string) {
	if (*string == '$') {
	    /* do a $-substitution */
	    string++;
	    if (*string == '{') {
		/*
		 * handle expansions of the form ${name}
		 */
		char *p, *q, *new;

		p = ++string;
		if (skip_nesting(&string) == FAIL) {
		    /* error logged by skip_nesting() */
		    return FAIL;
		}
		--string;			/* point to end of name, not '}' */
		q = skip_ident(p, string);
		if ((q - p) == 2 && strncmpic(p, "if", (size_t) 2) == 0) {
		    /*
		     * process conditional expansion
		     */
		    if (if_clause(addr, found, str, q, string) == FAIL) {
			DEBUG2(DBG_DRIVER_MID, "'${%S' (if stmt) expands to nothing\n", string - p + 1, p);
			return FAIL;
		    }
		} else if ((q - p) == 6 && strncmpic(p, "lookup", (size_t) 6) == 0) {
		    /*
		     * process database lookup
		     */
		    if (lookup_clause(addr, str, q, string) == FAIL) {
			DEBUG2(DBG_DRIVER_MID, "'${%S' (lookup stmt) expands to nothing\n", string - p + 1, p);
			return FAIL;
		    }
#if 0
		} else if ((q - p) == 7 && strncmpic(p, "foraddr", (size_t) 7) == 0) {
		    /*
		     * process loop expansion for each addr in the list
		     */
		    if (foraddr_clause(addr, str, q, string) == FAIL) {
			DEBUG2(DBG_DRIVER_MID, "'${%S' (foraddr stmt) expands to nothing\n", string - p + 1, p);
			return FAIL;
		    }
#endif
		} else if ((q - p) == 7 && strncmpic(p, "foreach", (size_t) 7) == 0) {
		    /*
		     * for each entry in list expand value in the string
		     */
		    if (foreach_clause(addr, str, q, string) == FAIL) {
			DEBUG2(DBG_DRIVER_MID, "'${%S' (foreach stmt) expands to nothing\n", string - p + 1, p);
			return FAIL;
		    }
		} else {
		    /*
		     * It's not a conditional expression, or a database lookup,
		     * so it must be just a variable identifier...
		     */
		    new = substitute(addr, (char *) NULL, found, p, (size_t) (string - p));
		    if (new) {
			STR_CAT(str, new);
		    } else {
			/* unrecognized substitution */
			bad_subst("variable", (size_t) 0, xprintf("${%S} unknown, or not set", (size_t) (string - p), p));
			return FAIL;
		    }
		}
		string++;
	    } else {
		/*
		 * handle plain $name expansions as plain variable names
		 */
		char *p = string;
		char *new;

		string = skip_ident(string + 1, (char *) NULL);
		new = substitute(addr, (char *) NULL, found, p, (size_t) (string - p));
		if (new) {
		    STR_CAT(str, new);
		} else {
		    /* unrecognized substitution */
		    bad_subst(p, (size_t) (string - p), "variable unknown, or not set");
		    return FAIL;
		}
	    }
	} else {
	    /*
	     * regular character, copy it into the result
	     */
	    STR_NEXT(str, *string++);
	}
    }
    return SUCCEED;
}

/*
 * if_clause - parse and perform ${if ...} expansion
 *
 *   Two forms are possible:
 *
 *	${if condition [:]then-clause}
 *	${if condition {then-clause} [else] [{else-clause}]}
 */

static int
if_clause(addr, found, str, s, es)
    struct addr *addr;
    char *found;
    struct str *str;
    char *s;
    char *es;
{
    int result;
    char *then_clause, *then_end, *else_clause, *else_end;
    char *q = s;

    while (isspace((int) *q)) {
	q++;
    }
    result = test_cond(addr, found, &q);
    if (result == FAIL) {
	return FAIL;
    }
    if (q >= es) {
	bad_subst(s, (size_t) 0, "syntax error in ${if condition");
	return FAIL;
    }
    while (isspace((int) *q)) {
	q++;
    }

    /*
     * find then and else clauses of if.
     */
    switch (*q) {
    case '{':
	then_clause = ++q;
	if (skip_nesting(&q) == FAIL) {
	    return FAIL;
	}
	then_end = q - 1;
	while (isspace((int) *q)) {
	    q++;
	}
	if (*q != '{' && strncmpic(q, "else", (size_t) 4) == 0) {
	    q += 4;
	    while (isspace((int) *q)) {
		q++;
	    }
	}
	if (*q == '}') {
	    else_clause = es;
	    else_end = es;
	    break;
	}
	if (*q != '{') {
	    return FAIL;			/* XXX syntax error? */
	}
	else_clause = ++q;
	if (skip_nesting(&q) == FAIL) {
	    return FAIL;
	}
	else_end = q - 1;
	while (isspace((int) *q)) {
	    q++;
	}
	if (q != es) {
	    return FAIL;			/* XXX syntax error? */
	}
	break;

    case ':':
	q++;
	/* FALLTHRU */

    default:
	then_clause = q;
	then_end = es;
	else_clause = es;
	else_end = es;
	break;
    }

    /*
     * expand the then clause of the result matches
     * the indicated test sense if: is a true sense,
     * if! is a false sense; otherwise, expand the
     * else clause.
     */

    if (result) {
	s = rcopy(then_clause, then_end);
    } else {
	s = rcopy(else_clause, else_end);
    }
    if (expand_string_to(str, s, addr, found) == FAIL) {
	xfree(s);
	return FAIL;
    }
    xfree(s);
    return SUCCEED;
}

/*
 * test_cond - test a conditional within an ${if: ...} or ${if! ...}
 *
 * Possible conditionals:
 *
 *	!condition	- reverse sense of test
 *	def:variable	- true if variable is defined and non-empty
 *	def{meta:variable} - true if ${meta:variable} is expands non-empty
 *	header:string	- true if indicated header exists in message
 *	origin:local	- true if message origin is local
 *	origin:remote	- true if message origin is remote
 *	dest:local	- true if message is being delivered locally
 *	dest:remote	- true if message is being delivered to remote host
 *	xform:local	- transform into a local header
 *	xform:inet	- transform into a RFC822-conformant envelope
 *	xform:uucp	- transform into a UUCP-zone envelope
 *	xform:none	- no specified transformation
 *	eq{var}{value}	- test for match of variable with a value
 *	eqic{var}{value}- test for match of variable with a value, ignoring case
 *	gt{var}{value}	- test for variable greater than value
 *	gtic{var}{value}- test for variable greater than value, ignoring case
 *	lt{var}{value}	- test for variable less than value
 *	ltic{var}{value}- test for variable less than value, ignoring case
 *	or{{cond}...}	- logical or of conditionals
 *	and{{cond}...}	- logical and of conditionals
 *
 * TODO:
 *	rx{var}{RE}	- test for match of variable with a regular expression
 */

static int
test_cond(addr, found, sp)
    struct addr *addr;
    char *found;
    char **sp;
{
#define RESULT(res)	((negate) ? ((res) ? FALSE : TRUE) : ((res) ? TRUE : FALSE))

    char *q = *sp;
    char *s, *s2;
    char *new;
    size_t len, len2;
    charplist_t *hdr;
    unsigned long int tpflags;
    struct str str;
    int result;
    int negate = FALSE;
    int compare_op;

    while (isspace((int) *q)) {
	q++;
    }
    if (*q == '!') {
	negate = TRUE;
	q++;
    }
    *sp = q;
    if (clause_token(sp, &s, &len) == FAIL) {
	DEBUG1(DBG_DRIVER_MID, "test_cond(): failed to find condition name at start of: '%v'", *sp);
	return FAIL;
    }
    if (len == 3 && strncmpic(s, "def", (size_t) 3) == 0) {
	if (clause_token(sp, &s, &len) == FAIL) {
	    DEBUG1(DBG_DRIVER_MID, "test_cond(): failed to find variable for 'def' at start of: '%v'", *sp);
	    return FAIL;
	}
	new = substitute(addr, (char *) NULL, found, s, len);
	return (new && *new) ? RESULT(TRUE) : RESULT(FALSE);
    }

    if (len == 6 && strncmpic(s, "header", (size_t) 6) == 0) {
	if (clause_token(sp, &s, &len) == FAIL) {
	    DEBUG1(DBG_DRIVER_MID, "test_cond(): failed to find name for 'header' at start of: '%v'", *sp);
	    return FAIL;
	}
	for (hdr = header; hdr; hdr = hdr->succ) {
	    if (strncmpic(s, hdr->text, len) == 0 &&
		(hdr->text[len] == ':' || isspace((int) hdr->text[len])))
	    {
		return RESULT(TRUE);
	    }
	}
	return RESULT(FALSE);
    }

    if (len == 6 && strncmpic(s, "origin", (size_t) 6) == 0) {
	if (clause_token(sp, &s, &len) == FAIL) {
	    DEBUG1(DBG_DRIVER_MID, "test_cond(): failed to find word for 'origin' at start of: '%v'", *sp);
	    return FAIL;
	}
	if (len == 5 && strncmpic(s, "local", (size_t) 5) == 0) {
	    return RESULT(islocal);
	}
	if (len == 6 && strncmpic(s, "remote", (size_t) 6) == 0) {
	    return RESULT(! islocal);
	}
	return FAIL;
    }

    if (len == 4 && strncmpic(s, "dest", (size_t) 4) == 0) {
	if (clause_token(sp, &s, &len) == FAIL) {
	    DEBUG1(DBG_DRIVER_MID, "test_cond(): failed to find word for 'dest' at start of: '%v'", *sp);
	    return FAIL;
	}
	if (addr == NULL || addr->transport == NULL) {
	    return RESULT(FALSE);
	}
	tpflags = addr->transport->flags;
	if (len == 5 && strncmpic(s, "local", (size_t) 5) == 0) {
	    return RESULT(tpflags & LOCAL_TPORT);
	}
	if (len == 6 && strncmpic(s, "remote", (size_t) 6) == 0) {
	    return RESULT(! (tpflags & LOCAL_TPORT));
	}
	return FAIL;
    }

    if (len == 5 && strncmpic(s, "xform", (size_t) 5) == 0) {
	if (clause_token(sp, &s, &len) == FAIL) {
	    DEBUG1(DBG_DRIVER_MID, "test_cond(): failed to find word for 'xform' at start of: '%v'", *sp);
	    return FAIL;
	}
	if (addr == NULL || addr->transport == NULL) {
	    return FALSE;
	}
	tpflags = addr->transport->flags;
	if (len == 5 && strncmpic(s, "local", (size_t) 5) == 0) {
	    return RESULT(tpflags & LOCAL_XFORM);
	}
	if (len == 6 && strncmpic(s, "uucp", (size_t) 6) == 0) {
	    return RESULT(tpflags & UUCP_XFORM);
	}
	if (len == 6 && strncmpic(s, "inet", (size_t) 6) == 0) {
	    return RESULT(tpflags & INET_XFORM);
	}
	if (len == 6 && strncmpic(s, "none", (size_t) 6) == 0) {
	    return RESULT(! (tpflags & (LOCAL_XFORM|UUCP_XFORM|INET_XFORM)));
	}
	return FAIL;
    }

    compare_op = 0;
    if ((len == 2 || len == 4) &&
	(((strncmpic(s, "eq", (size_t) 2) == 0) && (compare_op = 1)) ||
	 ((strncmpic(s, "eqic", (size_t) 4) == 0) && (compare_op = 2)) ||
	 ((strncmpic(s, "gt", (size_t) 2) == 0) && (compare_op = 3)) ||
	 ((strncmpic(s, "gtic", (size_t) 4) == 0) && (compare_op = 4)) ||
	 ((strncmpic(s, "lt", (size_t) 2) == 0) && (compare_op = 5)) ||
	 ((strncmpic(s, "ltic", (size_t) 4) == 0) && (compare_op = 6)))) {
	if (clause_token(sp, &s, &len) == FAIL) {
	    DEBUG2(DBG_DRIVER_MID, "test_cond(): failed to find variable for '%s' at start of: '%v'",
		   (compare_op == 1) ? "eq" :
		   (compare_op == 2) ? "eqic" :
		   (compare_op == 3) ? "gt" :
		   (compare_op == 4) ? "gtic" :
		   (compare_op == 5) ? "lt" :
		   (compare_op == 6) ? "ltic" : "UNKNOWN-COMPARE",
		   *sp);
	    return FAIL;
	}
	if (clause_token(sp, &s2, &len2) == FAIL) {
	    DEBUG2(DBG_DRIVER_MID, "test_cond(): failed to value for '%s' at start of: '%v'",
		   (compare_op == 1) ? "eq" :
		   (compare_op == 2) ? "eqic" :
		   (compare_op == 3) ? "gt" :
		   (compare_op == 4) ? "gtic" :
		   (compare_op == 5) ? "lt" :
		   (compare_op == 6) ? "ltic" : "UNKNOWN-COMPARE",
		   *sp);
	    return FAIL;
	}
	s2 = rcopy(s2, s2 + len2);
	STR_INIT(&str);
	if (expand_string_to(&str, s2, addr, found) == FAIL) {
	    result = FAIL;
	} else {
	    s = substitute(addr, (char *) NULL, found, s, len);
	    STR_NEXT(&str, '\0');
	    switch (compare_op) {
	    case 1:
		result = (strcmp(s, STR(&str)) == 0) ? RESULT(TRUE) : RESULT(FALSE);
		break;
	    case 2:
		result = (strcmpic(s, STR(&str)) == 0) ? RESULT(TRUE) : RESULT(FALSE);
		break;
	    case 3:
		result = (strcmp(s, STR(&str)) > 0) ? RESULT(TRUE) : RESULT(FALSE);
		break;
	    case 4:
		result = (strcmpic(s, STR(&str)) > 0) ? RESULT(TRUE) : RESULT(FALSE);
		break;
	    case 5:
		result = (strcmp(s, STR(&str)) < 0) ? RESULT(TRUE) : RESULT(FALSE);
		break;
	    case 6:
		result = (strcmpic(s, STR(&str)) < 0) ? RESULT(TRUE) : RESULT(FALSE);
		break;
	    default:
		result = FAIL;			/* for lint & GCC -Wunused */
		panic(EX_SOFTWARE, "test_cond(): unknown compare operation: '%v'", *sp);
		/* NOTREACHED */
	    }
	}
	STR_FREE(&str);
	xfree(s2);
	return result;
    }

    if (len == 3 && strncmpic(s, "and", (size_t) 3) == 0) {
	if (clause_token(sp, &s, &len) == FAIL) {
	    DEBUG1(DBG_DRIVER_MID, "test_cond(): failed to find expression for 'and' at start of: '%v'", *sp);
	    return FAIL;
	}
	s = rcopy(s, s + len);
	q = s;
	result = TRUE;
	for (;;) {
	    while (isspace((int) *q)) {
		q++;
	    }
	    if (*q == '\0') {
		break;
	    }
	    if (clause_token(&q, &s2, &len2) == FAIL) {
		result = FAIL;
		break;
	    }
	    result = test_cond(addr, found, &s2);
	    if (result != TRUE) {
		break;
	    }
	}
	xfree(s);
	return result == FAIL ? FAIL : RESULT(result);
    }

    if (len == 2 && strncmpic(s, "or", (size_t) 2) == 0) {
	if (clause_token(sp, &s, &len) == FAIL) {
	    DEBUG1(DBG_DRIVER_MID, "test_cond(): failed to find expression for 'or' at start of: '%v'", *sp);
	    return FAIL;
	}
	s = rcopy(s, s + len);
	q = s;
	result = FALSE;
	for (;;) {
	    while (isspace((int) *q)) {
		q++;
	    }
	    if (*q == '\0') {
		break;
	    }
	    if (clause_token(&q, &s2, &len2) == FAIL) {
		result = FAIL;
		break;
	    }
	    result = test_cond(addr, found, &s2);
	    if (result != FALSE) {
		break;
	    }
	}
	xfree(s);
	return result == FAIL ? FAIL : RESULT(result);
    }

    write_log(WRITE_LOG_PANIC | WRITE_LOG_TTY, "test_cond(): unknown condition at start of: '%v'", s);

    return FAIL;
#undef RESULT
}

/*
 * lookup_clause - expand a lookup expansion
 *
 * Form:
 *	${lookup:key:proto{file-expansion}
 *		[then] {then-clause}
 *		[else] {else-clause}}
 * or:
 *	${lookup:key:proto:file-expansion:then-clause}
 */

static int
lookup_clause(addr, str, s, es)
    struct addr *addr;
    struct str *str;
    char *s;
    char *es;
{
    char *q = s;
    char *key, *proto, *file;
    char *xkey = NULL;
    char *xproto = NULL;
    char *xfile = NULL;
    size_t key_len;
    size_t proto_len, file_len;
    char *then_clause, *then_end, *else_clause, *else_end;
    struct str file_str;
    char *dbinfo;
    char *error = NULL;
    char *found = NULL;
    int dbresult;
    int result;

    /*
     * get the key, proto, and database
     */

    if (clause_token(&q, &key, &key_len) == FAIL ||
	clause_token(&q, &proto, &proto_len) == FAIL ||
	clause_token(&q, &file, &file_len) == FAIL)
    {
	return FAIL;
    }

    /*
     * locate then and else clauses
     */

    while (isspace((int) *q)) {
	q++;
    }
    if (strncmpic(q, "then", (size_t) 4) == 0) {
	q += 4;
	while (isspace((int) *q)) {
	    q++;
	}
    }
    switch (*q) {
    case '{':
	then_clause = ++q;
	if (skip_nesting(&q) == FAIL) {
	    return FAIL;
	}
	then_end = q - 1;
	while (isspace((int) *q)) {
	    q++;
	}
	if (*q != '{' && strncmpic(q, "else", (size_t) 4) == 0) {
	    q += 4;
	    while (isspace((int) *q)) {
		q++;
	    }
	}
	if (*q == '}') {
	    else_clause = es;
	    else_end = es;
	    break;
	}
	if (*q != '{') {
	    return FAIL;			/* XXX syntax error? */
	}
	else_clause = ++q;
	if (skip_nesting(&q) == FAIL) {
	    return FAIL;
	}
	else_end = q - 1;
	while (isspace((int) *q)) {
	    q++;
	}
	if (q != es) {
	    return FAIL;			/* XXX syntax error */
	}
	break;

    case ':':
	q++;
	/* FALLTHRU */

    default:
	then_clause = q;
	then_end = es;
	else_clause = es;
	else_end = es;
	break;
    }

    /*
     * expand the key, file, and proto
     */

    STR_INIT(&file_str);
    xkey = substitute(addr, (char *) NULL, (char *) NULL, key, key_len);
    if (xkey == NULL) {
	goto do_else_clause;
    }
    xkey = COPY_STRING(xkey);
    xfile = rcopy(file, file + file_len);
    file = NULL;
    if (expand_string_to(&file_str, xfile, addr, (char *) NULL) == FAIL) {
	xfree(xkey);
	xfree(xfile);
	STR_FREE(&file_str);
	return FAIL;
    }
    STR_NEXT(&file_str, '\0');
    xproto = rcopy(proto, proto + proto_len);
    proto = NULL;

    /*
     * open the database and do a lookup with the key
     */

    dbresult = open_database(STR(&file_str), xproto, 1, 2, (struct stat *) NULL,
			     &dbinfo, &error);
    switch (dbresult) {
    case FILE_FAIL:
	DEBUG3(DBG_DRIVER_LO, "Warning: open_database(%v, %v) failed: %s\n",
	       STR(&file_str), xproto, error);
	goto do_else_clause;

    case FILE_AGAIN:
    case FILE_NOMATCH:
	goto do_else_clause;
    }

    dbresult = lookup_database(dbinfo, xkey, &found, &error);
    close_database(dbinfo);

    switch (dbresult) {
    case FILE_FAIL:
    case DB_FAIL:
	DEBUG4(DBG_DRIVER_LO, "Warning: lookup of %v in %v:%v failed: %s\n",
	       xkey, xproto, STR(&file_str), error);
	goto do_else_clause;

    case FILE_AGAIN:
    case FILE_NOMATCH:
    case DB_AGAIN:
    case DB_NOMATCH:
	goto do_else_clause;
    }

    /*
     * remove white-space from beginning and end of found string
     */
    while (isspace((int) *found)) {
	found++;
    }
    for (q = found; *q; q++) {
	if (*q == '#') {
	    break;
	}
    }
    while (q > found && isspace((int) *(q - 1))) {
	--q;
    }
    *q = '\0';

    /*
     * expand the then-clause
     */

    s = rcopy(then_clause, then_end);
    goto do_expand;

do_else_clause:
    s = rcopy(else_clause, else_end);
    /* FALLTHRU */

do_expand:
    result = expand_string_to(str, s, addr, found);
    if (xkey) {
	xfree(xkey);
    }
    if (xproto) {
	xfree(xproto);
    }
    if (xfile) {
	xfree(xfile);
    }
    if (s) {
	 xfree(s);
    }
    STR_FREE(&file_str);

    return result;
}

/*
 * foreach_clause - expand a list iterator
 *
 * Form:
 *	${foreach:list:pattern with ${value} expanded to each item}
 *
 * e.g.:
 *	${foreach:more_hostnames:${rxquote:value};a common comment about ${value}}
 */

static int
foreach_clause(addr, str, s, es)
    struct addr *addr;
    struct str *str;
    char *s;
    char *es;
{
    char *q = s;
    char *listname = NULL;
    size_t listname_len;
    char *list;
    char *pat;
    char *pat_end;
    char *ss;
    char *item;

    /*
     * get the list name
     */
    if (clause_token(&q, &listname, &listname_len) == FAIL) {
	return FAIL;
    }
    q++;					/* skip the colon */

    /*
     * get the string pattern
     */
    while (isspace((int) *q)) {
	q++;
    }
    switch (*q) {
    case '{':
	pat = ++q;
	if (skip_nesting(&q) == FAIL) {
	    return FAIL;
	}
	pat_end = q - 1;
	while (isspace((int) *q)) {
	    q++;
	}
	if (q != es) {
	    DEBUG2(DBG_CONF_LO, "foreach_clause(): junk after closing '}': '%V'\n", (size_t) (q - es), q);
	    return FAIL;			/* XXX syntax error */
	}
	break;

    default:
	pat = q;
	pat_end = es;
	break;
    }
    if (pat == pat_end) {
	DEBUG(DBG_CONF_LO, "foreach_clause(): empty string pattern\n");
	return FAIL;				/* emptiness */
    }
    /* XXX should check for (unescaped?) colons in pat... */

    /*
     * expand the list to see if there's anything in it to work on
     */
    list = substitute(addr, (char *) NULL, (char *) NULL, listname, listname_len);
    if (!list) {
	DEBUG2(DBG_CONF_LO, "foreach_clause(): substitute(addr, NULL, NULL, %S) failed\n", listname_len, listname);
	return FAIL;
    }
    list = COPY_STRING(list);
    ss = rcopy(pat, pat_end);

    DEBUG3(DBG_CONF_HI, "foreach_clause(): about to iterate over %S with '%v'\n", listname_len, listname, ss);
    /*
     * for each entry in the list, expand the the string segment and append the
     * result to the output list
     */
    for (item = strcolon(list); item; item = strcolon((char *) NULL)) {
	if (!q) {
	    STR_NEXT(str, ':');
	} else {
	    q = NULL;
	}
	if (expand_string_to(str, ss, addr, item) == FAIL) {
	    DEBUG3(DBG_CONF_LO, "foreach_clause(): expand_string_to(str, %S, '%v') failed\n", listname_len, listname, item);
	    return FAIL;
	}
    }

    if (list) {
	xfree(list);
    }
    if (ss) {
	 xfree(ss);
    }

    return SUCCEED;
}


/*
 * build_cmd_line - build up an arg vector suitable for execv()
 *
 * transports can call this to build up a command line in a standard
 * way.  Of course, if they want to they can build up a command line in
 * a totally different fashion.
 *
 * Caution: return value points to a region which may be reused by
 *	    subsequent calls to build_cmd_line()
 *
 * Notes on the replacement algorithm:
 *   o	Within a $( and $) pair, substitutions are made once for
 *	each address on the input list.
 *   o	Otherwise the substitution is made relative to the first
 *	address on the input list.
 *   o	Substitutions as per expand_string() [i.e. smail(5)]
 *   o	single quotes, double quotes and backslash work as with /bin/sh
 *   o  The "vector" is initially stored in a struct str with each item
 *	separated by a NUL byte, and then build_argv() is called to break
 *	it into a proper char *argv[];
 *
 * return NULL for parsing errors, and load `error' with a message
 * explaining the error.
 */
char **
build_cmd_line(cmd, addr, file, error)
    char *cmd;				/* input command line */
    struct addr *addr;			/* list of remote addresses */
    char *file;				/* substitution for $file */
    char **error;			/* error message */
{
    static struct str str;		/* generated region */
    static int inited = FALSE;		/* TRUE if str has been inited */
    char *mark;				/* temp mark in cmd line */
    char *var;				/* pointer to rcopy()ed $-expr from cmd */
    size_t olen;
    int ct = 1;				/* count of args, at least one */
    int state = 0;			/* notes about parse state */
    struct addr *save_addr = addr;	/* replace addr from this after $) */
    char *save_cmd = NULL;		/* start of a $( ... $) group */
    int last_char = '\0';		/* hold last *cmd value */

#define DQUOTE	0x01			/* double quote in effect */
#define GROUP	0x02			/* $( ... $) grouping in effect */

    DEBUG2(DBG_DRIVER_MID, "build_cmd_line(cmd='%v', file='%v') called\n", cmd, file);

    /* initialize for building up the arg vectors */
    if (! inited) {
	STR_INIT(&str);
	inited = TRUE;
    } else {
	STR_CHECK(&str);
	STR_CLEAR(&str);
    }

    while (*cmd) {
	switch (*cmd) {
	case '\'':
	    /* after "'" copy literally to before next "'" char */
	    /* XXX FIXME!!! what about quoted "'"? -- need skip_qstring(str, qchar) */
	    mark = strchr(cmd + 1, '\'');
	    if (mark == NULL) {
		*error = xprintf("no matching ' for cmd in transport %v",
				 addr->transport->name);
		return NULL;
	    }
	    STR_NCAT(&str, (cmd + 1), (size_t) (mark - cmd - 1));
	    last_char = '\'';
	    cmd = mark;
	    break;

	case '\\':
	    /*
	     * char after \ is literal, unless in quote, in which case
	     * this is not so if the following char is not " or $ or \
	     */
	    if (*cmd++ == '\0') {
		*error = "\\ at end of command";
		return NULL;
	    }
	    if (!(state & DQUOTE) || *cmd == '\\' || *cmd == '"' || *cmd == '$') {
		STR_NEXT(&str, *cmd);
	    } else {
		STR_NEXT(&str, '\\');
		STR_NEXT(&str, *cmd);
	    }
	    last_char = '\\';
	    break;

	case '"':			/* double quote is a toggle */
	    state ^= DQUOTE;
	    last_char = '"';
	    break;

	case '$':			/* perform parameter substitution */
	    cmd++;
	    if (*cmd == '\0') {
		*error = "$ at end of command";
		return NULL;
	    }
	    if (*cmd == '(') {
		if (state & GROUP) {
		    *error = "recursive $( ... $)";
		    return NULL;
		}
		if (state & DQUOTE) {
		    *error = "$( illegal inside \"...\"";
		    return NULL;
		}
		save_cmd = cmd;
		state |= GROUP;
		break;
	    }
	    if (*cmd == ')') {
		if ((state & GROUP) == 0) {
		    *error = "no match for $)";
		    return NULL;
		}
		if (state & DQUOTE) {
		    *error = "$) illegal inside \"...\"";
		    return NULL;
		}
		if (!isspace((int) last_char)) {
		    /* end previous vector, create a new one */
		    ct++;
		    STR_NEXT(&str, '\0');
		}
		addr = addr->succ;
		if (addr) {
		    cmd = save_cmd;
		} else {
		    /* no more addrs to put in group */
		    addr = save_addr;
		    state &= ~GROUP;
		}
		last_char = ' ';	/* don't create an extra vector */
		break;
	    }
	    if (*cmd == '{') {
		mark = cmd - 1;
		cmd++;
		if (skip_nesting(&cmd) == FAIL) {
		    *error = xprintf("no match for '{': %v", mark);
		    return NULL;
		}
	    } else {
		mark = cmd - 1;
		cmd = skip_ident(cmd + 1, cmd + strlen(cmd));
	    }
	    olen = STR_LEN(&str);	/* remember expanded len up to now */
	    /*
	     * we have to copy out the $-expr substring from cmd because the
	     * latter may not be in writable storage...
	     */
	    var = rcopy(mark, cmd);	/* XXX use a static struct str ??? */
	    if (expand_string_to(&str, var, addr, (char *) NULL) == FAIL) {
		/* XXX FIXME: This is a memory leak */
		*error = xprintf("bad substition: %v", mark);
		xfree(var);
		return NULL;
	    }
	    xfree(var);
	    if (STR_LEN(&str) == olen) {
		last_char = ' ';	/* expanded to nothing -- don't make a vector! */
	    } else {
		last_char = '$';
	    }
	    --cmd;			/* correct the next char pointer */
	    break;

	case ' ':			/* when not in a quote */
	case '\t':			/* white space separates words */
	case '\n':
	    if (state & DQUOTE) {
		STR_NEXT(&str, *cmd);
	    } else if (!isspace((int) last_char)) {
		/* end the previous arg vector */
		STR_NEXT(&str, '\0');
		ct++;			/* start a new one */
	    }
	    last_char = *cmd;
	    break;

	default:
	    STR_NEXT(&str, *cmd);
	    last_char = *cmd;
	}
	cmd++;				/* advance to next char */
    }
    if (state & DQUOTE) {
	*error = "no match for opening \"";
	return NULL;
    }
    if (state & GROUP) {
	*error = "no match for $(";
	return NULL;
    }
    if (isspace((int) last_char)) {
	--ct;				/* don't count just blanks */
    }
    STR_NEXT(&str, '\0');		/* null terminate the strings */

    DEBUG1(DBG_DRIVER_MID, "build_cmd_line() string count=%d.\n", ct);

    return build_argv(STR(&str), ct);
}

/*
 * build_argv - build arg vectors from inline strings
 *
 * build_cmd_line produces chars with null characters separating
 * strings.  build_argv takes these chars and turns them into
 * an arg vector suitable for execv.
 *
 * Caution: the value returned by build_argv() points to a region
 *	    which may be reused on subsequent calls to build_argv().
 */
static char **
build_argv(p, ct)
    register char *p;			/* strings, one after another */
    register int ct;			/* count of strings */
{
    static char **argv = NULL;		/* reusable vector area */
    static int argc;
    register char **argp;

    if (argv == NULL) {
	argc = ct + 1;
	argv = (char **)xmalloc(argc * sizeof(*argv));
    } else {
	if (ct + 1 > argc) {
	    X_CHECK(argv);
	    argc = ct + 1;
	    argv = (char **)xrealloc((char *)argv, argc * sizeof(*argv));
	}
    }
    argp = argv;
    DEBUG(DBG_DRIVER_MID, "cmd =");
    while (ct--) {
	*argp++ = p;
	DEBUG1(DBG_DRIVER_MID, " '%v'", p);
	if (ct) {
	    while (*p++) ;		/* scan for next string */
	}
    }
    DEBUG(DBG_DRIVER_MID, "\n");
    *argp = NULL;			/* terminate vectors */
    return argv;
}

/*
 * substitute - relace a $paramater with its value
 *
 * may return NULL if substitution fails or has no value
 *
 * XXX returns pointers to static storage or storage held in some other
 * structure such as configuration values or address related values.
 */
static char *
substitute(addr, file, found, var, len)
    struct addr *addr;			/* source for $host, $addr, $user */
    char *file;				/* source for $file */
    char *found;			/* $value value */
    register char *var;			/* start of variable */
    register size_t len;		/* length of variable */
{
    static char buf[MAXLONG_B10_DIGITS + 1];
    struct attr_table *conf_attr;
    char *varname;
    char *temp_str = NULL;
    char *result;

#define SLEN(x)		(sizeof(x) - 1)

    DEBUG6(DBG_DRIVER_HI, "substitute('%v', '%v', '%v', '%S', %u) called\n",
	   addr->in_addr ? addr->in_addr : "(no-addr)",
	   file ? file : "(no-file)",
	   found ? found : "(no-$value)",
	   var ? len : SLEN("(no-var)"),
	   var ? var : "(no-var)",
	   len);

    if (!var || len == 0) {
	return NULL;
    }

#define PREFMATCH(v, l, x)	(l > SLEN(x) && strncmpic(v, x, SLEN(x)) == 0)
#define MATCH(v, l, x)		(l == SLEN(x) && strncmpic(v, x, SLEN(x)) == 0)

    if (PREFMATCH(var, len, "lc:")) {
	if (!(temp_str = substitute(addr, file, found, var + SLEN("lc:"), len - SLEN("lc:")))) {
	    return NULL;
	}
	temp_str = COPY_STRING(temp_str);
	result = lc_fold(temp_str);
	xfree(temp_str);
	return result;
    }
    if (PREFMATCH(var, len, "uc:")) {
	if (!(temp_str = substitute(addr, file, found, var + SLEN("uc:"), len - SLEN("uc:")))) {
	    return NULL;
	}
	temp_str = COPY_STRING(temp_str);
	result = uc_fold(temp_str);
	xfree(temp_str);
	return result;
    }
    if (PREFMATCH(var, len, "quote:")) {
	if (!(temp_str = substitute(addr, file, found, var + SLEN("quote:"), len - SLEN("quote:")))) {
	    return NULL;
	}
	temp_str = COPY_STRING(temp_str);
	result = quote(temp_str, FALSE, FALSE);
	xfree(temp_str);
	return result;
    }
#ifdef notyet
    if (PREFMATCH(var, len, "phquote:")) {
	if (!(temp_str = substitute(addr, file, found, var + SLEN("phquote:"), len - SLEN("phquote:")))) {
	    return NULL;
	}
	temp_str = COPY_STRING(temp_str);
	result = phrase_quote(temp_str);
	xfree(temp_str);
	return result;
    }
#endif
    if (PREFMATCH(var, len, "shquote:")) {
	if (!(temp_str = substitute(addr, file, found, var + SLEN("shquote:"), len - SLEN("shquote:")))) {
	    return NULL;
	}
	temp_str = COPY_STRING(temp_str);
	result = shell_quote(temp_str);
	xfree(temp_str);
	return result;
    }
    if (PREFMATCH(var, len, "rxquote:")) {
	if (!(temp_str = substitute(addr, file, found, var + SLEN("rxquote:"), len - SLEN("rxquote:")))) {
	    return NULL;
	}
	temp_str = COPY_STRING(temp_str);
	result = regex_quote(temp_str);
	xfree(temp_str);
	return result;
    }
    if (PREFMATCH(var, len, "strip:")) {
	if (!(temp_str = substitute(addr, file, found, var + SLEN("strip:"), len - SLEN("strip:")))) {
	    return NULL;
	}
	temp_str = COPY_STRING(temp_str);
	result = strip_quotes(temp_str);
	xfree(temp_str);
	return result;
    }
    if (PREFMATCH(var, len, "eval:")) {
	static struct str str;		/* build strings here */

	if (!addr) {			/* expand string would fake it, but we won't... */
	    return NULL;
	}
	if (!(temp_str = substitute(addr, file, found, var + SLEN("eval:"), len - SLEN("eval:")))) {
	    return NULL;
	}
	temp_str = COPY_STRING(temp_str);
	DEBUG3(DBG_DRIVER_HI, "substitute(): about to re-evalutate value of %S: '%v'\n",
	       len - SLEN("eval:"), var + SLEN("eval:"), temp_str);
	STR_INIT(&str);
	if (expand_string_to(&str, temp_str, addr, (char *) NULL) == FAIL) {
	    return NULL;
	}
	STR_NEXT(&str, '\0');
#if 0
	xfree(temp_str);		/* may be included in expand_string_to() value */
#endif
	return STR(&str);
    }
    if (PREFMATCH(var, len, "parent:")) {
	if (!addr || !addr->parent) {
	    return NULL;
	}
	return substitute(addr->parent, file, found, var + SLEN("parent:"), len - SLEN("parent:"));
    }
    if (PREFMATCH(var, len, "true_addr:")) {
	if (!addr || !addr->true_addr) {
	    return NULL;
	}
	return substitute(addr->true_addr, file, found, var + SLEN("true_addr:"), len - SLEN("true_addr:"));
    }
    if (PREFMATCH(var, len, "top:")) {
	struct addr *top = addr;

	if (!top) {
	    return NULL;
	}
	while (top->parent) {
	    top = top->parent;
	}
	return substitute(top, file, found, var + SLEN("top:"), len - SLEN("top:"));
    }
    if (MATCH(var, len, "value")) {
	return found;
    }
    if (MATCH(var, len, "grade")) {
	static char grade_str[2] = { 0, 0 };
	grade_str[0] = msg_grade;
	return grade_str;
    }
    if (MATCH(var, len, "user") || MATCH(var, len, "addr")) {
	return addr ? addr->next_addr : NULL;
    }
    if (MATCH(var, len, "host")) {
	return addr ? addr->next_host : NULL;
    }
    if (MATCH(var, len, "route")) {
	return addr ? addr->route : NULL;
    }
    if (MATCH(var, len, "input_addr")) {
	return addr ? addr->in_addr : NULL;
    }
    if (MATCH(var, len, "mailbox")) {
	return addr ? addr->remainder : NULL;
    }
    if (MATCH(var, len, "user_prefix")) {
	return addr ? addr->rem_prefix : NULL;
    }
    if (MATCH(var, len, "user_suffix")) {
	return addr ? addr->rem_suffix : NULL;
    }
    if (MATCH(var, len, "target_domain")) {
	/*
	 * XXX do we ever want primary_name here???
	 *
	 * the problem is that the default received_field appends the following
	 * to the "for" field:
	 *
	 *	${if def{top:target_domain} {@${top:target_domain}}}
	 *
	 * For a "local" user a NULL return would fail to show where the mail
	 * was originally addressed to, but local_name should always be the
	 * right thing, but is there always a local_name?  If not then we do
	 * need to fall back to primary_name, or do we?
	 *
	 * The only other remaining use of target_domain should be also in the
	 * received_field where it's used similarly to above to qualify the
	 * "next:" comment included when resolving somehow results in a parent
	 * address.
	 */
	return addr ? (addr->target ? addr->target :
				      (addr->local_name ? addr->local_name : primary_name)) : NULL;
    }
    if (MATCH(var, len, "target")) {
	return addr ? addr->target : NULL;
    }
    if (MATCH(var, len, "local_domain")) {
	return addr ? addr->local_name : NULL;
    }
    if (MATCH(var, len, "HOME") || MATCH(var, len, "home")) {
	return addr ? addr->home : NULL;
    }
    if (MATCH(var, len, "sender") || MATCH(var, len, "from")) {
	return sender;
    }
    if (MATCH(var, len, "owner")) {
	return addr ? (addr->owner) : NULL;
    }
    if (MATCH(var, len, "sender_name") || MATCH(var, len, "fullname")) {
	if (sender_name == NULL && islocal) {
	    getfullname();
	}
	return sender_name;
    }
    if (MATCH(var, len, "file")) {
	return file;
    }
    if (MATCH(var, len, "message_id") || MATCH(var, len, "id")) {
	return message_id;
    }
    if (MATCH(var, len, "message_size") || MATCH(var, len, "size")) {
	sprintf(buf, "%lu", (unsigned long int) msg_size);
	return buf;
    }
    if (MATCH(var, len, "message_body_size") || MATCH(var, len, "body_size")) {
	sprintf(buf, "%lu", msg_body_size);
	return buf;
    }
    if (MATCH(var, len, "ctime")) {
	return unix_date();
    }
    if (MATCH(var, len, "date")) {
	/* get the current date in ARPA format */
	return get_arpa_date(time((time_t *) NULL));
    }
    if (MATCH(var, len, "spool_date")) {
	/* get the spool date in ARPA format */
	return get_arpa_date(message_date());
    }
    if (MATCH(var, len, "$") || MATCH(var, len, "pid")) {
	(void) sprintf(buf, "%ld", (long) getpid());
	return buf;
    }
    if (MATCH(var, len, "uucp_name")) {
	return uucp_name;
    }
    if (MATCH(var, len, "visible_name") || MATCH(var, len, "name")) {
	return visible_name;
    }
    if (MATCH(var, len, "primary_name") || MATCH(var, len, "primary")) {
	return primary_name;
    }
    if (MATCH(var, len, "version")) {
	return version_number;
    }
    if (MATCH(var, len, "version_string")) {
	return version();
    }
    if (MATCH(var, len, "release_date") || MATCH(var, len, "release")) {
	return release_date;
    }
    if (MATCH(var, len, "compile_num") || MATCH(var, len, "ld_num")) {
	static char s_compile_num[MAXINT_B10_DIGITS + 1];
	(void) sprintf(s_compile_num, "%d", compile_num);
	return s_compile_num;
    }
    if (MATCH(var, len, "compile_date") || MATCH(var, len, "ld_date")) {
	return compile_date;
    }
    if (MATCH(var, len, "smail_lib_dir") || MATCH(var, len, "lib_dir")) {
	return smail_lib_dir;
    }
    if (MATCH(var, len, "sender_host")) {
	return sender_host;
    }
    if (MATCH(var, len, "sender_host_addr")) {
	return sender_host_addr;
    }
    if (MATCH(var, len, "sender_host_port")) {
	return sender_host_port;
    }
    if (MATCH(var, len, "sender_host_really")) {
	return sender_host_really;
    }
    if (MATCH(var, len, "smtp_local_addr")) {
	return smtp_local_addr;
    }
    if (MATCH(var, len, "smtp_local_port")) {
	return smtp_local_port;
    }
    if (MATCH(var, len, "sender_proto")) {
	return sender_proto;
    }
    if (MATCH(var, len, "ident_sender")) {
	return ident_sender;
    }
    if (MATCH(var, len, "ident_method")) {
	return ident_method;
    }
    if (MATCH(var, len, "program")) {
	return program;
    }
    if (MATCH(var, len, "sender_program")) {
	return sender_program ? sender_program : program;
    }
    if (MATCH(var, len, "director")) {
	return (addr && addr->director) ? addr->director->name : NULL;
    }
    if (MATCH(var, len, "router")) {
	return (addr && addr->router) ? addr->router->name : NULL;
    }
    if (MATCH(var, len, "transport")) {
	return (addr && addr->transport) ? addr->transport->name : NULL;
    }
    varname = strncpy(xmalloc(len + 1), var, len);
    varname[len] = '\0';
    conf_attr = find_config_attribute(varname);
    if (conf_attr) {
	char *prefix;			/* we ignore it in this case */
	char *conf_value;
	int ret;
	unsigned long flags = 0;

	DEBUG1(DBG_CONF_HI, "substitute(): found config attr: %s\n", varname);
	if (conf_attr->type == t_boolean) {
	    flags = conf_attr->uptr->v_boolean;
	}
	/* pretty print it if debugging is enabled */
 	ret = format_attribute(conf_attr, var, FALSE, &prefix, &conf_value, (char *) NULL, flags);
	if (ret) {
	    xfree(varname);
	    if (conf_value) {
		/* config attrs may contain variable expansions.... */
		return conf_value;
	    } else {
		return "";
	    }
	} else {
	    DEBUG1(DBG_CONF_MID, "substitute(): format_attribute(%v): failed (unknown type?)!\n", varname);
	    /* XXX panic()? */
	}
    } else {
	DEBUG1(DBG_CONF_MID, "substitute(): unknown variable or config attribute: %v\n", varname);
    }
    xfree(varname);

    return NULL;			/* no match */
#undef MATCH
#undef SLEN
}

/*
 * lc_fold - meta substitution to convert value to lower case
 */
static char *
lc_fold(value)
    register char *value;
{
    static char *lc = NULL;		/* retained malloc region */
    static size_t lc_size;		/* keep size of allocated region */
    size_t value_size;
    register char *p;			/* for scanning through lc */

    if (value == NULL) {
	return NULL;
    }
    value_size = strlen(value) + 1;

    /* get a region at least large enough for the value */
    if (lc == NULL) {
	lc = xmalloc(lc_size = value_size);
    } else if (value_size > lc_size) {
	X_CHECK(lc);
	lc = xrealloc(lc, lc_size = value_size);
    }
    p = lc;
    while (*value) {
	*p++ = tolower((int) *value++);
    }
    *p = '\0';

    return lc;
}

/*
 * uc_fold - meta substitution to convert value to upper case
 */
static char *
uc_fold(value)
    register char *value;
{
    static char *uc = NULL;		/* retained malloc region */
    static size_t uc_size;		/* keep size of allocated region */
    size_t value_size;
    register char *p;			/* for scanning through lc */

    if (value == NULL) {
	return NULL;
    }
    value_size = strlen(value) + 1;

    /* get a region at least large enough for the value */
    if (uc == NULL) {
	uc = xmalloc(uc_size = value_size);
    } else if (value_size > uc_size) {
	X_CHECK(uc);
	uc = xrealloc(uc, uc_size = value_size);
    }
    p = uc;
    while (*value) {
	*p++ = toupper((unsigned char) *value++);
    }
    *p = '\0';

    return uc;
}

/*
 * shell_quote - meta substitution to quote value for shell
 *
 * Tidies up a string to allow it to be passed to a shell command
 * without nasty effects.  This makes the whole string single quoted
 * (ie one shell argument), and escapes any internal quote characters.
 *
 * XXX move to string.c?
 */
static char * 
shell_quote(value)
     register char * value;
{
    static struct str wkstr;
    static int initialised = FALSE;
    char * ptr;
    register int chr;

    if (!value) {
	return NULL;
    }
    if (!initialised) {
	initialised = TRUE;
	STR_INIT(&wkstr);
    } else {
	STR_CHECK(&wkstr);
	STR_CLEAR(&wkstr);
    }
    STR_NEXT(&wkstr, '\'');
    for (ptr = value; ((chr = *ptr) != 0); ptr++) {
	switch (chr) {
	case '\\':
	    STR_NEXT(&wkstr, chr);
	    if (! (chr = *++ptr)) {	/* premature end of string */
		STR_NEXT(&wkstr, '\\');
		ptr--;			/* will force loop to terminate */
	    } else {
		STR_NEXT(&wkstr, chr);
	    }
	    break;
	case '\'':
	    STR_NEXT(&wkstr, '\'');	/* end single-quoted */
	    STR_NEXT(&wkstr, '"');	/* start double-quoted */
	    STR_NEXT(&wkstr, '\'');	/* the single quote */
	    STR_NEXT(&wkstr, '"');	/* end double-quoted */
	    STR_NEXT(&wkstr, '\'');	/* restart single-quoted */
	    break;
	default:
	    STR_NEXT(&wkstr, chr);
	}
    }
    STR_NEXT(&wkstr, '\'');		/* Trailing quotes */
    STR_NEXT(&wkstr, '\0');		/* Trailing NUL */

    return STR(&wkstr);
}

/*
 * regex_quote - meta substitution to quote value for Regular Expressions
 *
 * Escapes any RE meta-characters in a string so that it will be matched
 * exactly, and not be treated as an RE.
 *
 * XXX move to string.c?
 */
static char * 
regex_quote(value)
     register char * value;
{
    static struct str wkstr;
    static int initialised = FALSE;
    register int chr;

    if (!value) {
	return NULL;
    }
    if (!initialised) {
	initialised = TRUE;
	STR_INIT(&wkstr);
    } else {
	STR_CHECK(&wkstr);
	STR_CLEAR(&wkstr);
    }
    while ((chr = *value++)) {
	if (isalnum((int) chr) || chr == ':' || chr == ';') {
	    STR_NEXT(&wkstr, chr);
	} else {
	    STR_NEXT(&wkstr, '\\');
	    STR_NEXT(&wkstr, chr);
	}
    }
    STR_NEXT(&wkstr, '\0');		/* Trailing NUL */

    return STR(&wkstr);
}


/*
 * strip_quotes - strip quotes and collapse spaces and dots
 *
 * strip quotes from the input string, then remove leading `-' characters and
 * collapse any sequence of one or more white space characters and `.'
 * characters into a single `.'.  (i.e. s|[\. \t\n\r]*|.|g)
 */
static char *
strip_quotes(value)
    char *value;
{
    static char *strip_buf = NULL;	/* retained malloc region */
    static size_t strip_size;		/* keep size of allocated region */
    size_t value_size;
    register char *p;			/* for scanning through strip_buf */
    register char *q;			/* also for scanning strip_buf */

    if (value == NULL) {
	return NULL;
    }
    value_size = strlen(value) + 1;
    if (strip_buf == NULL) {
	strip_buf = xmalloc(strip_size = value_size);
    } else if (value_size > strip_size) {
	X_CHECK(strip_buf);
	strip_buf = xrealloc(strip_buf, strip_size = value_size);
    }
    (void) strcpy(strip_buf, value);
    (void) strip(strip_buf);

    /* q reads and p writes */
    p = q = strip_buf;
    /* strip initial -'s */
    while (*q == '-') {
	q++;
    }
    while (*q) {
	/* collapse multiple white-space chars and .'s into single dots */
	if (isspace((int) *q) || *q == '.') {
	    while (isspace((int) *++q) || *q == '.') {
		;
	    }
	    *p++ = '.';
	    continue;
	}
	*p++ = *q++;
    }
    *p = '\0';			/* finish off strip_buf */

    return strip_buf;
}

/*
 * skip_ident - skip a variable identifier
 */
static char *
skip_ident(s, lim)
    char *s, *lim;
{
    char *p = s;
    while (*p && (lim == NULL || p < lim)) {
	if (p != s && !isalnum(*p & 0xFF) && *p != '_') {
	    break;
	}
	++p;
    }
    return p;
}

/*
 * clause_token - extract a name or {...} token from the expansion string
 *
 * Used within ${...} substrings.
 */

static int
clause_token(sp, startp, lenp)
    char **sp;
    char **startp;
    size_t *lenp;
{
    char *s = *sp;

    while (isspace((int) *s)) {
	s++;
    }
    switch (*s) {
    case '\0':
    case '}':
	return FAIL;

    case '{':
	s++;
	*startp = s;
	if (skip_nesting(&s) == FAIL) {
	    return FAIL;
	}
	*lenp = s - *startp - 1;
	*sp = s;
	return SUCCEED;

    case ':':
	s++;
	while (isspace((int) *s)) {
	    s++;
	}
	/* FALLTHRU */

    default:
	*startp = s;
	if (*s == ':' || *s == '{' || *s == '}') {
	    return FAIL;			/* XXX syntax error? */
	}
	s++;
	while (*s) {
	    switch (*s) {
	    case '_':
	    case '-':
	    case '.':
	    case '/':
		s++;
		continue;

	    default:
		if (isalnum((int) *s)) {
		    s++;
		    continue;
		}
		break;
	    }
	    break;
	}
	*lenp = s - *startp;
	*sp = s;
	break;
    }

    return SUCCEED;
}

static int
skip_nesting(sp)
    char **sp;
{
    int nesting = 1;
    char *s = *sp;

    while (nesting) {
	switch (*s++) {
	case '\0':
	    bad_subst(*sp, (size_t) 0, "no matching '}'");
	    return FAIL;
	case '{':
	    ++nesting;
	    break;
	case '}':
	    --nesting;
	    break;
	}
    }
    *sp = s;

    return SUCCEED;
}

/*
 * bad_subst - generate a debugging message for a failed substitution.
 */
static void
bad_subst(var, len, msg)
    char *var;
    size_t len;
    char *msg;
{
    if (len == 0) {
	len = strlen(var);
    }
    write_log(WRITE_LOG_PANIC | WRITE_LOG_TTY,
	      "expand_string(): expansion failed for ${%S}%s%s",
	      len, var,
	      msg ? ": " : "",
	      msg ? msg : "");

    return;
}

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