Tass newsreader (part 1 of 2)

Rich Skrenta skrenta at blekko.commodore.com
Fri Feb 15 06:33:07 AEST 1991


# This is a shell archive.  Remove anything before this line,
# then unpack it by saving it in a file and typing "sh file".
#
# This archive contains:
#	COPYRIGHT	Makefile	Todo		art.c		
#	curses.c	group.c		mail.c		main.c		
#	misc.c		
#

echo x - COPYRIGHT
cat >COPYRIGHT <<'@EOF'
/*
 *  Tass, a visual Usenet news reader
 *  (c) Copyright 1990 by Rich Skrenta
 *
 *  Distribution agreement:
 *
 *	You may freely copy or redistribute this software, so long
 *	as there is no profit made from its use, sale, trade or
 *	reproduction.  You may not change this copyright notice,
 *	and it must be included prominently in any copy made.
 */
@EOF

chmod 600 COPYRIGHT

echo x - Makefile
cat >Makefile <<'@EOF'

# Edit the defines in tass.h to point tass at the correct news libdir

#
# For Berkeley systems:
#
# CFLAGS= -DBSD
# LIBS= -lcurses -ltermcap

#  art.c needs to know whether readdir returns struct dirent or
#  struct direct.  You don't need to change anything if:
#	you're bsd and have BSD defined
#	you're Xenix 286
#	you're SCO Unix and have -UM_XENIX defined
#	you use struct dirent

#
# For System V and Xenix:
#
CFLAGS=-g
LIBS= -lcurses -lgen


OBJECTS	=	curses.o art.o group.o mail.o main.o misc.o page.o \
		prompt.o screen.o select.o time.o

tass: $(OBJECTS)
	cc $(CFLAGS) -o tass $(OBJECTS) $(LIBS)

shar:
	-mv -f ../tass.shar ../tass.shar-
	shar -v [A-Z]* *.[ch] > ../tass.shar


art.o:		art.c tass.h
curses.o:	curses.c
group.o:	group.c tass.h
mail.o:		mail.c
main.o:		main.c tass.h
misc.o:		misc.c tass.h
page.o:		page.c tass.h
prompt.o:	prompt.c tass.h
screen.o:	screen.c tass.h
select.o:	select.c tass.h
time.o:		time.c
@EOF

chmod 644 Makefile

echo x - Todo
cat >Todo <<'@EOF'
make initial .newsrc just be created but  contain nothing--must
subscribe

find_new_to doesnn't seem to work in mail_to_someone()
@EOF

chmod 644 Todo

echo x - art.c
cat >art.c <<'@EOF'


#include	<stdio.h>
#include	<signal.h>
#include	"tass.h"


/* Hopefully one of these is right for you. */

#ifdef BSD
#	include <sys/types.h>
#	include <sys/dir.h>
#	define		DIR_BUF		struct direct
#	define		D_LENGTH	d_namlen
#endif
#ifdef M_XENIX
#	include <sys/ndir.h>
#	define		DIR_BUF		struct direct
#	define		D_LENGTH	d_namlen
#endif
#ifndef DIR_BUF
#	include	<sys/types.h>
#	include	<dirent.h>
#	define		DIR_BUF		struct dirent
#	define		D_LENGTH	d_reclen
#endif


char index_file[LEN+1];
char *glob_art_group;


#ifdef SIGTSTP
void
art_susp(i)
int i;
{

	Raw(FALSE);
	putchar('\n');
	signal(SIGTSTP, SIG_DFL);
	kill(0, SIGTSTP);

	signal(SIGTSTP, art_susp);
	Raw(TRUE);

	mail_setup();
	ClearScreen();
	MoveCursor(LINES, 0);
	printf("Group %s...    ", glob_art_group);
	fflush(stdout);
}
#endif


/*
 *  Convert a string to a long, only look at first n characters
 */

my_atol(s, n)
char *s;
int n;
{
	long ret = 0;

	while (*s && n--) {
		if (*s >= '0' && *s <= '9')
			ret = ret * 10 + (*s - '0');
		else
			return -1;
		s++;
	}

	return ret;
}


/*
 *  Construct the pointers to the basenotes of each thread
 *  arts[] contains every article in the group.  inthread is
 *  set on each article that is after the first article in the
 *  thread.  Articles which have been expired have their thread
 *  set to -2.
 */

find_base() {
	int i;

	top_base = 0;

	for (i = 0; i < top; i++)
		if (!arts[i].inthread && arts[i].thread != -2) {
			if (top_base >= max_art)
				expand_art();
			base[top_base++] = i;
		}
}


/* 
 *  Count the number of non-expired articles in arts[]
 */

num_arts() {
	int sum = 0;

	int i;

	for (i = 0; i < top; i++)
		if (arts[i].thread != -2)
			sum++;

	return sum;
}


/*
 *  Do we have an entry for article art?
 */

valid_artnum(art)
long art;
{
	int i;

	for (i = 0; i < top; i++)
		if (arts[i].artnum == art)
			return i;

	return -1;
}


/*
 *  Return TRUE if arts[] contains any expired articles
 *  (articles we have an entry for which don't have a corresponding
 *   article file in the spool directory)
 */

purge_needed() {
	int i;

	for (i = 0; i < top; i++)
		if (arts[i].thread == -2)
			return TRUE;

	return FALSE;
}


/*
 *  Main group indexing routine.  Group should be the name of the
 *  newsgroup, i.e. "comp.unix.amiga".  group_path should be the
 *  same but with the .'s turned into /'s: "comp/unix/amiga"
 *
 *  Will read any existing index, create or incrementally update
 *  the index by looking at the articles in the spool directory,
 *  and attempt to write a new index if necessary.
 */

index_group(group, group_path)
char *group;
char *group_path;
{
	int modified;

	glob_art_group = group;

#ifdef SIGTSTP
	signal(SIGTSTP, art_susp);
#endif

	if (!update) {
		clear_message();
		MoveCursor(LINES, 0);
		printf("Group %s...    ", group);
		fflush(stdout);
	}

	if (local_index)
		find_local_index(group);
	else
		sprintf(index_file, "%s/%s/.tindex", SPOOLDIR, group_path);

	load_index();
	modified = read_group(group_path);
	make_threads();
	if (modified || purge_needed()) {
		if (local_index) {	/* writing index in home directory */
			setuid(real_uid);	/* so become them */
			setgid(real_gid);
		}
		dump_index(group);
		if (local_index) {
			setuid(tass_uid);
			setgid(tass_gid);
		}
	}
	find_base();

	if (modified && !update)
		clear_message();
}


/*
 *  Longword comparison routine for the qsort()
 */

base_comp(a, b)
long *a;
long *b;
{

	if (*a < *b)
		return -1;
	if (*a > *b)
		return 1;
	return 0;
}


/*
 *  Read the article numbers existing in a group's spool directory
 *  into base[] and sort them.  base_top is one past top.
 */

scan_dir(group)
char *group;
{
	DIR *d;
	DIR_BUF *e;
	long art;
	char buf[200];

	top_base = 0;

	sprintf(buf, "%s/%s", SPOOLDIR, group);

	d = opendir(buf);
	if (d != NULL) {
		while ((e = readdir(d)) != NULL) {
			art = my_atol(e->d_name, e->D_LENGTH);
			if (art >= 0) {
				if (top_base >= max_art)
					expand_art();
				base[top_base++] = art;
			}
		}
		closedir(d);
	}

	qsort(base, top_base, sizeof(long), base_comp);
}


/*
 *  Index a group.  Assumes any existing index has already been
 *  loaded.
 */

read_group(group)
char *group;
{
	char buf[200];
	int fd;
	long art;
	int count;
	int modified = FALSE;
	int respnum;
	int i;

	scan_dir(group);	/* load article numbers into base[] */

	count = 0;

	for (i = 0; i < top_base; i++) {	/* for each article # */
		art = base[i];

/*
 *  Do we already have this article in our index?  Change thread from
 *  -2 to -1 if so and skip the header eating.
 */

		if ((respnum = valid_artnum(art)) >= 0) {
			arts[respnum].thread = -1;
			arts[respnum].unread = 1;
			continue;
		}

		if (!modified) {
			modified = TRUE;   /* we've modified the index */
					   /* it will need to be re-written */
#if 0
			if (!update) {
				MoveCursor(LINES, 0);
				fputs("Indexing...    ", stdout);
				fflush(stdout);
			}
#endif
		}

/*
 *  Add article to arts[]
 */
		if (top >= max_art)
			expand_art();

		arts[top].artnum = art;
		arts[top].thread = -1;
		arts[top].inthread = FALSE;
		arts[top].unread = 1;

		sprintf(buf, "%s/%s/%ld", SPOOLDIR, group, art);
		fd = open(buf, 0);
		if (fd < 0) {
			fprintf(stderr, "can't open article %s: ", buf);
			perror("");
			continue;
		}

		if (!parse_headers(fd, &arts[top]))
			continue;
		top++;
		close(fd);

		if (++count % 10 == 0 && !update) {
			printf("\b\b\b\b%4d", count);
			fflush(stdout);
		}
	}

	return modified;
}


/*
 *  Go through the articles in arts[] and use .thread to snake threads
 *  through them.  Use the subject line to construct threads.  The
 *  first article in a thread should have .inthread set to FALSE, the
 *  rest TRUE.  Only do unexprired articles we haven't visited yet
 *  (arts[].thread == -1).
 */

