/*
 * expire - expire old news
 *
 * One modest flaw:  links are not preserved in archived copies, i.e. you
 * get multiple copies of multiply-posted articles.  Since link preservation
 * is arbitrarily hard when control files get complex, to hell with it.
 */

#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include "fixerrno.h"
#include <time.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/timeb.h>
#include <sys/stat.h>
#include "libc.h"
#include "news.h"
#include "config.h"
#include "fgetfln.h"
#include "case.h"
#include "dbz.h"
#include "ngmatch.h"

/* basic parameters of time, used in back() (and debugging code) */
#ifndef EPOCH
#define	EPOCH	((time_t)0)	/* the origin of time_t */
#endif
#ifndef FOREVER
#define	FOREVER	1e37		/* FOREVER > max value of time_t */
#endif
#define	DAY	((double)24*60*60)

/* structure for expiry-control records */
struct ctl {
	struct ctl *next;
	char *groups;		/* newsgroups */
	NGPAT *pat;		/* parsed "groups", if actually a pattern */
	int ismod;		/* moderated? */
#		define	UNMOD	'u'
#		define	MOD	'm'
#		define	EITHER	'x'
	time_t retain;		/* earliest arrival date not expired */
	time_t normal;		/* earliest not expired in default case */
	time_t purge;		/* latest arrival date always expired */
	char *dir;		/* Archive dir or NULL. */
	long ngroups;		/* for explist, number of groups dealt with */
	int lineno;		/* line number in explist */
};

/* header for internal form of control file */
struct ctl *ctls = NULL;
struct ctl *lastctl = NULL;

/*
 * Headers for lists by newsgroup, derived (mostly) from active file.
 * Hashing is by length of newsgroup name; this is quick and works well,
 * and there is no simple variation that does much better.  (Actually,
 * that statement is obsolete; there are now too many newsgroups for
 * this to work particularly well, although it's not a major profiling
 * hot spot.  Improvements are planned.)
 */
#define	NHASH	80
struct ctl *ngs[NHASH] = { NULL };

struct ctl *holdover = NULL;	/* "/expired/" control record */
struct ctl *bounds = NULL;	/* "/bounds/" control record */

int debug = 0;			/* for inews routines */
int expdebug = 0;		/* expire debugging */

int printexpiring = 0;		/* print info line for expiring articles? */
char *defarch = NULL;		/* default archive dir */
int spacetight = 0;		/* error-recovery actions remove evidence? */

char *subsep = "~";		/* subfield separator in middle field */
int checkonly = 0;		/* check control information only */
int testing = 0;		/* testing only, leave articles alone */
int leaders = 0;		/* only first link ("leader") is hard link */
int verbose = 0;		/* report statistics */
int rebuild = 1;		/* rebuild history files */
int holdarch = 0;		/* hold rather than archiving */
int dategrump = 0;		/* report incomprehensible expiry dates */
char *histdir = NULL;		/* where to find history files */

long nkept = 0;			/* count of articles not expired */
long ngone = 0;			/* count of articles removed (no links left) */
long nresid = 0;		/* count of residual entries kept */
long narched = 0;		/* count of links archived */
long njunked = 0;		/* count of links just removed */
long nmissing = 0;		/* count of links missing at cp/rm time */

long ncpfail = 0;		/* count of failed cp()s */

char dont[] = "don't";		/* magic cookie for whereexpire() return */

time_t now;			/* set once in startup, as reference */
#define	NODATE	((time_t)(-1))	/* time_t value indicating date not given */
time_t latest =	0;		/* most recent arrival date */

char subject[200] = "";		/* Subject line for -p, minus header */

/* Buffer etc. for readline and friends. */
#ifdef SMALLMEM
#define	RLBSIZ	(BUFSIZ)
#else
#define	RLBSIZ	(BUFSIZ*8)	/* we're going to read a biiiig file */
#endif
char rlbuf[RLBSIZ+1];		/* +1 for sentinel */
size_t rlbufsiz = RLBSIZ;
int rlnleft = 0;
char *rest = rlbuf;		/* relies on rlbuf initialized to '\0's */
int nlocked = 0;		/* has readline() locked the news system? */
char *rlline = NULL;		/* malloced when we need to copy a line */
size_t rllsiz = 200;		/* good initial size */

/*
 * Archive-copying buffer.
 * 8KB buffer is large enough to take most articles at one gulp,
 * and also large enough for virtual certainty of getting the
 * Subject: line in the first bufferload.
 */
#ifdef SMALLMEM
char abuf[2*1024];		/* expire reported to be tight on 11 */
#else
char abuf[8*1024];
#endif

char *progname;

extern struct tm *gmtime();
extern time_t time();

extern time_t getindate();

