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

#include "hostenv.h"
#ifdef linux
#define __USE_BSD 1
#endif
#include <ctype.h>
#include <pwd.h>
#include <sysexits.h>
#include <sys/param.h>
#include <sys/stat.h>

#include "zmsignal.h"
#include "zmalloc.h"
#include "zsyslog.h"

#include "ta.h"

#ifdef	HAVE_RESOLVER
#include "netdb.h"
#include <sys/socket.h>
#include <netinet/in.h>
#endif
#include "libz.h"

#if	defined(TRY_AGAIN) && defined(HAVE_RESOLVER)
#define	BIND		/* Want BIND (named) nameserver support enabled */
#endif	/* TRY_AGAIN */
#ifdef	BIND
#undef NOERROR /* Solaris  <sys/socket.h>  has  NOERROR too.. */
#include <arpa/nameser.h>
#include <resolv.h>

#ifndef	BIND_VER
#ifdef	GETLONG
/* 4.7.3 introduced the {GET,PUT}{LONG,SHORT} macros in nameser.h */
#define	BIND_VER	473
#else	/* !GETLONG */
#define	BIND_VER	472
#endif	/* GETLONG */
#endif	/* !BIND_VER */
#endif	/* BIND */

#if	defined(BIND_VER) && (BIND_VER >= 473)
typedef u_char msgdata;
#else	/* !defined(BIND_VER) || (BIND_VER < 473) */
typedef char msgdata;
#endif	/* defined(BIND_VER) && (BIND_VER >= 473) */


/* Define all those things which exist on newer BINDs, and which may
   get returned to us, when we make a query with  T_ANY ... */

#ifndef	T_TXT
# define T_TXT 16
#endif
#ifndef T_RP
# define T_RP 17
#endif
#ifndef T_AFSDB
# define T_AFSDB 18
#endif
#ifndef T_NSAP
# define T_NSAP 22
#endif
#ifndef T_AAAA
# define T_AAAA 28	/* IPv6 Address */
#endif
#ifndef T_NSAP_PTR
# define T_NSAP_PTR 23
#endif
#ifndef	T_UINFO
# define T_UINFO 100
#endif
#ifndef T_UID
# define T_UID 101
#endif
#ifndef T_GID
# define T_GID 102
#endif
#ifndef T_UNSPEC
# define T_UNSPEC 103
#endif
#ifndef T_SA
# define T_SA 200
#endif


#include "mail.h"
#include "splay.h"

#if HAVE_SYS_WAIT_H /* POSIX-thing ?  If not, declare it so.. */
# include <sys/wait.h>
#endif
#ifndef WEXITSTATUS
# define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8)
#endif
#ifndef WIFEXITED
# define WIFEXITED(stat_val) (((stat_val) & 255) == 0)
#endif

#ifndef	SEEK_SET
#define	SEEK_SET 0
#endif	/* SEEK_SET */

#define	PROGNAME	"hold"	/* for logging */
#define	CHANNEL		"hold"	/* the default channel name we look at */

char errormsg[BUFSIZ];
char *progname;
char *cmdline, *eocmdline;
int pid;

#ifdef MALLOC_TRACE
#define	MEM_MALLOC 1000
int	stickymem = MEM_MALLOC;	/* for strnsave() */
struct conshell *envarlist = NULL;
#endif
int	D_alloc = 0;

extern char *optarg;
extern int optind;
extern void process __((struct ctldesc *));
extern int  hold __((const char *, char **));
extern char **environ;

#ifndef strchr
extern char *strchr(), *strrchr();
#endif

#ifdef	lint
#undef	putc
#define	putc	fputc
#endif	/* lint */

static char filename[MAXPATHLEN+8000];