make_threads() {
	int i;
	int j;

	for (i = 0; i < top; i++) {
		if (arts[i].thread == -1)
		    for (j = i+1; j < top; j++)
			if (arts[i].hash == arts[j].hash
			&&  arts[j].thread != -2
			&&  strncmp(arts[i].nore, arts[j].nore, 10) == 0) {
				arts[i].thread = j;
				arts[j].inthread = TRUE;
				break;
			}
	}
}


/*
 *  Return a pointer into s eliminating any leading Re:'s.  Example:
 *
 *	  Re: Reorganization of misc.jobs
 *	  ^   ^
 */

char *
eat_re(s)
char *s;
{

#if 1
	while (*s == 'r' || *s == 'R') {
		if ((*(s+1) == 'e' || *(s+1) == 'E') && *(s+2) == ':')
			s += 3;
		else
			break;
		if (*s == ' ')
			s++;
	}
#else
	while (*s == 'R') {
		if (strncmp(s, "Re: ", 4) == 0)
			s += 4;
		else if (strncmp(s, "Re:", 3) == 0)
			s += 3;
		else if (strncmp(s, "Re^2: ", 6) == 0)
			s += 6;
		else
			break;
	}
#endif

	return s;
}


/*
 *  Hash the subjects (after eating the Re's off) for a quicker
 *  thread search later.  We store the hashes for subjects in the
 *  index file for speed.
 */

long
hash_s(s)
char *s;
{
	long h = 0;

	while (*s)
		h = h * 64 + *s++;

	return h;
}


parse_headers(fd, h)
int fd;
struct header *h;
{
	char buf[1024];
	char *p, *q;
	char flag;

	if (read(fd, buf, 1024) <= 0)
		return FALSE;

	buf[1023] = '\0';

	p = buf;

	h->from[0] = '\0';
	h->subject[0] = '\0';
	h->nore = h->subject;
	h->hash = 0;

	while (1) {
		q = p;
		while (*p && *p != '\n') {
			if (*p & 0x7F < 32)
				*p = ' ';
			p++;
		}
		flag = *p;
		*p++ = '\0';

		if (strncmp(q, "From: ", 6) == 0) {
			strncpy(h->from, &q[6], MAX_FROM);
			h->from[MAX_FROM-1] = '\0';
		} else if (strncmp(q, "Subject: ", 9) == 0) {
			h->hash = hash_s(eat_re(&q[9]));
			strncpy(h->subject, &q[9], MAX_SUBJ);
			h->subject[MAX_SUBJ-1] = '\0';
			h->nore = eat_re(h->subject);
		}

		if (!flag)
			break;
	}

	return TRUE;
}


/* 
 *  Write out a .tindex file.  Write the group name first so if
 *  local indexing is done we can disambiguate between group name
 *  hash collisions by looking at the index file.
 */

dump_index(group)
char *group;
{
	int i;
	char buf[200];
	FILE *fp;
	char *p, *q;
	long l;

	fp = fopen(index_file, "w");
	if (fp == NULL)
		return;

	fprintf(fp, "%s\n", group);
	fprintf(fp, "%d\n", num_arts());
	for (i = 0; i < top; i++)
	    if (arts[i].thread != -2) {
		p = arts[i].nore;
		q = arts[i].subject;
		l = p - q;
		fprintf(fp, "%ld\n%s\n%s\n%ld\n%ld\n",
				arts[i].artnum,
				arts[i].subject,
				arts[i].from,
				arts[i].hash,
#if 0
				(long) arts[i].nore - (long) arts[i].subject);
#else
				l);
#endif
	}

	fclose(fp);
	chmod(index_file, 0644);
}


/*
 *  strncpy that stops at a newline and null terminates
 */

my_strncpy(p, q, n)
char *p;
char *q;
int n;
{

	while (n--) {
		if (!*q || *q == '\n')
			break;
		*p++ = *q++;
	}
	*p = '\0';
}


/*
 *  Read in a .tindex file.
 */

load_index()
{
	int i;
	long j;
	char buf[200];
	FILE *fp;
	int first = TRUE;

	top = 0;

	fp = fopen(index_file, "r");
	if (fp == NULL)
		return;

	if (fgets(buf, 200, fp) == NULL
	||  fgets(buf, 200, fp) == NULL) {
		fprintf(stderr, "one\n");
		goto corrupt_index;
	}

	i = atol(buf);
	while (top < i) {
		if (top >= max_art)
			expand_art();

		arts[top].thread = -2;
		arts[top].inthread = FALSE;

		if (fgets(buf, 200, fp) == NULL) {
			fprintf(stderr, "two\n");
			goto corrupt_index;
		}
		arts[top].artnum = atol(buf);

		if (fgets(buf, 200, fp) == NULL) {
			fprintf(stderr, "three\n");
			goto corrupt_index;
		}

		my_strncpy(arts[top].subject, buf, MAX_SUBJ-1);
			
		if (fgets(buf, 200, fp) == NULL) {
			fprintf(stderr, "four\n");
			goto corrupt_index;
		}
		my_strncpy(arts[top].from, buf, MAX_FROM-1);

		if (fgets(buf, 200, fp) == NULL) {
			fprintf(stderr, "five\n");
			goto corrupt_index;
		}
		arts[top].hash = atol(buf);

		if (fgets(buf, 200, fp) == NULL) {
			fprintf(stderr, "six\n");
			goto corrupt_index;
		}

		j = atol(buf);
#if 0
		if (j < 0 || j > 100) {
#if 0
			goto corrupt_index;
#else
			arts[top].nore = eat_re(arts[top].subject);
#endif
		} else
			arts[top].nore = arts[top].subject + j;
#else
		arts[top].nore = eat_re(arts[top].subject);
#endif

		top++;
	}

	fclose(fp);
	return;

corrupt_index:
	fprintf(stderr, "index file %s corrupt\n", index_file);
	fprintf(stderr, "top = %d\n", top);
	exit(1);
	unlink(index_file);
	top = 0;
}


/*
 *  Look in the local $HOME/.tindex (or wherever) directory for the
 *  index file for the given group.  Hashing the group name gets
 *  a number.  See if that #.1 file exists; if so, read first line.
 *  Group we want?  If no, try #.2.  Repeat until no such file or
 *  we find an existing file that matches our group.
 */

find_local_index(group)
char *group;
{
	unsigned long h;
	static char buf[200];
	int i;
	char *p;
	FILE *fp;

	{
		char *t = group;

		h = *t++;
		while (*t)
			h = h * 64 + *t++;
	}

	i = 1;
	while (1) {
		sprintf(index_file, "%s/%lu.%d", indexdir, h, i);
		fp = fopen(index_file, "r");
		if (fp == NULL)
			return;

		if (fgets(buf, 200, fp) == NULL) {
			fclose(fp);
			return;
		}
		fclose(fp);

		for (p = buf; *p && *p != '\n'; p++) ;
		*p = '\0';

		if (strcmp(buf, group) == 0)
			return;

		i++;
	}
}


/*
 *  Run the index file updater only for the groups we've loaded.
 */

do_update() {
	int i;
	char group_path[200];
	char *p;

	for (i = 0; i < local_top; i++) {
		strcpy(group_path, active[my_group[i]].name);
		for (p = group_path; *p; p++)
			if (*p == '.')
				*p = '/';

		index_group(active[my_group[i]].name, group_path);
	}
}

@EOF

chmod 644 art.c

echo x - curses.c
cat >curses.c <<'@EOF'

/*
 *  This is a screen management library borrowed with permission from the
 *  Elm mail system (a great mailer--I highly recommend it!).
 *
 *  I've hacked this library to only provide what Tass needs.
 *
 *  Original copyright follows:
 */

/*******************************************************************************
 *  The Elm Mail System  -  $Revision: 2.1 $   $State: Exp $
 *
 * 			Copyright (c) 1986 Dave Taylor
 ******************************************************************************/

#include <stdio.h>
#include <curses.h>

#define		TRUE		1
#define		FALSE		0

#define		BACKSPACE	'\b'
#define		VERY_LONG_STRING	2500

int LINES=23;
int COLS=80;

int inverse_okay = TRUE;

/*
#ifdef BSD
#  ifndef BSD4_1
#    include <sgtty.h>
#  else
#    include <termio.h>
#  endif
# else
#  include <termio.h>
#endif
*/

#include <ctype.h>

/*
#ifdef BSD
#undef tolower
#endif
*/

#define TTYIN	0

#ifdef SHORTNAMES
# define _clearinverse	_clrinv
# define _cleartoeoln	_clrtoeoln
# define _cleartoeos	_clr2eos
#endif

#ifndef BSD
struct termio _raw_tty, 
              _original_tty;
#else
#define TCGETA	TIOCGETP
#define TCSETAW	TIOCSETP

struct sgttyb _raw_tty,
	      _original_tty;
#endif

static int _inraw = 0;                  /* are we IN rawmode?    */

#define DEFAULT_LINES_ON_TERMINAL	24
#define DEFAULT_COLUMNS_ON_TERMINAL	80

static int _memory_locked = 0;		/* are we IN memlock??   */

static int _intransmit;			/* are we transmitting keys? */

static
char *_clearscreen, *_moveto, *_cleartoeoln, *_cleartoeos,
	*_setinverse, *_clearinverse, *_setunderline, *_clearunderline;

static
int _lines,_columns;

static char _terminal[1024];              /* Storage for terminal entry */
static char _capabilities[1024];           /* String for cursor motion */

static char *ptr = _capabilities;	/* for buffering         */

int    outchar();			/* char output for tputs */
char  *tgetstr(),     		       /* Get termcap capability */
      *tgoto();				/* and the goto stuff    */

