/*
 *	Copyright 1988 by Rayan S. Zachariassen, all rights reserved.
 *	This will be free software, but only when it is finished.
 */

#include "hostenv.h"
#include <stdio.h>
#include <ctype.h>
#include <syslog.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <mail.h>
#include "scheduler.h"

/*
 * Parse a diagnostic string from a transport agent, and do whatever is
 * necessary to update our model of the world to reflect the new reality.
 *
 *	18453/3527/ok Quick SMTP connection!
 *	18453/3527/deferred Unable to contact host!
 *	18453/3527/error Unknown user name "fred".
 *	18453/3527/retryat [+]NNNN circuit unavailable.
 */

/* dispatch table for diagnostic types */

extern int	u_ok(), u_deferred(), u_error(), u_retryat();

static struct diagcodes {
	char	*name;
	int	(*fcn)();
} diags[] = {
		{	"ok",		u_ok		},
		{	"deferred",	u_deferred	},
		{	"error",	u_error		},
		{	"retryat",	u_retryat	},
		{	NULL,		NULL		}
};

void
update(diagnostic)
	char	*diagnostic;
{
	register char	*cp;
	char	*type, *message;
	int	inum, offset;
	struct diagcodes *dcp;
	extern int cistrcmp();

	inum = atoi(diagnostic);
	if ((cp = strchr(diagnostic, '/')) == NULL) {
		fprintf(stderr, "Misformed diagnostic: %s\n", diagnostic);
		return;
	}
	offset = atol(++cp);
	if ((cp = strchr(cp, '/')) == NULL) {
		fprintf(stderr, "Misformed diagnostic: %s\n", diagnostic);
		return;
	}
	type = ++cp;
	while (*cp != '\0' && isascii(*cp) && !isspace(*cp))
		++cp;
	if (*cp == '\0') {
		message = NULL;
	} else {
		*cp++ = '\0';
		message = cp;
	}
	for (dcp = &diags[0]; dcp->name != NULL; ++dcp) {
		if (cistrcmp(dcp->name, type) == 0) {
			(void) (dcp->fcn)(inum, offset, message);
			break;
		}
	}
	if (dcp->name == NULL)
		fprintf(stderr, "Unknown diagnostic type ignored: %s\n", type);
}

/*
 * Deallocate a web entry (host or channel vertex header structure).
 */

void
unweb(i, wp)
	int i;
	struct web *wp;
{
	struct spblk *spl;

	wp->link = wp->lastlink = NULL;
	if (wp->kids != 0)
		return;		/* too early to actually remove it */
	spl = sp_lookup(symbol((u_char *)wp->name), spt_mesh[i]);
	if (spl != NULL)
		sp_delete(spl, spt_mesh[i]);
	free(wp->name);
	free((char *)wp);
}

/*
 * Deallocate a control file descriptor.
 */

void
unctlfile(cfp)
	struct ctlfile *cfp;
{
	struct spblk *spl;
	char	path[MAXPATHLEN+1];
	extern void reporterrs();
	extern int eunlink();

	reporterrs(cfp);
#ifdef	LOG_INFO
	syslog(LOG_INFO, "%s: done", cfp->mid);
#endif	/* LOG_INFO */
	(void) sprintf(path, "../%s/%s", TRANSPORTDIR, cfp->mid);
	(void) eunlink(path);
/* printf("%s: unlink %s\n", cfp->logident, path); */
	(void) sprintf(path, "../%s/%s", QUEUEDIR, cfp->mid);
	(void) eunlink(path);
	if (cfp->contents != NULL)
		 free(cfp->contents);
	if (cfp->logident != NULL)
		free(cfp->logident);
	if (cfp->erroraddr != NULL)
		free(cfp->erroraddr);
	if (cfp->vfp != NULL) {
		(void) fprintf(cfp->vfp,
			       "scheduler done processing %s\n", cfp->mid);
		(void) fclose(cfp->vfp);
	}
	(void) free(cfp->mid);
	spl = sp_lookup((u_int)cfp->id, spt_mesh[L_CTLFILE]);
	if (spl != NULL)
		sp_delete(spl, spt_mesh[L_CTLFILE]);
	free((char *)cfp);
}

