/*
#ident	"@(#)smail/src:RELEASE-3_2_0_114:notify.c,v 1.46 2001/08/03 13:50:29 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.
 */

/*
 * notify.c:
 *	keep track of errors and handle notification of the appropriate
 *	users for errors.
 *
 *	external functions: defer_delivery, fail_delivery, succeed_delivery,
 *			    error_delivery, process_msg_log, notify,
 *                          hash_predelivered_addresses
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <ctype.h>
#include <pwd.h>
#include <time.h>
#include "defs.h"
#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif
#include "smail.h"
#include "parse.h"
#include "addr.h"
#include "hash.h"
#include "main.h"
#include "log.h"
#include "direct.h"
#include "route.h"
#include "transport.h"
#include "alloc.h"
#include "dys.h"
#include "child.h"
#include "exitcodes.h"
#include "extern.h"
#include "debug.h"

/* exported variables */
int send_to_postmaster;			/* set TRUE to mail to postmaster */
int return_to_sender;			/* set TRUE to mail log to sender */

/* functions local to this file */
static char *decode_x_line __P((char *, char **, char **));
static void put_defer_error __P((struct defer_addr **, char *, char *, char *));
static void classify_addr __P((struct addr *, struct addr **, struct addr **));
static void notify_new_message __P((FILE *, char *, char *, char *,char *));
static struct addr *remove_addr __P((struct addr *, char *, char *));
static void write_notify_header __P((FILE *));

/* variables local to this file */
static char *log_banner = "\
|------------------------- Message log follows: -------------------------|\n";
static char *addr_error_banner = "\
|------------------------- Failed addresses follow: ---------------------|\n";
static char *text_banner = "\
|------------------------- Message text follows: ------------------------|\n";


/*
 * fail_delivery - log that delivery to the set of addresses failed
 */
void
fail_delivery(addr)
    struct addr *addr;			/* list of failed addrs */
{
    register struct addr *cur;
    register struct addr *top;

    if (addr && exitvalue == EX_OK) {
	exitvalue = EX_UNAVAILABLE;
    }
    for (cur = addr; cur; cur = cur->succ) {
	/* find the top parent to log the original in_addr */
	for (top = cur; top->parent && top->parent->in_addr; top = top->parent) {
	    ;
	}
	if (top == cur) {
	    top = NULL;
	}
#if (SMAIL_LOG_STYLE == 2)
	/* Default new log style (as of 3.1.29) */
	/* This is SMAIL_LOG_STYLE=2 */
	write_log(WRITE_LOG_SYS,
		  "Failed TO:%s%s%s%s%s%s%s%s%s ERROR:(ERR%03ld) %s",
		  cur->next_addr ? cur->next_addr : cur->in_addr,
		  top ? " ORIG-TO:" : "", top ? top->in_addr : "",
		  cur->router ? " ROUTER:" : "", cur->router ? cur->router->name : "",
		  cur->director ? " DIRECTOR:" : "", cur->director ? cur->director->name : "",
		  cur->transport ? " TRANSPORT:" : "", cur->transport ? cur->transport->name : "",
		  cur->error->info & ERR_MASK,
		  cur->error->message);
#else
# if (!defined(SMAIL_LOG_STYLE)) || (SMAIL_LOG_STYLE == 1)
    /* Old log style (as used by 3.1.28) */
    /* This is SMAIL_LOG_STYLE=1 */
	write_log(WRITE_LOG_SYS,
		  "%s %s%s%s... failed: (ERR_%03ld) %s",
		  cur->next_addr ? cur->next_addr : cur->in_addr,
		  top ? "(orig-to: " : "",
		  top ? top->in_addr : "",
		  top ? ") " : "",
		  cur->error->info & ERR_MASK,
		  cur->error->message);
# else
#  include "ERROR - SMAIL_LOG_STYLE set to invalid value."
# endif
#endif
	/* write MLOG X entries for failed recipients so they be ignored up by
	 * queue run scans (see decode_x_line() below for record format details)
	 */
	write_log(WRITE_LOG_MLOG,
		  "Xfail: <%s>%s%s%s reason: (ERR%03ld) %s",
		  cur->in_addr,
		  cur->parent ? " parent: <" : "",
		  cur->parent ? cur->parent->in_addr : "",
		  cur->parent ? ">" : "",
		  cur->error->info & ERR_MASK,
		  cur->error->message);
    }
}

/*
 * defer_delivery - log that delivery to the set of addresses is deferred
 *
 * don't write an error message, if it would be a repeat of the most
 * recent deferal message written to the message log file for the
 * address.
 */
void
defer_delivery(addr, defer_errors)
    struct addr *addr;			/* list of succeeded addrs */
    struct defer_addr *defer_errors;	/* previous deferals */
{
    register struct addr *cur;
    register struct addr *top;
    struct defer_addr *defer;

    if (addr && exitvalue == EX_OK) {
	exitvalue = EX_TEMPFAIL;
    }