/* forwards */
FILE *eufopen();
void eufclose();
char *whereexpire();
time_t back();
void checkadir();
void fail();
void die();
void control();
void prime();
void checkused();
void doit();
void cd();
time_t readdate();
char *doarticle();
void warning();
void complain();
void printstuff();
int expire();
char *readline();
void mkparents();
void getsubj();
void refill();
void printlists();
void pctl();
void fillin();
void ctlline();

/*
 - main - parse arguments and handle options
 */
main(argc, argv)
int argc;
char *argv[];
{
	register int c;
	register int errflg = 0;
	register FILE *cf;
	extern int optind;
	extern char *optarg;

	progname = argv[0];
	now = time((time_t *)NULL);

	while ((c = getopt(argc, argv, "pa:sF:cn:tlvrhgH:B:d")) != EOF)
		switch (c) {
		case 'p':	/* print info line for archived articles */
			printexpiring = 1;
			break;
		case 'a':	/* archive in this directory */
			defarch = optarg;
			break;
		case 's':	/* maximize space during error recovery */
			spacetight = 1;
			break;
		case 'F':	/* subfield separator in middle field */
			subsep = optarg;
			break;
		case 'c':	/* check control-file format only */
			checkonly = 1;
			break;
		case 'n':	/* set value of "now" for testing */
			now = atol(optarg);
			break;
		case 't':	/* testing, do not mess with articles */
			testing = 1;
			break;
		case 'l':	/* leaders */
			leaders = 1;
			break;
		case 'v':	/* verbose -- report some statistics */
			verbose = 1;
			break;
		case 'r':	/* suppress history-file rebuild */
			rebuild = 0;
			break;
		case 'h':	/* hold all files meant to be archived */
			holdarch = 1;
			break;
		case 'g':	/* report incomprehensible expiry dates */
			dategrump = 1;
			break;
		case 'H':	/* where to find history files */
			histdir = optarg;
			break;
		case 'B':	/* internal buffer size */
			rlbufsiz = atoi(optarg);
			break;
		case 'd':	/* debug */
			expdebug = 1;
			break;
		case '?':
		default:
			errflg++;
			break;
		}
	if (errflg || optind < argc-1) {
		fprintf(stderr, "Usage: %s [-p] [-s] [-c] [-a archdir] [ctlfile]\n",
								progname);
		exit(2);
	}
	if (expdebug)
		setbuf(stderr, (char *)NULL);

	if (optind < argc) {
		cf = eufopen(argv[optind], "r");
		control(cf);
		(void) fclose(cf);
	} else
		control(stdin);
	prime(ctlfile("active"));

	if (expdebug)
		printlists();
	if (histdir == NULL)
		histdir = strsave(ctlfile((char *)NULL));
	if (defarch != NULL)
		checkadir(defarch);
	if (checkonly)
		exit(0);


	(void) umask(newsumask());
	doit();			/* side effect: newslock() */
	newsunlock();

	if (latest > time((time_t *)NULL)) {
		complain("some article arrival dates are in the future!", "");
		complain("\tis your system clock set wrong?", "");
	}

	if (verbose) {
		fprintf(stderr, "%ld kept, %ld expired\n", nkept, ngone);
		fprintf(stderr, "%ld residual lines\n", nresid);
		fprintf(stderr, "%ld links archived, %ld junked, %ld missing\n",
						narched, njunked, nmissing);
	}
	exit(0);
}

/*
 - control - pick up a control file
 */
void
control(f)
register FILE *f;
{
	register char *p;
	register int gotone = 0;
	register int lineno = 0;

	while ((p = fgetline(f, (size_t *)NULL)) != NULL) {
		lineno++;
		if (*p != '#')
			ctlline(p, lineno);
		gotone = 1;
	}

	if (!gotone)
		die("control file empty!", "");
}

/*
 - ctlline - process one control-file line
 */