void
unvertex(vp)
	register struct vertex *vp;
{
	register struct vertex *vv;
	int	i, remove;
	char	path[MAXPATHLEN+1], *cp;
	extern int subsubdirs;
	extern int ermdir(), eunlink();
	extern void disentangle();

	if (vp->ngroup > 0)
		return;
	/*
	 * If we remove a vertex while using channel/host/controlfile, we
	 * can unlink that file right here & now.
	 */
	if (subsubdirs) {
		(void) sprintf(path, "%s/%s/%lu",
			      vp->orig[L_CHANNEL]->name,
			      vp->orig[L_HOST]->name,
			      vp->cfp->id);
		(void) eunlink(path);
		if (vp->cfp->vfp != NULL)
			(void) fprintf(vp->cfp->vfp, "unlinked %s\n", path);
/* printf("%s: unlink %s\n", vp->cfp->logident, path); */
		/*
		 * If this is the last channel/host combination for this
		 * control file, remove it. (see large comment below).
		 */
		for (vv = vp->cfp->head; vv != NULL; vv = vv->next[L_CTLFILE]) {
			if (vv != vp
			    && vv->orig[L_CHANNEL] == vp->orig[L_CHANNEL]
			    && vv->orig[L_HOST] == vp->orig[L_HOST])
				break;
		}
		if (vv == NULL) {
			if ((cp = strrchr(path, '/')) != NULL)	/* assert! */
				*cp = '\0';
			(void) ermdir(path);	/* the directory */
/* printf("%s: rmdir %s\n", vp->cfp->logident, path); */
		}
	} else {
		/*
		 * If we use channel/controlfile, we can only unlink()
		 * the controlfile if there are no more vertices for this
		 * channel, belonging to this control file.
		 * Instead of getting into a long complicated boolean
		 * expression, we apply the KISS principle (as above).
		 * Unfortunately this makes the whole thing O(n^2)...
		 * Many optimizations are possible, but not obviously
		 * worthwhile.
		 */
		for (vv = vp->cfp->head; vv != NULL; vv = vv->next[L_CTLFILE]) {
			if (vv != vp
			    && vv->orig[L_CHANNEL] == vp->orig[L_CHANNEL])
				break;
		}
		if (vv == NULL) {
			sprintf(path, "%s/%lu",
				      vp->orig[L_CHANNEL]->name,
				      vp->cfp->id);
			(void) eunlink(path);
			if (vp->cfp->vfp != NULL)
				fprintf(vp->cfp->vfp, "unlinked %s\n", path);
/* printf("%s: unlink %s\n", vp->cfp->logident, path); */
		}
	}
	for (i = 0; i < SIZE_L; ++i) {
		if (vp->next[i] != NULL)
			vp->next[i]->prev[i] = vp->prev[i];
		if (vp->prev[i] != NULL)
			vp->prev[i]->next[i] = vp->next[i];
		if (i == L_CTLFILE)
			continue;
		remove = 0;
		if (vp->orig[i]->link == vp)
			if ((vp->orig[i]->link = vp->next[i]) == NULL)
				remove = 1;
		if (vp->orig[i]->lastlink == vp)
			if ((vp->orig[i]->lastlink = vp->prev[i]) == NULL)
				remove = 1;
		if (remove) {
			if (i == L_CHANNEL) {
				/*
				 * If we are removing a channel, obviously
				 * the directory is empty...
				 */
				sprintf(path, "%s", vp->orig[L_CHANNEL]->name);
				(void) ermdir(path);
/* printf("%s: rmdir %s\n", vp->cfp->logident, path); */
			}
			unweb(i, vp->orig[i]);
		}
	}
	if (vp->cfp->head == vp)
		if ((vp->cfp->head = vp->next[L_CTLFILE]) == NULL)
			unctlfile(vp->cfp);
	if (vp->message != NULL)
		free(vp->message);
	disentangle(vp);
	free((char *)vp);
}