    for (cur = addr; cur; cur = cur->succ) {
	/* find the top parent to log the original in_addr */
	for (top = cur; top->parent && top->parent->in_addr; top = top->parent) {
	    ;
	}
	if (top == cur) {
	    top = NULL;
	}
	/* if the only cause of the deferral is that the retry interval has not
	 * been reached we will be told not to log the cause...
	 */
	if (cur->error->info & ERR_DONTLOG) {
#ifndef NODEBUG
	    /* but, if debugging, generate a message for human reader */
	    if (errfile && debug > 1) {
		fprintf(errfile,
			"%s %s%s%s... deferred: (ERR_DONTLOG|ERR%03ld) %s\n",
			cur->next_addr ? cur->next_addr : cur->in_addr,
			top ? "(orig-to: " : "",
			top ? top->in_addr : "",
			top ? ") " : "",
			cur->error->info & ERR_MASK,
			cur->error->message);
	    }
#endif	/* ! NODEBUG */
	    continue;
	}
	/*
	 * avoid redundant messages
	 *
	 * first we search through the list of error addresses which were
	 * recorded in the MLOG file to find one that's got the same ...
	 */
	for (defer = defer_errors; defer; defer = defer->succ) {
	    if (EQ(defer->address, cur->next_addr ? cur->next_addr : cur->in_addr) ||
		(! defer->parent && ! top) ||
		(defer->parent && top && top->in_addr && EQ(defer->parent, top->in_addr)))
	    {
		break;
	    }
	}
	/*
	 * now if we've found a message matching the one we're about to log
	 * then we'll skip logging this one...
	 */
	if (defer) {
	    if (defer->error == (cur->error->info & ERR_MASK) &&
		EQ(defer->message, cur->error->message))
	    {
		DEBUG(DBG_NOTIFY_MID, "defer_delivery(): not logging repeated deferral message.\n");
		continue;			/* XXX was `break'! */
	    }
	}
#if (SMAIL_LOG_STYLE == 2)
	/* Default new log style (as of 3.1.29) */
	/* This is SMAIL_LOG_STYLE=2 */
	write_log((cur->error->info & ERR_CONFERR)? (WRITE_LOG_SYS|WRITE_LOG_TTY): WRITE_LOG_SYS,
		  "Deferred TO:%s%s%s%s%s%s%s%s%s ERROR:(ERR%03ld) %s",
		  cur->next_addr ? cur->next_addr : cur->in_addr,
		  top ? " ORIG-TO:" : "", top ? top->in_addr : "",
		  cur->router ? " ROUTER:" : "", cur->router ? cur->router->name : "",
		  cur->director ? " DIRECTOR:" : "", cur->director ? cur->director->name : "",
		  cur->transport ? " TRANSPORT:" : "", cur->transport ? cur->transport->name : "",
		  cur->error->info & ERR_MASK,
		  cur->error->message);
#else
# if (!defined(SMAIL_LOG_STYLE)) || (SMAIL_LOG_STYLE == 1)
    /* Old log style (as used by 3.1.28) */
    /* This is SMAIL_LOG_STYLE=1 */
	write_log((cur->error->info & ERR_CONFERR)? (WRITE_LOG_SYS|WRITE_LOG_TTY): WRITE_LOG_SYS,
		  "%s %s%s%s... deferred: (ERR_%03ld) %s",
		  cur->next_addr ? cur->next_addr : cur->in_addr,
		  top ? "(orig-to: " : "",
		  top ? top->in_addr : "",
		  top ? ") " : "",
                  cur->error->info & ERR_MASK,
                  cur->error->message);
# else
#   include "ERROR - SMAIL_LOG_STYLE set to invalid value."
# endif
#endif
	/* write MLOG X entries for recipients experiencing deferred delivery
	 * so they be picked up by queue run scans and used to avoid writing
	 * duplicate deferral messages as is done above (see decode_x_line()
	 * below for record format details)
	 */
	write_log(WRITE_LOG_MLOG,
		  "Xdefer: <%s>%s%s%s reason: (ERR%03ld) %s",
		  cur->in_addr,
		  cur->parent ? " parent: <" : "",
		  cur->parent ? cur->parent->in_addr : "",
		  cur->parent ? ">" : "",
		  cur->error->info & ERR_MASK,
		  cur->error->message);
    }
}

/*
 * succeed_delivery - log that delivery to the set of addresses succeeded
 */
void
succeed_delivery(addr)
    struct addr *addr;			/* list of succeeded addrs */
{
    register struct addr *cur;
    register struct addr *top;

    for (cur = addr; cur; cur = cur->succ) {
	/* find the top parent to log the original in_addr */
	for (top = cur; top->parent && top->parent->in_addr; top = top->parent) {
	    ;
	}
	if (top == cur) {
	    top = NULL;
	}
	if (dont_deliver) {
	    write_log(WRITE_LOG_SYS, "Debugging: don't deliver <%s>%s%s%s",
		      cur->next_addr,
		      top ? " orig-to: <" : "",
		      top ? top->in_addr : "",
		      top ? ">" : "");
	} else {
#if (SMAIL_LOG_STYLE == 2)
	    /* Default new log style (as of 3.1.29) */
	    /* This is SMAIL_LOG_STYLE=2 */
	    write_log(WRITE_LOG_SYS, "Delivered%s%s%s%s%s%s%s%s%s%s",
		      cur->next_host ? " VIA:" : "",
		      cur->next_host ?  cur->next_host : "",
		      " TO:",
		      cur->next_addr,
		      top ? " ORIG-TO:" : "",
		      top ? top->in_addr: "",
		      cur->router ? " ROUTER:" : "",
		      cur->router ? cur->router->name : "",
		      " TRANSPORT:",
		      cur->transport->name);
#else
# if (!defined(SMAIL_LOG_STYLE)) || (SMAIL_LOG_STYLE == 1)
	    /* Old log style (as used by 3.1.28) */
	    /* This is SMAIL_LOG_STYLE=1 */
	    write_log(WRITE_LOG_SYS, "delivered%s%s%s%s%s%s%s%s%s%s",
		      cur->next_host ? "\n|\t      via: " : "",
		      cur->next_host ? cur->next_host : "",
		      "\n|\t       to: ",
		      cur->next_addr,
		      top ? "\n|\t  orig-to: " : "",
		      top ? top->in_addr : "",
		      cur->router ? "\n|\t   router: " : "",
		      cur->router ? cur->router->name : "",
		      "\n|\ttransport: ",
		      cur->transport->name);
# else
#  include "ERROR - SMAIL_LOG_STYLE set to invalid value."
# endif
#endif
	}
	/* write MLOG X entries for all recipients that have already been
	 * delivered to so they can be ignored by queue run scans (see
	 * decode_x_line() below for record format details)
	 */
	write_log(WRITE_LOG_MLOG, "Xsucceed: <%s>%s%s%s",
		  cur->in_addr,
		  cur->parent ? " parent: <" : "",
		  cur->parent ? cur->parent->in_addr : "",
		  cur->parent ? ">" : "");
    }
}

