[alt.sources] Tass 3.2 newsreader part 1/3

skrenta@blekko.commodore.com (Rich Skrenta) (04/18/91)

# 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	README		Tass.man	
#	art.c		curses.c	group.c		hashstr.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'

#  Make sure LIBDIR, SPOOLDIR and MAILER are correct in tass.h
#
#  Make sure spool_open.c knows whether readdir returns struct direct or
#  struct dirent.  The defines below should take care of this.

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

#  For System V
#
CFLAGS=-g 
LIBS= -lcurses

#  For 286 Xenix
#
# CFLAGS=-g -M2l -F 8000
# LIBS= -lcurses -ltermcap -lx

#  For SCO Unix System V
#
# CFLAGS=-g -UM_XENIX -DSCO_UNIX
# LIBS= -lcurses -lgen


#  You only need to worry about the following two defines if you want
#  to build rtass (remote Tass via nntp)
#
# point NNTPLIB at the nntp clientlib.o support library
#
NNTPLIB=../nntp/common/clientlib.o
#
#  NETLIBS should be the networking libraries you need to link with
#  the nntp clientlib.o
#
NETLIBS=-lnsl -lsocket


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

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

rtass: $(OBJECTS) nntp_open.o
	cc $(CFLAGS) -o rtass $(OBJECTS) nntp_open.o $(LIBS) $(NNTPLIB) $(NETLIBS)

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

clean:
	rm -f *.o

clobber: clean
	rm -f tass rtass


art.o:		art.c tass.h
curses.o:	curses.c
group.o:	group.c tass.h
hashstr.o:	hashstr.c
mail.o:		mail.c
main.o:		main.c tass.h
misc.o:		misc.c tass.h
nntp_open.o:	nntp_open.c tass.h nntp.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
spool_open.o:	spool_open.c tass.h
time.o:		time.c
@EOF

chmod 644 Makefile

echo x - README
cat >README <<'@EOF'
Tass is a full screen threaded newsreader.

	o Organizes articles by threads.  Displays a really nice
	  article selection page.

	o Group selection page makes it easy to scan newsgroups, subscribe,
	  unsubscribe, reorder your .newsrc

	o If you've ever used Notes, this is the program for you.
	  Tass looks a lot like Notes, but has a few improvements:
		visual group selection page, Notes didn't have one
		rn style unread article detection as opposed to single timeline
		uses standard /usr/spool/news article layout

I wrote Tass because I "learned" the Usenet on Notes and couldn't stand rn.
No rn flames here, but if you've wished you could view the news a different
way, try Tass.

Newsreading style under Tass tends to be different than with rn.  Instead of
plowing through each group reading everything unread, you may find yourself
reading fewer articles in more groups.  It's easier to skip about and only
read interesting threads with Tass.

Tass keeps an index file for each group.  The first time you enter a group,
it will be a bit slow creating this file.  After that Tass will incrementally
update the index file and there should be little delay.

You can also run Tass in "update mode" out of cron to update the indexes.


Building Tass
-------- ----

	1)  Edit the Makefile.  Select CFLAGS and LIBS for your system.
	2)  Edit tass.h.  Make sure that LIBDIR, SPOOLDIR, MAILER and
	    DEF_EDITOR are correct for your system.
	3)  'make'

To build the remote NNTP Tass (rtass) you will need the nntp sources;
specifically, clientlib.o.  Point the Makefile variable NNTPLIB at your
clientlib.o and say 'make rtass'.


Installing Tass
---------- ----

Copy the tass executable to some useful place.  If you make tass setuid news,
it will keep the indexes in the news spool directory.  If not, tass will hide
indexes in the user's home directory.

Don't make rtass setuid news since it will get articles from the NNTP host
and not from /usr/spool/news.

There is a brief man page (Tass.man) which may be copied to the appropriate
man directory.


