/*
** inc.c -- incorporate messages from a maildrop into a folder
**
** This code is Copyright (c) 2002, by the authors of nmh.  See the
** COPYRIGHT file in the root directory of the nmh distribution for
** complete copyright information.
*/

#ifdef MAILGROUP
/*
** Revised: Sat Apr 14 17:08:17 PDT 1990 (marvit@hplabs)
** Added hpux hacks to set and reset gid to be "mail" as needed. The reset
** is necessary so inc'ed mail is the group of the inc'er, rather than
** "mail". We setgid to egid only when [un]locking the mail file. This
** is also a major security precaution which will not be explained here.
**
** Fri Feb  7 16:04:57 PST 1992  John Romine <bug-mh@ics.uci.edu>
**   NB: I'm not 100% sure that this setgid stuff is secure even now.
**
** See the *GROUPPRIVS() macros later. I'm reasonably happy with the setgid
** attribute. Running setuid root is probably not a terribly good idea, though.
**       -- Peter Maydell <pmaydell@chiark.greenend.org.uk>, 04/1998
**
** Peter Maydell's patch slightly modified for nmh 0.28-pre2.
** Ruud de Rooij <ruud@debian.org>  Wed, 22 Jul 1998 13:24:22 +0200
*/
#endif

#include <h/mh.h>
#include <h/utils.h>
#include <fcntl.h>
#include <h/fmt_scan.h>
#include <h/scansbr.h>
#include <h/signals.h>
#include <h/tws.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>
#include <locale.h>
#include <sysexits.h>

#ifdef HAVE_SYS_PARAM_H
# include <sys/param.h>
#endif

static struct swit switches[] = {
#define AUDSW  0
	{ "audit audit-file", 0 },
#define NAUDSW  1
	{ "noaudit", 2 },
#define CHGSW  2
	{ "changecur", 0 },
#define NCHGSW  3
	{ "nochangecur", 2 },
#define FILESW  4
	{ "file name", 0 },
#define FORMSW  5
	{ "form formatfile", 0 },
#define SILSW  6
	{ "silent", 0 },
#define NSILSW  7
	{ "nosilent", 2 },
#define TRNCSW  8
	{ "truncate", 0 },
#define NTRNCSW  9
	{ "notruncate", 2 },
#define WIDTHSW  10
	{ "width columns", 0 },
#define VERSIONSW  11
	{ "Version", 0 },
#define HELPSW  12
	{ "help", 0 },
	{ NULL, 0 },
};

char *version=VERSION;

/*
** This is an attempt to simplify things by putting all the
** privilege ops into macros.
** *GROUPPRIVS() is related to handling the setgid MAIL property,
** and only applies if MAILGROUP is defined.
** Basically, SAVEGROUPPRIVS() is called right at the top of main()
** to initialise things, and then DROPGROUPPRIVS() and GETGROUPPRIVS()
** do the obvious thing. TRYDROPGROUPPRIVS() has to be safe to call
** before DROPUSERPRIVS() is called [this is needed because setgid()
** sets both effective and real uids if euid is root.]
**
** There's probably a better implementation if we're allowed to use
** BSD-style setreuid() rather than using POSIX saved-ids.
** Anyway, if you're euid root it's a bit pointless to drop the group
** permissions...
**
** I'm pretty happy that the security is good provided we aren't setuid root.
** The only things we trust with group=mail privilege are lkfopen()
** and lkfclose().
*/

/*
** For setting and returning to "mail" gid
*/
#ifdef MAILGROUP
static int return_gid;
/*
** easy case; we're not setuid root, so can drop group privs immediately.
*/
#define TRYDROPGROUPPRIVS() DROPGROUPPRIVS()
#define DROPGROUPPRIVS() \
    if (setegid(getgid()) != 0) { \
        advise ("setegid", "unable to set group to %ld", (long) getgid()); \
		_exit (EX_OSERR); \
    }
#define GETGROUPPRIVS() \
    if (setegid(return_gid) != 0) { \
        advise ("setegid", "unable to set group to %ld", (long) return_gid); \
		_exit (EX_OSERR); \
    }
