/*  mail - send/receive mail 		 Author: Peter S. Housel */
/* Version 0.2 of September 1990: added -e, -t, * options - cwr */

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#undef EOF			/* temporary hack */
#include <signal.h>
#include <pwd.h>
#include <time.h>
#include <setjmp.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

#ifdef DEBUG
#define D(Q) (Q)
#else
#define D(Q)
#endif

#define SHELL		"/bin/sh"

#define DROPNAME 	"/usr/spool/mail/%s"
#define LOCKNAME	"/usr/spool/mail/%s.lock"
#define LOCKWAIT	5	/* seconds to wait after collision */
#define LOCKTRIES	4	/* maximum number of collisions */

#define MBOX		"mbox"

#define HELPFILE	"/usr/lib/mail.help"
#define PROMPT		"? "
#define PATHLEN		80
#define MAXRCPT		100	/* maximum number of recipients */
#define LINELEN		512

/* #define MAILER		"/usr/bin/smail"	*/ /* smart mailer */
#define MAILERARGS		/* (unused) */

#define UNREAD		1	/* 'not read yet' status */
#define DELETED		2	/* 'deleted' status */
#define READ		3	/* 'has been read' status */

struct letter {
  struct letter *prev, *next;	/* linked letter list */
  int status;			/* letter status */
  off_t location;		/* location within mailbox file */
};

struct letter *firstlet, *lastlet;

int usemailer = 1;		/* use MAILER to deliver (if any) */
int printmode = 0;		/* print-and-exit mode */
int quitmode = 0;		/* take interrupts */
int reversemode = 0;		/* print mailbox in reverse order */
int usedrop = 1;		/* read the maildrop (no -f given) */
int verbose = 0;		/* pass "-v" flag on to mailer */
int needupdate = 0;		/* need to update mailbox */
int msgstatus = 0;		/* return the mail status */
int distlist = 0;		/* include distribution list */
char mailbox[PATHLEN];		/* user's mailbox/maildrop */
char tempname[PATHLEN] = "/tmp/mailXXXXXX";	/* temporary file */
FILE *boxfp = NULL;		/* mailbox file */
jmp_buf printjump;		/* for quitting out of letters */
unsigned oldmask;		/* saved umask() */

extern int optind;
extern char *optarg;

_PROTOTYPE(int main, (int argc, char **argv));
_PROTOTYPE(int deliver, (int count, char *vec []));
_PROTOTYPE(FILE *makerewindable, (void));
_PROTOTYPE(int copy, (FILE *fromfp, FILE *tofp));
_PROTOTYPE(void readbox, (void));
_PROTOTYPE(void printall, (void));
_PROTOTYPE(void interact, (void));
_PROTOTYPE(void onint, (int dummy));
_PROTOTYPE(void savelet, (struct letter *let, char *savefile));
_PROTOTYPE(void updatebox, (void));
_PROTOTYPE(void printlet, (struct letter *let, FILE *tofp));
_PROTOTYPE(void doshell, (char *command));
_PROTOTYPE(void usage, (void));
_PROTOTYPE(char *basename, (char *name));
_PROTOTYPE(char *whoami, (void));
_PROTOTYPE(void dohelp, (void));
_PROTOTYPE(int filesize, (char *name));

int main(argc, argv)
int argc;
char *argv[];
{
  int c;

  if ('l' == (basename(argv[0]))[0])	/* 'lmail' link? */
	usemailer = 0;		/* yes, let's deliver it */

  (void) mktemp(tempname);	/* name the temp file */

  oldmask = umask(022);		/* change umask for security */

  while (EOF != (c = getopt(argc, argv, "epqrf:tdv"))) switch (c) {
	    case 'e':	++msgstatus;	break;	

	    case 't':	++distlist;	break;

	    case 'p':	++printmode;	break;

	    case 'q':	++quitmode;	break;

	    case 'r':	++reversemode;	break;

	    case 'f':
		setuid(getuid());	/* won't need to lock */
		usedrop = 0;
		strncpy(mailbox, optarg, (size_t)(PATHLEN - 1));
		break;

	    case 'd':	usemailer = 0;	break;

	    case 'v':	++verbose;	break;

	    default:
		usage();
		exit(1);
	}

  if (optind < argc) {
	if (deliver(argc - optind, argv + optind) < 0)
		exit(1);
	else
		exit(0);
  }
  if (usedrop) sprintf(mailbox, DROPNAME, whoami());

  D(printf("mailbox=%s\n", mailbox));

  if (msgstatus) {
	if (filesize(mailbox))
		exit(0);
	else
		exit(1);
  }

  readbox();

  if (printmode)
	printall();
  else
	interact();

  if (needupdate) updatebox();

  return(0);
}