struct vertex *
findvertex(inum, offset, idx)
	int	inum, *idx;
	long	offset;
{
	struct spblk *spl;
	struct ctlfile *cfp;
	struct vertex *vp;
	int	i;
	extern char *progname;

	/* It is NOT POSSIBLE to cache cfp, based on the inum */
	spl = sp_lookup((u_int)inum, spt_mesh[L_CTLFILE]);
	if (spl == NULL || (cfp = (struct ctlfile *)spl->data) == NULL) {
		(void) fprintf(stderr, "%s: cannot find control file for %d!\n",
				progname, inum);
		return NULL;
	}
	for (i = 0; i < cfp->nlines; ++i) {
		if (cfp->offset[i] == offset) {
			*idx = i;
			break;
		}
	}
	if (i >= cfp->nlines) {
		fprintf(stderr,
			"%s: unknown address offset %ld in control file %d!\n",
			progname, offset, inum);
		return NULL;
	}
	for (vp = cfp->head; vp != NULL; vp = vp->next[L_CTLFILE])
		for (i = 0; i < vp->ngroup; ++i)
			if (vp->index[i] == *idx)
				return vp;
	fprintf(stderr,
	    "%s: multiple processing of address at %ld in control file %d!\n",
	    progname, offset, inum);
	return NULL;
}

/*
 * To implement the CF_OBSOLETES command, we need to map a message-id to
 * a control file structure, unlink the unprocessed addresses from the web,
 * and physically get rid of the message.  We also need to implement some
 * form of security here; same file ownership and error return address might
 * be a good approximation.
 */

int
owner(cfp)
	struct ctlfile *cfp;
{
	char *path;
	struct stat stbuf;
	static int nope = 12345678;

	if (cfp->mid == NULL)
		abort();
	path = emalloc(5+strlen(cfp->mid)+sizeof QUEUEDIR);
	(void) sprintf(path, "../%s/%s", QUEUEDIR, cfp->mid);
	if (stat(path, &stbuf) < 0)
		return ++nope;
	free(path);
	return stbuf.st_uid;
}

void
deletemsg(msgid, curcfp)
	char *msgid;
	struct ctlfile *curcfp;
{
	struct ctlfile *cfp;
	struct spblk *spl;
	extern struct spblk *sp_fhead(), *sp_fnext();

	/* map message id to ctl structure */
	for (spl = sp_fhead(spt_mesh[L_CTLFILE]); spl != NULL ;
	     spl = sp_fnext(spl)) {
		cfp = (struct ctlfile *)spl->data;
		/* XX: message-id comparison is a Hard Problem. Approximate. */
		if (strcmp(cfp->logident, msgid) == 0)
			break;
	}
	if (spl == NULL)
		return;
	/* security checks */
	/* XX: address comparison is also a Hard Problem... sigh */
	if ((cfp->erroraddr == NULL && curcfp->erroraddr != NULL)
	    || (cfp->erroraddr != NULL && curcfp->erroraddr == NULL)
	    || (cfp->erroraddr != curcfp->erroraddr
		&& strcmp(cfp->erroraddr, curcfp->erroraddr) != 0))
		return;
	if (owner(cfp) != owner(curcfp))
		return;
	/*
	 * It might be useful to return a report about what happened, but
	 * for mailing lists this is dangerous.  Let's not, until we can
	 * test for some 'return-receipt-requested' flag.
	 */

#ifdef	LOG_INFO
	syslog(LOG_INFO, "%s: obsoleted by %s", cfp->mid, curcfp->mid);
#endif	/* LOG_INFO */

	/*
	 * unvertex() will do unctlfile() on the last vertex, hence
	 * this strange way of doing the unlink.
	 */
	while (cfp->head->next[L_CTLFILE] != NULL) {
		cfp->head->ngroup = 0;
		unvertex(cfp->head);
	}
	cfp->head->ngroup = 0;
	unvertex(cfp->head);
}


