/*
#ident	"@(#)smail/src:RELEASE-3_2_0_120:log.c,v 1.73 2004/08/18 21:55:27 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.
 */

/*
 * log.c:
 *	system and per-message logging functions
 *
 *	These functions send information to a per-system log file and to
 *	a per-message log file, manage the creation use and removal of
 *	per-message logs and handle panic and fatal messages.
 *
 *	external functions:  open_system_logs, close_system_logs,
 *			     open_msg_log, close_msg_log, unlink_msg_log,
 *			     panic, write_log, send_log, scan_msg_log
 *                           process_msg_log, decode_x_line
 */

#include "defs.h"

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

#if defined(UNIX_SYS5) || defined(POSIX_OS) || defined(USE_FCNTL)
# include <fcntl.h>
#else
# if defined(UNIX_BSD)
#  include <sys/file.h>
# endif
#endif
#ifdef __GLIBC__
# include <sys/file.h>
#endif

#include "smail.h"
#include "list.h"
#include "alloc.h"
#include "main.h"
#include "addr.h"
#include "hash.h"
#include "smailstring.h"
#include "dys.h"
#include "log.h"
#include "spool.h"
#include "exitcodes.h"
#include "extern.h"
#include "debug.h"
#include "smailport.h"

#ifdef STANDALONE
# define xmalloc malloc
# define xrealloc realloc
# define xfree free
#endif	/* STANDALONE */

/* exported variables */
FILE *msg_logfile = NULL;	/* open stream to per-message log */
FILE *panicfile = NULL;		/* open stream to panic log file */
FILE *logfile = NULL;		/* open stream to system log file */

/* variables local to this file */
static char *msg_log_fn;	/* name of per-message log file */

/* functions local to this file */
static void build_msg_log_fn __P((void));
static FILE *open_a_log __P((char *, char *, char *, unsigned int));
static void try_mkdir __P((char *));
static void write_log_va __P((int, char *, va_list));


/*
 * open_system_logs - open the panic and the general information log files.
 *
 * Access to the system log should not require that much be functional
 * or that resources be allocated to send to the system log.  For that
 * reason, we allocate all resources in advance, while resources are
 * available and the system is assumed to be somewhat sane.
 *
 * If reasonable, the system logs should be opened after a message has
 * been read to a spool file so that if a panic occurs because a log
 * could not be opened we can recover mail later when the problem is
 * solved.
 */
void
open_system_logs()
{
    static char panicbuf[BUFSIZ];	/* stdio buffer to avoid unchecked mallocs */
    static char logbuf[BUFSIZ];		/* stdio buffer to avoid unchecked mallocs */

    if (!panicfile) {
	panicfile = open_a_log(panic_fn, "panic log", panicbuf, log_mode);
	if (!panicfile) {
	    /*
	     * use separate fprintf() calls even though they are half the same
	     * so as to avoid having to preserve errno
	     */
	    if (!only_testing) {
		fprintf(stderr, "%s: [%ld] cannot open panic log %s: %s\n", program, (long int) getpid(), panic_fn, strerror(errno));
	    } else {
		fprintf(stderr, "%s: cannot open panic log %s: %s\n", program, panic_fn, strerror(errno));
	    }
	    panicfile = stderr;
	}
    }
    if (!logfile) {
	logfile = open_a_log(log_fn, "system log", logbuf, log_mode);
	if (!logfile) {
	    if (!only_testing) {
		fprintf(stderr, "%s: [%ld] cannot open system log %s: %s\n", program, (long int) getpid(), log_fn, strerror(errno));
	    } else {
		fprintf(stderr, "%s: cannot open system log %s: %s\n", program, log_fn, strerror(errno));
	    }
	    logfile = stderr;
	}
    }
    return;
}

/*
 * open_a_log() - open a log file for append, creating it if necessary, and if
 *                auto_mkdir is set then perhaps also creating its directory.
 *
 * If 'buf' is set then call setbuf() with it.
 *
 * Returns a pointer to a STDIO FILE structure.
 */
/*
 * XXX this should probably be rewritten to use fopen(fqfn, "a")
 * [with a fix to use umask((~mode) & 0777)) too of course]
 */
