/*
#ident	"@(#)smail/src:RELEASE-3_2_0_120:retry.c,v 1.69 2004/08/17 19:49:12 woods Exp"
 */

/*
 * retry.c:
 *      Control how often addresses are retried, and how long
 *      retries are attempted until Smail gives up.
 *
 *	This file was contributed by Chip Salzenberg <chip@tct.com>.
 *	It has been modified.
 *
 *	external functions: read_retry_file, retry_addr_before,
 *			    retry_addr_after, retry_addr_finished,
 *			    retry_host_lock, retry_host_unlock
 */

#include "defs.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <setjmp.h>
#include <signal.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
#ifdef HAVE_SYS_LOCKING_H
# include <sys/locking.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 "main.h"
#include "parse.h"
#include "addr.h"
#include "direct.h"
#include "route.h"
#include "log.h"
#include "alloc.h"
#include "list.h"
#include "smailstring.h"
#include "dys.h"
#include "lookup.h"
#include "jump.h"
#include "spool.h"
#include "transport.h"
#include "smailconf.h"
#include "extern.h"
#include "debug.h"
#include "error.h"
#include "smailport.h"

#define RETRY_MODE   0644		/* file creation modes */

#ifdef O_SYNC
# define O_SWYM O_SYNC
#else
# define O_SWYM 0
#endif

struct retry {
    struct retry *succ;			/* next in chain */
    char    *domains;			/* target domain, or "*" */
    time_t  interval;			/* minimum time between tries */
    time_t  duration;			/* how long to keep trying */
};

static struct retry *retries = NULL;	/* array of control structures */

static int locked_fd = -1;
static char *locked_path = NULL;
static JUMP_ENVBUF tryfile_jmp;

static struct retry *default_retry __P((void));
static int match_retry_domain __P((char *, char *));
static int allow_now __P((int, char *, time_t, time_t, struct retry *,
			 struct error **));
static int allow_later __P((int, char *, time_t, time_t, struct retry *, int, struct error **));
static char *tryfile_name __P((struct transport *, char *, int));
static int tryfile_cat __P((struct str *, char *, size_t, int, int, int));
static int tryfile_mkdir __P((struct str *));
static int tryfile_lock_wait __P((int, long));
static int tryfile_lock __P((int));
static void tryfile_unlock __P((int));
static void tryfile_alarm __P((int));
static void write_error_or_unlink __P((int, char *, struct error *));
static void read_retry_error __P((int, char *, struct error *));

/*
 * read_retry_file - read and remember contents of retry file
 */