/*
 * error_delivery - log that an error notification was sent for the given list
 * of recipients.
 *
 * detect mail sent to owners and log the original address.
 */
void
error_delivery(addr)
    struct addr *addr;		/* recipients errors have been sent for */
{
    register struct addr *cur;
    register struct addr *top;

    for (cur = addr; cur; cur = cur->succ) {
	struct addr *log_addr = NULL;

	/* find the top parent to log the original in_addr */
	for (top = cur; top->parent && top->parent->in_addr; top = top->parent) {
	    ;
	}
	if (top == cur) {
	    top = NULL;
	}

	if (top && top->true_addr) {
	    log_addr = top->true_addr;
	} else if (cur->true_addr) {
	    log_addr = cur->true_addr;
	}
	if (log_addr) {
#if (SMAIL_LOG_STYLE == 2)
	    write_log(WRITE_LOG_SYS, "Error sent FOR:%s TO:%s%s%s",
		      log_addr->in_addr,
		      cur->next_addr ? cur->next_addr : cur->in_addr,
		      top ? " ORIG-TO:" : "",
		      top ? top->in_addr : "");	/* XXX may be wrong or confusing */
#else
	    write_log(WRITE_LOG_SYS, "%s ... error sent to %s%s%s%s",
		      log_addr->in_addr,
		      cur->next_addr ? cur->next_addr : cur->in_addr,
		      top ? " (orig-to " : "",
		      top ? top->in_addr : "",
		      top ? ")" : "");
#endif
	} else if (cur->error) {
	    if (cur->error->info & ERR_NSENDER) {
#if (SMAIL_LOG_STYLE == 2)
		write_log(WRITE_LOG_SYS, "Returned error FOR:%s TO:%s",
			  top ? top->in_addr : cur->in_addr, sender);
#else
		write_log(WRITE_LOG_SYS, "%s ... error returned to %s",
			  top ? top->in_addr : cur->in_addr, sender);
#endif
	    }
	    if (cur->error->info & ERR_NPOSTMAST) {
#if (SMAIL_LOG_STYLE == 2)
		write_log(WRITE_LOG_SYS, "Sent error FOR:%s TO:postmaster",
			  top ? top->in_addr : cur->in_addr);
#else
		write_log(WRITE_LOG_SYS, "%s ... error sent to postmaster",
			  top ? top->in_addr : cur->in_addr);
#endif
	    }
	} else {			/* XXX this should be impossible! */
#if (SMAIL_LOG_STYLE == 2)
		write_log(WRITE_LOG_SYS, "Sent error without error info FOR:%s (parent=%s)",
			  cur->next_addr ? cur->next_addr : cur->in_addr,
			  top ? top->in_addr : "(none)");
#else
		write_log(WRITE_LOG_SYS, "%s ... error sent without error info (parent=%s)",
			  cur->next_addr ? cur->next_addr : cur->in_addr,
			  top ? top->in_addr : "(none)");
#endif
	}
	/* write MLOG X entries for recipients that error notifications have
	 * already been sent so so they can be ignored up by queue run scans
	 * (see decode_x_line() below for record format details)
	 */
	write_log(WRITE_LOG_MLOG, "Xsent_error: <%s>%s%s%s",
		  cur->in_addr,
		  cur->parent ? " parent: <" : "",
		  cur->parent ? cur->parent->in_addr : "",
		  cur->parent ? ">" : "");

    }
}


/*
 * process_msg_log - process X lines in the per-message log
 *
 * Lines beginning with X in the per-message log contain information on what
 * mail processing was completed in a previous run on the input spool file.
 *
 * This function takes, as input, the set of addresses which were produced by
 * resolve_addr_list() for delivery and returns a list which does not contain
 * addresses which have already been delivered, or which have already
 * permanently failed delivery.
 *
 * A list of addresses for which notifications have already been sent is
 * returned by reference through the sent_errors parameter.  This list is
 * passed to the notify() routine so it can avoid sending duplicate
 * notifications.
 *
 * A list of addresses which have been deferred is also created and returned by
 * reference through the defer_errors parameter so that defer_delivery() can
 * avoid recording duplicate deferral messages.
 */
struct addr *
process_msg_log(in, sent_errors, defer_errors)
    struct addr *in;			/* input resolved addresses */
    struct identify_addr **sent_errors;	/* return errors that have been sent */
    struct defer_addr **defer_errors;	/* last defer message for addrs */
{
    register struct addr *ret;		/* addr list to return */
    char *s;				/* line of text from scan_msg_log() */
    char *m;				/* defer message */

    ret = in;

    /* scan through all of the lines marked X in the message log file */
    for (s = scan_msg_log(TRUE); s; s = scan_msg_log(FALSE)) {
	char *address;			/* the address to remove */
	char *parent;			/* parent of address to remove */

	if (strncmp(s, "Xfail:", sizeof("Xfail:") - 1) == 0) {
	    /* get the failed address and the parent address */
	    (void) decode_x_line(s + sizeof("Xfail:") - 1 , &address, &parent);

	    /* remove any occurance from the input list */
	    if (address) {
		ret = remove_addr(ret, address, parent);
	    }
	} else if (strncmp(s, "Xsucceed:", sizeof("Xsucceed:") - 1) == 0) {
	    /* get the delivered address and the parent address */
	    (void) decode_x_line(s + sizeof("Xsucceed:") - 1, &address, &parent);

	    /* remove any occurance from the input list */
	    if (address) {
		ret = remove_addr(ret, address, parent);
	    }
	} else if (strncmp(s, "Xdefer:", sizeof("Xdefer:") - 1) == 0) {
	    /* grab the message and error number of the defered address */
	    m = decode_x_line(s + sizeof("Xdefer:") - 1, &address, &parent);

	    /* put defer message into output defer_error list */
	    if (m) {
		put_defer_error(defer_errors, m, address, parent);
	    }
	} else if (strncmp(s, "Xsent_error:", sizeof("Xsent_error:")-1) == 0) {
	    register struct identify_addr *new_sent;

	    /* get the sent error address and the parent address */
	    (void) decode_x_line(s + sizeof("Xsent_error:") - 1, &address, &parent);

	    if (! address) {
		continue;
	    }
	    /* give this address to notify */
	    new_sent = (struct identify_addr *)xmalloc(sizeof(*new_sent));
	    new_sent->address = COPY_STRING(address);
	    if (parent) {
		new_sent->parent = COPY_STRING(parent);
	    } else {
		new_sent->parent = NULL;
	    }
	    new_sent->succ = *sent_errors;
	    *sent_errors = new_sent;
	}
    }