static FILE *
open_a_log(fqfn, fn, buf, mode)
    char *fqfn;				/* fully qualified filename */
    char *fn;				/* internal filename (for debug/error msgs) */
    char *buf;				/* stdio buffer for fp */
    unsigned int mode;			/* access modes to create with */
{
    FILE *fp = NULL;
    int fd;
    unsigned int oumask = umask(0);	/* we supply an explicit mode */

#ifdef O_APPEND
    fd = open(fqfn, O_CREAT | O_APPEND | O_WRONLY, mode);
#else
    /* limited to V7 open semantics  XXX this old junk should go away.... */
    fd = open(fqfn, 1);
    if (fd < 0) {
	DEBUG3(DBG_LOG_HI, "open_a_log(%s): %s: open() failed, will try creat(): %s\n", fqfn, fn, strerror(errno));
	fd = creat(fqfn, mode);
    } else {
	(void) lseek(fd, (off_t) 0L, SEEK_END);	/* XXX could this ever fail? */
    }
#endif
    if (fd < 0) {
#ifdef O_APPEND
	DEBUG3(DBG_LOG_MID, "open_a_log(%s): %s: initial open() failed: %s\n", fqfn, fn, strerror(errno));
#else
	DEBUG3(DBG_LOG_MID, "open_a_log(%s): %s: initial creat() failed: %s\n", fqfn, fn, strerror(errno));
#endif

	/* perhaps the directory just needs to be created */
	if (auto_mkdir && errno == ENOENT) {
	    try_mkdir(fqfn);
#ifdef O_APPEND
	    fd = open(fqfn, O_CREAT | O_APPEND | O_WRONLY, mode);
#else
	    /* limited to V7 open semantics  XXX this old junk should go away.... */
	    /* this time we know it's not there so just create it */
	    fd = creat(fqfn, mode);
#endif
	    if (fd < 0) {
#ifdef O_APPEND
		DEBUG3(DBG_LOG_LO, "open_a_log(%s): %s: second try open() failed: %s\n", fqfn, fn, strerror(errno));
#else
		DEBUG3(DBG_LOG_LO, "open_a_log(%s): %s: second try creat() failed: %s\n", fqfn, fn, strerror(errno));
#endif
	    }
	}
    }
    if (fd >= 0) {
	if (!(fp = fdopen(fd, "a"))) {
	    DEBUG3(DBG_LOG_LO, "open_a_log(%s): %s: fdopen() failed: %s\n", fqfn, fn, strerror(errno));
	}
	if (fp && buf) {
	    setbuf(fp, buf);
	}
    }
    (void) umask(oumask);

    return fp;
}

/*
 * try_mkdir - try to build the directory for the given filename
 */
static void
try_mkdir(fn)
    char *fn;
{
    char *slash = strrchr(fn, '/');
    char *dr;
    struct stat st;

    if (slash == NULL) {
	return;				/* ignore bad filename */
    }

    /* figure directory name */
    while (slash > fn && *(slash - 1) == '/') {
	--slash;
    }
    if (slash == fn) {
	return;			/* root directory */
    }
    dr = xmalloc((size_t) (slash - fn + 1));
    (void) memcpy(dr, fn, (size_t) (slash - fn));
    dr[slash - fn] = '\0';

    DEBUG1(DBG_LOG_LO, "make directory %s: ", dr);
    errno = 0;
    (void) mkdir(dr, auto_mkdir_mode);
    DEBUG1(DBG_LOG_LO, "%s\n", (errno != 0) ? strerror(errno) : "OK");

    if (stat(dr, &st) == -1 && errno == ENOENT) {
	char *slash2 = strrchr(dr, '/');

	if (slash2) {
	    while (slash2 > dr && *(slash2 - 1) == '/') {
		--slash2;
	    }
	    if (slash2 != dr) {
		*slash2 = '\0';
		DEBUG1(DBG_LOG_LO, "    make parent directory %s: ", dr);
		errno = 0;
		(void) mkdir(dr, auto_mkdir_mode);
		DEBUG1(DBG_LOG_LO, "%s\n", (errno != 0) ? strerror(errno) : "OK");
		*slash2 = '/';
		DEBUG1(DBG_LOG_LO, "    re-make directory %s: ", dr);
		errno = 0;
		(void) mkdir(dr, auto_mkdir_mode);
		DEBUG1(DBG_LOG_LO, "%s\n", (errno != 0) ? strerror(errno) : "OK");
	    }
	}
    }

    xfree(dr);
}

/*
 * close_system_logs - close the panic and general info log files.
 */
void
close_system_logs()
{
    if (logfile) {
	(void) fclose(logfile);
	logfile = NULL;
    }
    if (panicfile) {
	(void) fclose(panicfile);
	panicfile = NULL;
    }
}


/*
 * open_msg_log - open message log file, one per message
 *
 * a per-message log should be opened once for each message and closed
 * when done processing a message.  It is intended to be information
 * that a sender might be interested in, and will be sent back to
 * the sender if the return_to_sender flag is set when processing of
 * the message is completed.
 */
void
open_msg_log()
{
    static char msgbuf[BUFSIZ];		/* stdio buffer to avoid unchecked mallocs */

    /*
     * if msg_fn not yet set up create a suitably unique value
     */
    if (msg_logfile) {
	(void) fclose(msg_logfile);
	msg_logfile = NULL;
    }
    build_msg_log_fn();
    msg_logfile = open_a_log(msg_log_fn, "message log", msgbuf, message_log_mode);
    if (!msg_logfile) {
	/*
	 * otherwise, panic.  We are assuming that the mail queue entry can be
	 * scanned at a later date (note: errno may not be useful from open_a_log)
	 */
	panic(EX_OSFILE, "cannot open %s/%s: %s", spool_dir, msg_log_fn, strerror(errno));
	/*NOTREACHED*/
    }
    return;
}

/*
 * build_msg_log_fn - build the name for the per-message log file
 */