char *
read_retry_file()
{
    struct stat statbuf;
    struct retry *r, *nr;
    FILE *f;
    char *entry, *error;

    /*
     * forget any previously-read retry data
     */
    for (r = retries; r; r = nr) {
	nr = r->succ;
	xfree(r->domains);
	xfree((char *) r);
    }
    retries = NULL;

    /*
     * try to open retry file, stat file if possible
     */
    if (retry_file == NULL || EQ(retry_file, "-")) {
	return NULL;
    }
    f = fopen(retry_file, "r");
    if (f == NULL) {
	char *emsg = xprintf("cannot open %v: %s", retry_file, strerror(errno));

	if (require_configs) {
	    return emsg;
	} else if (errno != ENOENT) {
	    write_log(WRITE_LOG_PANIC, "%s", emsg);
	}
	xfree(emsg);
	add_config_stat(retry_file, (struct stat *) NULL);
	retries = default_retry();
	return NULL;
    }

    if (fstat(fileno(f), &statbuf) == -1) {
	char *emsg = xprintf("%v: fstat() failed: %s", retry_file, strerror(errno));

	/* realistically this could only ever be EIO... */
	if (require_configs) {
	    return emsg;
	} else {
	    write_log(WRITE_LOG_PANIC, "%s", emsg);
	}
	xfree(emsg);
	add_config_stat(retry_file, (struct stat *) NULL);
	/* ... but go on and try reading from 'f' anyway... */
    } else {
	add_config_stat(retry_file, &statbuf);
    }

    /*
     * loop and read all of the table entries in the retry file
     */
    error = NULL;
    nr = NULL;
    while ((entry = read_entry(f))) {
	struct attribute *new;
	char *p;
	int len;
	time_t interval, duration;

	new = parse_table(entry, &error);
	if (new == NULL) {
	    break;
	}
	/* trim any comment for the error messages */
	if ((p = strchr(entry, '#'))) {
	    *p = '\0';
	}
	/* trim the trailing newline, if any, for error messages */
	len = strlen(entry);
	if (entry[len - 1] == '\n') {
	    entry[len - 1] = '\0';
	}

	/* 
	 * split the string into two parts on the '/'
	 */
	p = strchr(new->value, '/');
	if (p == NULL) {
	    error = xprintf("%v: %s: invalid entry, no '/': \"%v\".", retry_file, new->name, entry);
	    xfree(new->name);
	    /* note that new->value points to the same storage as new->name */
	    xfree((char *) new);
	    break;
	}

	/* terminate interval part and point at the duration part */
	*p++ = '\0';

	/* parse the interval, if specified */
	if (*(new->value)) {
	    interval = ivaltol(new->value);
	    if (interval < 0) {
		error = xprintf("%v: %s: invalid retry interval: %s (from entry \"%v\").", retry_file, new->name, new->value, entry);
		xfree(new->name);
		xfree((char *) new);
		break;
	    }
	} else {
	    interval = retry_interval;
	}

	/* parse the duration, if specified */
	if (*p) {
	    duration = ivaltol(p);
	    if (duration < 0) {
		error = xprintf("%v: %s: invalid retry duration: %s (from entry \"%v\").", retry_file, new->name, p, entry);
		xfree(new->name);
		xfree((char *) new);
		break;
	    }
	} else {
	    duration = retry_duration;
	}

	/* allocate new structure */
	r = (struct retry *) xmalloc(sizeof(struct retry));

	/* assign new structure's domain */
	/* shrink the storage from parse_table() to toss the "value" part */
	r->domains = xrealloc(new->name, (size_t) (strlen(new->name) + 1));

	/* assign new structure's limits */
	r->interval = interval;
	r->duration = duration;

	/* append new structure to chain */
	r->succ = NULL;
	if (nr == NULL) {
	    retries = r;
	} else {
	    nr->succ = r;
	}
	nr = r;

	/* free record and value returned by parse_table() but don't free the
         * name as we're using it in the retry record above
	 */
	xfree((char *) new);

	/* a lone wildcard record obviates any need to read further */
	if (EQ(nr->domains, "*")) {
	    break;
	}
    }
    (void) fclose(f);

    /*
     * if there were no errors reading the retry file, and there is still no
     * default record, and if global configuration requires a default record,
     * add it.
     */
    if (!error && nr && !EQ(nr->domains, "*") && (r = default_retry())) {
	if (nr == NULL) {
	    retries = r;
	} else {
	    nr->succ = r;
	}
    }

    /* return error message, or NULL for success */
    return error;
}

/*
 * dump_retry_config - dump out the current retry configuration
 */
void
dump_retry_config(f)
     FILE *f;
{
    struct retry *rp;

    fputs("#\n# -- retry configuration\n#\n", f);
    for (rp = retries; (rp); rp = rp->succ) {
	char *value;
	char *comment = NULL;

	/* note also that ltoival() uses static memory which means that it
	 * cannot be called twice in the same expression even if it didn't
	 * return NULL....
	 */
	fprintf(f, "%s\t", rp->domains);
	if (rp->interval == 0) {
	    /* queue_interval might not be set yet so just print it symbolically */
	    fputs("0/", f);
	    comment = "#\tnote: a retry_interval of zero implies (queue_interval * 2), unless debugging\n";
	} else {
	    if (!(value = ltoival(rp->interval))) {
		comment = "#\tWARNING: invalid 'interval' part in entry above\n";
		value = "-1";
	    }
	    fprintf(f, "%s/", value);
	}
	if (!(value = ltoival(rp->duration))) {
	    comment = "#\tWARNING: invalid 'duration' part in entry above\n";
	    value = "-1";
	}
	fprintf(f, "%s\n", value);
	if (comment) {
	    fputs(comment, f);
	}
    }
    fputs("#\n# -- end of retry configuration\n#\n", f);
}


/*
 * default_retry - build default retry record from global configuration
 */
static struct retry *
default_retry()
{
    struct retry *r = NULL;

    if (retry_duration >= 0) {
	r = (struct retry *) xmalloc(sizeof(struct retry));
	r->domains = COPY_STRING("*");
	r->interval = retry_interval;
	r->duration = retry_duration;
	r->succ = NULL;
    }
    return r;
}