    return ret;
}

/*
 * decode_x_line - decode an X-line from the per-message log file
 *
 * The input should be of the form:
 *
 *	<address> orig-to: <parent_address> message...
 *	[more message...]
 *
 * or:
 *
 *	<address> message...
 *	[more message...]
 *
 * this routine returns the address and the parent_address.  It also returns a
 * pointer to the message (which may include the reason code which will be
 * parsed by put_defer_error() below).
 */
static char *
decode_x_line(line, address, parent)
    char *line;				/* input line */
    char **address;			/* return address here */
    char **parent;			/* return parent address here */
{
    int level;				/* count < and > nesting */
    register char *p = line;		/* point to parts of the line */

    /* skip initial white-space */
    while (isspace(*p)) {
	p++;
    }
    /* ignore anything not of the proper form */
    if (*p != '<') {
	DEBUG1(DBG_NOTIFY_LO, "decode_x_line(): WARNING: no '<' for address: %s\n", line);
	*address = NULL;
	return NULL;
    }
    p++;				/* skip the initial < */

    /*
     * extract the address as the text up until before the balancing
     * > character, in <address>.
     */
    *address = p;			/* mark this spot as the address */
    level = 1;				/* start at nesting level 1 */
    if (*p == '<') {
	level++;
    }
    while (level > 0) {
	if (!(p = address_token(p))) {
	    DEBUG1(DBG_NOTIFY_LO, "decode_x_line(): WARNING: no address token in address: %s\n", *address);
	    *address = NULL;		/* bad input form */
	    return NULL;
	}
	if (*p == '<') {
	    level++;
	} else if (*p == '>') {
	    --level;
	}
    }
    *p++ = '\0';			/* the last > is the end of address */

    /* scan for a possible parent address */
    while (isspace(*p)) {		/* skip up until parent: */
	p++;
    }
    /* is there a parent address? */
    if (strncmp(p, "parent:", sizeof("parent:") - 1) != 0) {
	DEBUG1(DBG_NOTIFY_HI, "decode_x_line(): no parent for: %s\n", *address);
	*parent = NULL;
	return p;
    }
    p += sizeof("parent:") - 1;

    /* skip initial white-space before actual parent address */
    while (isspace(*p)) {
	p++;
    }

    /* ignore anything not of the proper form */
    if (*p != '<') {
	DEBUG1(DBG_NOTIFY_MID, "decode_x_line(): WARNING: no '<' for parent: %s\n", *parent);
	*parent = NULL;
	return NULL;
    }
    p++;				/* skip the initial < */

    /*
     * extract the parent as the text up until before the balancing
     * > character, in <parent_addres>
     */
    *parent = p;
    level = 1;
    if (*p == '<') {
	level++;
    }
    while (level > 0){
	if (!(p = address_token(p))) {
	    DEBUG1(DBG_NOTIFY_MID, "decode_x_line(): WARNING: no address token in parent: %s\n", *parent);
	    *parent = NULL;
	    return NULL;
	}
	if (*p == '<') {
	    level++;
	} else if (*p == '>') {
	    --level;
	}
    }
    *p++ = '\0';

    while (isspace(*p)) {
	p++;
    }

    return p;
}

/*
 * put_defer_error - put a deferal message for an address in a list
 *
 * The reason code and error message text are parsed:
 *
 *	reason: (ERRnnn) messge_text
 *
 * remove previous entries for this address from the list.
 */
static void
put_defer_error(list, message, address, parent)
    struct defer_addr **list;
    char *message;
    char *address;
    char *parent;
{
    int error;
    static char reason_pfx[] = "reason: (ERR";
    char *p;
    struct defer_addr *defaddr, *next_link, *new_list;

    if (strncmp(message, reason_pfx, sizeof(reason_pfx) - 1) != 0) {
	/* doesn't match the correct format */
	DEBUG(DBG_NOTIFY_HI, "put_defer_error(): did not find error code.\n");
	return;
    }
    message += sizeof(reason_pfx) - 1;
    p = message;
    while (isdigit(*message)) {
	message++;
    }
    if (*message != ')') {
	/* doesn't match the correct format */
	DEBUG(DBG_NOTIFY_HI, "put_defer_error(): did not find trailing ')' after error code.\n");
	return;
    }
    *message = 0;
    error = atoi(p);
    *message++ = ')';
    while (isspace(*message)) {
	message++;
    }
    new_list = (struct defer_addr *) xmalloc(sizeof(*new_list));
    new_list->succ = NULL;
    new_list->error = error;
    new_list->message = COPY_STRING(message);
    new_list->address = COPY_STRING(address);
    new_list->parent = parent ? COPY_STRING(parent) : NULL;

    DEBUG3(DBG_NOTIFY_MID, "put_defer_error(): found defer error for <%s> (ERR%d): '%s'.\n",
	   address, error, message);

    for (defaddr = *list; defaddr; defaddr = next_link) {
	next_link = defaddr->succ;
	if (EQ(defaddr->address, address) ||
	    (! defaddr->parent && ! parent) ||
	    (defaddr->parent && parent && EQ(defaddr->parent, parent)))
	{
	    continue;
	}
	defaddr->succ = new_list;
	new_list = defaddr;
    }
    *list = new_list;

    return;
}