/* Lifted from BIND res/res_debug.c */
/*
 * Return a mnemonic for a time to live
 */
char *
saytime(value, buf, shortform)
	u_long value;
	char *buf;
	int shortform;
{
	int secs, mins, hours, fields = 0;
	register char *p;

	p = buf;

	secs = value % 60;
	value /= 60;
	mins = value % 60;
	value /= 60;
	hours = value % 24;
	value /= 24;

#define	PLURALIZE(x)	x, (x == 1) ? "" : "s"
	while (*p) ++p;
	if (value) {
		if (shortform)
			(void) sprintf(p, "%dd", value);
		else
			(void) sprintf(p, "%d day%s", PLURALIZE(value));
		++fields;
		while (*++p);
	}
	if (hours) {
		if (shortform)
			(void) sprintf(p, "%dh", hours);
		else {
			if (value && p != buf)
				*p++ = ' ';
			(void)sprintf(p, "%d hour%s", PLURALIZE(hours));
		}
		++fields;
		while (*++p);
	}
	if (mins && fields < 2) {
		if (shortform)
			(void) sprintf(p, "%dm", mins);
		else {
			if ((hours || value) && p != buf)
				*p++ = ' ';
			(void)sprintf(p, "%d min%s", PLURALIZE(mins));
		}
		while (*++p);
	}
	if (secs && fields < 2) {
		if (shortform)
			(void) sprintf(p, "%ds", secs);
		else {
			if ((mins || hours || value) && p != buf)
				*p++ = ' ';
			(void)sprintf(p, "%d sec%s", PLURALIZE(secs));
		}
		while (*++p);
	}
	*p = '\0';
	return buf;
}

void
vtxupdate(vp, index)
	struct vertex *vp;
	int index;
{
	int i;

	for (i = 0; i < vp->ngroup; ++i)
		if (vp->index[i] == index) {
			for (++i; i < vp->ngroup; ++i)
				vp->index[i-1] = vp->index[i];
			vp->ngroup -= 1;
			if (vp->ngroup <= 0)
				unvertex(vp);
			break;
		}
}

void
expaux(vp, index, buf)
	struct vertex *vp;
	int index;
	char *buf;
{
	int i;
	extern char *progname;
	extern void msgerror();

	for (i = 0 ; i < vp->ngroup; ++i)
		if (vp->index[i] == index)
			msgerror(vp, vp->cfp->offset[index], buf);
	(void) fprintf(stderr, "%s: %s/%s from %s %s\n", progname,
		vp->orig[L_CHANNEL]->name, vp->orig[L_HOST]->name,
		vp->cfp->erroraddr == NULL ? "?" : vp->cfp->erroraddr, buf);
	vtxupdate(vp, index);
}

void
expire(vp, index)
	struct vertex *vp;
	int index;
{
	int i;
	char *emsg, buf[BUFSIZ];
	static char *fmt = "%s, problem was: %s";
	extern char *saytime();

	(void) strcpy(buf, "expired after ");
	(void) saytime((u_long)(vp->ce.expiry - vp->cfp->ctime), buf, 0);

	if (vp->message != NULL && *(vp->message) != '\0') {
		emsg = malloc(strlen(buf) + strlen(vp->message) + strlen(fmt));
		(void) sprintf(emsg, fmt, buf, vp->message);
	} else
		emsg = buf;

	if (index < 0) {
		for (i = 0 ; i < vp->ngroup; ++i)
			expaux(vp, vp->index[i], emsg);
	} else
		expaux(vp, index, emsg);

	if (emsg != buf)
		(void) free(emsg);
}

/*ARGSUSED*/
int
u_ok(inum, offset, message)
	int	inum;
	long	offset;
	char	*message;
{
	int	index;
	register struct vertex *vp;

	if ((vp = findvertex(inum, offset, &index)) == NULL)
		return 0;
	vp->attempts += 1;
/* printf("%s: %d/%d/ok %s\n", vp->cfp->logident, inum, offset, message ? message : "-"); */
	if (vp->cfp->vfp != NULL && vp->cfp->contents != NULL) {
		(void) fprintf(vp->cfp->vfp, "%s: ok %s\n",
				      vp->cfp->contents + offset + 2,
				      message == NULL ? "(sent)" : message);
	}
	vtxupdate(vp, index);
	return 1;
}