int
main(argc, argv)
	int argc;
	char *argv[];
{
	const char *channel, *host;
	int errflg, c, i;
	struct ctldesc *dp;
	RETSIGTYPE (*oldsig) __((int));

	pid = getpid();
	cmdline = &argv[0][0];
	eocmdline = cmdline;
	for (i = 0; i < argc; ++i)
	  eocmdline += strlen(argv[i]) + 1;

	SIGNAL_HANDLESAVE(SIGINT, SIG_IGN, oldsig);
	if (oldsig != SIG_IGN)
	  SIGNAL_HANDLE(SIGINT, wantout);
	SIGNAL_HANDLESAVE(SIGTERM, SIG_IGN, oldsig);
	if (oldsig != SIG_IGN)
	  SIGNAL_HANDLE(SIGTERM, wantout);
	SIGNAL_HANDLESAVE(SIGQUIT, SIG_IGN, oldsig);
	if (oldsig != SIG_IGN)
	  SIGNAL_HANDLE(SIGQUIT, wantout);
	SIGNAL_HANDLESAVE(SIGHUP, SIG_IGN, oldsig);
	if (oldsig != SIG_IGN)
	  SIGNAL_HANDLE(SIGHUP, wantout);

	if ((progname = strrchr(argv[0], '/')) == NULL)
	  progname = argv[0];
	else
	  ++progname;
	errflg = 0;
	channel = CHANNEL;
	while (1) {
	  c = getopt(argc, argv, "c:V");
	  if (c == EOF)
	    break;
	  switch (c) {
	  case 'c':		/* specify channel scanned for */
	    channel = optarg;
	    break;
	  case 'V':
	    prversion(PROGNAME);
	    exit(EX_OK);
	    break;
	  default:
	    ++errflg;
	    break;
	  }
	}
	if (errflg || optind != argc) {
	  fprintf(stderr, "Usage: %s [-V] [-c channel]\n",
		  argv[0]);
	  exit(EX_USAGE);
	}

	/* We need this latter on .. */
	zopenlog("hold", LOG_PID, LOG_MAIL);

	while (!getout) {
	  char *s;
	  /* Input:
	       spool/file/name [ \t host.info ] \n
	   */

	  printf("#hungry\n");
	  fflush(stdout);
	  if (fgets(filename, sizeof(filename), stdin) == NULL)
	    break;
	  if (strchr(filename, '\n') == NULL) break; /* No ending '\n' !  Must
						    have been partial input! */
	  if (strcmp(filename, "#idle\n") == 0)
	    continue; /* Ah well, we can stay idle.. */
	  if (emptyline(filename, sizeof(filename)))
	    break;

	  s = strchr(filename,'\t');
	  host = NULL;
	  if (s != NULL) {
	    /* Ignore the host part */
	    *s++ = 0;
	    host = s;
	  }

	  dp = ctlopen(filename, channel, host, &getout, NULL, NULL, NULL, NULL);
	  if (dp != NULL) {
	    process(dp);
	    ctlclose(dp);
	  } else {
	    printf("#resync %s\n",filename);
	    fflush(stdout);
	  }
	}
	exit(0);
	/* NOTREACHED */
	return 0;
}

/*
 * process - resubmit the message if the hold condition has disappeared.
 */

void
process(dp)
	struct ctldesc *dp;
{
	FILE *mfp;
	int n, sawok, code;
	struct rcpt *rp;
	const char *cp;
	char buf[BUFSIZ];

	sawok = 0;
	for (rp = dp->recipients; rp != NULL; rp = rp->next) {
	  cp = rp->addr->user;
	  rp->status = hold(rp->addr->host, (char**)&(rp->addr->user));
	  if (rp->status == EX_OK) {
	    rp->addr->user = cp;
	    sawok = 1;
	  } else {
	    const char *action = NULL;
	    const char *status = NULL;
	    const char *diagnostics = NULL;
	    switch (rp->status) {
	    case EX_PROTOCOL:
	    case EX_SOFTWARE:
	      action = "failed";
	      status = "5.0.0 (ZMailer internal protocol error)";
	      diagnostics = "x-local; 500 (Protocol failure inside ZMAILER! AARGH!)";
	      break;
	    case EX_TEMPFAIL:
	      action = "delayed";
	      status = "4.4.3 (Temporary routing lookup failure)";
	      diagnostics = "x-local; 466 (Temporary routing lookup failure)";
	      break;
	    default:
	      action = "failed";
	      status = "4.4.0 (Unknown ZMailer HOLD status ** CAN'T HAPPEN)";
	      diagnostics = "x-local; 500 (Unknown HOLD return code **CAN'T HAPPEN**)";
	      break;
	    }
	    notaryreport(rp->addr->user,action,status,diagnostics);
	    diagnostic(rp, rp->status, 0, "%s", rp->addr->user);
	  }
	}

	if (!sawok)
	  return;

	if (lseek(dp->msgfd, (off_t)(dp->msgbodyoffset), SEEK_SET) < 0L)
	  warning("Cannot seek to message body! (%m)", (char *)NULL);

	setreuid(-1, atoi(dp->senders->misc));