NOTE:
-----
	Tass 3.2 uses a different name for the index files.  If you've been
	using Tass 3.0 or 3.1, you should remove these old indexes before
	running 3.2.  Do this with

		rm -rf $HOME/.tindex
	   or
		find /usr/spool/news -name '.tindex' -exec rm {} \;


Changes from 3.0
------- ---- ---

	o Index files are now 1/2 to 1/3 their previous size
	o Tass is much more conservative in its memory usage
	o Tass recognizes .signature and .Sig files
	o Screen updating is more efficient in many places
	o Job control fixed for BSD systems
	o The various mailing commands should work much better
	o Author search
	o Support for NNTP (rtass)
	o Many other enhancements


Rich Skrenta
-- 
skrenta@blekko.commodore.com

@EOF

chmod 644 README

echo x - Tass.man
cat >Tass.man <<'@EOF'
.TH TASS 1A
.SH NAME
tass, rtass \- Visual threaded Usenet news reader
.SH SYNOPSIS
.nf
tass [options] [newsgroups]
rtass [options] [newsgroups]
.fi
.SH DESCRIPTION
Tass is a full screen threaded Usenet newsreader.
Tass has three newsreading levels:
the newsgroup selection page, the group index page and the article viewer.
Use the 'h' (help) command to view a list of the commands available at a
particular level.
.PP
On startup Tass will show a list of the newsgroups found in $HOME/.newsrc.
An arrow will point to the first newsgroup.  Move the arrow by either using
the terminal arrow keys or 'j' and 'k'.  Control-D will page down, control-U
will page up.  Enter a newsgroup by pressing RETURN.
.PP
The TAB key may be used to advance to the next newsgroup with unread articles
and enter it.
.PP
rtass will attempt to connect to the NNTP port on the machine named in the
environment variable NNTPSERVER or contained in the file /etc/nntpserver.
rtass will index somewhat slower because the articles must be retrieved
via the NNTP protocol.
.PP
Refer to the Tass help screens for further commands.
.SH TASS INDEX FILES
In order to keep track of threads, Tass maintains an index for each group.
If Tass is made setuid to news, the indexes will be stored in the news spool
directory (typically /usr/spool/news).  If
Tass is not setuid, it will store
index files in the user's home directory, in a subdirectory called .tindx.
.PP
Entering a group the first time tends to be slow because the index file must
be built from scratch.  Subsequent readings of a group will cause
Tass to incrementally update the index file, adding or removing entries as new
articles come in or as news expires.
.PP
Tass may be run in update mode (the -u option) to update a series of groups
at one time.  tass -u is usually run from cron.
.PP
Do not make rtass setuid news since news will be obtained via NNTP and not
from /usr/spool/news.
.SH SIGNATURES
Tass will recognize a signature in either $HOME/.signature or $HOME/.Sig.
If .signature exists, then the signature will be pulled into the editor
for Tass mail commands.  A signature in .signature will not be pulled
into the editor for posting commands since the inews program
will append the signature itself.
.PP
A signature in .Sig will be pulled into the editor for both posting
and mailing commands.
.SH OPTIONS
.I Tass
recognizes the following options:
.TP
-f file
Use the indicated file in place of $HOME/.newsrc.
.TP
-u
Run Tass in update mode.  Tass will make indexes current for every group
in its newsrc.

A good way to keep Tass index files current is to run tass -u from cron:

.nf
20 6 * * *	/usr/local/bin/tass -u -f /usr/lib/news/tass_groups
.fi

This would update the index files for those groups appearing in
/usr/lib/news/tass_groups.  To index all of the groups on the system,
run tass -u with -f indicating the active file:

.nf
20 6 * * *	/usr/local/bin/tass -u -f /usr/lib/news/active
.fi

.SH AUTHOR
.nf
Rich Skrenta
skrenta@blekko.commodore.com or skrenta@blekko.uucp.
.fi
@EOF

chmod 644 Tass.man

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


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


char index_file[LEN+1];
char *glob_art_group;
extern char *hash_str();


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

	Raw(FALSE);
	putchar('\n');
	signal(SIGTSTP, SIG_DFL);