static void
build_msg_log_fn()
{
    static char buf[sizeof("msglog/") + SPOOL_FN_LEN + 1];

    (void) sprintf(buf, "msglog/%s", spool_fn);
    msg_log_fn = buf;
}

/*
 * close_msg_log - close the per-message log file
 *
 * This should be called when further processing of a message is
 * being postponed to some point in the future.
 */
void
close_msg_log()
{
    if (msg_logfile) {
	(void) fclose(msg_logfile);
	msg_logfile = NULL;
    }
}

/*
 * unlink_msg_log - close and unlink the per-message log file
 *
 * use this when a message has been processed completely.
 */
void
unlink_msg_log()
{
    close_msg_log();
    if (msg_log_fn) {
	(void) unlink(msg_log_fn);
	msg_log_fn = NULL;
    }
}


/*
 * panic - panic and die!
 *
 * Attempt to write to the panic and system log files. and no matter what we
 * also try to write to stderr.
 *
 * If this is called after spooling a message, then the message will be put in
 * the error queue so that the postmaster can reprocess it after correcting
 * whatever error caused the panic in the first place.
 *
 */
/*VARARGS2*/
#ifdef __STDC__
void
panic(int exitcode, char *fmt, ...)
#else
void
panic(exitcode, fmt, va_alist)
    int exitcode;			/* we will call exit(exitcode) */
    char *fmt;				/* printf(3) format */
    va_dcl                              /* arguments for printf */
#endif
{
    va_list ap;

    if (errfile) {
	VA_START(ap, fmt);
	write_log_va(WRITE_LOG_TTY, fmt, ap);
	va_end(ap);
	if (only_testing) {
	    exit(force_zero_exitvalue ? 0 : exitcode);
	}
    }
    VA_START(ap, fmt);
    write_log_va(WRITE_LOG_PANIC, fmt, ap);
    va_end(ap);

    VA_START(ap, fmt);
    write_log_va(WRITE_LOG_SYS, fmt, ap);
    va_end(ap);

    if (daemon_pid != 0 && daemon_pid == getpid()) {
	write_log(WRITE_LOG_SYS, "smail daemon exiting on panic");
	if (daemon_pidfile && *daemon_pidfile == '/') {
	    (void) unlink(daemon_pidfile);
	}
    }
    if (spool_fn) {
	freeze_message();		/* put message in error/ directory */
    }

    /* XXX we _REALLY_ should have an option to (try to) trigger a core dump here.... */

    exit(force_zero_exitvalue ? 0 : exitcode);
    /* NOTREACHED */
}

#ifndef NODEBUG
# define WRITE_LOG_DEBUG	(WRITE_LOG_LAST << 2) /* magic for write_log_va() */
#endif

/*
 * write_log - write to the per-message log, and perhaps the system log
 *
 * write a log message to the various log files, where `who' is the
 * bitwise OR of WRITE_LOG_SYS, WRITE_LOG_MLOG and/or WRITE_LOG_PANIC.
 *
 * Note that messages with a leading 'X' are for per-message logs.
 */
/*VARARGS2*/
#ifdef __STDC__
void
write_log(int who, char *fmt, ...)
#else
void
write_log(who, fmt, va_alist)
    int who;				/* mask of log files to be written */
    char *fmt;				/* printf(3) format */
    va_dcl                              /* arguments for printf */
#endif
{
    va_list ap;
#ifndef NODEBUG
    int wct = 0;

    if (debug && errfile && (debug > 1 || fmt[0] != 'X')) {
	/* if we are debugging at all, print all logged messages */
	(void) fputs("write_log(", errfile);
	if (who & WRITE_LOG_SYS) {
	    (void) fputs("SYS", errfile);
	    wct++;
	}
	if (who & WRITE_LOG_PANIC) {
	    (void) fprintf(errfile, "%sPANIC", wct++ ? "|" : "");
	}
	if (who & WRITE_LOG_MLOG) {
	    (void) fprintf(errfile, "%sMLOG", wct++ ? "|" : "");
	}
	if (who & WRITE_LOG_TTY) {
	    (void) fprintf(errfile, "%sTTY", wct++ ? "|" : "");
	}
	if (who & WRITE_LOG_DEBUG) {
	    (void) fprintf(errfile, "%sDEBUG", wct++ ? "|" : "");
	}
	(void) fputs("): ", errfile);
	VA_START(ap, fmt);
	write_log_va(WRITE_LOG_DEBUG, fmt, ap);
	va_end(ap);
	if (only_testing || who == WRITE_LOG_TTY) {
	    return;
	}
    }
#endif /* NODEBUG */
    if (errfile && ((who & WRITE_LOG_TTY) ||
		   ((who & (WRITE_LOG_MLOG|WRITE_LOG_PANIC)) &&
		    (error_processing == TERMINAL ||
		     error_processing == ERROR_DEFAULT) && /* XXX ??? */
		    fmt[0] != 'X'))) {
	VA_START(ap, fmt);
	write_log_va(WRITE_LOG_TTY, fmt, ap);
	va_end(ap);
    }
    if (only_testing) {
	return;
    }
    if (who & WRITE_LOG_SYS) {
	VA_START(ap, fmt);
	write_log_va(WRITE_LOG_SYS, fmt, ap);
	va_end(ap);
    }
    if (who & WRITE_LOG_PANIC) {
	VA_START(ap, fmt);
	write_log_va(WRITE_LOG_PANIC, fmt, ap);
	va_end(ap);
    }
    if (who & WRITE_LOG_MLOG) {
	VA_START(ap, fmt);
	write_log_va(WRITE_LOG_MLOG, fmt, ap);
	va_end(ap);
    }
    return;
}