/*
 * retry_addr_before - retry processing before transport attempt
 *
 * called from transport drivers that do local or external delivery
 * (eg. appendfile and pipe).
 */
struct addr *
retry_addr_before(in, defer, fail)
    struct addr *in;
    struct addr **defer;
    struct addr **fail;
{
    time_t spooled = message_date();
    struct addr *out, *cur, *next;

    out = NULL;
    for (cur = in; cur; cur = next) {
	struct addr **where = &out;
	struct retry *r;
	char *lock_host = (cur->flags & ADDR_SMARTHOST) ? cur->target : cur->next_host;

	next = cur->succ;

	/* skip if no host name to lock */
	if (!lock_host) {
	    cur->succ = out;
	    out = cur;
	    continue;
	}

	for (r = retries; r; r = r->succ) {
	    if (match_retry_domain(r->domains, lock_host)) {
		char *tryname;
		int may_deliver = FALSE;
		int fd;
		time_t started;

		/* calculate path of retry file */
		if (!(tryname = tryfile_name(cur->transport, lock_host, FALSE))) {
		    break;
		}

		DEBUG1(DBG_RETRY_LO, "retry_addr_before(): examining %v\n", tryname);

		/*
		 * examine the retry file: lock and call allow_now() and then unlock;
		 * ignore empties or missing files
		 */
		for (;;) {
		    int lk;

		    if ((fd = open(tryname, O_RDWR|O_SWYM)) == -1) {
			time(&started);
			DEBUG2(DBG_RETRY_LO, "retry_addr_before(): open of %v failed: %s\n", tryname, strerror(errno));
			may_deliver = TRUE;
			break;
		    }
		    lk = tryfile_lock_wait(fd, 0L);
		    if (lk == DB_FAIL) {
			DEBUG2(DBG_RETRY_LO, "retry_addr_before(): lock of %v failed: %s\n", tryname, strerror(errno));
			/* XXX should this be written to the system log instead? */
			(void) close(fd);
			(void) unlink(tryname);	/* XXX really???  well there was no timeout... */
			where = defer;
			break;
		    }
		    if (lk == DB_AGAIN) {	/* it was removed from under us... */
			(void) close(fd);
			continue;
		    }

		    /* the rest happens only once */
		    if (lk == DB_SUCCEED) {
			time(&started);
			may_deliver = allow_now(fd, tryname, spooled, started,
						r, &cur->error);
			(void) tryfile_unlock(fd);
		    }
		    break;
		}

		if (may_deliver) {
		    /* try it; if it fails, we'll touch the retry file */
		    cur->flags |= ADDR_RETRY_FILE;
		} else {
		    struct error *lckerr;

		    /*
		     * retry time has not yet come so check to see if we must
		     * wait for another retry interval or whether we should
		     * bounce the message right away...,
		     */
		    where = defer;
		    if (!allow_later(fd, tryname, spooled, started, r, TRUE, &lckerr)) {
			where = fail;
			/*
			 * now merge the retry error with any existing error.
			 */
			if (cur->error) {
			    struct error *newerr;

			    newerr = note_error(cur->error->info | lckerr->info,
						xprintf("%v: %v", /* XXX bound to get ugly... */
							cur->error->message,
							lckerr->message));
			    free_error(lckerr);
			    free_error(cur->error);
			    cur->error = newerr;
			} else {
			    cur->error = lckerr;
			}
		    }
		}
		(void) close(fd);

		/* stop looking for retry specs after first match */
		break;
	    }
	}

	cur->succ = *where;
	*where = cur;
    }

    return out;
}

/*
 * retry_addr_after - retry processing after delivery attempt (NOP for now)
 */
/* ARGSUSED */
struct addr *
retry_addr_after(started, in, fail)
    time_t started GCC_UNUSED_HACK;
    struct addr *in;
    struct addr **fail GCC_UNUSED_HACK;
{
    return in;
}

/*
 * retry_addr_finished - record success or failure
 */
void
retry_addr_finished(addr)
    struct addr *addr;
{
    char *prev_host = NULL;
    struct addr *cur;