int
u_deferred(inum, offset, message)
	int	inum;
	long	offset;
	char	*message;
{
	int	index;
	register struct vertex *vp;
	extern void reschedule();

	if ((vp = findvertex(inum, offset, &index)) == NULL)
		return 0;
	vp->attempts += 1;
/* printf("%s: %d/%d/deferred %s\n", vp->cfp->logident, inum, offset, message ? message : "-"); */
	if (message != NULL) {
		if (vp->message != NULL)
			free(vp->message);
		/* fprintf(stderr, "add message '%s' to node %s/%s\n",
				message, vp->orig[L_CHANNEL]->name,
				vp->orig[L_HOST]->name); */
		vp->message = strnsave(message, strlen(message));
	}
	if (vp->cfp->vfp != NULL && vp->cfp->contents != NULL) {
		(void) fprintf(vp->cfp->vfp, "%s: deferred %s\n",
				      vp->cfp->contents + offset + 2,
				      message == NULL ? "(unknown)" : message);
	}
	/*
	 * Even though we may get several of these per web entry,
	 * the heuristic in reschedule() to ignore the request if
	 * the time is already in the future should help out.
	 */
	reschedule(vp, -1, index);
	return 1;
}

int
u_error(inum, offset, message)
	int	inum;
	long	offset;
	char	*message;
{
	int	index;
	register struct vertex *vp;
	extern void msgerror();

	if ((vp = findvertex(inum, offset, &index)) == NULL)
		return 0;
	vp->attempts += 1;
	if (message != NULL) {
		(void)fprintf(stderr,
			"%s: %d/%d/error %s\n",
			vp->ce.command, inum, offset, message);
		msgerror(vp, offset, message);
	}
	if (vp->cfp->vfp != NULL && vp->cfp->contents != NULL) {
		fprintf(vp->cfp->vfp, "%s: error %s\n",
				      vp->cfp->contents + offset + 2,
				      message == NULL ? "(unknown)" : message);
	}
	vtxupdate(vp, index);
	return 1;
}

/*
 * specify relative (w/ leading +) or absolute (w/o leading +) retry time.
 */

int
u_retryat(inum, offset, message)
	int	inum;
	long	offset;
	char	*message;
{
	int	index;
	time_t	retrytime;
	char	*cp;
	register struct vertex *vp;
	extern time_t time();
	extern void reschedule();

	if ((vp = findvertex(inum, offset, &index)) == NULL)
		return 0;
	vp->attempts += 1;
	if (*message == '+') {
		(void) time(&retrytime);
		++message;
	} else
		retrytime = 0;
	for (cp = message; *cp != '\0' && isdigit(*cp); ++cp)
		continue;
	if (*cp != '\0')
		*cp++ = '\0';
	retrytime += atoi(message);
	message = cp;
	if (*message == '\0')
		message = NULL;
	
	if (message != NULL) {
		if (vp->message != NULL)
			free(vp->message);
		/* fprintf(stderr, "add message '%s' to node %s/%s\n",
				message, vp->orig[L_CHANNEL]->name,
				vp->orig[L_HOST]->name); */
		vp->message = strnsave(message, strlen(message));
	}
	if (vp->cfp->vfp != NULL && vp->cfp->contents != NULL) {
		(void) fprintf(vp->cfp->vfp, "%s: retryat %d %s\n",
				      vp->cfp->contents + offset + 2, retrytime,
				      message == NULL ? "(unknown)" : message);
	}
	/*
	 * Even though we may get several of these per web entry,
	 * the heuristic in reschedule() to ignore the request if
	 * the time is already in the future should help out.
	 */
	reschedule(vp, -(int)retrytime, index);
	return 1;
}