int deliver(count, vec)
int count;
char *vec[];
{
  int i, j;
  int errs = 0;			/* count of errors */
  int dropfd;			/* file descriptor for user's drop */
  int created = 0;		/* true if we created the maildrop */
  FILE *mailfp;			/* fp for mail */
  struct stat stb;		/* for checking drop modes, owners */
#ifdef __STDC__
  void (*sigint)(int), (*sighup)(int), (*sigquit)(int);/* saving signal state */
#else
  void (*sigint) (), (*sighup) (), (*sigquit) ();      /* saving signal state */
#endif
  time_t now;			/* for datestamping the postmark */
  char sender[32];		/* sender's login name */
  char lockname[PATHLEN];	/* maildrop lock */
  int locktries;		/* tries when box is locked */
  struct passwd *pw;		/* sender and recipent */
  int to_console;		/* deliver to console if everything fails */

  if (count > MAXRCPT) {
	fprintf(stderr, "mail: too many recipients\n");
	return -1;
  }
#ifdef MAILER
  if (usemailer) {
	char *argvec[MAXRCPT + 3];
	char **argp;

	setuid(getuid());

	argp = argvec;
	*argp++ = "send-mail";
	if (verbose) *argp++ = "-v";

	for (i = 0; i < count; ++i) *argp++ = vec[i];

	*argp = NULL;
	execv(MAILER, argvec);
	fprintf(stderr, "mail: couldn't exec %s\n", MAILER);
	return -1;
  }
#endif /* MAILER */

  if (NULL == (pw = getpwuid(getuid()))) {
	fprintf(stderr, "mail: unknown sender\n");
	return -1;
  }
  strcpy(sender, pw->pw_name);

  /* If we need to rewind stdin and it isn't rewindable, make a copy */
  if (isatty(0) || (count > 1 && lseek(0, 0L, 0) == (off_t) -1)) {
	mailfp = makerewindable();
  } else
	mailfp = stdin;

  /* Shut off signals during the delivery */
  sigint = signal(SIGINT, SIG_IGN);
  sighup = signal(SIGHUP, SIG_IGN);
  sigquit = signal(SIGQUIT, SIG_IGN);

  for (i = 0; i < count; ++i) {
	if (count > 1) rewind(mailfp);

	D(printf("deliver to %s\n", vec[i]));

	if (NULL == (pw = getpwnam(vec[i]))) {
		fprintf(stderr, "mail: user %s not known\n", vec[i]);
		++errs;
		continue;
	}
	sprintf(mailbox, DROPNAME, pw->pw_name);
	sprintf(lockname, LOCKNAME, pw->pw_name);

	D(printf("maildrop='%s', lock='%s'\n", mailbox, lockname));

	/* Lock the maildrop while we're messing with it. Races are
	 * possible (though not very likely) when we have to create
	 * the maildrop, but not otherwise. If the box is already
	 * locked, wait awhile and try again. */
	locktries = created = to_console = 0;
trylock:
	if (link(mailbox, lockname) != 0) {
		if (ENOENT == errno) {	/* user doesn't have a drop yet */
			dropfd = creat(mailbox, 0600);
			if (dropfd < 0 && errno == ENOENT) {
				/* Probably missing spool dir; to console. */
				boxfp = fopen("/dev/console", "w");
				if (boxfp != NULL) {
					to_console = 1;
					goto nobox;
				}
			}
			if (dropfd < 0) {
				fprintf(stderr, "mail: couln't create a maildrop for user %s\n",
					vec[i]);
				++errs;
				continue;
			}
			++created;
			goto trylock;
		} else {	/* somebody else has it locked, it seems -
			 * wait */
			if (++locktries >= LOCKTRIES) {
				fprintf(stderr, "mail: couldn't lock maildrop for user %s\n",
					vec[i]);
				++errs;
				continue;
			}
			sleep(LOCKWAIT);
			goto trylock;
		}
	}
	if (created) {
		(void) chown(mailbox, pw->pw_uid, pw->pw_gid);
		boxfp = fdopen(dropfd, "a");
	} else
		boxfp = fopen(mailbox, "a");

	if (NULL == boxfp || stat(mailbox, &stb) < 0) {
		fprintf(stderr, "mail: serious maildrop problems for %s\n", vec[i]);
		unlink(lockname);
		++errs;
		continue;
	}
	if (stb.st_uid != pw->pw_uid || (stb.st_mode & S_IFMT) != S_IFREG) {
		fprintf(stderr, "mail: mailbox for user %s is illegal\n", vec[i]);
		unlink(lockname);
		++errs;
		continue;
	}
nobox:
	if (to_console) {
		fprintf(boxfp,
			"-------------\n| Mail from %s to %s\n-------------\n",
			sender, vec[i]);
	} else {
		(void) time(&now);
		fprintf(boxfp, "From %s %24.24s\n", sender, ctime(&now));
	}
	if (distlist) {
		fprintf(boxfp, "Dist: ");
		for (j = 0; j < count; ++j)
			if (getpwnam(vec[j]) != NULL && j != i)
				fprintf(boxfp, "%s ", vec[j]) ;
		fprintf(boxfp, "\n\n");
	}
	if ((copy(mailfp, boxfp) < 0) || (fclose(boxfp) != 0)) {
		fprintf(stderr, "mail: error delivering to user %s", vec[i]);
		perror(" ");
		++errs;
	}
	unlink(lockname);
  }

  fclose(mailfp);

  /* Put signals back the way they were */
  signal(SIGINT, sigint);
  signal(SIGHUP, sighup);
  signal(SIGQUIT, sigquit);

  return(0 == errs) ? 0 : -1;
}