#ifdef BSD
        sigsetmask(sigblock(0) & ~(1 << (SIGTSTP - 1)));
#endif
	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);
	}

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

	load_index();
	modified = read_group(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();
}


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

read_group(group, group_path)
char *group;
char *group_path;
{
	int fd;
	long art;
	int count;
	int modified = FALSE;
	int respnum;
	int i;

	setup_base(group, group_path);	  /* 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 */

		fd = open_header_fd(group_path, art);
		if (fd < 0)
			continue;

/*
 *  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;

		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[j].thread != -2
			&&  arts[i].subject == arts[j].subject) {
				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;
{

	while (*s == 'r' || *s == 'R') {
		if ((*(s+1) == 'e' || *(s+1) == 'E')) {
			if (*(s+2) == ':')
				s += 3;
			else if (*(s+2) == '^' && isdigit(*(s+3)) && *(s+4) == ':')
				s += 5;			/* hurray nn */
			else
				break;
		} else
			break;
		while (*s == ' ')
			s++;
	}

	return s;
}


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

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

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

	h->subject = "<no subject>";
	h->from = "<no from>";

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

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

		if (!flag || *p == '\n')
			break;
	}

	return TRUE;
}


/* 
 *  Write out a .tindx 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];
	char nam[200];
	FILE *fp;
	int *iptr;
	int realnum;

	sprintf(nam, "%s.%d", index_file, getpid());
	fp = fopen(nam, "w");

	if (fp == NULL)
		return;

	fprintf(fp, "%s\n", group);
	fprintf(fp, "%d\n", num_arts());

	realnum = 0;
	for (i = 0; i < top; i++)
		if (arts[i].thread != -2) {
			fprintf(fp, "%ld\n", arts[i].artnum);

			iptr = (int *) arts[i].subject;
			iptr--;

			if (*iptr < 0) {
				fprintf(fp, " %s\n", arts[i].subject);
				*iptr = realnum;
			} else
				fprintf(fp, "%%%d\n", *iptr);

			iptr = (int *) arts[i].from;
			iptr--;

			if (*iptr < 0) {
				fprintf(fp, " %s\n", arts[i].from);
				*iptr = realnum;
			} else
				fprintf(fp, "%%%d\n", *iptr);

			realnum++;
		}

	fclose(fp);
	chmod(nam, 0644);
	unlink(index_file);
	link(nam, index_file);
	unlink(nam);
}


/*
 *  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 .tindx file.
 */

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

	top = 0;

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

	if (fgets(buf, 200, fp) == NULL
	||  fgets(buf, 200, fp) == NULL) {
		err = "one";
		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) {
			err = "two";
			goto corrupt_index;
		}
		arts[top].artnum = atol(buf);

		if (fgets(buf, 200, fp) == NULL) {
			err = "three";
			goto corrupt_index;
		}

		if (buf[0] == '%') {
			n = atoi(&buf[1]);
			if (n >= top || n < 0) {
				err = "eight";
				goto corrupt_index;
			}
			arts[top].subject = arts[n].subject;
		} else if (buf[0] == ' ') {
			for (p = &buf[1]; *p && *p != '\n'; p++) ;
			*p = '\0';
			buf[MAX_SUBJ] = '\0';
			arts[top].subject = hash_str(&buf[1]);
		} else {
			err = "six";
			goto corrupt_index;
		}
				
		if (fgets(buf, 200, fp) == NULL) {
			err = "four";
			goto corrupt_index;
		}

		if (buf[0] == '%') {
			n = atoi(&buf[1]);
			if (n >= top || n < 0) {
				err = "nine";
				goto corrupt_index;
			}
			arts[top].from = arts[n].from;
		} else if (buf[0] == ' ') {
			for (p = &buf[1]; *p && *p != '\n'; p++) ;
			*p = '\0';
			buf[MAX_FROM] = '\0';
			arts[top].from = hash_str(&buf[1]);
		} else {
			err = "seven";
			goto corrupt_index;
		}

		top++;
	}

	fclose(fp);
	return;

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