	mfp = mail_open(MSG_RFC822);
	if (mfp == NULL) {
	  for (rp = dp->recipients; rp != NULL; rp = rp->next)
	    if (rp->status == EX_OK) {
	      notaryreport(rp->addr->user,"delayed",
			   "4.3.1 (System spool full?)",
			   "x-local; 400 (Cannot resubmit anything, out of spool space?)");
	      diagnostic(rp, EX_TEMPFAIL, 0,
			 "cannot resubmit anything!");
	    }
	  setreuid(-1, getuid());
	  return;
	}
	setreuid(-1, getuid());

	fprintf(mfp, "via suspension\n");
	if (dp->senders->user[0] == 0)
	  fprintf(mfp, "channel error\n");
	else
	  fprintf(mfp, "from <%s>\n", dp->senders->user);
	if (dp->envid != NULL)
	  fprintf(mfp, "envid %s\n", dp->envid);
	if (dp->dsnretmode)
	  fprintf(mfp, "notaryret %s\n", dp->dsnretmode);

	for (rp = dp->recipients; rp != NULL; rp = rp->next)
	  if (rp->status == EX_OK) {
	    fprintf(mfp, "to <%s>\n", rp->addr->user);
	    if (rp->notify || rp->orcpt) {

	      fputs("todsn",mfp);
	      if (rp->orcpt != NULL)
		fprintf(mfp," ORCPT=%s",rp->orcpt);

	      if (rp->notify)
		fprintf(mfp," NOTIFY=%s", rp->notify);

	      putc('\n',mfp);
	    }
	  }

	fwriteheaders(dp->recipients,mfp,"\n",0,0,NULL);
	fprintf(mfp,"\n");

	/* append message body itself */
	while ((n = read(dp->msgfd, buf, sizeof buf)) > 0)
	  fwrite(buf, sizeof buf[0], n, mfp);

	if (ferror(mfp)) {
	  mail_abort(mfp);
	  code = EX_TEMPFAIL;
	  cp = "write error during resubmission";
	} else if (mail_close(mfp) == EOF) {
	  code = EX_TEMPFAIL;
	  cp = "message not resubmitted";
	} else {
	  code = EX_OK;
	  cp = NULL;
	}

	for (rp = dp->recipients; rp != NULL; rp = rp->next)
	  if (rp->status == EX_OK) {
	    notaryreport(rp->addr->user,"relayed",
			 "2.2.0 (Relayed via deferral channel)",
			 "x-local; 250 (Relayed via deferral channel)");
	    diagnostic(rp, code, 0, cp);
	  }
}


/*
 * The hostname for the hold channel describes a wait condition that can
 * be tested (here) before the message can be resubmitted.  The condition
 * string is canonicalized and parsed hierarchically.
 */

extern int hold_ns	__(( const char* ));
extern int hold_timeout	__(( const char* ));
extern int hold_io	__(( const char* ));
extern int hold_script	__(( const char* ));
extern int hold_home	__(( const char* ));

struct holds_info {
	const char	*type;
	int	(*f) __(( const char * ));
} holds[] = {
	{	"ns",		hold_ns		},
	{	"timeout",	hold_timeout	},
	{	"io",		hold_io		},
	{	"script",	hold_script	},
	{	"home",		hold_home	},
	{	NULL,		NULL		},
};


int
hold(s, errmsgp)
	const char *s;
	char **errmsgp;
{
	char *cp, *colon;
	struct holds_info *hip;
	static struct sptree *spt_hash = NULL;
	struct spblk *spl;
	int v;
	spkey_t symid;

	colon = NULL;
	for (cp = (char*)s; *cp != '\0'; ++cp) {
	  unsigned char c = *cp;
	  if (isascii(c) && isupper(c))
	    *cp = tolower(c);
	  else if (c == ':')
	    colon = cp;
	}

	if (colon == NULL)
	  return EX_PROTOCOL;	/* invalid hold condition */

	symid = symbol((void*)s);
	if (spt_hash == NULL)
	  spt_hash = sp_init();
	if ((spl = sp_lookup(symid, spt_hash)) != NULL) {
	  *errmsgp = (char *)spl->data;
	  return spl->mark;
	}

	*colon++ = '\0';
	for (hip = &holds[0]; hip->type != NULL ; ++hip)
	  if (strcmp(s, hip->type) == 0)
	    break;
	if (hip->type == NULL)
	  return EX_SOFTWARE;	/* unsupported hold condition */

	errormsg[0] = '\0';

	if ((hip->f)(colon))
	  v = EX_OK;		/* resubmit the message address */
	else
	  v = EX_TEMPFAIL;	/* defer resubmission */

	if (errormsg[0] != '\0') {
	  cp = emalloc((u_int)strlen(errormsg)+1);
	  strcpy(cp, errormsg);
	} else
	  cp = "deferred";
	sp_install(symid, (const void *)cp, v, spt_hash);
	*errmsgp = cp;

	return v;
}