/* 'stdin' isn't rewindable. Make a temp file that is.
 * Note that if one wanted to catch SIGINT and write a '~/dead.letter'
 * for interactive mails, this might be the place to do it (though the
 * case where a MAILER is being used would also need to be handled).
 */
FILE *makerewindable()
{
  FILE *tempfp;			/* temp file used for copy */
  int c;			/* character being copied */
  int state;			/* ".\n" detection state */

  if (NULL == (tempfp = fopen(tempname, "w"))) {
	fprintf(stderr, "mail: can't create temporary file\n");
	return NULL;
  }

  /* Here we copy until we reach the end of the letter (end of file or
   * a line containing only a '.'), painstakingly avoiding setting a
   * line length limit. */
  state = '\n';
  while (EOF != (c = getc(stdin))) switch (state) {
	    case '\n':
		if ('.' == c)
			state = '.';
		else {
			if ('\n' != c) state = '\0';
			putc(c, tempfp);
		}
		break;
	    case '.':
		if ('\n' == c) goto done;
		state = '\0';
		putc('.', tempfp);
		putc(c, tempfp);
		break;
	    default:
		state = ('\n' == c) ? '\n' : '\0';
		putc(c, tempfp);
	}
done:
  if (ferror(tempfp) || fclose(tempfp)) {
	fprintf(stderr, "mail: couldn't copy letter to temporary file\n");
	return NULL;
  }
  tempfp = freopen(tempname, "r", stdin);
  unlink(tempname);		/* unlink name; file lingers on in limbo */
  return tempfp;
}

int copy(fromfp, tofp)
FILE *fromfp, *tofp;
{
  int c;			/* character being copied */
  int state;			/* ".\n" and postmark detection state */
  int blankline = 0;		/* was most recent line completely blank? */
  static char postmark[] = "From ";
  char *p, *q;

  /* Here we copy until we reach the end of the letter (end of file or
   * a line containing only a '.'). Postmarks (lines beginning with
   * "From ") are copied with a ">" prepended. Here we also complicate
   * things by not setting a line limit. */
  state = '\n';
  p = postmark;
  while (EOF != (c = getc(fromfp))) {
	switch (state) {
	    case '\n':
		if ('.' == c)	/* '.' at BOL */
			state = '.';
		else if (*p == c) {	/* start of postmark */
			++p;
			state = 'P';
		} else {	/* anything else */
			if ('\n' == c)
				blankline = 1;
			else {
				state = '\0';
				blankline = 0;
			}
			putc(c, tofp);
		}
		break;
	    case '.':
		if ('\n' == c) goto done;
		state = '\0';
		putc('.', tofp);
		putc(c, tofp);
		break;
	    case 'P':
		if (*p == c) {
			if (*++p == '\0') {	/* successfully reached end */
				p = postmark;
				putc('>', tofp);
				fputs(postmark, tofp);
				state = '\0';
				break;
			}
			break;	/* not there yet */
		}
		state = ('\n' == c) ? '\n' : '\0';
		for (q = postmark; q < p; ++q) putc(*q, tofp);
		putc(c, tofp);
		blankline = 0;
		p = postmark;
		break;
	    default:
		state = ('\n' == c) ? '\n' : '\0';
		putc(c, tofp);
	}
  }
  if ('\n' != state) putc('\n', tofp);
done:
  if (!blankline) putc('\n', tofp);
  if (ferror(tofp)) return -1;
  return 0;
}