InitScreen()
{
int  tgetent(),      /* get termcap entry */
     err;
char termname[40];
char *strcpy(), *getenv();
	
	if (getenv("TERM") == NULL) {
		fprintf(stderr,
		  "TERM variable not set; Tass requires screen capabilities\n");
		return(FALSE);
	}
	if (strcpy(termname, getenv("TERM")) == NULL) {
		fprintf(stderr,"Can't get TERM variable\n");
		return(FALSE);
	}
	if ((err = tgetent(_terminal, termname)) != 1) {
		fprintf(stderr,"Can't get entry for TERM\n");
		return(FALSE);
	}

	/* load in all those pesky values */
	_clearscreen       = tgetstr("cl", &ptr);
	_moveto            = tgetstr("cm", &ptr);
	_cleartoeoln       = tgetstr("ce", &ptr);
	_cleartoeos        = tgetstr("cd", &ptr);
	_lines	      	   = tgetnum("li");
	_columns	   = tgetnum("co");
	_setinverse        = tgetstr("so", &ptr);
	_clearinverse      = tgetstr("se", &ptr);
	_setunderline      = tgetstr("us", &ptr);
	_clearunderline    = tgetstr("ue", &ptr);

	if (!_clearscreen) {
		fprintf(stderr,
			"Terminal must have clearscreen (cl) capability\n");
		return(FALSE);
	}
	if (!_moveto) {
		fprintf(stderr,
			"Terminal must have cursor motion (cm)\n");
		return(FALSE);
	}
	if (!_cleartoeoln) {
		fprintf(stderr,
			"Terminal must have clear to end-of-line (ce)\n");
		return(FALSE);
	}
	if (!_cleartoeos) {
		fprintf(stderr,
			"Terminal must have clear to end-of-screen (cd)\n");
		return(FALSE);
	}
	if (_lines == -1)
		_lines = DEFAULT_LINES_ON_TERMINAL;
	if (_columns == -1)
		_columns = DEFAULT_COLUMNS_ON_TERMINAL;
	return(TRUE);
}

ScreenSize(lines, columns)
int *lines, *columns;
{
	/** returns the number of lines and columns on the display. **/

	if (_lines == 0) _lines = DEFAULT_LINES_ON_TERMINAL;
	if (_columns == 0) _columns = DEFAULT_COLUMNS_ON_TERMINAL;

	*lines = _lines - 1;		/* assume index from zero*/
	*columns = _columns;		/* assume index from one */
}

ClearScreen()
{
	/* clear the screen: returns -1 if not capable */

	tputs(_clearscreen, 1, outchar);
	fflush(stdout);      /* clear the output buffer */
}

MoveCursor(row, col)
int row, col;
{
	/** move cursor to the specified row column on the screen.
            0,0 is the top left! **/

	char *stuff, *tgoto();

	stuff = tgoto(_moveto, col, row);
	tputs(stuff, 1, outchar);
	fflush(stdout);
}

CleartoEOLN()
{
	/** clear to end of line **/

	tputs(_cleartoeoln, 1, outchar);
	fflush(stdout);  /* clear the output buffer */
}

CleartoEOS()
{
	/** clear to end of screen **/

	tputs(_cleartoeos, 1, outchar);
	fflush(stdout);  /* clear the output buffer */
}

StartInverse()
{
	/** set inverse video mode **/

	if (_setinverse && inverse_okay)
		tputs(_setinverse, 1, outchar);
/*	fflush(stdout);	*/
}


EndInverse()
{
	/** compliment of startinverse **/

	if (_clearinverse && inverse_okay)
		tputs(_clearinverse, 1, outchar);
/*	fflush(stdout);	*/
}

#if 0
StartUnderline()
{
	/** start underline mode **/

	if (!_setunderline)
		return(-1);

	tputs(_setunderline, 1, outchar);
	fflush(stdout);
	return(0);
}


EndUnderline()
{
	/** the compliment of start underline mode **/

	if (!_clearunderline)
		return(-1);

	tputs(_clearunderline, 1, outchar);
	fflush(stdout);
	return(0);
}
#endif

RawState()
{
	/** returns either 1 or 0, for ON or OFF **/

	return( _inraw );
}

Raw(state)
int state;
{
	/** state is either TRUE or FALSE, as indicated by call **/

	if (state == FALSE && _inraw) {
	  (void) ioctl(TTYIN, TCSETAW, &_original_tty);
	  _inraw = 0;
	}
	else if (state == TRUE && ! _inraw) {

	  (void) ioctl(TTYIN, TCGETA, &_original_tty);	/** current setting **/

	  (void) ioctl(TTYIN, TCGETA, &_raw_tty);    /** again! **/
#ifdef BSD
	  _raw_tty.sg_flags &= ~(ECHO | CRMOD);	/* echo off */
	  _raw_tty.sg_flags |= CBREAK;	/* raw on    */
#else
	  _raw_tty.c_lflag &= ~(ICANON | ECHO);	/* noecho raw mode        */

	  _raw_tty.c_cc[VMIN] = '\01';	/* minimum # of chars to queue    */
	  _raw_tty.c_cc[VTIME] = '\0';	/* minimum time to wait for input */

#endif
	  (void) ioctl(TTYIN, TCSETAW, &_raw_tty);

	  _inraw = 1;
	}
}

int
ReadCh()
{
	/** read a character with Raw mode set! **/

	register int result;
	char ch;
	result = read(0, &ch, 1);
        return((result <= 0 ) ? EOF : ch & 0x7F);
}


outchar(c)
char c;
{
	/** output the given character.  From tputs... **/
	/** Note: this CANNOT be a macro!              **/

	putc(c, stdout);
}

@EOF

chmod 644 curses.c

echo x - group.c
cat >group.c <<'@EOF'


#include	<stdio.h>
#include	<signal.h>
#include	"tass.h"


int index_point;
int first_subj_on_screen;
int last_subj_on_screen;
char subject_search_string[LEN+1];	/* last search pattern */
extern int cur_groupnum;
extern int last_resp;		/* page.c */
extern int this_resp;		/* page.c */
extern int space_mode;		/* select.c */
extern char *cvers;

char *glob_group;


#ifdef SIGTSTP
void
group_susp(i)
int i;
{

	Raw(FALSE);
	putchar('\n');
	signal(SIGTSTP, SIG_DFL);
	kill(0, SIGTSTP);

	signal(SIGTSTP, group_susp);
	Raw(TRUE);
	mail_setup();
	show_group_page(glob_group);
}
#endif