static void
write_log_va(who, fmt, ap)
    int who;				/* mask of log files to be written */
    char *fmt;				/* printf(3) format */
    va_list ap;                         /* arguments for vfprintf() */
{
    static struct str logstr;
    static int initialised = FALSE;

    if (!initialised) {
	STR_INIT(&logstr);
	initialised = TRUE;
    } else {
	STR_CLEAR(&logstr);
	STR_CHECK(&logstr);
    }
    str_printf_va(&logstr, fmt, ap);
    /* log messages don't normally come with their own \n */
    if (STR_GET(&logstr, STR_LEN(&logstr) - 1) != '\n') {
	STR_CAT(&logstr, "\n");
    }

    switch (who) {
#ifndef NODEBUG
    case WRITE_LOG_DEBUG:
	(void) fputs(STR(&logstr), errfile);
	(void) fflush(errfile);
	break;

#endif	/* NODEBUG */

    case WRITE_LOG_TTY:
	if (only_testing) {
	    (void) fprintf(errfile, "%s: ", program);
	} else {
	    (void) fprintf(errfile, "%s: %s: [%ld] ", program, time_stamp(), (long int) getpid());
	}
	if (message_id) {
	    (void) fprintf(errfile, "[%s] ", message_id);
	}
	(void) fputs(STR(&logstr), errfile);
	(void) fflush(errfile);
	if (ferror(errfile)) {
	    int oerrno = errno;
	    int fd;

	    fd = fileno(errfile);
	    errfile = NULL;
	    panic(EX_IOERR, "error writing %s[%d]: %s", "errfile", fd, strerror(oerrno));
	    /*NOTREACHED*/
	}
	break;

    case WRITE_LOG_SYS:
	/* write out permanent log to the system log file */
	if (logfile == NULL || panicfile == NULL) {
	    open_system_logs();		/* if system log not open, open it */
	}
#ifdef LOG_EXTRA_NEWLINE
	(void) fputc('\n', logfile);
#endif
	if (only_testing) {
	    (void) fprintf(logfile, "%s: logging: ", program);
	}
	(void) fprintf(logfile, "%s: [%ld] ", time_stamp(), (long int) getpid());
	if (message_id) {
	    (void) fprintf(logfile, "[%s] ", message_id);
	}
	(void) fputs(STR(&logstr), logfile);
	(void) fflush(logfile);
	if (ferror(logfile)) {
	    int oerrno = errno;
	    int fd;

	    fd = fileno(logfile);
	    logfile = NULL;
	    panic(EX_IOERR, "error writing the following message to %s[%d]: %s\n%s",
		  log_fn, fd, strerror(oerrno), STR(&logstr));
	    /*NOTREACHED*/
	}
	break;

    case WRITE_LOG_PANIC:
	if (panicfile == NULL) {
	    open_system_logs();		/* if system log not open, open it */
	}
#ifdef LOG_EXTRA_NEWLINE
	(void) fputc('\n', logfile);
#endif
	if (only_testing) {
	    (void) fprintf(panicfile, "%s: panic: ", program);
	}
	(void) fprintf(panicfile, "%s: [%ld] ", time_stamp(), (long int) getpid());
	if (message_id) {
	    (void) fprintf(panicfile, "[%s] ", message_id);
	}
	(void) fputs(STR(&logstr), panicfile);
	(void) fflush(panicfile);
	if (ferror(panicfile)) {
	    int oerrno = errno;
	    int fd;

	    /*
	     * Note: no sense potentially going in a loop here by calling
	     * panic() again, so instead just clear the errant panicfile so
	     * that nothing tries again, and so in case panic() itself calls
	     * itself then mabye the open_system_logs() above will work.  In
	     * the mean time just try writing the message to errfile and
	     * logfile if possible.
	     */
	    fd = fileno(panicfile);
	    panicfile = NULL;
	    if (errfile) {
		(void) fprintf(errfile, "error writing following message to %s[%d]: %s\n", panic_fn, fd, strerror(oerrno));
		(void) fputs(STR(&logstr), errfile);
		(void) fflush(errfile);
	    }
	    if (logfile) {
		(void) fprintf(logfile, "error writing following message to %s[%d]: %s", panic_fn, fd, strerror(oerrno));
		(void) fputs(STR(&logstr), errfile);
		(void) fflush(logfile);
	    }
	}
	break;

    /*
     * NOTE:  if there is no spool_dir set, then a per-message logfile
     *	      cannot be created, so don't bother writing per-message
     *	      log entries.
     */
    case WRITE_LOG_MLOG:
	if (spool_dir) {
	    if (msg_logfile == NULL) {
		open_msg_log();		/* if message log not open, open it */
	    }

	    /* write out the message to the per-message log file */
	    (void) fputs(STR(&logstr), msg_logfile);
	    /* finally append the timestamp and PID */
	    (void) fprintf(msg_logfile, "[%s: [%ld]]\n", time_stamp(), (long int) getpid());
	    (void) fflush(msg_logfile);
	    if (ferror(msg_logfile)) {
		int oerrno = errno;

		msg_logfile = NULL;
		panic(EX_IOERR, "error writing the following message to %s/%s[%d]: %s\n%s",
		      spool_dir, msg_log_fn, fileno(msg_logfile), strerror(oerrno), STR(&logstr));
		/*NOTREACHED*/
	    }
	}
	break;

    }
}