/*
 * notify - notify all users who are to receive a note about errors
 *
 * also, note any configuration errors and set call_freeze_message
 * if any are found.  If any addrs exist in the defer list, set
 * some_deferred_addrs to prevent main from calling unlink_spool().
 */
void
notify(defer, fail, sent_errors)
    struct addr *defer;			/* list of deferred addresses */
    struct addr *fail;			/* list of failed addresses */
    struct identify_addr *sent_errors;	/* addrs already handled */
{
    register struct addr *cur;		/* current addr being processed */
    struct addr *next;			/* next addr to process */
    struct addr *to_owner = NULL;	/* addrs to send to addr owners */
    struct addr *verify = NULL;		/* list of owner addrs to verify */
    struct addr *to_other = NULL;	/* to sender and/or postmaster */
    static char *mailargs[] = {		/* setup argv for exec("smail") */
	NULL,
	"-bS",				/* use Batched-SMTP mode */
	NULL,				/* will be -v$debug if in debug mode */
	NULL,
    };
    FILE *f;				/* stdin to child process */
    int pid;				/* child process id */
    int status;				/* child process status */
    char *cc1;				/* Cc: address */
    char *cc2;				/* another Cc: address */

    DEBUG(DBG_NOTIFY_HI, "notify(): called\n");

    /* set the exec name to the smail binary */
    mailargs[0] = smail;
    if (debug) {
	mailargs[2] = xprintf("-v%d", debug); /* XXX this wee bit of memory will leak */
    }

    /* remove any address which have already been taken care of */
    while (sent_errors) {
	defer = remove_addr(defer, sent_errors->address, sent_errors->parent);
	fail = remove_addr(fail, sent_errors->address, sent_errors->parent);
	sent_errors = sent_errors->succ;
    }

    /*
     * for deferred addresses, don't return anything, though note config
     * errors and defer the message if any were found.
     */
    for (cur = defer; cur; cur = cur->succ) {
	some_deferred_addrs = TRUE;	/* main should not call unlink_spool */
	if (!cur->error) {
	    panic(EX_SOFTWARE, "notify(): called for deferred message without error info (in_addr=%s)", cur->in_addr);
	}
	if (cur->error->info & ERR_CONFERR) {
	    call_freeze_message = TRUE;	/* config error, freeze the message in error/ */
	}
    }

    /*
     * Put failed addresses in two lists, one list going to the sender or the
     * postmaster, the other list going to the owner of address.  The list of
     * owners will have to be verified.
     */
    for (cur = fail; cur; cur = next) {
	DEBUG1(DBG_NOTIFY_MID, "notify(): classifying %s.\n", cur->in_addr)
	next = cur->succ;
	if (!cur->error) {
	    panic(EX_SOFTWARE, "notify(): called for failed message without error info (in_addr=%s)", cur->in_addr);
	}
	classify_addr(cur, &to_other, &verify);
    }

    /*
     * Move entries from the verify list to the to_owner list if they are valid
     * owner address.  If some don't verify, retry classify_addr() until they
     * do...
     */
    while (verify) {
	struct addr *new_list = NULL;	/* unverified owner addrs */
	int oexitval = exitvalue;	/* resolve_addr_list() can clobber this... */

	DEBUG1(DBG_NOTIFY_MID, "notify(): verifying owner address(es) (first is %s).\n", verify->work_addr);
	/*
	 * treat defer and fail lists as identical, since we cannot
	 * reasonably handle defer addresses here.
	 */
	resolve_addr_list(verify, &to_owner, &new_list, &new_list, FALSE);
	exitvalue = oexitval;

	/* take any failed/deferred addrs and send through classify_addr */
	verify = NULL;
	for (cur = new_list; cur; cur = next) {
	    DEBUG1(DBG_NOTIFY_MID, "notify(): re-classifying %s.\n", cur->in_addr)
	    next = cur->succ;
	    if (!cur->error) {
		panic(EX_SOFTWARE, "notify(): attempt to re-classify address without error info (in_addr=%s)", cur->in_addr);
	    }
	    cur->flags |= ADDR_VRFY_ONLY;
	    classify_addr(cur, &to_other, &verify);
	}
    }

    /* sort the list of verified addresses */
    if (to_owner) {
	to_owner = addr_sort(to_owner, OFFSET(addr, in_addr));
    }

    if (return_to_sender) {
	/*
	 * if return_to_sender is on but the error_processing flag
	 * doesn't call for a return message, then turn the flag off.
	 */
	if (error_processing != MAIL_BACK && error_processing != WRITE_BACK) {
	    return_to_sender = FALSE;
	}
	/* turn it off, as well, for really low priority messages */
	if (islower(msg_grade) && msg_grade >= 'n') {
	    return_to_sender = FALSE;
	}
    }

    /*
     * lastly, if the sender was given as the special string <> or <+>,
     * then an error message failed.  Rather than risk an infinite
     * loop here, defer the message rather than delivering a recursive
     * error message.
     *
     * error message loops can still exist if SMTP is not used or if
     * an intermediate site does not correctly handle the special
     * sender string <>.
     */
    if (error_sender) {
	if (return_to_sender || send_to_postmaster || to_owner) {
	    call_freeze_message = TRUE;
	    return;
	}
    }

    /* if nobody needs to be notified, then nothing needs to be done */
    if (! return_to_sender &&
	! send_to_postmaster &&
	! to_owner)
    {
	DEBUG(DBG_NOTIFY_LO, "notify: nothing to do.\n");
	return;
    }

    /*
     * open a channel to a new smail process, operating in bsmtp mode,
     * and send it messages for all of the recipients.
     */
    pid = open_child(mailargs, (char **) NULL, &f, (FILE **) NULL, -1,
		     CHILD_MINENV|CHILD_RETRY, prog_euid, prog_egid);
    if (pid < 0) {
	DEBUG(DBG_NOTIFY_LO, "notify: open_child failed, cannot notify\n");
	/* if we can't notify, let humans interfere later */
	call_freeze_message = TRUE;
	return;
    }