void
ctlline(ctl, lineno)
char *ctl;
int lineno;
{
	register struct ctl *ct;
	char *field[4];
	char datebuf[50];
	char *dates[3];
	register int nf;
	int ndates;

	nf = split(ctl, field, 4, "");
	if (nf == 0)
		return;		/* blank line */
	if (nf != 4)
		die("control line for `%s' hasn't got 4 fields", field[0]);

	ct = (struct ctl *)malloc(sizeof(struct ctl));
	if (ct == NULL)
		fail("out of memory for control list", "");

	ct->groups = strsave(field[0]);
	ct->pat = ngparse(strsave(ct->groups));
	if (ct->pat == NULL)
		die("can't parse control file pattern `%s'", ct->groups);
	if (STREQ(field[1], "m"))
		ct->ismod = MOD;
	else if (STREQ(field[1], "u"))
		ct->ismod = UNMOD;
	else if (STREQ(field[1], "x"))
		ct->ismod = EITHER;
	else
		die("strange mod field `%s' in control file", field[1]);

	if (strlen(field[2]) > sizeof(datebuf)-1)
		die("date specification `%s' too long", field[2]);
	(void) strcpy(datebuf, field[2]);
	ndates = split(datebuf, dates, 3, "-");
	switch (ndates) {
	case 3:
		ct->retain = back(dates[0]);
		ct->normal = back(dates[1]);
		ct->purge = back(dates[2]);
		break;
	case 2:
		ct->retain = (bounds != NULL) ? bounds->retain : back("0");
		ct->normal = back(dates[0]);
		ct->purge = back(dates[1]);
		break;
	case 1:
		ct->retain = (bounds != NULL) ? bounds->retain : back("0");
		ct->normal = back(dates[0]);
		ct->purge = (bounds != NULL) ? bounds->purge : back("never");
		break;
	default:
		die("invalid date specification `%s'", field[2]);
		/* NOTREACHED */
		break;
	}
	if (ct->retain < ct->normal && ndates <= 2)	/* stretch defaults */
		ct->retain = ct->normal;
	if (ct->normal < ct->purge && ndates == 1)
		ct->purge = ct->normal;

	if (ct->retain < ct->normal || ct->normal < ct->purge)
		die("preposterous dates: `%s'", field[2]);

	if (STREQ(field[3], "-"))
		ct->dir = NULL;
	else if (STREQ(field[3], "@")) {
		if (defarch == NULL)
			die("@ in control file but no -a", "");
		ct->dir = defarch;
	} else {
		ct->dir = strsave(field[3]);
		checkadir(ct->dir);
	}

	ct->ngroups = 0;
	ct->lineno = lineno;

	/* put it where it belongs */
	if (STREQ(ct->groups, "/expired/"))
		holdover = ct;
	else if (STREQ(ct->groups, "/bounds/"))
		bounds = ct;
	else if (ct->groups[0] == '/')
		die("unknown special line name `%s'", ct->groups);
	else {
		ct->next = NULL;
		if (lastctl == NULL)
			ctls = ct;
		else
			lastctl->next = ct;
		lastctl = ct;
	}
}

/*
 - prime - prime control lists from active file
 */
void
prime(afile)
char *afile;
{
	register char *line;
	register FILE *af;
	register struct ctl *ct;
#	define	NFACT	4
	char *field[NFACT];
	int nf;
	register int hash;

	af = eufopen(afile, "r");
	while ((line = fgetline(af, (size_t *)NULL)) != NULL) {
		nf = split(line, field, NFACT, "");
		if (nf != NFACT)
			die("wrong number of fields in active for `%s'", field[0]);
		ct = (struct ctl *)malloc(sizeof(struct ctl));
		if (ct == NULL)
			fail("out of memory at newsgroup `%s'", field[0]);
		ct->groups = strsave(field[0]);
		ct->ismod = (strchr(field[3], 'm') != NULL) ? MOD : UNMOD;
		fillin(ct);
		hash = strlen(field[0]);
		if (hash > NHASH-1)
			hash = NHASH-1;
		ct->next = ngs[hash];
		ngs[hash] = ct;
	}
	(void) fclose(af);

	checkused();
}

/*
 - fillin - fill in a ctl struct for a newsgroup from the control-file list
 */
void
fillin(ct)
register struct ctl *ct;
{
	register struct ctl *cscan;
	char grump[100];

	for (cscan = ctls; cscan != NULL; cscan = cscan->next)
		if ((cscan->ismod == ct->ismod || cscan->ismod == EITHER) &&
		    ngpatmat(cscan->pat, ct->groups)) {
			ct->retain = cscan->retain;
			ct->normal = cscan->normal;
			ct->purge = cscan->purge;
			ct->dir = cscan->dir;
			cscan->ngroups++;
			return;
		}

	/* oooooops... */
	sprintf(grump, "group `%%s' (%smoderated) not covered by control file",
					(ct->ismod == MOD) ? "" : "un");
	die(grump, ct->groups);
}

/*
 - checkused - check that all lines of the control file got used
 */
void
checkused()
{
	register struct ctl *cscan;
	char grump[100];

	for (cscan = ctls; cscan != NULL; cscan = cscan->next)
		if (cscan->ngroups == 0) {
			sprintf(grump,
	"warning: line %d of control file controls no active newsgroups",
								cscan->lineno);
			complain(grump, "");
		}
}

/*
 - doit - file manipulation and master control
 */