void readbox()
{
  char linebuf[512];
  struct letter *let;
  off_t current;

  firstlet = lastlet = NULL;

  if (access(mailbox, 4) < 0 || NULL == (boxfp = fopen(mailbox, "r"))) {
	if (usedrop && ENOENT == errno) return;
	fprintf(stderr, "can't access mailbox ");
	perror(mailbox);
	exit(1);
  }
  current = 0L;
  while (1) {
	if (NULL == fgets(linebuf, sizeof linebuf, boxfp)) break;

	if (!strncmp(linebuf, "From ", (size_t)5)) {
		if (NULL == (let = (struct letter *) malloc(sizeof(struct letter)))) {
			fprintf(stderr, "Out of memory.\n");
			exit(1);
		}
		if (NULL == lastlet) {
			firstlet = let;
			let->prev = NULL;
		} else {
			let->prev = lastlet;
			lastlet->next = let;
		}
		lastlet = let;
		let->next = NULL;

		let->status = UNREAD;
		let->location = current;
		D(printf("letter at %ld\n", current));
	}
	current += strlen(linebuf);
  }
}

void printall()
{
  struct letter *let;

  let = reversemode ? firstlet : lastlet;

  if (NULL == let) {
	printf("No mail.\n");
	return;
  }
  while (NULL != let) {
	printlet(let, stdout);
	let = reversemode ? let->next : let->prev;
  }
}

void interact()
{
  char linebuf[512];		/* user input line */
  struct letter *let, *next;	/* current and next letter */
  int interrupted = 0;		/* SIGINT hit during letter print */
  int needprint = 1;		/* need to print this letter */
  char *savefile;		/* filename to save into */

  if (NULL == firstlet) {
	printf("No mail.\n");
	return;
  }
  let = reversemode ? firstlet : lastlet;

  while (1) {
	next = reversemode ? let->next : let->prev;
	if (NULL == next) next = let;

	if (!quitmode) {
		interrupted = setjmp(printjump);
		signal(SIGINT, onint);
	}
	if (!interrupted && needprint) {
		if (DELETED != let->status) let->status = READ;
		printlet(let, stdout);
	}
	if (interrupted) putchar('\n');
	needprint = 0;
	fputs(PROMPT, stdout);
	fflush(stdout);

	if (fgets(linebuf, sizeof linebuf, stdin) == NULL) break;

	if (!quitmode) signal(SIGINT, SIG_IGN);

	switch (linebuf[0]) {
	    case '\n':
		let = next;
		needprint = 1;
		continue;
	    case 'd':
		let->status = DELETED;
		if (next != let)/* look into this */
			needprint = 1;
		needupdate = 1;
		let = next;
		continue;
	    case 'p':
		needprint = 1;
		continue;
	    case '-':
		next = reversemode ? let->prev : let->next;
		if (NULL == next) next = let;
		let = next;
		needprint = 1;
		continue;
	    case 's':
		for (savefile = strtok(linebuf + 1, " \t\n");
		     savefile != NULL;
		     savefile = strtok((char *) NULL, " \t\n")) {
			savelet(let, savefile);
		}
		continue;
	    case '!':
		doshell(linebuf + 1);
		continue;
	    case '*':
		dohelp();
		continue;
	    case 'q':
		return;
	    case 'x':
		exit(0);
	    default:
		fprintf(stderr, "Illegal command\n");
		continue;
	}
  }
}

void onint(dummy)
int dummy;	/* to satisfy ANSI compilers */
{
  longjmp(printjump, 1);
}