group_page(group)
char *group;
{
	char ch;
	int i, n;
	char group_path[200];
	char *p;
	char buf[200];

	glob_group = group;

	strcpy(group_path, group);		/* turn comp.unix.amiga into */
	for (p = group_path; *p; p++)		/* comp/unix/amiga */
		if (*p == '.')
			*p = '/';

	last_resp = -1;
	this_resp = -1;
	index_group(group, group_path);		/* update index file */
	read_newsrc_line(group);		/* get sequencer information */

	if (space_mode) {
		for (i = 0; i < top_base; i++)
			if (new_responses(i))
				break;
		if (i < top_base)
			index_point = i;
		else
			index_point = top_base - 1;
	} else
		index_point = top_base - 1;

	show_group_page(group);

	while (1) {
		ch = ReadCh();

		if (ch > '0' && ch <= '9') {	/* 0 goes to basenote */
			prompt_subject_num(ch, group);
		} else switch (ch) {
			case 'I':	/* toggle inverse video */
				inverse_okay = !inverse_okay;
				if (inverse_okay)
					info_message("Inverse video enabled");
				else
					info_message("Inverse video disabled");
				break;

			case 's':	/* subscribe to this group */
				subscribe(group, ':', my_group[cur_groupnum],
									TRUE);
				sprintf(buf, "subscribed to %s", group);
				info_message(buf);
				break;

			case 'u':	/* unsubscribe to this group */
				subscribe(group, '!', my_group[cur_groupnum],
									TRUE);
				sprintf(buf, "unsubscribed to %s", group);
				info_message(buf);
				break;

			case 'g':	/* choose a new group by name */
				n = choose_new_group();
				if (n >= 0 && n != cur_groupnum) {
					fix_new_highest(cur_groupnum);
					cur_groupnum = n;
					index_point = -3;
					goto group_done;
				}
				break;

			case 'c':	/* catchup--mark all articles as read */
			    if (prompt_yn("Mark everything as read? (y/n): ")) {
				for (n = 0; n < top; n++)
					arts[n].unread = 0;
				show_group_page(group);
				info_message("All articles marked as read");
			    }
			    break;

			case 27:	/* common arrow keys */
				ch = ReadCh();
				if (ch == '[' || ch == 'O')
					ch = ReadCh();
				switch (ch) {
				case 'A':
				case 'D':
				case 'i':
					goto group_up;

				case 'B':
				case 'I':
				case 'C':
					goto group_down;
				}
				break;

			case 'n':	/* next group */
				clear_message();
				if (cur_groupnum + 1 >= local_top)
					info_message("No more groups");
				else {
					fix_new_highest(cur_groupnum);
					cur_groupnum++;
					index_point = -3;
					space_mode = FALSE;
					goto group_done;
				}
				break;

			case 'p':	/* previous group */
				clear_message();
				if (cur_groupnum <= 0)
					info_message("No previous group");
				else {
					fix_new_highest(cur_groupnum);
					cur_groupnum--;
					index_point = -3;
					space_mode = FALSE;
					goto group_done;
				}
				break;

			case ' ':
				if (top_base <= 0)
					info_message("*** No Articles ***");
				else if (last_subj_on_screen == top_base)
					info_message("*** End of Articles ***");
				else
					clear_message();
				break;

			case '\t':
				fix_new_highest(cur_groupnum);
				space_mode = TRUE;

				if (index_point < 0
				|| (n=next_unread((int) base[index_point]))<0) {
					for (i = cur_groupnum+1;
							i < local_top; i++)
						if (unread[i] > 0)
							break;
					if (i >= local_top)
						goto group_done;

					cur_groupnum = i;
					index_point = -3;
					goto group_done;
				}
				index_point = show_page(n, group, group_path);
				if (index_point < 0)
					goto group_done;
				show_group_page(group);
				break;
	

			case 'N':	/* go to next unread article */
				if (index_point < 0) {
					info_message("No next unread article");
					break;
				}

				n = next_unread( (int) base[index_point]);
				if (n == -1)
					info_message("No next unread article");
				else {
					index_point =
						show_page(n, group, group_path);
					if (index_point < 0) {
						fix_new_highest(cur_groupnum);
						space_mode = FALSE;
						goto group_done;
					}
					show_group_page(group);
				}
				break;

			case 'P':	/* go to previous unread article */
				if (index_point < 0) {
				    info_message("No previous unread article");
				    break;
				}

				n = prev_response( (int) base[index_point]);
				n = prev_unread(n);
				if (n == -1)
				    info_message("No previous unread article");
				else {
					index_point =
						show_page(n, group, group_path);
					if (index_point < 0) {
						fix_new_highest(cur_groupnum);
						space_mode = FALSE;
						goto group_done;
					}
					show_group_page(group);
				}
				break;

			case 'w':	/* post a basenote */
				post_base(group);
				update_newsrc(group, my_group[cur_groupnum]);
				index_group(group, group_path);
				read_newsrc_line(group);
				index_point = top_base - 1;
				show_group_page(group);
				break;

			case 't':	/* return to group selection page */
				fix_new_highest(cur_groupnum);
				goto group_done;

			case '\r':
			case '\n':	/* read current basenote */
				if (index_point < 0) {
					info_message("*** No Articles ***");
					break;
				}
				index_point = show_page((int) base[index_point],
							group, group_path);
				if (index_point < 0) {
					fix_new_highest(cur_groupnum);
					space_mode = FALSE;
					goto group_done;
				}
				show_group_page(group);
				break;

			case ctrl('D'):		/* page down */
				if (!top_base || index_point == top_base - 1)
					break;

				erase_subject_arrow();
				index_point += NOTESLINES / 2;
				if (index_point >= top_base)
					index_point = top_base - 1;

				if (index_point < first_subj_on_screen
				|| index_point >= last_subj_on_screen)
					show_group_page(group);
				else
					draw_subject_arrow();
				break;

			case '-':	/* go to last viewed article */
				if (this_resp < 0) {
					info_message("No last message");
					break;
				}
				index_point = show_page(this_resp,
							group, group_path);
				if (index_point < 0) {
					fix_new_highest(cur_groupnum);
					space_mode = FALSE;
					goto group_done;
				}
				show_group_page(group);
				break;

			case ctrl('U'):		/* page up */
				if (!top_base)
					break;

				erase_subject_arrow();
				index_point -= NOTESLINES / 2;
				if (index_point < 0)
					index_point = 0;
				if (index_point < first_subj_on_screen
				|| index_point >= last_subj_on_screen)
					show_group_page(group);
				else
					draw_subject_arrow();
				break;

			case 'v':
				info_message(cvers);
				break;

			case '!':
				shell_escape();
				show_group_page(group);
				break;

			case ctrl('N'):
			case 'j':		/* line down */
group_down:
				if (!top_base || index_point + 1 >= top_base)
					break;

				if (index_point + 1 >= last_subj_on_screen) {
					index_point++;
					show_group_page(group);
				} else {
					erase_subject_arrow();
					index_point++;
					draw_subject_arrow();
				}
				break;

			case ctrl('P'):
			case 'k':		/* line up */
group_up:
				if (!top_base || !index_point)
					break;

				if (index_point <= first_subj_on_screen) {
					index_point--;
					show_group_page(group);
				} else {
					erase_subject_arrow();
					index_point--;
					draw_subject_arrow();
				}
				break;

			case ctrl('R'):
			case ctrl('L'):
			case ctrl('W'):
			case 'i':		/* return to index */
					show_group_page(group);
					break;

			case '/':		/* forward search */
					search_subject(TRUE, group);
					break;

			case '?':		/* backward search */
					search_subject(FALSE, group);
					break;

			case 'q':		/* quit */
					index_point = -2;
					fix_new_highest(cur_groupnum);
					space_mode = FALSE;
					goto group_done;

			case 'h':
				tass_group_help();
				show_group_page(group);
				break;

			default:
			    info_message("Bad command.  Type 'h' for help.");
		}
	}

group_done:
	update_newsrc(group, my_group[cur_groupnum]);

	if (index_point == -2)
		tass_done(0);
}


/*
 *  Correct highest[] for the group selection page display since
 *  new articles may have been read or marked unread
 */

fix_new_highest(groupnum)
int groupnum;
{
	int i;
	int sum = 0;

	for (i = 0; i < top; i++)
		if (arts[i].unread)
			sum++;

	unread[groupnum] = sum;
}


show_group_page(group)
char *group;
{
	int i;
	int n;
	char resps[10];
	char new_resps;
	int respnum;

#ifdef SIGTSTP
	signal(SIGTSTP, group_susp);
#endif

	ClearScreen();
	printf("%s\r\n", nice_time());	/* time in upper left */
	center_line(1, group);

	if (mail_check()) {			/* you have mail message in */
		MoveCursor(0, 66);		/* upper right */
		printf("you have mail\n");
	}

	MoveCursor(INDEX_TOP, 0);

	first_subj_on_screen = (index_point / NOTESLINES) * NOTESLINES;
	if (first_subj_on_screen < 0)
		first_subj_on_screen = 0;

	last_subj_on_screen = first_subj_on_screen + NOTESLINES;
	if (last_subj_on_screen >= top_base) {
		last_subj_on_screen = top_base;
		first_subj_on_screen = top_base - NOTESLINES;

		if (first_subj_on_screen < 0)
			first_subj_on_screen = 0;
	}

	for (i = first_subj_on_screen; i < last_subj_on_screen; i++) {
		if (new_responses(i))
			new_resps = '+';
		else
			new_resps = ' ';

		n = nresp(i);
		if (n)
			sprintf(resps, "%4d", n);
		else
			strcpy(resps, "    ");

		respnum = base[i];

		printf("  %4d  %-*s %s %-*s %c\r\n",
				i + 1,
				MAX_SUBJ,
				arts[respnum].subject,
				resps,
				MAX_FROM,
				arts[respnum].from,
				new_resps);
	}

	if (top_base <= 0)
		info_message("*** No Articles ***");
	else if (last_subj_on_screen == top_base)
		info_message("*** End of Articles ***");

	if (top_base > 0)
		draw_subject_arrow();
}

draw_subject_arrow() {

	draw_arrow(INDEX_TOP + (index_point-first_subj_on_screen) );
}

erase_subject_arrow() {

	erase_arrow(INDEX_TOP + (index_point-first_subj_on_screen) );
}


prompt_subject_num(ch, group)
char ch;
char *group;
{
int num;


	clear_message();

	if ((num = parse_num(ch, "Read article> ")) == -1) {
		clear_message();
		return FALSE;
	}
	num--;		/* index from 0 (internal) vs. 1 (user) */

	if (num >= top_base)
		num = top_base - 1;

	if (num >= first_subj_on_screen
	&&  num < last_subj_on_screen) {
		erase_subject_arrow();
		index_point = num;
		draw_subject_arrow();
	} else {
		index_point = num;
		show_group_page(group);
	}
}


search_subject(forward, group)
int forward;
char *group;
{
	char buf[LEN+1];
	int i;
	extern char *regcmp();
	extern char *regex();
	char *re;
	char *prompt;

	clear_message();

	if (forward)
		prompt = "/";
	else
		prompt = "?";

	if (!parse_string(prompt, buf))
		return;

	if (strlen(buf))
		strcpy(subject_search_string, buf);
	else if (!strlen(subject_search_string)) {
		info_message("No search pattern");
		return;
	}

	i = index_point;

	glob_name(subject_search_string, buf);

	if ((re = regcmp(buf, NULL)) == NULL) {
		info_message("Bad search pattern");
		return;
	}

	do {
		if (forward)
			i++;
		else
			i--;

		if (i >= top_base)
			i = 0;
		if (i < 0)
			i = top_base - 1;

		if (regex(re, arts[ base[i] ].subject) != NULL) {
			if (i >= first_subj_on_screen
			&&  i < last_subj_on_screen) {
				erase_subject_arrow();
				index_point = i;
				draw_subject_arrow();
			} else {
				index_point = i;
				show_group_page(group);
			}
			return;
		}
	} while (i != index_point);

	info_message("No match");
}


/*
 *  Post an original article (not a followup)
 */