    /* start up the session */
    (void) fprintf(f, "HELO %s\n", primary_name);

    /*
     * if any owners are to receive a message, then send to each
     * owner in turn
     */
    cur = to_owner;
    while (cur) {
	char *last_owner;

	DEBUG1(DBG_NOTIFY_LO, "notify(): sending to address owner: %s\n", cur->in_addr);

	/* initiate a new message */
	cc1 = cc2 = NULL;
	if (send_to_postmaster) {
	    DEBUG(DBG_NOTIFY_LO, "      ... also sending to postmaster\n");
	    cc1 = "postmaster";
	}
	if (return_to_sender) {
	    DEBUG1(DBG_NOTIFY_LO, "     ... also returning to sender: %s\n", sender);
	    cc2 = sender;
	}
	notify_new_message(f, cur->in_addr, cc1, cc2, "sending to address owner");
	send_log(f, FALSE, log_banner);	/* don't include X lines from log */
	(void) fputs(addr_error_banner, f);
	last_owner = cur->in_addr;
	/*
	 * write out all/any of the address errors for messages with the same
	 * owner
	 */
	do {
	    /* XXX this first case is almost always the only one used.... */
	    if (cur->parent && cur->parent->true_addr && cur->parent->true_addr->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->parent->true_addr->in_addr ? cur->parent->true_addr->in_addr : "(unknown)",
			       cur->parent->true_addr->error->message);
	    }
	    if (cur->parent && cur->parent->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->parent->in_addr ? cur->parent->in_addr : "(unknown)",
			       cur->parent->error->message);
	    }
	    if (cur->true_addr && cur->true_addr->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->true_addr->in_addr ? cur->true_addr->in_addr : "(unknown)",
			       cur->true_addr->error->message);
	    }
	    if (cur->true_addr && cur->true_addr->true_addr && cur->true_addr->true_addr->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->true_addr->true_addr->in_addr ? cur->true_addr->true_addr->in_addr : "(unknown)",
			       cur->true_addr->true_addr->error->message);
	    }
	    if (cur->true_addr && cur->true_addr->parent && cur->true_addr->parent->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->true_addr->parent->in_addr ? cur->true_addr->parent->in_addr : "(unknown)",
			       cur->true_addr->parent->error->message);
	    }
	    if (cur->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->in_addr ? cur->in_addr : "(unknown)",
			       cur->error->message);
	    }
	    cur = cur->succ;
	} while (cur && EQIC(last_owner, cur->in_addr));
	(void) fputs(text_banner, f);
	write_notify_header(f);
	putc('\n', f);
	write_body(f, (long)PUT_DOTS);
	(void) fputs(".\n", f);		/* finish the message */
    }

    /* if the postmaster is to receive mail, then send a copy */
    if (send_to_postmaster) {
	DEBUG(DBG_NOTIFY_LO, "notify(): sending to postmaster\n");

	cc1 = cc2 = NULL;
	if (return_to_sender) {
	    DEBUG1(DBG_NOTIFY_LO, "     ... also returning to sender: %s\n", sender);
	    cc2 = sender;
	}
	notify_new_message(f, "postmaster", cc1, cc2, "sending to postmaster");
	send_log(f, TRUE, log_banner);	/* include X lines from log */
	/* write messages in to_other destined to the postmaster */
	(void) fputs(addr_error_banner, f);
	for (cur = to_other; cur; cur = cur->succ) {
	    if (cur->parent && cur->parent->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->parent->in_addr ? cur->parent->in_addr : "(unknown)",
			       cur->parent->error->message);
	    }
	    if (cur->parent && cur->parent->true_addr && cur->parent->true_addr->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->parent->true_addr->in_addr ? cur->parent->true_addr->in_addr : "(unknown)",
			       cur->parent->true_addr->error->message);
	    }
	    if (cur->true_addr && cur->true_addr->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->true_addr->in_addr ? cur->true_addr->in_addr : "(unknown)",
			       cur->true_addr->error->message);
	    }
	    if (cur->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->in_addr ? cur->in_addr : "(unknown)",
			       cur->error->message);
	    }
	}
	(void) fputs(text_banner, f);
	write_notify_header(f);
	putc('\n', f);
	write_body(f, (long)PUT_DOTS);
	(void) fputs(".\n", f);		/* finish the message */
    }

    /* if the sender is to receive mail, then send a copy */
    if (return_to_sender) {
	DEBUG1(DBG_NOTIFY_LO, "notify(): returning to sender: %s\n", sender);

	cc1 = cc2 = NULL;
	if (send_to_postmaster) {
	    DEBUG(DBG_NOTIFY_LO, "      ... also sending to postmaster\n");
	    cc1 = "postmaster";
	}
	notify_new_message(f, sender, cc1, cc2, "returning to sender");
	send_log(f, FALSE, log_banner);	/* don't include X lines from log */
	/* write messages in to_other destined to the sender */
	(void) fputs(addr_error_banner, f);
	for (cur = to_other; cur; cur = cur->succ) {
	    if (cur->parent && cur->parent->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->parent->in_addr ? cur->parent->in_addr : "(unknown)",
			       cur->parent->error->message);
	    }
	    if (cur->parent && cur->parent->true_addr && cur->parent->true_addr->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->parent->true_addr->in_addr ? cur->parent->true_addr->in_addr : "(unknown)",
			       cur->parent->true_addr->error->message);
	    }
	    if (cur->true_addr && cur->true_addr->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->true_addr->in_addr ? cur->true_addr->in_addr : "(unknown)",
			       cur->true_addr->error->message);
	    }
	    if (cur->error) {
		(void) fprintf(f, " address: %s ... failed: %s\n",
			       cur->in_addr ? cur->in_addr : "(unknown)",
			       cur->error->message);
	    }
	}
	(void) fputs(text_banner, f);
	write_notify_header(f);
	putc('\n', f);
	/*
	 * a message grade between a and z causes body not to be returned.
	 * This is handy when notification of errors is desired, but
	 * when the overhead of sending the entire message back is not
	 * necessary.
	 *
	 * Note that grades >= 'n' have the return_to_sender flag turned off
	 * completely above.
	 */
	if (islower(msg_grade)) {
	    fputs("[low-priority message, body not included]\n", f);
	} else {
	    write_body(f, (long)PUT_DOTS);
	}
	(void) fputs(".\n", f);		/* finish the message */
    }

    (void) fputs("QUIT\n", f);

    if ((status = close_child(f, (FILE *)NULL, pid)) != 0) {
	/* XXX or maybe we should call panic()? */
	write_log(WRITE_LOG_SYS|WRITE_LOG_PANIC, "notify(): child [%d] process returned failure: %d", pid, status);
	/*
	 * this isn't the best, but if we can't send the bounce, let humans
	 * interfere later
	 */
	call_freeze_message = TRUE;
	return;					/* ??? */
    }

    /* note the addresses for which error messages have been sent */
    error_delivery(to_owner);
    error_delivery(to_other);

    return;
}