void
doit()
{
	register int old;
	register FILE *new;
	extern void mainloop();

	cd(histdir);
	old = open("history", 0);
	if (old < 0)
		fail("cannot open `%s'", "history");
	if (rebuild) {
		(void) remove("history.n");
		(void) remove("history.n.dir");
		(void) remove("history.n.pag");
		if (spacetight)
			(void) remove("history.o");
		new = eufopen("history.n", "w");
		(void) fclose(eufopen("history.n.dir", "w"));
		(void) fclose(eufopen("history.n.pag", "w"));
		(void) dbzincore(1);
		errno = 0;
		if (dbzagain("history.n", "history") < 0)
			fail("dbzagain(history.n) failed", "");
	}

	cd(artfile((char *)NULL));
	mainloop(old, new);
	/* side effect of mainloop():  newslock() */

	(void) close(old);
	if (rebuild) {
		eufclose(new, "history.n");
		if (dbmclose() < 0)
			fail("dbmclose() failed", "");
	}

	if (testing)
		return;
	if (rebuild) {
		cd(histdir);
		if (rename("history", "history.o") != 0)
			fail("can't move history", "");
		if (rename("history.n", "history") != 0)
			fail("disaster -- can't reinstate history!", "");
		if (rename("history.n.dir", "history.dir") != 0)
			fail("disaster -- can't reinstate history.dir!", "");
		if (rename("history.n.pag", "history.pag") != 0)
			fail("disaster -- can't reinstate history.pag!", "");
	}
}

/*
 - mainloop - main loop, reading old history file and building new one
 */
void
mainloop(old, new)
register int old;
register FILE *new;
{
	register char *line;
	long here;
	datum lhs;
	datum rhs;
	register int ret;
#	define	NF	3
	char *field[NF];	/* fields in line */
	register int nf;
#	define	NSF	3	/* lump subfields after second into one */
	char *subfield[NSF];	/* subfields in middle field */
	register long lineno = 0;
	char linenobuf[20];
	register int nsf;
	register int i;

	while ((line = readline(old)) != NULL) {
		lineno++;
		if (expdebug) {
			fprintf(stderr, "\nline %ld `", lineno);
			fputs(line, stderr);
			fputs("'\n", stderr);
		}
		nf = split(line, field, NF, "\t");
		if (nf == 2)
			field[2] = NULL;

		if (nf < 2 || nf > 3 || *field[0] == '\0') {
			sprintf(linenobuf, "%ld", lineno);
			complain("garbled history entry, line %s", linenobuf);
			ret = 0;
		} else {
			nsf = split(field[1], subfield, NSF, subsep);
			if (nsf > NSF)
				nsf = NSF;
			ret = doline(field, nf, subfield, nsf);
		}

		if (ret >= 0 && rebuild) {
			/* make the DBM entry */
			lhs.dptr = field[0];
			lhs.dsize = strlen(field[0])+1;
			here = ftell(new);
			rhs.dptr = (char *)&here;
			rhs.dsize = sizeof(here);
			ret = dbzstore(lhs, rhs);
			if (ret < 0)
				die("dbzstore failure on `%s'", field[0]);

			/* make the history entry */
			fputs(field[0], new);
			putc('\t', new);
			fputs(subfield[0], new);
			for (i = 1; i < nsf; i++) {
				putc(*subsep, new);
				fputs(subfield[i], new);
			}
			if (field[2] != NULL) {
				putc('\t', new);
				fputs(field[2], new);
			}
			putc('\n', new);

			if (expdebug)
				fprintf(stderr, "new line `%s\t%s%c%s\t%s'\n",
					field[0], subfield[0], *subsep,
					subfield[1],
					(field[2] == NULL) ? "" : field[2]);
		}
	}
	/* side effect of readline() == NULL:  newslock() */
}

/*
 - doline - handle one history line, possibly modifying it
 */
int				/* 0 keep it, <0 don't */
doline(field, nf, subfield, nsf)
char *field[NF];		/* fields in line */
register int nf;
char *subfield[NSF];		/* subfields in middle field */
register int nsf;
{
	register time_t recdate;
	register time_t expdate;
	static char expbuf[25];		/* plenty for decimal time_t */
	register char *oldf2;
	register char *sf1 = subfield[1];

	/* sort out the dates */
	if (nsf < 2 || *sf1 == '\0' || (*sf1 == '-' && *(sf1+1) == '\0'))
		expdate = NODATE;
	else {
		expdate = readdate(sf1);
		if (expdate == NODATE && dategrump)
			complain("ignoring bad expiry date `%s',", sf1);
	}
	recdate = readdate(subfield[0]);
	if (recdate == NODATE) {
		complain("bad arrival date `%s' -- expiring", subfield[0]);
		recdate = readdate("0");
		expdate = recdate;
	}
	if (recdate > latest)
		latest = recdate;
	if (expdebug)
		fprintf(stderr, "rec %ld, expire %ld\n", (long)recdate,
								(long)expdate);

	/* deal with it */
	oldf2 = field[2];
	field[2] = doarticle(oldf2, recdate, expdate, field[0]);
	if (oldf2 != NULL) {
		if (field[2] == NULL)
			ngone++;
		else
			nkept++;
	}
	if (field[2] == NULL) {
		if (holdover == NULL || shouldgo(recdate, NODATE, holdover))
			return(-1);	/* easy case -- get rid of it */
		nresid++;
	}

	/* hard case -- must rebuild expiry date */
	if (expdate != NODATE) {
		sprintf(expbuf, "%ld", (long)expdate);
		subfield[1] = expbuf;
	} else
		subfield[1] = "-";
	return(0);
}