    for (cur = addr; cur; cur = cur->succ) {

	if (cur->flags & ADDR_RETRY_FILE) {	/* but only if we're supposed
						 * to touch the retry file */
	    struct error *error = cur->error;
	    char *tryname;
	    int fd;
	    char *lock_host = (cur->flags & ADDR_SMARTHOST) ? cur->target : cur->next_host;

	    /* don't bother writing to the same file twice in a row */
	    if (prev_host && EQIC(prev_host, lock_host)) {
		continue;
	    }
	    prev_host = lock_host;

	    /* calculate path of retry file */
	    if (!(tryname = tryfile_name(cur->transport, lock_host, (error != NULL)))) {
		continue;
	    }

	    DEBUG2(DBG_RETRY_LO, "retry_addr_finished(): %s %v\n",
		   error ? "writing error message in" : "removing", tryname);

	    /* write to, or remove, retry file */
	    for (;;) {
		int lk;

		if ((fd = open(tryname, O_WRONLY | (error ? O_CREAT : 0),
			       RETRY_MODE)) == -1)
		{
		    /* worth noting if we were not going to remove it... */
		    if (error) {
		        write_log(WRITE_LOG_SYS, "retry_addr_finished(): open(%v) failed: %s",
				  tryname, strerror(errno));
		    }
		    break;
		}
		lk = tryfile_lock_wait(fd, 0L);
		if (lk == DB_FAIL) {
		    write_log(WRITE_LOG_SYS, "retry_addr_finished(): lock of %v failed: %s%s %s",
			      tryname,
			      strerror(errno),
			      error ? "," : "",
			      error ? error->message : "(would have removed it)");
		    (void) close(fd);
		    break;
		}
		if (lk == DB_AGAIN) {	/* it was removed from under us but... */
		    (void) close(fd);
		    if (error) {
			continue;	/* ...we still have something to say! */
		    } else {
			break;		/* ...or maybe we don't care */
		    }
		}

		/* the rest happens only once */
		if (lk == DB_SUCCEED) {
		    /* write the error, or unlink if none */
		    write_error_or_unlink(fd, tryname, error);
		    tryfile_unlock(fd);
		}
		(void) close(fd);
		break;
	    }
	}
    }
}

/*
 * retry_host_lock - lock the target host for delivery the given "hostname"
 *
 * return SUCCEED if lock succeeded, FAIL if it failed.
 * if failure is temporary, set *defer_failure.
 *
 * some transports will use this function.  those that do so won't
 * call retry_addr_before(), so retry_addr_*() will do nothing.
 *
 * Transports calling this function will/must always do the right thing to
 * preserve or merge the most important error for the message to be delivered.
 */
int
retry_host_lock(transport, host, defer_failurep, errorp)
    struct transport *transport; /* transport name to use under retry_file */
    char *host;			/* target hostname to lock */
    int *defer_failurep;   	/* set to TRUE if retry duration not yet exceeded */
    struct error **errorp;	/* locking error if FAIL, prev. defer error if above */
{
    time_t spooled = message_date();
    time_t started;
    struct retry *r;
    char *tryname;
    int fd, dfok, ret;

    /* safety: unlock previously locked retry file */
    if (locked_fd >= 0) {
	tryfile_unlock(locked_fd);
	(void) close(locked_fd);
	locked_fd = -1;
    }
    if (locked_path) {
	xfree(locked_path);
	locked_path = NULL;
    }

    /* calculate path of retry file, creating directories */
    if ((tryname = tryfile_name(transport, host, TRUE)) == NULL) {
	*errorp = note_error(ERR_NPOSTMAST | ERR_175, xprintf("cannot create retry file directory for %v/%s", transport, host));
	*defer_failurep = TRUE;
	return FAIL;
    }