/*
 * send_log - write out the per-message log file.
 *
 * Send the "interesting" entries of the per-message log to a file 'fp',
 * without their leading 'X' marker characters.  Prepend an optional banner
 * string before any log data, IFF there is any log data.
 */
void
send_log(fp, banner)
    FILE *fp;				/* send to this file */
    char *banner;			/* if non-NULL precedes log output */
{
    char *s;				/* line of text from scan_msg_log() */
    int seen_some = FALSE;
    struct defer_addr *deferrals = NULL;
    struct defer_addr *cd;

#ifndef NDEBUG
    if (debug > DBG_LOG_HI) {
	FILE *mlog;

	/*
	 * just copy the whole message-log to the specified file so that we can
	 * see exactly what's in it without worrying about odd-ball stuff
	 * hiding from scan_msg_log().
	 *
	 * Coincidently this will avoid some duplication of the scan_msg_log()
	 * debug output.
	 */
	build_msg_log_fn();
	/* open log file separately to avoid changing the write position */
	if (! (mlog = fopen(msg_log_fn, "r"))) {
	    DEBUG3(DBG_LOG_MID, "send_log(): no message log file: %s/%s: %s.\n", spool_dir, msg_log_fn, strerror(errno));
	    return;
	}
	while ((s = read_line(mlog))) {
	    if (banner) {
		(void) fputs(banner, fp);
		banner = NULL;		/* output banner at most once */
	    }
	    /* the %v escapes trailing newline so include an explict one */
	    dprintf(fp, " %v\n", s);	/* do use %v to make sure it's clean */
	}
	(void) fclose(mlog);		/* don't need this anymore */
	(void) fputc('\n', fp);		/* always leave a blank line */

	return;
    }
#endif
    /*
     * first scan through all of the entries in the message log file to gather
     * up the most recent deferral entries for each address.
     *
     * (unless debug is turned up -- in which case just print everything)
     */
    for (s = scan_msg_log(TRUE); s; s = scan_msg_log(FALSE)) {
	if (!seen_some) {
	    seen_some = TRUE;
	    if (banner) {
		(void) fputs(banner, fp);
		banner = NULL;		/* output banner at most once */
	    }
	}
	if (debug > DBG_LOG_MID) {
	    /* scan_msg_log() trims the trailing newline */
 	    (void) fprintf(fp, "%s\n", s + 1);
	} else if (HDREQ("Xdefer", s)) {
	    char *address = NULL;	/* the address to remove */
	    char *parent = NULL;	/* parent of address to remove */
	    unsigned long int ecode;	/* error code from address */
	    char *entry;		/* wrk storage for decode_x_line() */
	    struct defer_addr *nd;

	    entry = COPY_STRING(s);	/* decode_x_line() is destructive */
	    (void) decode_x_line(strchr(entry, ':') + 2, &address, &parent, &ecode);
	    if (!address) {
		DEBUG1(DBG_LOG_LO, "found bogus Xdefer:\n%s\n", s);
		xfree(entry);
		continue;
	    }
	    for (cd = deferrals; cd; cd = cd->succ) {
		if (EQ(cd->address, address) &&
		    (!parent ||
		     (cd->parent && EQ(cd->parent, parent)))) {
		    /* match! */
		    break;
		}
	    }
	    if (cd) {
		xfree(cd->message);
		cd->message = COPY_STRING(s);
		xfree(cd->address);
		cd->address = COPY_STRING(address);
		if (cd->parent) {
		    xfree(cd->parent);
		    cd->parent = NULL;
		}
		if (parent) {
		    cd->parent = COPY_STRING(parent);
		}
	    } else {
		nd = (struct defer_addr *) xmalloc(sizeof(*nd));
		nd->message = COPY_STRING(s);
		nd->address = COPY_STRING(address);
		if (parent) {
		    nd->parent = COPY_STRING(parent);
		} else {
		    nd->parent = NULL;
		}
		nd->succ = deferrals;
		deferrals = nd;
	    }
	    xfree(entry);
	}
    }
    if (debug > DBG_LOG_MID) {
	/* if we've already printed everything... */
	if (seen_some) {
	    (void) fputc('\n', fp);	/* leave a blank line */
	}
	/* ... we're done. */
	return;
    }

    /*
     * First we print any deferrals from the most-recent deferrals list.  This
     * conveniently puts the most interesting entries first for "mailq -v"
     * readers, and it also ensures they appear before any final Xfail entry
     * should the message bounce after the retry_duration has expired for the
     * destination.  [[ ideally we would merge them back into the other entries
     * in cronological order, but that would require a lot more processing and
     * for very little gain ]]
     */
    for (cd = deferrals; cd; ) {
	struct defer_addr *ocd = cd;

	/* XXX we don't want to escape newlines, but currently dprintf()'s "%v" does so... */
	fprintf(fp, "%s\n", (cd->message) + 1);
	/* XXX there's no free_defer_addr() so we do it by hand */
	xfree(cd->message);
	xfree(cd->address);
	if (cd->parent) {
	    xfree(cd->parent);
	}
	cd = cd->succ;
	xfree((char *) ocd);
    }
    /* now go through the log again and print everything else... */
    for (s = scan_msg_log(TRUE); s; s = scan_msg_log(FALSE)) {
	if (! HDREQ("Xdefer", s)) {
	    /* scan_msg_log() trims the trailing newline */
	    /* XXX we don't want to escape newlines, but currently dprintf()'s "%v" does so... */
 	    (void) fprintf(fp, "%s\n", s + 1);
	}
    }
    if (seen_some) {
	(void) fputc('\n', fp);		/* leave a blank line */
    }

    return;
}