void savelet(let, savefile)
struct letter *let;
char *savefile;
{
  int waitstat, pid;
  FILE *savefp;

  if ((pid = fork()) < 0) {
	perror("mail: couldn't fork");
	return;
  } else if (pid != 0) {	/* parent */
	wait(&waitstat);
	return;
  }

  /* Child */
  setgid(getgid());
  setuid(getuid());
  if ((savefp = fopen(savefile, "a")) == NULL) {
	perror(savefile);
	exit(0);
  }
  printlet(let, savefp);
  if ((ferror(savefp) != 0) | (fclose(savefp) != 0)) {
	fprintf(stderr, "savefile write error:");
	perror(savefile);
  }
  exit(0);
}

void updatebox()
{
  FILE *tempfp;			/* fp for tempfile */
  char lockname[PATHLEN];	/* maildrop lock */
  int locktries = 0;		/* tries when box is locked */
  struct letter *let;		/* current letter */
  int c;

  sprintf(lockname, LOCKNAME, whoami());

  if (NULL == (tempfp = fopen(tempname, "w"))) {
	perror("mail: can't create temporary file");
	return;
  }
  for (let = firstlet; let != NULL; let = let->next) {
	if (let->status != DELETED) {
		printlet(let, tempfp);
		D(printf("printed letter at %ld\n", let->location));
	}
  }

  if (ferror(tempfp) || NULL == (tempfp = freopen(tempname, "r", tempfp))) {
	perror("mail: temporary file write error");
	unlink(tempname);
	return;
  }

  /* Shut off signals during the update */
  signal(SIGINT, SIG_IGN);
  signal(SIGHUP, SIG_IGN);
  signal(SIGQUIT, SIG_IGN);

  if (usedrop) while (link(mailbox, lockname) != 0) {
		if (++locktries >= LOCKTRIES) {
			fprintf(stderr, "mail: couldn't lock maildrop for update\n");
			return;
		}
		sleep(LOCKWAIT);
	}

  if (NULL == (boxfp = freopen(mailbox, "w", boxfp))) {
	perror("mail: couldn't reopen maildrop");
	fprintf(stderr, "mail may have been lost; look in %s\n", tempname);
	if (usedrop) unlink(lockname);
	return;
  }
  unlink(tempname);

  while ((c = getc(tempfp)) != EOF) putc(c, boxfp);

  fclose(boxfp);

  if (usedrop) unlink(lockname);
}

void printlet(let, tofp)
struct letter *let;
FILE *tofp;
{
  off_t current, limit;
  int c;

  fseek(boxfp, (current = let->location), 0);
  limit = (NULL != let->next) ? let->next->location : -1;

  while (current != limit && (c = getc(boxfp)) != EOF) {
	putc(c, tofp);
	++current;
  }
}

void doshell(command)
char *command;
{
  int waitstat, pid;
  char *shell;

  if (NULL == (shell = getenv("SHELL"))) shell = SHELL;

  if ((pid = fork()) < 0) {
	perror("mail: couldn't fork");
	return;
  } else if (pid != 0) {	/* parent */
	wait(&waitstat);
	return;
  }

  /* Child */
  setgid(getgid());
  setuid(getuid());
  umask(oldmask);

  execl(shell, shell, "-c", command, (char *) NULL);
  fprintf(stderr, "can't exec shell\n");
  exit(127);
}

void usage()
{
  fprintf(stderr, "usage: mail [-v] user [...]\n");
  fprintf(stderr, "       mail [-epqr] [-f file] [-t arg]\n");
}

char *basename(name)
char *name;
{
  char *p;

  if (NULL == (p = rindex(name, '/')))
	return name;
  else
	return p + 1;
}

char *whoami()
{
  struct passwd *pw;

  if (NULL != (pw = getpwuid(getuid())))
	return pw->pw_name;
  else
	return "nobody";
}

void dohelp()
{
  FILE *fp;
  char buffer[80];

  if ( (fp = fopen(HELPFILE, "r")) == NULL)
	fprintf(stdout, "can't open helpfile %s\n", HELPFILE);
  else
	while (fgets(buffer, 80, fp))
		fputs(buffer, stdout);
}

int filesize(name)
char *name ;
{
  struct stat buf;
 
  if (stat(name, &buf) == -1)
	buf.st_size = 0L;

  return (buf.st_size ? 1 : 0);
}