post_base(group)
char *group;
{
	FILE *fp;
	char nam[100];
	char ch;
	char subj[LEN+1];
	char buf[200];

	if (!parse_string("Subject: ", subj))
		return;
	if (subj[0] == '\0')
		return;

	setuid(real_uid);
	setgid(real_gid);

	sprintf(nam, "%s/.article", homedir);
	if ((fp = fopen(nam, "w")) == NULL) {
		fprintf(stderr, "can't open %s: ", nam);
		perror("");
		return(FALSE);
	}
	chmod(nam, 0600);

	fprintf(fp, "Subject: %s\n", subj);
	fprintf(fp, "Newsgroups: %s\n", group);
	fprintf(fp, "Distribution: \n");
	if (*my_org)
		fprintf(fp, "Organization: %s\n", my_org);
	fprintf(fp, "\n");

	fclose(fp);

	ch = 'e';
	while (1) {
		switch (ch) {
		case 'e':
			invoke_editor(nam);
			break;

		case 'a':
			return FALSE;

		case 'p':
			printf("\nPosting...  ");
			fflush(stdout);
			sprintf(buf, "%s/inews -h < %s", LIBDIR, nam);
			if (invoke_cmd(buf)) {
				printf("article posted\n");
				fflush(stdout);
				goto post_base_done;
			} else {
				printf("article rejected\n");
				fflush(stdout);
				break;
			}
		}

		do {
			MoveCursor(LINES, 0);
			fputs("abort, edit, post: ", stdout);
			fflush(stdout);
			ch = ReadCh();
		} while (ch != 'a' && ch != 'e' && ch != 'p');
	}

post_base_done:
	setuid(tass_uid);
	setgid(tass_gid);

	continue_prompt();

	return(TRUE);
}


/*
 *  Return the number of unread articles there are within a thread
 */

new_responses(thread)
int thread;
{
	int i;
	int sum = 0;

	for (i = base[thread]; i >= 0; i = arts[i].thread)
		if (arts[i].unread)
			sum++;

	return sum;
}


tass_group_help() {
	char ch;

group_help_start:

	ClearScreen();
	center_line(0, TASS_HEADER);
	center_line(1, "Index Page Commands (page 1 of 2)");

	MoveCursor(3, 0);

	printf("4        Select article 4\r\n");
	printf("^D       Page down\r\n");
	printf("^U       Page up\r\n");
	printf("<CR>     Read current article\r\n");
	printf("<TAB>    View next unread article or group\r\n");
	printf("c        Mark all articles as read\r\n");
	printf("g        Choose a new group by name\r\n");
	printf("j        Down a line\r\n");
	printf("k        Up a line\r\n");
	printf("n        Go to next group\n");
	printf("N        Go to next unread article\n");
	printf("p        Go to previous group\n");
	printf("P        Go to previous unread article\n");
	printf("q        Quit\r\n");

	center_line(LINES, "-- hit space for more commands --");
	ch = ReadCh();
	if (ch != ' ')
		return;

	ClearScreen();
	center_line(0, TASS_HEADER);
	center_line(1, "Index Page Commands (page 2 of 2)");

	MoveCursor(3, 0);

	printf("s        Subscribe to this group\r\n");
	printf("t        Return to group selection index\r\n");
	printf("u        Unsubscribe to this group\r\n");
	printf("w        Post an article\r\n");
	printf("/        Search forward for subject\r\n");
	printf("?        Search backward for subject\r\n");
	printf("-        Show last message\r\n");

	center_line(LINES, "-- hit any key --");
	ch = ReadCh();
	if (ch == 'b')
		goto group_help_start;
}

@EOF

chmod 644 group.c

echo x - mail.c
cat >mail.c <<'@EOF'

#include	<stdio.h>
#include	<sys/types.h>
#include	<sys/stat.h>

#define		TRUE		1
#define		FALSE		0


char *mailbox_name = NULL;
off_t mailbox_size;


/*
 *  Record size of mailbox so we can detect if new mail has arrived
 */

mail_setup() {
	struct stat buf;
	extern char *getenv();

	if (mailbox_name == NULL)
		mailbox_name = getenv("MAIL");

	if (mailbox_name == NULL)
		mailbox_size = 0;
	else {
		if (stat(mailbox_name, &buf) >= 0)
			mailbox_size = buf.st_size;
		else
			mailbox_size = 0;
	}
}


/*
 *  Return TRUE if new mail has arrived
 */

mail_check() {
	struct stat buf;

	if (mailbox_name != NULL
	&&  stat(mailbox_name, &buf) >= 0
	&&  mailbox_size < buf.st_size)
		return TRUE;

	return FALSE;
}

@EOF

chmod 640 mail.c

echo x - main.c
cat >main.c <<'@EOF'

/*
 *  Tass, a visual Usenet news reader
 *  (c) Copyright 1990 by Rich Skrenta
 *
 *  Distribution agreement:
 *
 *	You may freely copy or redistribute this software, so long
 *	as there is no profit made from its use, sale, trade or
 *	reproduction.  You may not change this copyright notice,
 *	and it must be included prominently in any copy made.
 */

#include	<stdio.h>
#include	<signal.h>
#include	"tass.h"


int LINES, COLS;

int max_active;
struct group_ent *active;		/* active file */
int group_hash[TABLE_SIZE];		/* group name --> active[] */
int *my_group;				/* .newsrc --> active[] */
int *unread;				/* highest art read in group */
int num_active;				/* one past top of active */
int local_top;				/* one past top of my_group */
int update = FALSE;			/* update index files only mode */

struct header *arts;
long *base;
int max_art;
int top = 0;
int top_base;

int tass_uid;
int tass_gid;
int real_uid;
int real_gid;

int local_index;			/* do private indexing? */

char *cvers = "Tass 3.0  (c) Copyright 1991 by Rich Skrenta.  All rights reserved";


#ifdef SIGTSTP
void
main_susp(i)
int i;
{

	Raw(FALSE);
	putchar('\n');
	signal(SIGTSTP, SIG_DFL);
	kill(0, SIGTSTP);

	signal(SIGTSTP, main_susp);
	mail_setup();
	Raw(TRUE);
}
#endif


main(argc, argv)
int argc;
char **argv;
{
	extern int optind, opterr;
	extern char *optarg;
	int errflag = 0;
	int i;
	int c;

	for (i = 0; i < TABLE_SIZE; i++)
		group_hash[i] = -1;

	signal(SIGPIPE, SIG_IGN);
#ifdef SIGTSTP
	signal(SIGTSTP, main_susp);
#endif

	tass_uid = geteuid();
	tass_gid = getegid();
	real_uid = getuid();
	real_gid = getgid();

	init_selfinfo();	/* set up char *'s: homedir, newsrc, etc. */
	init_alloc();		/* allocate initial array sizes */

	if (tass_uid == real_uid) {	/* run out of someone's account */
		local_index = TRUE;	/* index in their home directory */
		mkdir(indexdir, 0755);
	} else			/* we're setuid, index in /usr/spool/news */
		local_index = FALSE;

	read_active();		/* load the active file into active[] */

	while ((c = getopt(argc, argv, "f:u")) != -1) {
		switch(c) {
		case 'f':
			strcpy(newsrc, optarg);
			break;

		case 'u':
			update = TRUE;
			break;

		case '?':
		default:
			errflag++;
		}
	}

	if (errflag) {
	    fprintf(stderr, "usage: tass [options] [newsgroups]\n");
	    fprintf(stderr, "   -f file   use file instead of $HOME/.newsrc\n");
	    fprintf(stderr, "   -u        update index files only\n");
	    exit(1);
	}

	if (!update)
		printf("Tass 3.0\n");

	if (optind < argc) {
		while (optind < argc) {
			if (add_group(argv[optind], TRUE) < 0)
				fprintf(stderr,
					"group %s not found in active file\n",
								argv[optind]);
			optind++;
		}
	} else
		read_newsrc(TRUE);

	if (update) {			/* index file updater only */
		do_update();
		exit(0);
	}

	if (InitScreen() == FALSE) {
		fprintf(stderr,"Screen initialization failed\n");
		exit(1);
	}

	ScreenSize(&LINES, &COLS);
	Raw(TRUE);

	mail_setup();		/* record mailbox size for "you have mail" */
	selection_index();

	tass_done(0);
}



tass_done(ret)
int ret;
{

	MoveCursor(LINES, 0);
	printf("\r\n");
	Raw(FALSE);
	exit(ret);
}


/*
 *  Dynamic table management
 *  These settings are memory conservative:  small initial allocations
 *  and a 50% expansion on table overflow.  A fast vm system with
 *  much memory might want to start with higher initial allocations
 *  and a 100% expansion on overflow, especially for the arts[] array.
 */

init_alloc() {

	max_active = 100;	/* initial alloc */

	active = (struct group_ent *) my_malloc(sizeof(*active) * max_active);
	my_group = (int *) my_malloc(sizeof(int) * max_active);
	unread = (int *) my_malloc(sizeof(int) * max_active);

	max_art = 300;		/* initial alloc */

	arts = (struct header *) my_malloc(sizeof(*arts) * max_art);
	base = (long *) my_malloc(sizeof(long) * max_art);
}


expand_art() {

	max_art += max_art / 2;		/* increase by 50% */

	arts = (struct header *) my_realloc(arts, sizeof(*arts) * max_art);
	base = (long *) my_realloc(base, sizeof(long) * max_art);
}


expand_active() {

	max_active += max_active / 2;		/* increase by 50% */

	active = (struct group_ent *) my_realloc(active,
						sizeof(*active) * max_active);
	my_group = (int *) my_realloc(my_group, sizeof(int) * max_active);
	unread = (int *) my_realloc(unread, sizeof(int) * max_active);
}


/*
 *  Load the active file into active[]
 */