/*
 * scan_msg_log - scan for X entries in the per-message logfile
 *
 * If 'first' is TRUE, open the message log and scan for the first line marked
 * with an X, return the entry starting with that line.
 *
 * On successive calls, where 'first' is FALSE, return successive entries marked
 * with a line starting with an X.  Return NULL if no more such lines are found.
 *
 * returned value points to a string area which may be reused on subsequent
 * calls to scan_msg_log.
 *
 * Note:  we keep a private file pointer (and thus file descriptor) to keep
 * from changing the write position of msg_logfile while reading.
 */
char *
scan_msg_log(first)
    int first;				/* TRUE to re-open the message log */
{
    static FILE *mlog = NULL;		/* opened message log */
    static struct str entry;		/* entry marked with X */
    static int inited_entry = FALSE;	/* TRUE if STR_INIT called for entry */
    register int c, last_c;

    build_msg_log_fn();
    if (!mlog) {
	DEBUG2(DBG_LOG_HI, "scan_msg_log(): %s scanning message log file: %s.\n", first ? "starting" : "re-starting", msg_log_fn);
	if (!(mlog = fopen(msg_log_fn, "r"))) {	/* reopen log file */
	    DEBUG2(DBG_LOG_MID, "scan_msg_log(): no message log file: %s: %s.\n", msg_log_fn, strerror(errno));
	    return NULL;
	}
    }
    if (first) {
	rewind(mlog);
    }
    last_c = '\n';			/* assume starting at BOL */

    /* scan for a line beginning with an 'X' */
    while ((c = getc(mlog)) != EOF) {
	if (c == 'X' && last_c == '\n') {
	    break;
	}
	last_c = c;
    }
    if (c == EOF) {
	/* reached end of file without finding another X line */
	DEBUG1(DBG_LOG_HI, "scan_msg_log(): finished scanning message log file: %s.\n", msg_log_fn);
	(void) fclose(mlog);
	mlog = NULL;
	return NULL;
    }
    /*
     * we've found a line marked with a leading 'X', read it and any following
     * lines in until another line starting with 'X' is found, or EOF is
     * reached.
     */
    if (! inited_entry) {
	STR_INIT(&entry);
	inited_entry = TRUE;
    } else {
	STR_CHECK(&entry);
	STR_CLEAR(&entry);
    }
    STR_NEXT(&entry, 'X');
    last_c = 'X';
    while ((c = getc(mlog)) != EOF) {
	if (last_c == '\n' && c == 'X') {
	    break;
	}
	STR_NEXT(&entry, c);
	last_c = c;
    }
    if (c == 'X') {
	ungetc(c, mlog);		/* put back 'X' for next time */
    }
    if (last_c == '\n') {
	STR_PREV(&entry);		/* trim off the last newline */
    }
    STR_NEXT(&entry, '\0');

    /* return the entry */
    DEBUG1(DBG_LOG_HI, "scan_msg_log() returns:\n'%v'\n", STR(&entry));

    return STR(&entry);
}


/*
 * 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 "identify" addresses for which notifications have already been
 * sent is returned by reference through the sent_errors parameter.  This list
 * is used to avoid further processing of failed (and deferred) addresses if
 * notifications have already been sent for them.
 */
struct addr *
process_msg_log(in, sent_errors)
    struct addr *in;			/* input resolved addresses */
    struct identify_addr **sent_errors;	/* return errors that have been sent */
{
    register struct addr *ret;		/* addr list to return */
    char *s;				/* line of text from scan_msg_log() */

    ret = in;