    /* open and lock retry file */
    for (;;) {
	int lk;
	char *errstr;

	if ((fd = open(tryname, O_RDWR|O_CREAT|O_SWYM, RETRY_MODE)) == -1) {
	    if (!*errorp) {
		*errorp = note_error(ERR_NPOSTMAST | ERR_175,
				     xprintf("cannot open host retry file %v: %s",
					     tryname, strerror(errno)));
	    }
	    *defer_failurep = TRUE;
	    return FAIL;
	}
	if (process_queue) {
	    lk = tryfile_lock(fd);	/* don't bother waiting! */
	} else {
	    lk = tryfile_lock_wait(fd, host_lock_timeout);
	}
	if (lk == DB_SUCCEED) {
	    break;			/* GOT IT!  Get outta the loop! */
	}
	if (lk == DB_AGAIN) {		/* lock was removed from under us... */
	    (void) close(fd);
	    continue;			/* ... so try again */
	}

	/* else (lk == DB_FAIL) */
	errstr = strerror(errno);
	DEBUG2(DBG_RETRY_LO, "retry_host_lock(): %s: %v.\n",
	       process_queue ? "already locked" : "timed out", errstr);
	(void) close(fd);
	if (!*errorp) {
	    *errorp = note_error(ERR_DONTLOG | ERR_175,
				 xprintf("host retry file %v lock failed: %s (%s)",
					 tryname,
					 process_queue ? "already locked?" : "timed out?",
					 errstr));
	}

	/*
	 * XXX does this cause any problems?  What if there is another process
	 * working on this message and it's just about to read the message log
	 * before we write an Xdefer entry to it?
	 */
	*defer_failurep = TRUE;

	/* XXX should we unlink(tryname) here? */

	return FAIL;
    }

    /* now is the official start of attempt */
    (void) time(&started);

    /* assume the best */
    ret = SUCCEED;
    dfok = TRUE;

    /* if host matches any retry entries, enforce the matching one */
    for (r = retries; r; r = r->succ) {
	if (match_retry_domain(r->domains, host)) {
	    DEBUG2(EQ(r->domains, "*") ? DBG_RETRY_HI : DBG_RETRY_MID,
		   "host %v matched retry entry for '%s'.\n", host, r->domains);

	    /* notice if it's too soon to try delivery */
	    ret = allow_now(fd, tryname, spooled, started, r, errorp) ? SUCCEED : FAIL;

	    /* we can defer if further retries would be soon enough */
	    /*
	     * XXX we don't always want to include the previous retry file
	     * error message -- only if the invoking transport driver doesn't
	     * already have a better message to include.  However at this point
	     * it's impossible for us to know what the transport driver will do
	     * with the error message.
	     */
	    dfok = allow_later(fd, tryname, spooled, started, r, TRUE, errorp);

	    /* stop looking for retry specs after first match */
	    break;
	}
    }

    /* keep human informed */
    DEBUG5(DBG_RETRY_LO,
	   "lock of %v %s (will%s defer if delivery fails)%s%v\n",
	   tryname,
	   (ret == SUCCEED) ? "succeeded" : "failed",
	   dfok ? "" : " NOT",
	   (errorp && *errorp) ? ": " : "",
	   (errorp && *errorp) ? (*errorp)->message : "");

    /* if retry control allows deferral, inform caller */
    *defer_failurep = dfok;

    if (ret == SUCCEED) {
	/* remember name, fd and retry parameters */
	locked_fd = fd;
	locked_path = COPY_STRING(tryname);
    } else {
	/* we won't be needing the lock file after all */
	tryfile_unlock(fd);
	(void) close(fd);
    }

    return ret;
}

/*
 * retry_host_unlock - unlock the target host for delivery, recording status
 *
 * set *fail if retry time has expired.
 *
 * some transports will use this function after using retry_host_lock().  those
 * that do so won't call retry_addr_before(), so retry_addr_after() and
 * retry_addr_finish() will do nothing because ADDR_RETRY_FILE isn't set by
 * retry_host_lock().
 */
/* ARGSUSED */
void
retry_host_unlock(started, error)
    time_t started GCC_UNUSED_HACK;
    struct error *error;
{
    if (locked_fd < 0) {
	return;
    }

    DEBUG3(DBG_RETRY_LO, "unlocking %s and %s%v.\n",
	   locked_path,
	   error ? "storing error: " : "unlinking",
	   error ? error->message : "");

    /* write the error, or unlink if none */
    write_error_or_unlink(locked_fd, locked_path, error);

    /* unlock and close the retry file */
    tryfile_unlock(locked_fd);
    (void) close(locked_fd);

    /* forget that we ever saw the lock file */
    locked_fd = -1;
    xfree(locked_path);
    locked_path = NULL;

    return;
}

/*
 * match_retry_domain - determine if domain pattern matches given host
 */
static int
match_retry_domain(domains, target)
    char *domains;			/* colon separated list of domains */
    char *target;			/* target to test against */
{
    register char *cur;			/* current domain being checked */

    if (EQ(domains, "*")) {
	return TRUE;
    }

    for (cur = strcolon(domains); cur; cur = strcolon((char *)NULL)) {
	if (is_suffix(cur, target, FALSE)) {
	    return TRUE;
	}
    }

    return FALSE;
}