read_active()
{
	FILE *fp;
	char *p, *q;
	char buf[200];
	long h;
	int i;

	num_active = 0;

	if ((fp = fopen(active_file, "r")) == NULL) {
		fprintf(stderr, "can't open %s: ", active_file);
		perror("");
		exit(1);
	}

	while (fgets(buf, 200, fp) != NULL) {
		for (p = buf; *p && *p != ' '; p++) ;
		if (*p != ' ') {
			fprintf(stderr, "active file corrupt\n");
			continue;
		}
		*p++ = '\0';

		if (num_active >= max_active)
			expand_active();

		{		/* hash group name for fast lookup later */
			char *t = buf;

			h = *t++;
			while (*t)
				h = (h * 64 + *t++) % TABLE_SIZE;
		}

		if (group_hash[h] == -1)
			group_hash[h] = num_active;
		else {				/* hash linked list chaining */
			for (i = group_hash[h]; active[i].next >= 0;
						i = active[i].next) {
				if (strcmp(active[i].name, buf) == 0)
					goto read_active_continue;
							/* kill dups */
			}
			if (strcmp(active[i].name, buf) == 0)
				goto read_active_continue;
			active[i].next = num_active;
		}

		for (q = p; *q && *q != ' '; q++) ;
		if (*q != ' ') {
			fprintf(stderr, "active file corrupt\n");
			continue;
		}

		active[num_active].name = str_save(buf);
		active[num_active].max = atol(p);
		active[num_active].min = atol(q);
		active[num_active].next = -1;	    /* hash chaining */
		active[num_active].flag = NOTGOT;   /* not in my_group[] yet */

		num_active++;

read_active_continue:;

	}

	fclose(fp);
}



/*
 *  Read $HOME/.newsrc into my_group[].  my_group[] ints point to
 *  active[] entries.  Sub_only determines whether we just read
 *  subscribed groups or all of them.
 */

read_newsrc(sub_only)
int sub_only;		/* TRUE=subscribed groups only, FALSE=all groups */
{
	FILE *fp;
	char *p;
	char buf[8192];
	char c;
	int i;

	local_top = 0;

	fp = fopen(newsrc, "r");
	if (fp == NULL) {		/* attempt to make a .newsrc */
		for (i = 0; i < num_active; i++) {
			if (local_top >= max_active)
				expand_active();
			my_group[local_top] = i;
			active[i].flag = 0;
#if 0
			unread[local_top] = active[i].max - active[i].min;
#else
			unread[local_top] = -1;
#endif
			local_top++;
		}
		write_newsrc();
		return;
	}

	while (fgets(buf, 8192, fp) != NULL) {
		p = buf;
		while (*p && *p != '\n' && *p != ' ' && *p != ':' && *p != '!')
			p++;
		c = *p;
		*p++ = '\0';
		if (c == '!' && sub_only)
			continue;		/* unsubscribed */

		if ((i = add_group(buf, FALSE)) < 0) {
		    fprintf(stderr, "group %s not found in active file\n", buf);
		    continue;
		}

		if (c != '!')		/* if we're subscribed to it */
			active[my_group[i]].flag |= SUBS;

		unread[i] = parse_unread(p, my_group[i]);
	}
	fclose(fp);
}


/*
 *  Write a new newsrc from my_group[] and active[]
 *  Used to a create a new .newsrc if there isn't one already, or when
 *  the newsrc is reset.
 */

write_newsrc() {
	FILE *fp;
	int i;

	setuid(real_uid);	/* become the user to write in his */
	setgid(real_gid);	/* home directory */

	fp = fopen(newsrc, "w");
	if (fp == NULL)
		goto write_newsrc_done;

	for (i = 0; i < num_active; i++)
		fprintf(fp, "%s: \n", active[i].name);

	fclose(fp);

write_newsrc_done:
	setuid(tass_uid);
	setgid(tass_gid);
}


/*
 *  Load the sequencer rang lists and mark arts[] according to the
 *  .newsrc info for a particular group.  i.e.  rec.arts.comics: 1-94,97
 */

read_newsrc_line(group)
char *group;
{
	FILE *fp;
	char buf[8192];
	char *p;

	fp = fopen(newsrc, "r");
	if (fp == NULL)
		return;

	while (fgets(buf, 8192, fp) != NULL) {
		p = buf;
		while (*p && *p != '\n' && *p != ' ' && *p != ':' && *p != '!')
			p++;
		*p++ = '\0';
		if (strcmp(buf, group) != 0)
			continue;
		parse_seq(p);
		break;
	}

	fclose(fp);
}


/*
 *  For our current group, update the sequencer information in .newsrc
 */

update_newsrc(group, groupnum)
char *group;
int groupnum;			/* index into active[] for this group */
{
	FILE *fp;
	FILE *newfp;
	char buf[8192];
	char *p;
	char c;
	int gotit = FALSE;

	setuid(real_uid);
	setgid(real_gid);

	fp = fopen(newsrc, "r");
	newfp = fopen(newnewsrc, "w");
	if (newfp == NULL)
		goto update_done;

	if (fp != NULL) {
		while (fgets(buf, 8192, fp) != NULL) {
			for (p = buf; *p; p++)
				if (*p == '\n') {
					*p = '\0';
					break;
				}

			p = buf;
			while (*p && *p != ' ' && *p != ':' && *p != '!')
					p++;
			c = *p;
			if (c != '\0')
				*p++ = '\0';

			if (c != '!')
				c = ':';

			if (strcmp(buf, group) == 0) {
				fprintf(newfp, "%s%c ", buf, c);
				gotit = TRUE;
				print_seq(newfp, groupnum);
				fprintf(newfp, "\n");
			} else
				fprintf(newfp, "%s%c%s\n", buf, c, p);
		}
		fclose(fp);
	}

	fclose(newfp);
	unlink(newsrc);
	link(newnewsrc, newsrc);
	unlink(newnewsrc);

update_done:
	setuid(tass_uid);
	setgid(tass_gid);
}


/*
 *  Subscribe/unsubscribe to a group in .newsrc.  ch should either be
 *  '!' to unsubscribe or ':' to subscribe.  num is the group's index
 *  in active[].
 */

subscribe(group, ch, num, out_seq)
char *group;
char ch;
int num;
int out_seq;				/* output sequencer info? */
{
	FILE *fp;
	FILE *newfp;
	char buf[8192];
	char *p;
	char c;
	int gotit = FALSE;

	if (ch == '!')
		active[num].flag &= ~SUBS;
	else
		active[num].flag |= SUBS;

	setuid(real_uid);
	setgid(real_gid);

	fp = fopen(newsrc, "r");
	newfp = fopen(newnewsrc, "w");
	if (newfp == NULL)
		goto update_done;

	if (fp != NULL) {
		while (fgets(buf, 8192, fp) != NULL) {
			for (p = buf; *p; p++)
				if (*p == '\n') {
					*p = '\0';
					break;
				}

			p = buf;
			while (*p && *p != ' ' && *p != ':' && *p != '!')
					p++;
			c = *p;
			if (c != '\0')
				*p++ = '\0';

			if (c != '!')
				c = ':';

			if (strcmp(buf, group) == 0) {
				fprintf(newfp, "%s%c%s\n", buf, ch, p);
				gotit = TRUE;
			} else
				fprintf(newfp, "%s%c%s\n", buf, c, p);
		}
		fclose(fp);
	}

	if (!gotit) {
		if (out_seq) {
			fprintf(newfp, "%s%c ", group, ch);
			print_seq(newfp, num);
			fprintf(newfp, "\n");
		} else
			fprintf(newfp, "%s%c\n", group, ch);
	}

	fclose(newfp);
	unlink(newsrc);
	link(newnewsrc, newsrc);
	unlink(newnewsrc);

update_done:
	setuid(tass_uid);
	setgid(tass_gid);
}


print_seq(fp, groupnum)
FILE *fp;
int groupnum;			/* index into active[] for this group */
{
	int i;
	int flag = FALSE;

	if (top <= 0) {
		if (active[groupnum].min > 1) {
			fprintf(fp, "1-%ld", active[groupnum].min);
			fflush(fp);
		}
		return;
	}

	i = 0;
	if (arts[0].artnum > 1) {
		for (; i < top && !arts[i].unread; i++) ;
		if (i > 0)
			fprintf(fp, "1-%ld", arts[i-1].artnum);
		else
			fprintf(fp, "1-%ld", arts[0].artnum - 1);
		flag = TRUE;
	}

	for (; i < top; i++) {
		if (!arts[i].unread) {
			if (flag)
				fprintf(fp, ",");
			else
				flag = TRUE;
			fprintf(fp, "%ld", arts[i].artnum);
			if (i+1 < top && !arts[i+1].unread) {
				while (i+1 < top && !arts[i+1].unread)
					i++;
				fprintf(fp, "-%ld", arts[i].artnum);
			}
		}
	}

	if (!flag && active[groupnum].min > 1)
		fprintf(fp, "1-%ld", active[groupnum].min);
	fflush(fp);
}


parse_seq(s)
char *s;
{
	long low, high;
	int i;

	while (*s) {
		while (*s && (*s < '0' || *s > '9'))
			s++;

		if (*s && *s >= '0' && *s <= '9') {
			low = atol(s);
			while (*s && *s >= '0' && *s <= '9')
				s++;
			if (*s == '-') {
				s++;
				high = atol(s);
				while (*s && *s >= '0' && *s <= '9')
					s++;
			}  else
				high = low;

			for (i = 0; i < top; i++)
				if (arts[i].artnum >= low &&
				    arts[i].artnum <= high)
					arts[i].unread = 0;
		}
	}
}


