/*
 *	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"
#include NDIR_H

extern char *strchr();
extern char *strrchr();
extern char *progname;
extern char *saytime();
extern int cistrcmp();
extern int ermdir();
extern int rmdir();
extern int eunlink();
extern int subsubdirs;
extern struct spblk *sp_fhead(), *sp_fnext();
extern time_t time();
extern void disentangle();
extern void msgerror();
extern void reporterrs();
extern void reschedule();

extern int verbose;

/*
 * 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\tIETF-NOTARY\tok Quick SMTP connection!
 *	18453/3527\tIETF-NOTARY\tdeferred Unable to contact host!
 *	18453/3527\tIETF-NOTARY\terror Unknown user name "fred".
 *	18453/3527\tIETF-NOTARY\tretryat [+]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, *notary;
	int	inum, offset;
	struct diagcodes *dcp;

	inum = atoi(diagnostic);
	if ((cp = strchr(diagnostic, '/')) == NULL) {
	  fprintf(stderr, "Misformed diagnostic: %s\n", diagnostic);
	  return;
	}
	offset = atol(++cp);
	if ((cp = strchr(cp, '\t')) == NULL) {
	  fprintf(stderr, "Misformed diagnostic: %s\n", diagnostic);
	  return;
	}
	notary = ++cp;
	if ((cp = strchr(cp, '\t')) == NULL) {
	  fprintf(stderr, "Misformed diagnostic: %s\n", diagnostic);
	  return;
	}
	*cp = 0; /* Trailing TAB after notary string, NULL it */
	type = ++cp;
	while (*cp != '\0' && isascii(*cp) && !isspace(*cp))
	  ++cp;
	if (*cp == '\0') {
	  message = NULL;
	} else {
	  *cp++ = '\0';
	  message = cp;
	}
	if (verbose)
	  printf("diagnostic: %d/%d/%s/%s\n",inum,offset,notary,type);

	for (dcp = &diags[0]; dcp->name != NULL; ++dcp) {
	  if (cistrcmp(dcp->name, type) == 0) {
	    (dcp->fcn)(inum, offset, notary, 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];

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

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

	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) {
	  sprintf(path, "%s/%s/%lu",
		  vp->orig[L_CHANNEL]->name,
		  vp->orig[L_HOST]->name,
		  vp->cfp->id);
	  eunlink(path);
	  if (vp->cfp->vfp != NULL) {
	    fseek(vp->cfp->vfp, 0, 2);
	    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';
	    rmdir(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);
	    eunlink(path);
	    if (vp->cfp->vfp != NULL) {
	      fseek(vp->cfp->vfp, 0, 2);
	      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);
	      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);
	if (vp->notary != NULL)
	  free(vp->notary);
	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;

	/* 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) {
	  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);
	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 = NULL;
	struct spblk *spl;

	/* 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)
	    sprintf(p, "%dd", value);
	  else
	    sprintf(p, "%d day%s", PLURALIZE(value));
	  ++fields;
	  while (*++p);
	}
	if (hours) {
	  if (shortform)
	    sprintf(p, "%dh", hours);
	  else {
	    if (value && p != buf)
	      *p++ = ' ';
	    sprintf(p, "%d hour%s", PLURALIZE(hours));
	  }
	  ++fields;
	  while (*++p);
	}
	if (mins && fields < 2) {
	  if (shortform)
	    sprintf(p, "%dm", mins);
	  else {
	    if ((hours || value) && p != buf)
	      *p++ = ' ';
	    sprintf(p, "%d min%s", PLURALIZE(mins));
	  }
	  while (*++p);
	}
	if (secs && fields < 2) {
	  if (shortform)
	    sprintf(p, "%ds", secs);
	  else {
	    if ((mins || hours || value) && p != buf)
	      *p++ = ' ';
	    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;

	for (i = 0 ; i < vp->ngroup; ++i)
	  if (vp->index[i] == index)
	    msgerror(vp, vp->cfp->offset[index], buf);
	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 = "\r%s, problem was:\r%s";

	if (vp->notary == NULL) {
	  sprintf(buf,"%s\001%s\001%s",
		  "**ADDR**", /* XX: recipient address! */
		  "failed",
		  "500 (Expired after");
	  saytime((u_long)(vp->ce.expiry - vp->cfp->ctime), buf, 0);
	  strcat(buf,")");
	  vp->notary = strsave(buf);
	}

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

	if (vp->message != NULL && *(vp->message) != '\0') {
	  emsg = emalloc(strlen(buf) + strlen(vp->message) + strlen(fmt));
	  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)
	  free(emsg);
}

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

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

	if (vp->notary != NULL)
	  free(vp->notary);
	vp->notary = NULL;
	if (*notary)
	  vp->notary = strsave(notary);

	vtxupdate(vp, index);
	return 1;
}

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

	if ((vp = findvertex(inum, offset, &index)) == NULL)
	  return 0;
	vp->attempts += 1;
	/* printf("%s: %d/%d/%s/deferred %s\n", vp->cfp->logident,
	   inum, offset, notary, 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 = strsave(message);
	}

	if (vp->notary != NULL)
	  free(vp->notary);
	vp->notary = NULL;
	if (*notary)
	  vp->notary = strsave(notary);

	if (vp->cfp->vfp != NULL && vp->cfp->contents != NULL) {
	  fseek(vp->cfp->vfp, 0, 2);
	  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, notary, message)
	int	inum;
	long	offset;
	char	*notary;
	char	*message;
{
	int	index;
	register struct vertex *vp;

	if ((vp = findvertex(inum, offset, &index)) == NULL)
		return 0;
	vp->attempts += 1;
	if (message == NULL)
	  message = "(unknown)";
	fprintf(stderr,
		"%s: %d/%d/%s/error %s\n",
		vp->ce.command, inum, offset, notary, message);

	if (vp->notary != NULL)
	  free(vp->notary);
	vp->notary = NULL;
	if (*notary)
	  vp->notary = strsave(notary);

	msgerror(vp, offset, message);

	if (vp->cfp->vfp != NULL && vp->cfp->contents != NULL) {
	  fseek(vp->cfp->vfp, 0, 2);
	  fprintf(vp->cfp->vfp, "%s: error %s\n",
		  vp->cfp->contents + offset + 2,
		  message);
	}
	vtxupdate(vp, index);
	return 1;
}

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

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

	if ((vp = findvertex(inum, offset, &index)) == NULL)
	  return 0;
	vp->attempts += 1;
	if (*message == '+') {
	  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 = strsave(message);
	}

	if (vp->notary != NULL)
	  free(vp->notary);
	vp->notary = NULL;
	if (*notary)
	  vp->notary = strsave(notary);

	if (vp->cfp->vfp != NULL && vp->cfp->contents != NULL) {
	  fseek(vp->cfp->vfp, 0, 2);
	  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;
}