/*
 * classify_addr - determine who is to be sent an error message
 *
 * If postmaster or the sender is to receive an error, link into the
 * to_other list, setting send_to_postmaster and/or return_to_sender
 * appropriately.  If a config error is found, set call_freeze_message.
 *
 * If we need to send to an owner, determine if an owner string is
 * defined and link an addr structure for that owner into the to_owner
 * list.
 */
static void
classify_addr(addr, to_other, to_owner)
    register struct addr *addr;		/* input address */
    struct addr **to_other;		/* list to sender or postmaster */
    struct addr **to_owner;		/* list to address owner */
{
    int link_to_other = FALSE;		/* TRUE to link into to_other list */

    if (error_copy_postmaster) {
	addr->error->info |= ERR_NPOSTMAST;
    }
    if (addr->error->info & (ERR_NSENDER|ERR_NPOSTMAST)) {
	/* send to either the sender or the postmaster */
	link_to_other = TRUE;

	/* note which one */
	if (addr->error->info & ERR_NSENDER) {
	    return_to_sender = TRUE;
	}
	if (addr->error->info & ERR_NPOSTMAST) {
	    send_to_postmaster = TRUE;
	}
    }
    /*
     * try bouncing to an address owner, if one is defined (and valid)
     */
    if (addr->error->info & (ERR_NSOWNER|ERR_NPOWNER)) {
	register char *s = NULL;	/* expanded owner string */
	register struct addr *cur;	/* addr ptr for scanning parents */
	char *work_addr = NULL;		/* output from preparse_address() */

	/*
	 * first find the closest parent associated with a director that
	 * defines an owner string...
	 */
	for (cur = addr->parent; cur; cur = cur->parent) {
	    char *error;		/* temp for storing parse error */

	    /*
	     * must have a reasonable, expandable owner address
	     */
	    if (cur->director &&
		cur->director->owner &&
		(s = expand_string(cur->director->owner,
				   (struct addr *) NULL,
				   cur->home,
				   cur->remainder)) &&
		(work_addr = preparse_address(s, &error)))
	    {
		DEBUG2(DBG_NOTIFY_MID, "classify_addr(): for %s: found a valid owner address: %s.\n", addr->in_addr, cur->director->owner);
		break;
	    }
	}
	/*
	 * if we didn't walk off the end of the parent list then an owner was
	 * found for the address, so build a new entry in to_owner list...
	 */
	if (cur) {
	    struct addr *new = alloc_addr();

	    new->in_addr = COPY_STRING(s);	/* copy owner address */
	    /*
	     * if this addr has already been seen by classify_addr() use
	     * the true_addr assigned previously, otherwise this is the
	     * first time through, so assign the original addr
	     */
	    if (addr->true_addr) {
		new->true_addr = addr->true_addr;
	    } else {
		new->true_addr = addr;		/* keep track of true addr */
	    }
	    new->work_addr = work_addr;		/* use the preparsed address */
	    /* disable use of smartuser driver, and only allow verify */
	    new->flags = ADDR_SMARTUSER | ADDR_VRFY_ONLY;
	    /*
	     * if resolve_addr_list() fails to verify this list then prepare to
	     * look higher up for a parent that has an associated owner when
	     * re-classifying...
	     */
	    new->parent = cur->parent;
	    /* finally link it into the to_owner list */
	    new->succ = *to_owner;
	    *to_owner = new;
	} else {
	    /*
	     * otherwise no owner was found for the address, bounce to the
	     * sender and/or postmaster...
	     */
	    if (addr->true_addr) {
		int perhaps_link = FALSE; /* true to see link, if not linked */
		struct error *true_error = addr->true_addr->error;

		if ((addr->error->info & ERR_NSOWNER) ||
		    (addr->error->info & ERR_NPOWNER))
		{
		    perhaps_link = TRUE;
		}

		/* only link if this has not been linked to to_other before */
		if (perhaps_link) {
		    if (! (true_error->info & (ERR_NSENDER|ERR_NPOSTMAST)) ) {
			/* link the true address, not this address */
			addr = addr->true_addr;
			link_to_other = TRUE;
		    }
		}
	    } else {
		link_to_other = TRUE;
	    }
	    if (addr->error->info & ERR_NSOWNER) {
		addr->error->info |= ERR_NSENDER;
		return_to_sender = TRUE;
	    }
	    if (addr->error->info & ERR_NPOWNER) {
		addr->error->info |= ERR_NPOSTMAST;
		send_to_postmaster = TRUE;
	    }
	}
    }

    /* if link_to_other was set anywhere above, link into to_other list */
    if (link_to_other) {
	addr->succ = *to_other;
	*to_other = addr;
    }
}