#define SAVEGROUPPRIVS() return_gid = getegid()
#else
/* define *GROUPPRIVS() as null; this avoids having lots of "#ifdef MAILGROUP"s */
#define TRYDROPGROUPPRIVS()
#define DROPGROUPPRIVS()
#define GETGROUPPRIVS()
#define SAVEGROUPPRIVS()
#endif /* not MAILGROUP */

/*
** these variables have to be globals so that done() can correctly clean
** up the lockfile
*/
static int locked = 0;
static char *newmail;
static FILE *in;

/*
** prototypes
*/
void inc_done();


int
main(int argc, char **argv)
{
	int chgflag = 1, trnflag = 1;
	int noisy = 1, width = 0;
	int hghnum = 0, msgnum = 0;
	int incerr = 0;  /*
			** <0 if inc hits an error which means it should
			** not truncate mailspool
			*/
	char *cp, *maildir = NULL, *folder = NULL;
	char *form = NULL;
	char *audfile = NULL, *from = NULL;
	char buf[BUFSIZ], **argp, *fmtstr, **arguments;
	struct msgs *mp = NULL;
	struct stat st, s1;
	FILE *aud = NULL;
	char b[MAXPATHLEN + 1];
	/* copy of mail directory because the static gets overwritten */
	char *maildir_copy = NULL;

	if (atexit(inc_done) != 0) {
		adios(EX_OSERR, NULL, "atexit failed");
	}

	/*
	** absolutely the first thing we do is save our privileges,
	** and drop them if we can.
	*/
	SAVEGROUPPRIVS();
	TRYDROPGROUPPRIVS();

	setlocale(LC_ALL, "");
	invo_name = mhbasename(argv[0]);

	context_read();

	arguments = getarguments(invo_name, argc, argv, 1);
	argp = arguments;

	while ((cp = *argp++)) {
		if (*cp == '-') {
			switch (smatch(++cp, switches)) {
			case AMBIGSW:
				ambigsw(cp, switches);
				exit(EX_USAGE);
			case UNKWNSW:
				adios(EX_USAGE, NULL, "-%s unknown", cp);

			case HELPSW:
				snprintf(buf, sizeof(buf), "%s [+folder] [switches]", invo_name);
				print_help(buf, switches, 1);
				exit(argc == 2 ? EX_OK : EX_USAGE);
			case VERSIONSW:
				print_version(invo_name);
				exit(argc == 2 ? EX_OK : EX_USAGE);

			case AUDSW:
				if (!(cp = *argp++) || *cp == '-')
					adios(EX_USAGE, NULL, "missing argument to %s", argp[-2]);
				audfile = mh_xstrdup(expanddir(cp));
				continue;
			case NAUDSW:
				audfile = NULL;
				continue;

			case CHGSW:
				chgflag++;
				continue;
			case NCHGSW:
				chgflag = 0;
				continue;

			/*
			** The flag `trnflag' has the value:
			**
			** 2 if -truncate is given
			** 1 by default (truncating is default)
			** 0 if -notruncate is given
			*/
			case TRNCSW:
				trnflag = 2;
				continue;
			case NTRNCSW:
				trnflag = 0;
				continue;

			case FILESW:
				if (!(cp = *argp++))
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				from = (strcmp(cp, "-")==0) ? "-" : mh_xstrdup(expanddir(cp));

				/*
				** If the truncate file is in default state,
				** change to not truncate.
				*/
				if (trnflag == 1)
					trnflag = 0;
				continue;

			case SILSW:
				noisy = 0;
				continue;
			case NSILSW:
				noisy++;
				continue;

			case FORMSW:
				if (!(form = *argp++) || *form == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				continue;

			case WIDTHSW:
				if (!(cp = *argp++) || *cp == '-')
					adios(EX_USAGE, NULL, "missing argument to %s",
							argp[-2]);
				width = atoi(cp);
				continue;
			}
		}
		if (*cp == '+' || *cp == '@') {
			if (folder)
				adios(EX_USAGE, NULL, "only one folder at a time!");
			else
				folder = mh_xstrdup(expandfol(cp));
		} else {
			adios(EX_USAGE, NULL, "usage: %s [+folder] [switches]",
					invo_name);
		}
	}

	/*
	** NOTE: above this point you should use TRYDROPGROUPPRIVS(),
	** not DROPGROUPPRIVS().
	*/
	/* guarantee dropping group privileges; we might not have done so earlier */
	DROPGROUPPRIVS();

	if (from && strcmp(from, "-")==0) {
		/* We'll read mail from stdin. */
		newmail = NULL;
	} else {
		/* We'll read mail from a file. */
		if (from)
			newmail = from;
		else if ((newmail = getenv("MAILDROP")) && *newmail)
			newmail = toabsdir(newmail);
		else if ((newmail = context_find("maildrop")) && *newmail)
			newmail = toabsdir(newmail);
		else {
			newmail = concat(mailspool, "/", getusername(), NULL);
		}
		if (stat(newmail, &s1) == NOTOK || s1.st_size == 0)
			adios(EX_DATAERR, NULL, "no mail to incorporate");

		if ((cp = strdup(newmail)) == NULL)
			adios(EX_OSERR, NULL, "error allocating memory to copy newmail");

		newmail = cp;
	}

	if (!folder)
		folder = getdeffol();
	maildir = toabsdir(folder);

	if ((maildir_copy = strdup(maildir)) == NULL)
		adios(EX_OSERR, maildir, "error allocating memory to copy maildir");

	create_folder(maildir, noisy ? 0 : 1, exit);

	if (chdir(maildir) == NOTOK)
		adios(EX_OSERR, maildir, "unable to change directory to");

	if (!(mp = folder_read(folder)))
		adios(EX_IOERR, NULL, "unable to read folder %s", folder);

	if (!newmail) {
		trnflag = 0;
		in = stdin;
	} else if (access(newmail, W_OK) != NOTOK) {
		locked++;
		if (trnflag) {
			SIGNAL(SIGHUP, SIG_IGN);
			SIGNAL(SIGINT, SIG_IGN);
			SIGNAL(SIGQUIT, SIG_IGN);
			SIGNAL(SIGTERM, SIG_IGN);
		}

		GETGROUPPRIVS();  /* Reset gid to lock mail file */
		in = lkfopen(newmail, "r");
		DROPGROUPPRIVS();
		if (in == NULL)
			adios(EX_IOERR, NULL, "unable to lock and fopen %s", newmail);
		fstat(fileno(in), &s1);
	} else {
		trnflag = 0;
		if ((in = fopen(newmail, "r")) == NULL)
			adios(EX_IOERR, newmail, "unable to read");
	}

	/* This shouldn't be necessary but it can't hurt. */
	DROPGROUPPRIVS();

	if (audfile) {
		int i;
		if ((i = stat(audfile, &st)) == NOTOK)
			advise(NULL, "Creating Receive-Audit: %s", audfile);
		if ((aud = fopen(audfile, "a")) == NULL)
			adios(EX_IOERR, audfile, "unable to append to");
		else if (i == NOTOK)
			chmod(audfile, m_gmprot());

		fprintf(aud, from ? "<<inc>> %s  -ms %s\n" : "<<inc>> %s\n",
			 dtimenow(), from);
	}

	/* Set format string */
	fmtstr = new_fs(form, scanformat);

	if (noisy) {
		printf("Incorporating new mail into %s...\n\n", folder);
		fflush(stdout);
	}

	/* check if readable and nonempty */
	if (!fgets(buf, sizeof(buf), in)) {
		if (ferror(in)) {
			advise("read", "unable to");
			incerr = SCNFAT;
		} else {
			incerr = SCNEOF;
		}
		goto giveup;
	}
	if (strncmp("From ", buf, 5)!=0) {
		advise(NULL, "not in mbox format");
		incerr = SCNFAT;
		goto giveup;
	}

	/*
	** Get the mail from file (usually mail spool)
	*/
	hghnum = msgnum = mp->hghmsg;
	for (;;) {
		/*
		** Check if we need to allocate more space for message status.
		** If so, then add space for an additional 100 messages.
		*/
		if (msgnum >= mp->hghoff && !(mp = folder_realloc(mp, mp->lowoff, mp->hghoff + 100))) {
			advise(NULL, "unable to allocate folder storage");
			incerr = NOTOK;
			break;
		}

		/* create scanline for new message */
		switch (incerr = scan(in, msgnum + 1, msgnum + 1,
				noisy ? fmtstr : NULL, width,
				msgnum == hghnum && chgflag, 1)) {
		case SCNFAT:
		case SCNEOF:
			break;

		case SCNERR:
			if (aud)
				fputs("inc aborted!\n", aud);
			/* doesn't clean up locks! */
			advise(NULL, "aborted!");
			break;

		case SCNNUM:
			advise(NULL, "BUG in %s, number out of range",
					invo_name);
			break;

		default:
			advise(NULL, "BUG in %s, scan() botch (%d)",
					invo_name, incerr);
			break;

		case SCNMSG:
			/*
			**  Run the external program hook on the message.
			*/

			snprintf(b, sizeof (b), "%s/%d", maildir_copy,
					msgnum + 1);
			ext_hook("add-hook", b, NULL);

			if (aud)
				fputs(scanl, aud);
			if (noisy)
				fflush(stdout);
			msgnum++;
			mp->hghmsg++;
			mp->nummsg++;
			if (mp->lowmsg == 0)
				mp->lowmsg = 1;
			clear_msg_flags(mp, msgnum);
			set_exists(mp, msgnum);
			set_unseen(mp, msgnum);
			mp->msgflags |= SEQMOD;
			continue;
		}
		/*
		** If we get here there was some sort of error from scan(),
		** so stop processing anything more from the spool.
		*/
		break;
	}
giveup:;
	mh_free0(&maildir_copy);

	if (incerr < 0) {  /* error */
		if (locked) {
			GETGROUPPRIVS();  /* Be sure we can unlock mail file */
			lkfclose(in, newmail); in = NULL;
			DROPGROUPPRIVS();  /*
					** And then return us to normal
					** privileges
					*/
		} else {
			fclose(in); in = NULL;
		}
		adios(EX_SOFTWARE, NULL, "failed");
	}

	if (aud)
		fclose(aud);

	if (noisy)
		fflush(stdout);

	/*
	** truncate file we are incorporating from
	*/
	if (trnflag) {
		if (stat(newmail, &st) != NOTOK && s1.st_mtime != st.st_mtime)
			advise(NULL, "new messages have arrived!\007");
		else {
			int newfd;
			if ((newfd = creat(newmail, 0600)) != NOTOK)
				close(newfd);
			else
				admonish(newmail, "error zero'ing");
		}
	} else if (noisy && newmail) {
		printf("%s not zero'd\n", newmail);
	}

	if (msgnum == hghnum) {
		admonish(NULL, "no messages incorporated");
	} else {
		context_replace(curfolder, folder); /* update current folder */
		if (chgflag)
			mp->curmsg = hghnum + 1;
		mp->hghmsg = msgnum;
		if (mp->lowmsg == 0)
			mp->lowmsg = 1;
		if (chgflag)  /* sigh... */
			seq_setcur(mp, mp->curmsg);
	}

	/*
	** unlock the mail spool
	*/
	if (locked) {
		GETGROUPPRIVS();  /* Be sure we can unlock mail file */
		lkfclose(in, newmail); in = NULL;
		DROPGROUPPRIVS();  /* And then return us to normal privileges */
	} else {
		fclose(in); in = NULL;
	}

	seq_setunseen(mp, 1);
	seq_save(mp);
	context_save();
	return 0;
}

void
inc_done()
{
	if (locked) {
		GETGROUPPRIVS();
		lkfclose(in, newmail);
		DROPGROUPPRIVS();
	}
}