    /* scan through all of the message log entries, if any */
    for (s = scan_msg_log(TRUE); s; s = scan_msg_log(FALSE)) {
	char *content;			/* content of log entry */
	char *address = NULL;		/* the address to remove */
	char *parent = NULL;		/* parent of address to remove */
	unsigned long int ecode = 0;	/* error info from log */
	char *msg;			/* error message from log */

	if (!(content = strchr(s, ':'))) {
	    DEBUG1(DBG_LOG_LO, "process_msg_log(): encountered non-header entry:\n%v\n-- end entry.\n", s);
	    continue;
	}
	content += 2;
	msg = decode_x_line(content, &address, &parent, &ecode);

	if (HDREQ("Xfail", s)) {
	    /*
	     * remove any matching occurance from the input list
	     */
	    if (address) {
		ret = remove_addr(ret, address, parent);
	    }
	} else if (HDREQ("Xsucceed", s)) {
	    /*
	     * remove any matching occurance from the input list
	     */
	    if (address) {
		ret = remove_addr(ret, address, parent);
	    }
	} else if (HDREQ("Xdefer", s)) {
	    /*
	     * overwrite the address' error->message with the deferal message,
	     * adjusting the error->info code if we can discern it.
	     *
	     * this will happen for every deferral message in the log, leaving
	     * only the most recent in addr->error.
	     *
	     * Note also we may still remove this address if there's a later
	     * Xfail entry for it.
	     */
	    if (address && msg) {
		struct addr *cur;
		struct addr *next;

		for (cur = ret; cur; cur = next) {
		    int found_it = FALSE;
		    register struct addr *top;	/* the ultimate parent address to compare */

		    next = 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 (EQ(cur->in_addr, address)) {
			/* the address does match */
			if (parent) {
			    /* a matching parent is also required for a match */
			    if (top && EQ(parent, top->in_addr)) {
				found_it = TRUE;
			    }
			} else if (top == NULL) {
			    found_it = TRUE;
			}
		    }
		    if (found_it) {
			if (cur->error) {
			    free_error(cur->error);
			}
			cur->error = note_error(ecode, xprintf("deferred: %s", msg));
			/* XXX there should never be another the same.... */
			break;
		    }
		}
	    }
	} else if (HDREQ("Xstderr", s)) {
	    /* append the stderr text to the address' error */
	    if (address && msg) {
		/*
		 * NOTE: The pipe driver may be handed several addresses to
		 * deliver at once (which would be expanded on its command-line
		 * with the "$(${user}$)" iterator trick), and thus it creates
		 * msglog entries with possibly multiple comma-separated
		 * "<address> [orig-to: <parent>]" sub-fields and if we were to
		 * really handle these properly we would have to parse them all
		 * out and append to all the appropriate addresses.
		 */
	    }
	} else if (HDREQ("Xnotice", s)) {
	    /*
	     * basically we just process then ignore these entries as there's
	     * usually no address (other than <>) associated with them.
	     */
	} else if (strncmp(s, "Xsent_error:", sizeof("Xsent_error:")-1) == 0) {
	    register struct identify_addr *new_sent;

	    if (address) {
		/* add this address to the list returned via sent_errors */
		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;
		/*
		 * in theory this address has already been removed from the
		 * input list since there should also be an Xfail entry for it
		 *
		 * XXX we should check to be sure!
		 */
	    }
	} else {
	    DEBUG1(DBG_LOG_LO, "process_msg_log(): encountered misc. entry:\n%v\n-- end entry.\n", s);
	}
    }

    return ret;
}

/*
 * decode_x_line - decode an X-line from the per-message log file
 *
 * The input string pointed to by 'line' should be of the form:
 *
 *	"<"address"> orig-to: <"parent_address"> "message...
 *	[more message...]
 *	"["MM"/"DD"/"YYYY" "HH":"MM":"SS": ["PID"]]" 
 *
 * or:
 *
 *	"<"address"> "message...
 *	[more message...]
 *	"["MM"/"DD"/"YYYY" "HH":"MM":"SS": ["PID"]]" 
 *
 * (i.e. with the "X*: " field name removed)
 *
 * this routine returns the address and the parent_address.  It also returns a
 * pointer to the explanitory message (which may include the reason code).
 */