/*
 *  Look in the local $HOME/.tindx (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;

	h = hash_groupname(group);

	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;

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);

	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);	*/
}

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];
char author_search_string[LEN+1];
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);
#ifdef BSD
        sigsetmask(sigblock(0) & ~(1 << (SIGTSTP - 1)));
#endif
	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];
	int flag;
	int sav_groupnum;

	glob_group = group;
	sav_groupnum = cur_groupnum;

	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 'a':	/* author search forward */
			case 'A':	/* author search backward */
				if (index_point < 0) {
					info_message("No articles");
					break;
				}

				i = (ch == 'a');

				n = search_author((int) base[index_point],
								i, group);
				if (n < 0)
					break;

				index_point = show_page(n, group, group_path);
				if (index_point < 0) {
					space_mode = FALSE;
					goto group_done;
				}
				show_group_page(group);
				break;

			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) {
					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;
 				for (n = INDEX_TOP ;
					n < NOTESLINES + INDEX_TOP; n++ ) {
 					MoveCursor(n, COLS - 2);
					putchar(' ');
 				}
				fflush(stdout);
 /*				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 {
					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 {
					cur_groupnum--;
					index_point = -3;
					space_mode = FALSE;
					goto group_done;
				}
				break;

			case '\t':
				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 'K':	/* mark rest of thread as read */
				if (new_responses(index_point)) {
				    for (i = base[index_point]; i >= 0;
							i = arts[i].thread)
					arts[i].unread = 0;
				    MoveCursor(INDEX_TOP +
				      (index_point - first_subj_on_screen), 78);
				    putchar(' ');
				    fflush(stdout);
				    flag = FALSE;
				} else
				    flag = TRUE;

				n = next_unread(
					next_response(base[index_point]));
				if (n < 0) {
				    if (flag)
					info_message("No next unread article");
				    else
					MoveCursor(LINES, 0);
				    break;
				}

				n = which_base(n);
				if (n < 0) {
					info_message(
					    "Internal error: K which_base < 0");
					break;
				}

				if (n >= last_subj_on_screen) {
					index_point = n;
					show_group_page(group);
				} else {
					erase_subject_arrow();
					index_point = n;
					draw_subject_arrow();
				}
				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) {
						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) {
						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 */
				goto group_done;

			case ' ':
			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) {
					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) {
					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;
					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:
	fix_new_highest(sav_groupnum);
	update_newsrc(group, my_group[sav_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_author(current_art, forward, group)
int current_art;
int forward;
char *group;
{
	char buf[LEN+1];
	char buf2[LEN+1];
	int i;
	int len;
	char *prompt;

	clear_message();

	if (forward)
		prompt = "Author search forward: ";
	else
		prompt = "Author search backward: ";

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

	if (strlen(buf))
		strcpy(author_search_string, buf);
	else if (!strlen(author_search_string)) {
		info_message("No search string");
		return -1;
	}

	make_lower(author_search_string, buf);
	len = strlen(buf);

	i = current_art;

	do {
		if (forward) {
			i = next_response(i);
			if (i < 0)
				i = 0;
		} else {
			i = prev_response(i);
			if (i < 0)
				i = top - 1;
		}

		make_lower(arts[i].from, buf2);
		if (match(buf, buf2, len))
			return i;
	} while (i != current_art);

	info_message("No match");
	return -1;
}


search_subject(forward, group)
int forward;
char *group;
{
	char buf[LEN+1];
	char buf2[LEN+1];
	int i;
	int len;
	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 string");
		return;
	}

	i = index_point;

	make_lower(subject_search_string, buf);
	len = strlen(buf);

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

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

		make_lower(arts[base[i]].subject, buf2);
		if (match(buf, buf2, len)) {
			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("");
		setuid(tass_uid);
		setgid(tass_gid);
		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");

	add_signature(fp, FALSE);
	fclose(fp);

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

		case 'a':
			setuid(tass_uid);
			setgid(tass_gid);
			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 title[100];

	sprintf(title, "%s, Index Page Commands", TASS_HEADER);
	ClearScreen();
	center_line(0, title);

	MoveCursor(2, 0);

	printf("\t4\tSelect article 4\r\n");
	printf("\t<CR>\tRead current article\r\n");
	printf("\t<TAB>\tView next unread article or group\r\n");
	printf("\t^D^U\tPage down (^U=page up)\r\n");
	printf("\taA\tAuthor search forward (A=backward)\r\n");
	printf("\tc\tMark all articles as read\r\n");
	printf("\tg\tChoose a new group by name\r\n");
	printf("\tjk\tDown (k=up) a line\r\n");
	printf("\tK\tMark thread as read & advance\r\n");
	printf("\tnp\tGo to next (p=previous) group\r\n");
	printf("\tNP\tGo to next (P=previous) unread article\r\n");
	printf("\tq\tQuit\r\n");
	printf("\tsu\tSubscribe (u=unsubscribe) to this group\r\n");
	printf("\tt\tReturn to group selection index\r\n");
	printf("\tw\tPost an article\r\n");
	printf("\t/?\tSearch forward (?=backward) for subject\r\n");
	printf("\t-\tShow last article\r\n");

	center_line(LINES, "-- hit any key --");
	ReadCh();
}

@EOF

chmod 644 group.c

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

#include	<stdio.h>


/*
 *  Maintain a table of all strings we have seen.
 *  If a new string comes in, add it to the table and return a pointer
 *  to it.  If we've seen it before, just return the pointer to it.
 *
 *  Usage:  hash_str("some string") returns char *
 *
 *  Spillovers are chained on the end
 */


/*
 *  Arbitrary table size, but make sure it's prime!
 */

/* #define		TABLE_SIZE	1409	*/

#define		TABLE_SIZE	2411



struct hashnode {
	char *s;			/* the string we're saving */
	struct hashnode *next;		/* chain for spillover */
};

struct hashnode *table[ TABLE_SIZE ];

extern char *my_malloc();
struct hashnode *add_string();


char *
hash_str(s)
char *s;
{
	struct hashnode *p;	/* used to descend the spillover structs */
	long h;			/* result of hash:  index into hash table */

	if (s == NULL)
		return(NULL);

	{
		char *t = s;

		h = *t++;
		while (*t)
			h = ((h << 1) ^ *t++) % TABLE_SIZE;
	/*		h = (h * 128 + *t++) % TABLE_SIZE;	*/
	}

	p = table[h];

	if (p == NULL) {
		table[h] = add_string(s);
		return table[h]->s;
	}

	while (1) {
		if (strcmp(s, p->s) == 0)
			return(p->s);

		if (p->next == NULL) {
			p->next = add_string(s);
			return p->next->s;
		} else
			p = p->next;
	}
}


struct hashnode *
add_string(s)
char *s;
{
	struct hashnode *p;
	extern char *strcpy();
	int *iptr;

	p = (struct hashnode *) my_malloc(sizeof(*p));

	p->next = NULL;
	iptr = (int *) my_malloc(strlen(s) + sizeof(int) + 1);
	*iptr++ = -1;
	p->s = (char *) iptr;
	strcpy(p->s, s);
	return(p);
}


hash_init() {
	int i;

	for (i = 0; i < TABLE_SIZE; i++)
		table[i] = NULL;
}


hash_reclaim() {
	int i;
	struct hashnode *p, *next;
	int *iptr;

	for (i = 0; i < TABLE_SIZE; i++)
		if (table[i] != NULL) {
			p = table[i];
			while (p != NULL) {
				next = p->next;
				iptr = (int *) p->s;
				free(--iptr);
				free(p);
				p = next;
			}
			table[i] = NULL;
		}
}

@EOF

chmod 600 hashstr.c

exit 0