/*
 * allow_now - can we deliver now?  and if not, can we defer?
 */
static int
allow_now(fd, tryname, spooled, started, r, error)
    int fd;
    char *tryname;
    time_t spooled GCC_UNUSED_HACK;	/* when was the msg spooled? */
    time_t started;			/* when was this lock attempt started? */
    struct retry *r;
    struct error **error;
{
    struct stat statbuf;
    time_t interval;

    if (fstat(fd, &statbuf) == -1) {
	/*
	 * EIO is the only plausible error -- treat it as a fresh empty file
	 */
	write_log(WRITE_LOG_PANIC, "retry file %v: fstat() failed: %s", tryname, strerror(errno));
	return TRUE;
    }
    if (statbuf.st_size == 0) {
	/* fresh empty retry files mean retry time is right now */
	DEBUG1(DBG_RETRY_HI, "retry file (%v) freshly created -- retry now!\n", tryname);
	return TRUE;
    }
    if (r->interval == 0) {
	interval = (time_t) ((queue_interval > 0) ? (queue_interval * 2) : 1);
    } else {
	interval = r->interval;
    }
    DEBUG3(DBG_RETRY_HI, "%v: retry interval: %s (queue_interval = %ld)\n",
	   tryname, ltoival(interval), (long) queue_interval);
    if (statbuf.st_mtime <= started && (started - statbuf.st_mtime) < interval) {
	/* just wait a little longer */
	DEBUG1(DBG_RETRY_MID, "%v: retry time not yet reached\n", tryname);
	if (*error == NULL) {
	    *error = note_error(ERR_DONTLOG | ERR_174,
				xprintf("%v: retry time not yet reached", tryname));
	}
	return FALSE;
    }

    /* it must be retry time.... */
    DEBUG1(DBG_RETRY_HI, "%v: retry time is now!\n", tryname);

    return TRUE;
}

/*
 * allow_later - can we make more retries after this one?
 */
static int
allow_later(fd, tryname, spooled, started, r, inclrtry, errorp)
    int fd;
    char *tryname;
    time_t spooled;			/* when was the msg spooled? */
    time_t started;			/* when was this lock attempt started? */
    struct retry *r;
    int inclrtry;			/* include retry error msg? */
    struct error **errorp;
{
    /* if this was the last try... */
    DEBUG1(DBG_RETRY_HI, "retry duration: %s\n", ltoival(r->duration));
    if ((started - spooled) > r->duration) {
	struct error rtry_err;

	rtry_err.info = ERR_173;
	rtry_err.message = NULL;

	/*
	 * if we are asked to keep the retry error then try to read the
	 * previous one from the retry file
	 */
	if (inclrtry) {
	    read_retry_error(fd, tryname, &rtry_err);
	}
	*errorp = note_error(ERR_NSOWNER | rtry_err.info,
			     xprintf("%s%sRetry duration (%s) has been exceeded.\n\tNo further delivery attempts will be made.",
				     rtry_err.message ? rtry_err.message : "",
				     rtry_err.message ? "\n\t" : "",
				     ltoival(r->duration)));
	return FALSE;
    }
    /* there is still time for more retries */
    DEBUG(DBG_RETRY_MID, "retry timeout duration not yet reached\n");

    return TRUE;
}

/*
 * tryfile_name - figure name of file used to hold timestamp for
 *		  the given host.
 *
 * Note: returns address of static area.
 */