/*
 - readdate - turn a date into internal form
 */
time_t
readdate(text)
char *text;
{
	register time_t ret;

	/* heuristic:  non-numeric dates never involve numbers >4 digits */
	ret = atol(text);
	if (ret < 10000 && strspn(text, "0123456789") != strlen(text)) {
		ret = getindate(text, (struct timeb *)NULL);
		if (ret == -1)
			ret = NODATE;
	}

	return(ret);
}

/*
 - doarticle - possibly expire an article
 *
 * Re-uses the space of its first argument.
 */
char *				/* new name list, in space of old, or NULL */
doarticle(oldnames, recdate, expdate, msgid)
char *oldnames;			/* may be destroyed */
time_t recdate;
time_t expdate;
char *msgid;			/* for printstuff() */
{
	register char *src;
	register char *dst;
	register char *name;
	register char *dir;
	register char *p;
	register char srcc;
	register int nleft;
	register int nexpired;
	register int ret;
#	define	NDELIM	" ,"

	if (oldnames == NULL)
		return(NULL);

	src = oldnames;
	dst = oldnames;
	nleft = 0;
	nexpired = 0;
	for (;;) {
		src += strspn(src, NDELIM);
		name = src;
		src += strcspn(src, NDELIM);
		srcc = *src;
		*src = '\0';
		if (*name == '\0')
			break;		/* NOTE BREAK OUT */
		if (expdebug)
			fprintf(stderr, "name `%s'\n", name);

		ret = -1;
		dir = whereexpire(recdate, expdate, name);
		if (dir != dont && !(leaders && nleft == 0 && srcc != '\0')) {
			if (expdebug)
				fprintf(stderr, "expire into `%s'\n",
					(dir == NULL) ? "(null)" : dir);
			for (p = strchr(name, '.'); p != NULL;
							p = strchr(p+1, '.'))
				*p = '/';
			ret = expire(name, dir);
			if (ret < 0) {	/* oops -- couldn't expire it! */
				for (p = strchr(name, '/'); p != NULL;
							p = strchr(p+1, '/'))
					*p = '.';
				p = strrchr(name, '.');
				if (p != NULL)
					*p = '/';
			}
		}
		if (ret > 0) {		/* we got rid of it */
			if (dir != NULL && printexpiring)
				printstuff(msgid, name, recdate);
			nexpired++;
		} else if (ret < 0) {	/* it's still around */
			if (dst != oldnames)
				*dst++ = ' ';
			while (*name != '\0')
				*dst++ = *name++;
			nleft++;
		}
		*src = srcc;
	}

	if (nleft == 0)
		return(NULL);
	*dst++ = '\0';
	if (leaders && nleft == 1 && nexpired > 0)	/* aging leader */
		return(doarticle(oldnames, recdate, expdate, msgid));
	return(oldnames);
}

/*
 - whereexpire - where should this name expire to, and should it?
 *
 * The "dont" variable's address is used as the don't-expire return value,
 * since NULL means "to nowhere".
 */
char *				/* archive directory, NULL, or dont */
whereexpire(recdate, expdate, name)
time_t recdate;
time_t expdate;
char *name;
{
	register char *group;
	register char *slash;
	register struct ctl *ct;
	register int hash;

	group = name;
	slash = strchr(group, '/');
	if (slash == NULL)
		die("no slash in article path `%s'", name);
	else
		*slash = '\0';
	if (strchr(slash+1, '/') != NULL) {
		*slash = '/';
		die("multiple slashes in article path `%s'", name);
	}

	/* find applicable expiry-control struct (make it if necessary) */
	hash = strlen(group);
	if (hash > NHASH-1)
		hash = NHASH-1;
	for (ct = ngs[hash]; ct != NULL && !STREQ(ct->groups, group);
								ct = ct->next)
		continue;
	if (ct == NULL) {	/* oops, there wasn't one */
		if (expdebug)
			fprintf(stderr, "new group `%s'\n", group);
		ct = (struct ctl *)malloc(sizeof(struct ctl));
		if (ct == NULL)
			fail("out of memory for newsgroup `%s'", group);
		ct->groups = strsave(group);
		ct->ismod = UNMOD;	/* unknown -- treat it as mundane */
		fillin(ct);
		ct->next = ngs[hash];
		ngs[hash] = ct;
	}
	*slash = '/';

	/* and decide */
	if (!shouldgo(recdate, expdate, ct) || (holdarch && ct->dir != NULL))
		return(dont);
	else
		return(ct->dir);
}