parse_unread(s, groupnum)
char *s;
int groupnum;			/* index for group in active[] */
{
	long low, high;
	long last_high;
	int i;
	int sum = 0;
	int gotone = FALSE;
	int n;

/*
 *  Read the first range from the .newsrc sequencer information.  If the
 *  top of the first range is higher than what the active file claims is
 *  the bottom, use it as the new bottom instead
 */

	high = 0;
	if (*s) {
		while (*s && (*s < '0' || *s > '9'))
			s++;

		if (*s && *s >= '0' && *s <= '9') {
			low = atol(s);
			while (*s && *s >= '0' && *s <= '9')
				s++;
			if (*s == '-') {
				s++;
				high = atol(s);
				while (*s && *s >= '0' && *s <= '9')
					s++;
			}  else
				high = low;
			gotone = TRUE;
		}
	}

	if (high < active[groupnum].min)
		high = active[groupnum].min;

	while (*s) {
		last_high = high;

		while (*s && (*s < '0' || *s > '9'))
			s++;

		if (*s && *s >= '0' && *s <= '9') {
			low = atol(s);
			while (*s && *s >= '0' && *s <= '9')
				s++;
			if (*s == '-') {
				s++;
				high = atol(s);
				while (*s && *s >= '0' && *s <= '9')
					s++;
			}  else
				high = low;

			if (low > last_high)	/* otherwise seq out of order */
				sum += (low - last_high) - 1;
		}
	}

	if (gotone) {
		if (active[groupnum].max > high)
			sum += active[groupnum].max - high;
		return sum;
	}

	n = (int) (active[groupnum].max - active[groupnum].min);
	if (n < 2)
		return 0;

	return -1;
}


get_line_unread(group, groupnum)
char *group;
int groupnum;				/* index for group in active[] */
{
	FILE *fp;
	char buf[8192];
	char *p;
	int ret = -1;

	fp = fopen(newsrc, "r");
	if (fp == NULL)
		return -1;

	while (fgets(buf, 8192, fp) != NULL) {
		p = buf;
		while (*p && *p != '\n' && *p != ' ' && *p != ':' && *p != '!')
			p++;
		*p++ = '\0';
		if (strcmp(buf, group) != 0)
			continue;
		ret = parse_unread(p, groupnum);
		break;
	}

	fclose(fp);
	return ret;
}


reset_newsrc()
{
	FILE *fp;
	FILE *newfp;
	char buf[8192];
	char *p;
	char c;
	int gotit = FALSE;
	int i;

	setuid(real_uid);
	setgid(real_gid);

	fp = fopen(newsrc, "r");
	newfp = fopen(newnewsrc, "w");
	if (newfp == NULL)
		goto update_done;

	if (fp != NULL) {
		while (fgets(buf, 8192, fp) != NULL) {
			for (p = buf; *p && *p != '\n'; p++) ;
			*p = '\0';

			p = buf;
			while (*p && *p != ' ' && *p != ':' && *p != '!')
					p++;
			c = *p;
			if (c != '\0')
				*p++ = '\0';

			if (c != '!')
				c = ':';

			fprintf(newfp, "%s%c\n", buf, c);
		}
		fclose(fp);
	}

	fclose(newfp);
	unlink(newsrc);
	link(newnewsrc, newsrc);
	unlink(newnewsrc);

update_done:
	setuid(tass_uid);
	setgid(tass_gid);

	for (i = 0; i < local_top; i++)
		unread[i] = -1;
}


delete_group(group)
char *group;
{
	FILE *fp;
	FILE *newfp;
	char buf[8192];
	char *p;
	char c;
	int gotit = FALSE;
	FILE *del;

	setuid(real_uid);
	setgid(real_gid);

	fp = fopen(newsrc, "r");
	newfp = fopen(newnewsrc, "w");
	if (newfp == NULL)
		goto del_done;
	del = fopen(delgroups, "a+");
	if (del == NULL)
		goto del_done;

	if (fp != NULL) {
		while (fgets(buf, 8192, fp) != NULL) {
			for (p = buf; *p && *p != '\n'; p++) ;
			*p = '\0';

			p = buf;
			while (*p && *p != ' ' && *p != ':' && *p != '!')
					p++;
			c = *p;
			if (c != '\0')
				*p++ = '\0';

			if (c != '!')
				c = ':';

			if (strcmp(buf, group) == 0) {
				fprintf(del, "%s%c%s\n", buf, c, p);
				gotit = TRUE;
			} else
				fprintf(newfp, "%s%c%s\n", buf, c, p);
		}
		fclose(fp);
	}

	fclose(newfp);

	if (!gotit)
		fprintf(del, "%s! \n", group);

	fclose(del);
	unlink(newsrc);
	link(newnewsrc, newsrc);
	unlink(newnewsrc);

del_done:
	setuid(tass_uid);
	setgid(tass_gid);
}


undel_group() {
	FILE *del;
	FILE *newfp;
	FILE *fp;
	char buf[2][8192];
	char *p;
	int which = 0;
	long h;
	extern int cur_groupnum;
	int i, j;
	char c;

	setuid(real_uid);
	setgid(real_gid);

	del = fopen(delgroups, "r");
	if (del == NULL) {
		setuid(tass_uid);
		setgid(tass_gid);
		return FALSE;
	}
	unlink(delgroups);
	newfp = fopen(delgroups, "w");
	if (newfp == NULL) {
		setuid(tass_uid);
		setgid(tass_gid);
		return FALSE;
	}

	buf[0][0] = '\0';
	buf[1][0] = '\0';

	while (fgets(buf[which], 8192, del) != NULL) {
		which = !which;
		if (*buf[which])
			fputs(buf[which], newfp);
	}

	fclose(del);
	fclose(newfp);
	which = !which;

	if (!*buf[which]) {
		setuid(tass_uid);
		setgid(tass_gid);
		return FALSE;
	}

	for (p = buf[which]; *p && *p != '\n'; p++) ;
	*p = '\0';

	p = buf[which];
	while (*p && *p != ' ' && *p != ':' && *p != '!')
		p++;
	c = *p;
	if (c != '\0')
		*p++ = '\0';

	if (c != '!')
		c = ':';

	{			/* find the hash of the group name */
		char *t = buf[which];

		h = *t++;
		while (*t)
			h = (h * 64 + *t++) % TABLE_SIZE;
	}

	for (i = group_hash[h]; i >= 0; i = active[i].next) {
		if (strcmp(buf[which], active[i].name) == 0) {
			for (j = 0; j < local_top; j++)
				if (my_group[j] == i) {
					setuid(tass_uid);
					setgid(tass_gid);
					return j;
				}

			active[i].flag &= ~NOTGOT;   /* mark that we got it */
			if (c != '!')
				active[i].flag |= SUBS;

			if (local_top >= max_active)
				expand_active();
			local_top++;
			for (j = local_top; j > cur_groupnum; j--) {
				my_group[j] = my_group[j-1];
				unread[j] = unread[j-1];
			}
			my_group[cur_groupnum] = i;
			unread[cur_groupnum] = parse_unread(p, i);

			fp = fopen(newsrc, "r");
			if (fp == NULL) {
				setuid(tass_uid);
				setgid(tass_gid);
				return FALSE;
			}
			newfp = fopen(newnewsrc, "w");
			if (newfp == NULL) {
				fclose(fp);
				setuid(tass_uid);
				setgid(tass_gid);
				return FALSE;
			}
			i = 0;
			while (fgets(buf[!which], 8192, fp) != NULL) {
				for (p = buf[!which]; *p && *p != '\n'; p++) ;
				*p = '\0';

				p = buf[!which];
				while (*p && *p!=' ' && *p != ':' && *p != '!')
					p++;
				c = *p;
				if (c != '\0')
					*p++ = '\0';

				if (c != '!')
					c = ':';

				while (i < cur_groupnum) {
					if (strcmp(buf[!which],
					  active[my_group[i]].name) == 0) {
						fprintf(newfp, "%s%c%s\n",
							buf[!which], c, p);
						goto foo_cont;
					}
					i++;
				}
				fprintf(newfp, "%s%c%s\n", buf[which], c, p);
				fprintf(newfp, "%s%c%s\n", buf[!which], c, p);
				break;
foo_cont:;
			}

			while (fgets(buf[!which], 8192, fp) != NULL)
				fputs(buf[!which], newfp);

			fclose(newfp);
			fclose(fp);
			unlink(newsrc);
			link(newnewsrc, newsrc);
			unlink(newnewsrc);
			setuid(tass_uid);
			setgid(tass_gid);
			return TRUE;
		}
	}

	setuid(tass_uid);
	setgid(tass_gid);

	return FALSE;
}


mark_group_read(group, groupnum)
char *group;
int groupnum;			/* index into active[] for this group */
{
	FILE *fp;
	FILE *newfp;
	char buf[8192];
	char *p;
	char c;
	int gotit = FALSE;

	if (active[groupnum].max < 2)
		return;

	setuid(real_uid);
	setgid(real_gid);

	fp = fopen(newsrc, "r");
	newfp = fopen(newnewsrc, "w");
	if (newfp == NULL)
		goto mark_group_read_done;

	if (fp != NULL) {
		while (fgets(buf, 8192, fp) != NULL) {
			for (p = buf; *p; p++)
				if (*p == '\n') {
					*p = '\0';
					break;
				}

			p = buf;
			while (*p && *p != ' ' && *p != ':' && *p != '!')
					p++;
			c = *p;
			if (c != '\0')
				*p++ = '\0';

			if (c != '!')
				c = ':';

			if (strcmp(buf, group) == 0) {
				fprintf(newfp, "%s%c 1-%ld\n", buf, c,
						active[groupnum].max);
				gotit = TRUE;
			} else
				fprintf(newfp, "%s%c%s\n", buf, c, p);
		}
		fclose(fp);
	}

	fclose(newfp);
	unlink(newsrc);
	link(newnewsrc, newsrc);
	unlink(newnewsrc);

mark_group_read_done:
	setuid(tass_uid);
	setgid(tass_gid);
}