/* return true if the nameserver lookup of (name,type) succeeds */

#ifdef	BIND
extern int	h_errno;

struct qtypes {
	const char	*typename;
	int		 value;
} qt[] = {
	{	"cname",	T_CNAME		},
	{	"mx",		T_MX		},
	{	"a",		T_A		},
	{	"aaaa",		T_AAAA		},
#ifdef  T_ANY
	{	"any",		T_ANY		},
#endif
#ifdef	T_MP
	{	"mp",		T_MP		},
#endif	/* T_MP */
#ifdef	T_UNAME
	{	"uname",	T_UNAME		},
#endif	/* T_UNAME */
#ifdef	T_TXT
	{	"txt",		T_TXT		},
#endif	/* T_TXT */
	{	"wks",		T_WKS		},
	{	"ptr",		T_PTR		},
	{	NULL,		0		}
};

int
hold_ns(s)
	const char *s;
{
	struct qtypes *qtp;
	char *cp, host[1024]; /* 256 should be enough .. */
	int ttl;

	res_init();

	if ((cp = strrchr(s, '/')) == NULL)
	  return 1;	/* human error, lets be nice */
	if (cp > s && *(cp-1) == '.')
	  *(cp-1) = '\0';
	else
	  *cp = '\0';
	++cp;

	for (qtp = &qt[0]; qtp->typename != NULL ; ++qtp) {
	  if (strcmp(qtp->typename, cp) == 0)
	    break;
	}
	if (qtp->typename == NULL) {
	  fprintf(stderr, "%s: unknown nameserver type '%s'\n",
		  progname, cp);
	  return 1;		/* inconsistency with search_res.c, yell! */
	}
	strncpy(host, s, sizeof(host));
	host[sizeof(host)-1] = 0;
	switch (getrrtype(host, &ttl, sizeof host, qtp->value, 2, NULL)) {
	case 0:
		return 1;	/* negative reply */
	case 1:
		return 1;	/* positive reply */
	case -1:
	case -2:
	case -3:
		return 0;	/* no reply */
	}
	return 0;
}
#else	/* !BIND */

struct qtypes {
	char	*typename;
	u_short	value;
} qt[] = {
	{	"cname",	1		},
	{	"a",		1		},
	{	"aaaa",		1		},
	{	"ptr",		2		},
	{	NULL,		NULL		}
};

int
hold_ns(s)
	char *s;
{
	struct qtypes *qtp;
	char *cp, host[BUFSIZ];
	struct hostent *hp;
	struct in_addr ia;

	if ((cp = strrchr(s, '/')) == NULL)
	  return 1;	/* human error, lets be nice */
	if (cp > s && *(cp-1) == '.')
	  *(cp-1) = '\0';
	else
	  *cp = '\0';
	++cp;

	for (qtp = &qt[0]; qtp->typename != NULL ; ++qtp) {
	  if (strcmp(qtp->typename, cp) == 0)
	    break;
	}
	if (qtp->typename == NULL) {
	  fprintf(stderr, "%s: unknown hosts file lookup '%s'\n",
		  progname, cp);
	  return 1;		/* inconsistency with search_res.c, yell! */
	}
	strcpy(host, s);
	hp = NULL;
	switch (qtp->value) {
	case 1:
	  /* XXX: getaddrinfo() */
	  hp = gethostbyname(host);
	  break;
	case 2:
/* XXX: use  inet_pton() here ?! */
	  ia.s_addr = inet_addr(host);
	  hp = gethostbyaddr(&ia, sizeof ia.s_addr, AF_INET);
	  break;
	}
	return hp != NULL;
}
#endif	/* !BIND */

/* return true if the seconds-since-epoch in argument has passed */

int
hold_timeout(s)
	const char *s;
{
	time_t now, then;

	time(&now);
	then = (time_t)atol(s);
	return now >= then;
}

/* return true if we should retry the I/O operation causing the error */

int
hold_io(s)
	const char *s;
{
	return ranny(9) == 0;	/* 10% of the time */
}

/* based on tuple "hold" "script:command/$user" "$address"
   return exit status of "$MAILBIN/bin/command $user" command */