/*
 - shouldgo - should article with these dates expire now?
 */
int				/* predicate */
shouldgo(recdate, expdate, ct)
time_t recdate;
time_t expdate;
register struct ctl *ct;
{
	if (recdate >= ct->retain)	/* within retention period */
		return(0);
	if (recdate <= ct->purge)	/* past purge date */
		return(1);
	if (expdate != NODATE) {
		if (now >= expdate)	/* past its explicit date */
			return(1);
		else
			return(0);
	} else {
		if (recdate < ct->normal)	/* past default date */
			return(1);
		else
			return(0);
	}
	/* NOTREACHED */
}

/*
 - expire - expire an article
 */
int				/* >0 success, 0 missing, <0 failure */
expire(name, dir)
char *name;
char *dir;
{
	register char *old;
	register char *new;
	register int ret;

	if (testing) {
		if (dir != NULL)
			fprintf(stderr, "copy %s %s ; ", name, dir);
		fprintf(stderr, "remove %s\n", name);
		return(1);
	}

	if (dir != NULL) {		/* should archive */
		/* sort out filenames */
		old = strsave(artfile(name));
		if (*dir == '=') {
			new = strrchr(name, '/');
			if (new == NULL)
				die("no slash in `%s'", name);
			new++;
			new = str3save(dir+1, "/", new);
		} else
			new = str3save(dir, "/", name);

		/* cp() usually succeeds, so try it before getting fancy */
		ret = cp(old, new);
		if (ret == 'n')		/* new could not be created */
			if (*dir != '=') {
				mkparents(name, dir);
				ret = cp(old, new);	/* try again */
			}
		switch (ret) {		/* report problems, if any */
		case 0:			/* success */
			narched++;
			break;
		case 'o':		/* old did not exist */
			nmissing++;
			ret = 0;	/* not strictly an error */
			break;
		case 'n':		/* new could not be created */
			warning("can't create `%s'", new);
			ncpfail++;
			break;
		case 'r':		/* read error */
			warning("error reading `%s'", old);
			ncpfail++;
			break;
		case 'w':		/* write error */
			warning("error writing `%s'", new);
			ncpfail++;
			break;
		default:
			warning("error archiving `%s'", name);
			ncpfail++;
			break;
		}

		/* limited patience for archiving failures */
		if (ret != 0 && ncpfail == 3) {
			warning("multiple archiving failures, forcing -h", "");
			holdarch = 1;
		}

		free(new);
		free(old);
		if (ret != 0)
			return(-1);	/* without removing original */
	}
	if (unlink(artfile(name)) < 0) {
		if (errno != ENOENT) {
			warning("can't remove `%s'", name);
			return(-1);
		} else
			nmissing++;
	} else if (dir == NULL)
		njunked++;
	return(1);
}

/*
 - cp - try to copy an article (top level, administration)
 */
int				/* 0 success, other character failure */
cp(old, new)
char *old;			/* pathnames good from here */
char *new;
{
	register int ret;
	register int in, out;

	in = open(old, 0);
	if (in < 0)
		return('o');
	out = creat(new, 0666);
	if (out < 0) {
		(void) close(in);
		return('n');
	}

	ret = cploop(in, out);

	(void) close(in);
	if (fsync(out) < 0)
		ret = 'w';
	if (close(out) < 0)
		ret = 'w';

	return(ret);
}

/*
 - cploop - try to copy an article (bottom level, copy loop)
 */
int				/* 0 success, other character failure */
cploop(in, out)
int in;
int out;
{
	register int ret;
	register int count;
	register int firstblock = 1;

	while ((count = read(in, abuf, sizeof(abuf))) > 0) {
		ret = write(out, abuf, count);
		if (ret != count)
			return('w');
		if (firstblock) {
			getsubj(abuf, count);
			firstblock = 0;
		}
	}
	if (count < 0)
		return('r');

	return(0);
}

/*
 - getsubj - try to find the Subject: line in a buffer
 *
 * Result goes in "subject", and is never empty.  Tabs become spaces,
 * since they are the output delimiters.
 */