@EOF

chmod 644 main.c

echo x - misc.c
cat >misc.c <<'@EOF'

#include	<stdio.h>
#include	<signal.h>
#include	<pwd.h>
#include	<sys/types.h>
#include	<sys/stat.h>
#include	"tass.h"


char active_file[LEN];
char homedir[LEN];
char userid[LEN];
char delgroups[LEN];
char newsrc[LEN];
char newnewsrc[LEN];
char indexdir[LEN];
char my_org[LEN];		/* organization */


/*
 *  Which base note (an index into base[]) does a respnum
 *  (an index into arts[]) corresponsd to?
 *
 *  In other words, base[] points to an entry in arts[] which is
 *  the head of a thread, linked with arts[].thread.  For any q: arts[q],
 *  find i such that base[i]->arts[n]->arts[o]->...->arts[q]
 */

which_base(n)
int n;
{
	int i, j;

	for (i = 0; i < top_base; i++)
		for (j = base[i]; j >= 0; j = arts[j].thread)
			if (j == n)
				return i;

	fprintf(stderr, "can't find base article\n");
	return 0;
}


/*
 *  Find how deep in a thread a response is.  Start counting at zero
 */

which_resp(n)
int n;
{
	int i, j;
	int num = 0;

	i = which_base(n);

	for (j = base[i]; j != -1; j = arts[j].thread)
		if (j == n)
			break;
		else
			num++;

	return num;
}


/*
 *  Given an index into base[], find the number of responses for
 *  that basenote
 */

nresp(n)
int n;
{
	int i;
	int oldi = -3;
	int sum = 0;

	assert(n < top_base);

	for (i = base[n]; i != -1; i = arts[i].thread) {
		assert(i != -2);
		assert(i != oldi);
		oldi = i;
		sum++;
	}

	return sum - 1;
}


asfail(file, line, cond)
char	*file;
int	line;
char	*cond;
{
	fprintf(stderr, "tass: assertion failure: %s (%d): %s\n",
							file, line, cond);
	exit(1);
}


/*
 *  Make regular expressions pleasant for the masses:  glob them
 */

glob_name(group, grp)
char *group;
char *grp;
{
char *p, *q;

/*
 *  Prefix the .'s in the group name so they won't be interpreted
 *  as regular expression commands.  Change 'all' into '*'
 */

	p = group;
	q = grp;

	if (strncmp(p, "all", 3) == 0 && (p[3] == '.' || p[3] == '\0')) {
		*q++ = '.';
		*q++ = '*';
		p = &p[3];
	}
	while (*p != '\0') {
		if (*p == '.') {
			*q++ = '\\';
			*q++ = '.';
			p++;

			if (strncmp(p, "all", 3) == 0 &&
				(p[3] == '.' || p[3] == '\0')) {
					*q++ = '.';
					*q++ = '*';
					p = &p[3];
				}
		} else if (*p == '*') {
			*q++ = '.';
			*q++ = '*';
			p++;
		} else
			*q++ = *p++;
	}
	*q = '\0';
}


/*
 * init_selfinfo
 *   Deterimines users home directory, userid, and a path
 *   for an rc file in the home directory
 */

init_selfinfo()
{
	struct passwd *myentry;
	extern struct passwd *getpwuid();
	struct stat sb;
	char nam[LEN];
	char *p;
	extern char *getenv();
	FILE *fp;

	myentry = getpwuid(getuid());
	strcpy(userid, myentry->pw_name);
	strcpy(homedir, myentry->pw_dir);

	sprintf(newsrc, "%s/.newsrc", homedir);
	sprintf(newnewsrc, "%s/.newnewsrc", homedir);
	sprintf(delgroups, "%s/.delgroups", homedir);
	sprintf(indexdir, "%s/.tindex", homedir);
	sprintf(active_file, "%s/active", LIBDIR);
	if (stat(active_file, &sb) >= 0)
		goto got_active;

/*
 *  I hate forgetting to define LIBDIR correctly.  Guess a
 *  couple of likely places if it's not where LIBDIR says it is.
 */

	strcpy(active_file, "/usr/lib/news/active");
	if (stat(active_file, &sb) >= 0)
		goto got_active;

	strcpy(active_file, "/usr/local/lib/news/active");
	if (stat(active_file, &sb) >= 0)
		goto got_active;

	strcpy(active_file, "/usr/public/lib/news/active");
	if (stat(active_file, &sb) >= 0)
		goto got_active;

/*
 *  Oh well.  Revert to what LIBDIR says it is to produce a
 *  useful error message when read_active() fails later.
 */

	sprintf(active_file, "%s/active", LIBDIR);

got_active:

	*my_org = '\0';
	p = getenv("ORGANIZATION");
	if (p != NULL) {
		strcpy(my_org, p);
		goto got_org;
	}

	sprintf(nam, "%s/organization", LIBDIR);
	fp = fopen(nam, "r");

	if (fp == NULL) {
		sprintf(nam, "/usr/lib/news/organization");
		fp = fopen(nam, "r");
	}

	if (fp == NULL) {
		sprintf(nam, "/usr/local/lib/news/organization");
		fp = fopen(nam, "r");
	}

	if (fp == NULL) {
		sprintf(nam, "/usr/public/lib/news/organization");
		fp = fopen(nam, "r");
	}

	if (fp == NULL) {
		sprintf(nam, "/etc/organization");
		fp = fopen(nam, "r");
	}

	if (fp != NULL) {
		if (fgets(my_org, LEN, fp) != NULL) {
			for (p = my_org; *p && *p != '\n'; p++) ;
			*p = '\0';
		}
		fclose(fp);
	}

got_org:;

}


char *
my_malloc(size)
unsigned size;
{
	char *p;
	extern char *malloc();

	p = malloc(size);
	if (p == NULL) {
		fprintf(stderr, "tass: out of memory\n");
		exit(1);
	}
	return p;
}


char *
my_realloc(p, size)
char *p;
unsigned size;
{
	extern char *malloc();
	extern char *realloc();

	if (p == NULL)
		p = malloc(size);
	else
		p = realloc(p, size);

	if (p == NULL) {
		fprintf(stderr, "tass: out of memory\n");
		exit(1);
	}
	return p;
}


char *
str_save(s)
char *s;
{
char *p;

	assert(s != NULL);
	
	p = my_malloc(strlen(s) + 1);
	strcpy(p, s);

	return(p);
}


copy_fp(a, b, prefix)
FILE *a;
FILE *b;
char *prefix;
{
	char buf[8192];

	while (fgets(buf, 8192, a) != NULL)
		fprintf(b, "%s%s", prefix, buf);
}


char *
get_val(env, def)
char *env;		/* Environment variable we're looking for	*/
char *def;		/* Default value if no environ value found	*/
{
	extern char *getenv();
	char *ptr;

	if ((ptr = getenv(env)) != NULL)
		return(ptr);
	else
		return(def);
}


invoke_editor(nam)
char *nam;
{
	char buf[200];
	static int first = TRUE;
	static char editor[200];

	if (first) {
		strcpy(editor, get_val("EDITOR", "/usr/bin/vi"));
		first = FALSE;
	}

	sprintf(buf, "%s %s", editor, nam);
	printf("%s\n", buf);
	return invoke_cmd(buf);
}


invoke_cmd(nam)
char *nam;
{
	int ret;
#ifdef SIGTSTP
	void (*susp)();
#endif

	Raw(FALSE);
	setuid(real_uid);
	setgid(real_gid);

#ifdef SIGTSTP
	susp = signal(SIGTSTP, SIG_DFL);
#endif

	ret = system(nam);

#ifdef SIGTSTP
	signal(SIGTSTP, susp);
#endif

	setuid(tass_uid);
	setgid(tass_gid);
	Raw(TRUE);

	return ret == 0;
}


shell_escape() {
	char shell[LEN];
	char *p;
#ifdef SIGTSTP
	void (*susp)();
#endif

	if (!parse_string("!", shell))
		strcpy(shell, get_val("SHELL", "/bin/sh"));

	for (p = shell; *p && (*p == ' ' || *p == '\t'); p++) ;

	if (!*p)
		strcpy(shell, get_val("SHELL", "/bin/sh"));
	
	Raw(FALSE);

	setuid(real_uid);
	setgid(real_gid);

	fputs("\r\n", stdout);

#ifdef SIGTSTP
	susp = signal(SIGTSTP, SIG_DFL);
#endif

	system(p);

#ifdef SIGTSTP
	signal(SIGTSTP, susp);
#endif

	setuid(tass_uid);
	setgid(tass_gid);

	Raw(TRUE);

	continue_prompt();
	mail_setup();
}


/*
 *  Find the next unread response in this group 
 */

next_unread(n)
int n;
{

	while (n >= 0) {
		if (arts[n].unread == 1)
			return n;
		n = next_response(n);
	}

	return -1;
}


/*
 *  Find the previous unread response in this thread
 */

prev_unread(n)
int n;
{

	while (n >= 0) {
		if (arts[n].unread == 1)
			return n;
		n = prev_response(n);
	}

	return -1;
}

@EOF

chmod 644 misc.c

exit 0
-- 
skrenta at blekko.commodore.com



More information about the Alt.sources mailing list