int
hold_script(command)
	const char *command;
{
	int i, in[2];
	const char *env[20], *s;
	char buf[8192], *cp, *arg;
	FILE *errfp;
	int status;

	arg = strchr(command, '/');
	if (arg != NULL)
	  *arg++ = '\0';
	else
	  arg = NULL;
	i = 0;
	env[i++] = "SHELL=/bin/sh";
	cp = buf;
	s = getzenv("PATH");
	if (s == NULL)
	  env[i++] = "PATH=/usr/ucb:/usr/bin:/bin";
	else {
	  sprintf(cp, "PATH=%s", s);
	  env[i++] = cp;
	  cp += strlen(cp) + 1;
	}
	env[i++] = "HOME=/tmp";
	env[i++] = "USER=anonymous";
	s = getzenv("ZCONFIG");
	if (s == NULL)
	  s = ZMAILER_ENV_FILE;
	sprintf(cp, "ZCONFIG=%s", s);
	env[i++] = cp;
	s = getzenv("MAILSHARE");
	if (s == NULL)
	  s = MAILBIN;
	cp += strlen(cp) + 1;
	sprintf(cp, "MAILSHARE=%s", s);
	env[i++] = cp;
	s = getzenv("MAILBIN");
	if (s == NULL)
	  s = MAILSHARE;
	cp += strlen(cp) + 1;
	sprintf(cp, "MAILBIN=%s", s);
	env[i++] = cp;
	env[i] = NULL;

	/* now we can fork off and run the command... */
	if (pipe(in) < 0) {
	  sprintf(errormsg,
		  "cannot create pipe from \"%s\"", command);
	  return 0;
	}
	cp += strlen(cp) + 1;
	sprintf(cp, "%s/bin/%s", s, command);

	if ((pid = fork()) == 0) { /* child */
	  environ = (char**) env;
	  dup2(in[1],1);
	  dup2(in[1],2);
	  close(0);
	  if (in[0] != 1 && in[0] != 2)
	    close(in[0]);
	  if (in[1] != 1 && in[1] != 2)
	    close(in[1]);
	  SIGNAL_IGNORE(SIGINT);
	  SIGNAL_IGNORE(SIGHUP);
	  SIGNAL_HANDLE(SIGTERM, SIG_DFL);
	  /*
	   * Note that argv[0] is set to the command we are running.
	   * That way, we should get some better error messages, at
	   * least more understandable in rejection messages.
	   * Some bourne shells may go into restricted mode if the
	   * stuff to run contains an 'r'. XX: investigate.
	   */
	  execl("/bin/sh", "sh", cp, arg, (char *)NULL);
	  execl("/sbin/sh", "sh", cp, arg, (char *)NULL);
	  write(2, "Cannot exec /bin/sh\n", 20);
	  _exit(128);
	} else if (pid < 0) {	/* fork failed */
	  sprintf(errormsg, "cannot fork");
	  return 0;
	} /* parent */
	close(in[1]);
	errfp = fdopen(in[0], "r");
	/* read any messages from its stdout/err on in[0] */
	cp = errormsg;
	if (fgets(errormsg, sizeof errormsg, errfp) == NULL)
		errormsg[0] = '\0';
	else if ((cp = strchr(errormsg, '\n')) != NULL)
		*cp = '\0';
	wait(&status);
	fclose(errfp);
	close(in[0]);

        if (status & 0177) { /* any signals ? */
	  if (cp != errormsg)
	    *cp++ = ' ';
	  sprintf(cp, "[signal %d", status & 0177);
	  if (status & 0200)
	    strcat(cp, " (Core dumped)");
	  strcat(cp, "]");
	  return 0;
        } else if (WEXITSTATUS(status) != 0) {
	  if (cp != errormsg)
	    *cp++ = ' ';
	  sprintf(cp, "[exit status %d]", WEXITSTATUS(status));
	  return 0;
        }
	return 1;
}


/* The transient condition is the user's home directory is not available.
   So if the user doesn't exist any more, or if his home directory is
   null, then we "succeed", to try to redeliver the mail. */

int
hold_home(user)
const char *user;
{
	struct passwd *pw;
	struct stat st;

	if ((pw = getpwnam(user)) == NULL) return 1;
	if (pw->pw_dir == NULL || pw->pw_dir[0] == '\0') return 1;
	if (stat(pw->pw_dir, &st) == 0 &&
	    S_ISDIR(st.st_mode)) return 1;
	return 0;
}