void
getsubj(buf, bsize)
char *buf;
int bsize;
{
	register char *scan;
	register char *limit;
	register int len;
	register int clipped;
	static char sline[] = "Subject:";

	len = strlen(sline);
	limit = buf + bsize - len;
	for (scan = buf; scan < limit; scan++)
		if (CISTREQN(scan, sline, len) &&
				(scan == buf || *(scan-1) == '\n')) {
			scan += len;
			for (limit = scan; limit < buf+bsize; limit++)
				if (*limit == '\n')
					break;
			while (scan < limit && isascii(*scan) && isspace(*scan))
				scan++;
			len = limit-scan;
			clipped = 0;
			if (len > sizeof(subject)-1) {
				len = sizeof(subject) - 1 - strlen("...");
				clipped = 1;
			}
			if (len > 0) {
				(void) strncpy(subject, scan, len);
				subject[len] = '\0';
			} else
				(void) strcpy(subject, "???");
			if (clipped)
				(void) strcat(subject, "...");
			for (scan = strchr(subject, '\t'); scan != NULL;
					scan = strchr(scan+1, '\t'))
				*scan = ' ';
			return;
		} else if (*scan == '\n' && scan+1 < limit && *(scan+1) == '\n')
			break;		/* empty line terminates header */

	/* didn't find one -- fill in *something* */
	(void) strcpy(subject, "???");
}

/*
 - mkparents - try to make directories for archiving an article
 *
 * Assumes it can mess with first argument if it puts it all back at the end.
 */
void
mkparents(art, dir)
char *art;			/* name relative to dir */
char *dir;
{
	register char *cmd;
	register char *ocmd;
	register char *p;

	ocmd = str3save("PATH=", ctlfile("bin"), ":");
	cmd = str3save(ocmd, binfile((char *)NULL), ":");
	free(ocmd);
	ocmd = cmd;
	/* the semicolon here avoids problems with some buggy shells */
	cmd = str3save(ocmd, newspath(), " ; mkpdir ");
	free(ocmd);
	ocmd = cmd;
	cmd = str3save(ocmd, dir, "/");
	free(ocmd);
	p = strrchr(art, '/');
	*p = '\0';
	ocmd = cmd;
	cmd = str3save(ocmd, art, "");
	free(ocmd);
	*p = '/';
	(void) system(cmd);
	free(cmd);
}

char *months[12] = {
	"Jan",
	"Feb",
	"Mar",
	"Apr",
	"May",
	"Jun",
	"Jul",
	"Aug",
	"Sep",
	"Oct",
	"Nov",
	"Dec",
};

/*
 - printstuff - print information about an expiring article
 */
void
printstuff(msgid, name, recdate)
char *msgid;
char *name;
time_t recdate;
{
	struct tm *gmt;

	gmt = gmtime(&recdate);
	printf("%s\t%s\t%d-%s-%d\t%s\n", name, msgid, gmt->tm_mday,
			months[gmt->tm_mon], gmt->tm_year+1900, subject);
}

/*
 - eufopen - fopen, with fail if doesn't succeed
 */
FILE *
eufopen(name, mode)
char *name;
char *mode;
{
	FILE *f;
	static char grump[50] = "can't open `%s' for `";

	f = fopen(name, mode);
	if (f == NULL) {
		(void) strcat(grump, mode);
		(void) strcat(grump, "'");
		fail(grump, name);
	}
	return(f);
}

/*
 - eufclose - fclose with failure checking
 */
void
eufclose(f, name)
FILE *f;
char *name;
{
	if (nfclose(f) == EOF)
		fail("error in closing file `%s'", name);
}

/*
 - checkadir - check archiving directory is real, writable, and full pathname
 */
void				/* set -h if not */
checkadir(dir)
char *dir;
{
	struct stat stbuf;
	register int hforce = 0;
#	define	GRUMP(a,b)	{warning(a, b); hforce = 1;}

	if (*dir == '=')	/* disregard leading '=' */
		dir++;
	errno = 0;
	if (stat(dir, &stbuf) < 0)
		GRUMP("archiving directory `%s' does not exist", dir);
	if (access(dir, 02) < 0)
		GRUMP("archiving directory `%s' not writable", dir);
	if (dir[0] != '/')
		GRUMP("archiving directory `%s' not a full pathname", dir);
	if (hforce) {
		warning("forcing -h option as a stopgap", "");
		holdarch = 1;
	}
}

/*
 - back - get a date n days back, with overflow check
 *
 * Requires that "now" be set first.
 */
time_t
back(ndaystr)
char *ndaystr;
{
	register double goback;		/* how far before now it is */

	if (STREQ(ndaystr, "never"))
		goback = FOREVER;	/* > now-EPOCH */
	else
		goback = atof(ndaystr) * DAY;

	if (goback > now-EPOCH)		/* before EPOCH */
		return(EPOCH);
	return((time_t)(now - goback));
}

/*
 - printlists - print control lists for debugging
 */