static char *
tryfile_name(transport, host, create_dirs)
    struct transport *transport;
    char *host;
    int create_dirs;
{
    static long pathmax = 0;
    static struct str path;
    static int inited = FALSE;

#ifdef POSIX_OS
    if (pathmax == 0) {
	long n = pathconf(".", _PC_NAME_MAX);
	if (n != -1) {
	    pathmax = n;
	}
    }
#else
#ifdef UNIX_BSD
    if (pathmax == 0) {
	pathmax = 250;
    }
#endif /* UNIX_BSD */
#endif /* POSIX_OS */
    if (pathmax == 0) {
	pathmax = 14;
    }

    if (!inited) {
	STR_INIT(&path);
	inited = TRUE;
    } else {
	STR_CHECK(&path);
	STR_CLEAR(&path);
    }

    str_cat(&path, "retry");
    if (create_dirs && tryfile_mkdir(&path) == FAIL) {
	return NULL;
    }

    STR_NEXT(&path, '/');
    str_cat(&path, (transport->retry_dir && transport->retry_dir[0])
		   ? transport->retry_dir : transport->name);
    if (create_dirs && tryfile_mkdir(&path) == FAIL) {
	return NULL;
    }

    if ((int)strlen(host) <= pathmax) {
	if (tryfile_cat(&path, host, strlen(host), TRUE, FALSE,
			create_dirs) == FAIL)
	{
	    return NULL;
	}
    } else {
	char *p, *q;
	int first, partial;

	first = TRUE;
	p = host + strlen(host);
	for (;;) {
	    while (p > host && *(p - 1) == '.') {
		--p;
	    }
	    if (p == host) {
		break;
	    }
	    q = p;
	    partial = FALSE;
	    while (p > host && *(p - 1) != '.') {
		if ((q - p) >= (pathmax - 1)) {
		    partial = TRUE;
		    break;
		}
		--p;
	    }
	    if (tryfile_cat(&path, p, (size_t) (q - p), first, partial,
			    create_dirs) == FAIL)
	    {
		return NULL;
	    }
	    first = FALSE;
	}
    }
    STR_NEXT(&path, '\0');

    return STR(&path);
}

static int
tryfile_cat(sp, p, len, first, partial, create_dirs)
    struct str *sp;
    char *p;
    size_t len;
    int first;
    int partial;
    int create_dirs;
{
    size_t i;

    if (!first && STR_LEN(sp) && STR(sp)[STR_LEN(sp) - 1] != '_') {
	STR_NEXT(sp, '.');
    }

    if (create_dirs && tryfile_mkdir(sp) == FAIL) {
	return FAIL;
    }

    STR_NEXT(sp, '/');
    for (i = 0; i < len; ++i) {
	STR_NEXT(sp, tolower((int) (*(p + i) & 0xFF))); /*  hostnames are case insensitive */
    }
    if (partial) {
	STR_NEXT(sp, '_');
    }

    return SUCCEED;
}

static int
tryfile_mkdir(sp)
    struct str *sp;
{
    struct stat statbuf;
    int ret = SUCCEED;

    STR_NEXT(sp, '\0');				/* NUL terminate */
    if (stat(STR(sp), &statbuf) == -1) {
	DEBUG1(DBG_RETRY_LO, "make directory %s\n", STR(sp));
	(void) mkdir(STR(sp), auto_mkdir_mode);
	if (stat(STR(sp), &statbuf) == -1) {
	    write_log(WRITE_LOG_SYS, "can't create directory %s", STR(sp));
	    ret = FAIL;
	}
    }
    STR_PREV(sp);				/* drop the NUL just added */

    return ret;
}

#if ((__STDC__ - 0) <= 0)
/* most traditional compilers won't optimize global variables */
JUMPSIG old_sigalrm;
unsigned int old_alarm;
#else
static volatile JUMPSIG old_sigalrm;
static volatile unsigned int old_alarm;
#endif

/*
 * tryfile_lock_wait - lock an open retry file, waiting until an exclusive lock
 * can be obtained; optionally, time out
 *
 * Returns DB_SUCCESS (good), DB_FAIL (bad),
 * or DB_AGAIN (if file was removed from under us).
 */
static int
tryfile_lock_wait(fd, timeout)
    int fd;
    long timeout;
{
    int ret;

    if (timeout && JUMP_SETJMP(tryfile_jmp)) {
	ret = DB_FAIL;
#ifdef ETIMEDOUT
	errno = ETIMEDOUT;
#else
	errno = EBUSY;		/* not an ideal msg on most systems but better
				 * than none....
				 */
#endif
    } else {
	/* lock retry file with optional timeout */
	if (timeout) {
	    old_alarm = alarm(0);
	    JUMP_SETSIG(SIGALRM, tryfile_alarm, &old_sigalrm);
	    (void) alarm(timeout < 2 ? 2 : (unsigned)timeout);
	}

	if (lock_fd_wait(fd) == FAIL) {
	    ret = DB_FAIL;
	} else {
	    struct stat st;

	    ret = (fstat(fd, &st) == 0 && st.st_nlink > 0)
		   ? DB_SUCCEED : DB_AGAIN;
	}
	if (timeout) {
	    (void) alarm(0);
	    JUMP_CLEARSIG(SIGALRM, &old_sigalrm);
	    if (old_alarm) {
		(void) alarm(old_alarm);
	    }
	}
    }

    return ret;
}