char *
decode_x_line(content, addressp, parentp, ecodep)
    char *content;			/* content of log entry field */
    char **addressp;			/* return address here */
    char **parentp;			/* return parent address here */
    unsigned long int *ecodep;		/* error code parsed from log entry */
{
    int level;				/* count < and > nesting */
    register char *p = content;		/* points to parts of the line */

    *addressp = NULL;
    *parentp = NULL;

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

    /*
     * extract the address as the text up until before the balancing
     * > character, in <address>.
     */
    *addressp = 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_LOG_LO, "decode_x_line(): WARNING: no address token in address: %v\n", p);
	    return NULL;
	}
	if (*p == '<') {
	    level++;
	} else if (*p == '>') {
	    --level;
	}
    }
    /*
     * XXX instead of destructively turning line into multiple strings we
     * should be copying the substrings and returning them as separate storage.
     */
    *p++ = '\0';			/* the last > is the end of the address */

    /*
     * XXX in order to be able to store quoted addresses in the msglog entries
     * we'll have to also decode the quoting so that when we go back to compare
     * to input addresses they would match....
     */

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

    /* is there a parent address? */
    if (strncmp(p, "orig-to:", sizeof("orig-to:") - 1) == 0) {

	p += sizeof("orig-to:") - 1;

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

	/*
	 * extract the parent as the text up until before the balancing
	 * > character, in <parent_addres>
	 */
	*parentp = p;
	level = 1;
	if (*p == '<') {
	    level++;
	}
	while (level > 0){
	    if (!(p = address_token(p))) {
		DEBUG1(DBG_LOG_LO, "decode_x_line(): WARNING: no address token in parent: %v\n", p);
		return NULL;
	    }
	    if (*p == '<') {
		level++;
	    } else if (*p == '>') {
		--level;
	    }
	}
	/*
	 * XXX instead of destructively turning line into multiple strings we
	 * should be copying the substrings and returning them as separate
	 * storage.
	 */
	*p++ = '\0';			/* the last > is the end of the address */

	/*
	 * XXX in order to be able to store quoted addresses in the msglog
	 * entries we'll have to also decode the quoting so that when we go
	 * back to compare to input addresses they would match....
	 */
	while (isspace((int) *p)) {
	    p++;
	}
    } else {
	DEBUG1(DBG_LOG_HI, "decode_x_line(): no parent ('orig-to:') for: %v\n", *addressp);
    }

    if (strncmp(p, "(ERR", (size_t) 4) == 0) {
	/*
	 * This is a bit of a hack since we assume the value of the error code
	 * constant is the same as the number in its name...
	 *
	 * XXX we should use c_atol() but it expects the string to end at the
	 * end of the number, and we don't use strtoul() as it's not as
	 * portable as we'd like.
	 */
	*ecodep = (unsigned long int) atol(p + 4);
	if (*ecodep == ULONG_MAX) {
	    DEBUG1(DBG_LOG_LO, "decode_x_line(): NOTICE: bad error code value in: %s", p);
	    *ecodep = ERR_NPOSTMAST;	/* leave it zero, but set NPOSTMAST flag */
	}
	*ecodep |= ERR_NSOWNER;		/* the best default notify choice */
    }

    /* XXX for now we leave the timestamp on the end of the message */

    return p;
}


#ifdef STANDALONE

/*
cc -g -DSTANDALONE -o log.stand log.c spool.o alloc.o string.o sysdep.o config.o ascii.o
*/

int debug = 0;				/* adjust as desired */

static char *panic_fn = "paniclog";
static char *log_fn = "logfile";
static char *cons_fn = "/dev/console";
static char *spool_fn = "spoolfile";
char *msg_log_dir = "/tmp";
char *program = "log";
int exitvalue = 0;
enum er_proc error_processing = MAIL_BACK;
int return_to_sender = FALSE;
int islocal = TRUE;
char *sender = "nsc!tron";
char *sender_name = "MasterControlProgram";
char *local_sender;
int force_zero_exitvalue = FALSE;
FILE *errfile = stderr;
long pid_t daemon_pid = 4000000000;	/* 0xEE6B2800 */
uid_t real_uid = 0;

char *
bind_compute_domain()
{
    return NULL;
}

/*
 * excersize the logging code by performing the various operations
 * in a sequence given at run time on the standard input.
 */
/* ARGSUSED */
int
main(argc, argv)
    int argc;
    char *argv[];
{
    register int c;			/* hold key for current operation */
    char *line;				/* a line of input */
    char *p;

    while ((c = getchar()) != EOF) {
	switch (c) {
	case 'O':
	    open_system_logs();
	    break;
	case 'C':
	    close_system_logs();
	    break;
	case 'o':
	    open_msg_log();
	    break;
	case 'c':
	    close_msg_log();
	    break;
	case 's':
	    send_log(stdout, FALSE, "|----- log without X lines -----|\n");
	    break;
	case 'S':
	    send_log(stdout, TRUE, "|----- log with X lines -----|\n");
	    break;
	case 'X':
	    for (p = scan_msg_log(TRUE); p; p = scan_msg_log(FALSE)) {
		printf("%s\n", p);
	    }
	    break;
	case 'p':
	    if ((line = read_line(stdin)))
		panic(EX_OSERR, line);
	    break;
	case 'l':
	    if ((line = read_line(stdin)))
		write_log(WRITE_LOG_SYS, line);
	    break;
	case 'm':
	    if ((line = read_line(stdin)))
		write_log(WRITE_LOG_MLOG, line);
	    break;
	case 'L':
	    if ((line = read_line(stdin)))
		write_log(WRITE_LOG_MLOG|WRITE_LOG_SYS|WRITE_LOG_PANIC, line);
	    break;
	case ' ':
	case '\t':
	case '\n':
	    break;
	default:
	    (void)fprintf(stderr, "%c -- huh?\n", c);
	    break;
	}
    }
    exit(EX_OK);
}
#endif	/* STANDALONE */

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