void
printlists()
{
	register int i;
	register struct ctl *ct;

	fprintf(stderr, "control file:\n");
	for (ct = ctls; ct != NULL; ct = ct->next)
		pctl(ct);
	fprintf(stderr, "\n");

	for (i = 0; i < NHASH; i++)
		if (ngs[i] != NULL) {
			fprintf(stderr, "list %d:\n", i);
			for (ct = ngs[i]; ct != NULL; ct = ct->next)
				pctl(ct);
		}
	fprintf(stderr, "\n");
}

/*
 - pctl - print one control-list entry
 */
void
pctl(ct)
register struct ctl *ct;
{
#	define	DAYS(x)	((now-(x))/DAY)

	fprintf(stderr, "%s(%c) %.2f-%.2f-%.2f %s\n", ct->groups, ct->ismod,
			DAYS(ct->retain), DAYS(ct->normal), DAYS(ct->purge),
			(ct->dir == NULL) ? "(null)" : ct->dir);
}

/*
 - unprivileged - no-op needed to keep the pathname stuff happy
 */
void
unprivileged(reason)
char *reason;
{
}

/*
 - fail - call errunlock, possibly after cleanup
 */
void
fail(s1, s2)
char *s1;
char *s2;
{
	int saveerr = errno;

	if (spacetight) {
		cd(histdir);
		(void) remove("history.n");
		(void) remove("history.n.dir");
		(void) remove("history.n.pag");
	}
	errno = saveerr;
	errunlock(s1, s2);
	/* NOTREACHED */
}

/*
 - die - like fail, but errno contains no information
 */
void
die(s1, s2)
char *s1;
char *s2;
{
	errno = 0;
	fail(s1, s2);
}

/*
 - readline - read history line (sans newline), with locking when we hit EOF
 *
 * Data pointed to may be altered but not extended.  Note that initialization
 * is cleverly set up so that the first time this is called, it falls through
 * to the "hard case" logic.
 *
 * Minor flaw:  will lose a last line which lacks a newline.
 */
char *				/* NULL is EOF */
readline(fd)
int fd;				/* Note descriptor, not FILE *. */
{
	register char *line;		/* line buffer */
	register size_t linesize;
	register char *linep;		/* unused part of line buffer */
	register char *endp;		/* newline */
	register size_t len;		/* length of line (fragment) */
	register int n;
	extern void refill();

	/* try for the easy case -- whole line in buffer */
	endp = strchr(rest, '\n');
	if (endp != NULL) {
		*endp++ = '\0';
		rlnleft -= endp - rest;
		line = rest;
		rest = endp;
		return(line);
	}

	/* oh well, have to put it together in malloced area... */
	line = rlline;
	linesize = rllsiz;
	if (line == NULL) {
		line = malloc(linesize);
		if (line == NULL)
			fail("out of space when reading history", "");
	}

	linep = line;
	for (;;) {
		if (rlnleft <= 0) {
			refill(fd);
			if (rlnleft <= 0)	/* refill gave up. */
				return(NULL);
		}

		endp = strchr(rest, '\n');
		if (endp == NULL)	/* hit the sentinel */
			len = rlnleft;
		else
			len = endp - rest + 1;
		while (linep + len > line + linesize) {	/* not enough room */
			linesize = (linesize * 3) / 2;
			n = linep - line;
			line = realloc(line, linesize);
			if (line == NULL)
				fail("out of memory in readline", "");
			linep = line + n;
		}

		(void) memcpy(linep, rest, len);
		linep += len;
		rest += len;
		rlnleft -= len;
		if (endp != NULL) {
			*(linep-1) = '\0';
			rlline = line;
			rllsiz = linesize;
			return(line);
		}
	}
	/* NOTREACHED */
}

/*
 - refill - refill readline's buffer, with locking on EOF
 */
void
refill(fd)
int fd;
{
	register int ret;

	/* Just in case... */
	if (rlnleft > 0)
		return;

	/* Try ordinary read. */
	ret = read(fd, rlbuf, (int)rlbufsiz);
	if (ret < 0)
		fail("read error in history", "");
	if (ret > 0) {
		rlnleft = ret;
		rest = rlbuf;
		rlbuf[ret] = '\0';	/* sentinel */
		return;
	}

	/* EOF. */
	if (nlocked)
		return;		/* We're really done. */

	/* EOF but we haven't locked yet.  Lock and try again. */
	(void) signal(SIGINT, SIG_IGN);
	(void) signal(SIGQUIT, SIG_IGN);
	(void) signal(SIGHUP, SIG_IGN);
	(void) signal(SIGTERM, SIG_IGN);
	newslock();
	nlocked = 1;
	refill(fd);
}


syntax highlighted by Code2HTML, v. 0.9.1