/*
 * tryfile_lock - lock an open retry file, non-blocking mode
 *
 * Returns DB_SUCCESS (good), DB_FAIL (bad),
 * or DB_AGAIN (if file was removed from under us).
 */
static int
tryfile_lock(fd)
    int fd;
{
    int ret;

    if (lock_fd(fd) == FAIL) {
	ret = DB_FAIL;
    } else {
	struct stat st;

	ret = (fstat(fd, &st) == 0 && st.st_nlink > 0) ? DB_SUCCEED : DB_AGAIN;
    }

    return ret;
}

/*
 * tryfile_unlock - unlock a retry file locked with timeout
 */
static void
tryfile_unlock(fd)
    int fd;
{
    unlock_fd_wait(fd);
}

/*
 * tryfile_alarm - alarm function
 */
/* ARGSUSED */
static void
tryfile_alarm(sig)
     int sig GCC_UNUSED_HACK;
{
    JUMP_LONGJMP(tryfile_jmp, 1);
}

/*
 * write_error_or_unlink - write an error description to a retry file, or remove it
 */
static void
write_error_or_unlink(fd, tryfile, error)
    int fd;
    char *tryfile;
    struct error *error;
{
    if (error == NULL) {
	/* no error: remove file */
	(void) unlink(tryfile);
    } else {
	char *e;
	int elen;

	/* write error code and message */
	e = xprintf("%ld %s\n", error->info & ERR_MASK, error->message);
	elen = (int)strlen(e);
	(void) lseek(fd, (off_t) 0L, SEEK_SET);
	(void) write(fd, e, (size_t) (elen + 1));
	(void) xfree(e);

	/* eliminate remaining bytes, to avoid confusing human readers */
	(void) fsetsize(fd, (off_t)elen, (off_t)elen + 1);

	/* flush changes */
#if defined(HAVE_FSYNC) && !defined(O_SYNC)
	(void) fsync(fd);
#endif
    }
}

/*
 * read_retry_error - read an error description from a retry file
 */
/* ARGSUSED */
static void
read_retry_error(fd, tryfile, err)
    int fd;				/* FD for the retry file */
    char *tryfile;			/* name of the retry file */
    struct error *err;			/* ptr to struct to fill */
{
    struct stat statbuf;
    char *buf, *errmsg, *p;
    long errcode;
    size_t len;

    /* assume that we can't read the file */
    err->info = ERR_173;
    err->message = "(no message in retry file)";

    /* read the whole file, if it exists, and if it's not too big */
    if (fstat(fd, &statbuf) == -1) {
	if (errno != ENOENT) {
	    err->info |= ERR_NPOSTMAST;
	}
	err->message = xprintf("(fstat(%v) failed: %s)", tryfile, strerror(errno));
	return;
    }
    if (statbuf.st_size > 0x7FFF) { /* XXX ??? */
	err->info |= ERR_NPOSTMAST;
	err->message = xprintf("(retry file %v is insanely large (%ld))",
			       tryfile, (long) statbuf.st_size);
	return;
    }
    len = statbuf.st_size;
    buf = xmalloc(len + 1);
    if ((size_t) read(fd, buf, (size_t) (len + 1)) != len) {
	err->message = xprintf("(could not read all %ld bytes of retry file %v: %s)",
			       (long) statbuf.st_size, tryfile, strerror(errno));
	err->info |= ERR_NPOSTMAST;
	xfree(buf);
	return;
    }
    buf[len--] = '\0';
    if (buf[len] == '\n') {
	buf[len--] = '\0';		/* trim any trailing newline */
    }

    /* parse the error number and message */
    errcode = 0;
    errmsg = NULL;

    p = buf;
    while (isdigit((int) *p)) {
	++p;
    }
    if (*p == ' ') {
	*p++ = '\0';
	errcode = atoi(buf);
	errmsg = p;
    }

    /* if both code and message are parsable, use them */
    if (errcode && errmsg && *errmsg) {
	err->info = errcode;
	err->message = COPY_STRING(errmsg);
    }

    /* free copy of file */
    xfree(buf);
}

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