static void
notify_new_message(f, to, cc1, cc2, subject_to)
    FILE *f;
    char *to;				/* recipient */
    char *cc1, *cc2;			/* show carbon copy addresses */
    char *subject_to;			/* message for inclusion in subject */
{
    /*
     * initiate a new message
     * NOTE:  <+> is a smail-internal address form that causes the
     * sender to be considered local, while the SMTP <> address is
     * considered remote.  This is actually important.
     */
    (void) fprintf(f, "MAIL FROM:<+>\n");

    /* state the recipient */
    (void) fprintf(f, "RCPT TO:<%s>\n", to);

    /* start the message text, with the complete header */
    (void) fprintf(f, "DATA\nFrom: <MAILER-DAEMON@%s>\n", visible_name);
    if (to[0] == '@') {
	/* watch out for route-addrs */
	(void) fprintf(f, "To: <%s>\n", to);
    } else {
	(void) fprintf(f, "To: %s\n", to);
    }
    if (cc1 == NULL && cc2) {
	cc1 = cc2;
	cc2 = NULL;
    }
    if (cc1) {
	if (cc1[0] == '@') {
	    fprintf(f, "Cc: <%s>", cc1);
	} else {
	    fprintf(f, "Cc: %s", cc1);
	}
	if (cc2) {
	    if (cc2[0] == '@') {
		fprintf(f, ", <%s>", cc2);
	    } else {
		fprintf(f, ", %s", cc2);
	    }
	}
	putc('\n', f);
    }
    (void) fprintf(f, "Subject: mail failed, %s\nReference: <%s@%s>\n\n",
		   subject_to, message_id, primary_name);
}

/*
 * remove_addr - remove any matched addresses from an input list
 *
 * XXX this should probably be in addr.c
 *
 * given an address string and (perhaps) a parent address string and
 * an input address list, remove any occurance of an address in the
 * input list whose in_addr matches the specified address string and
 * whose parent in_addr string matches the specified parent string.
 * If parent is NULL then there must not be a parent address, otherwise
 * there must be a matching parent address.
 */
static struct addr *
remove_addr(in, address, parent)
    struct addr *in;			/* input addr list */
    char *address;			/* address to match against */
    char *parent;			/* parent of address to match */
{
    register struct addr *cur;		/* current address to process */
    struct addr *next;			/* next address to process */
    struct addr *out = NULL;		/* output address list */

    for (cur = in; cur; cur = next) {
	next = cur->succ;

	if (EQ(cur->in_addr, address)) {
	    /* the address does match */
	    if (parent) {
		/* a matching parent is required for a match */
		if (cur->parent && EQ(parent, cur->parent->in_addr)) {
		    /* match, don't put it on the output queue */
		    DEBUG1(DBG_NOTIFY_LO, "%s ... already delivered\n",
			   cur->in_addr);
		    continue;
		}
	    } else if (cur->parent == NULL) {
		/* match, don't put it on the output queue */
		DEBUG1(DBG_NOTIFY_LO, "%s ... already delivered\n",
		       cur->in_addr);
		continue;
	    }
	}

	/* no match, put the address on the output queue */
	cur->succ = out;
	out = cur;
    }

    return out;				/* return the new list */
}

static void
write_notify_header(f)
    FILE *f;
{
    struct transport tport;
    struct addr addr;

    bzero(&tport, sizeof(tport));
    bzero(&addr, sizeof(addr));
    addr.transport = &tport;
    tport.name = "error";
    tport.flags = PUT_RECEIVED | STRICT_TPORT;
    write_header(f, &addr);
}


/*
 * This code attempts to optimise the reprocessing of queued mail by putting
 * sucessfully delivered remote addresses into the hash table to prevent them
 * being put through the routers.  (We can't effectively pre-hash locally
 * deliverable addresses because of course they are transformed into something
 * else before they are added to the hash table.)
 *
 * This cuts down the time taken doing DNS lookups dramatically especially for
 * mailing list mail -- it is often far cheaper to read the MLOG file twice
 * (which doing this will indeed cause to happen) than it is to do the
 * additional DNS queries.
 *
 * It is also in the wrong place - its just that I use decode_x_line() which is
 * local to this file...!  (sorry)
 */
void
hash_predelivered_addresses()
{
    char *s;				/* working string */
    struct str work_address;
    int  offset;
    char *address;			/* the address to remove */
    char *parent;			/* parent of address to remove */
    char *target;			/* Working info from parsed address */
    char *remainder;			/* Working info from parsed address */
    int  parse_flag;			/* user by parse_address */
    char *error; 			/* error returned by preparse_address() */
    char *preparsed_address;		/* address stripped of non-essencials*/

    DEBUG(DBG_NOTIFY_HI, "hash_predelivered_addresses called...\n");
    STR_INIT(&work_address);
    for (s = scan_msg_log(TRUE); s; s = scan_msg_log(FALSE)) {
	if (strncmp(s, "Xfail:", sizeof("Xfail:") - 1) == 0) {
	    offset = sizeof("Xfail:") - 1;
	} else if (strncmp(s, "Xsucceed:", sizeof("Xsucceed:") - 1) == 0) {
	    offset = sizeof("Xsucceed:") - 1;
	} else {
	    offset = 0;
	}
	if (offset) {
	    /* get the address (and the parent address) */
	    (void) decode_x_line(s + offset, &address, &parent); 
	  
	    /* Strip off any <> etc. */
	    preparsed_address = preparse_address(address, &error);
	    if (preparsed_address == NULL) {
		DEBUG1(DBG_NOTIFY_LO, "Oops, log entry found without a valid address:\n\t%s\n", s);
		continue;
	    }

	    /* Find if  address is non-local */
	    if (*preparsed_address != '|') { /* Ensure piped addresses are stripped out */
		STR_CLEAR(&work_address);
		STR_CAT(&work_address, preparsed_address);
		parse_flag = 0;
		if ((parse_address(STR(&work_address),
				   &target,
				   &remainder,
				   &parse_flag) != LOCAL) && !islocalhost(target)) {
		    DEBUG2(DBG_NOTIFY_MID, "Hashing out %s (%s) ... previously processed.\n", address, preparsed_address);
		    (void) add_to_hash(preparsed_address, (char *) NULL, 0, hit_table);
		}
	    }
	}
    }
}
