[net.sources] newacct - an account request program

chris@umcp-cs.UUCP (Chris Torek) (09/03/85)

# Newacct is a program that creates an "account request" form,
# has the user fill it out, then sends mail off to someone to
# get the account installed.  We use it for account requests on
# our local machines.  It could certainly be fancier, but it
# works well enough as is.
#
# No manual entry (sorry), but the program is self explanatory.
# You will need 4.2BSD (or remove the gethostname() system calls)
# and the Maryland Window Library to run it (or convert it to
# curses).
#
# It also uses the error() routine we have in our C library.  I've
# included the Vax/Sun/NS32000 version with the rest of the code.

: Run this shell script with "sh" not "csh"
PATH=:/bin:/usr/bin:/usr/ucb
export PATH
all=FALSE
if [ x$1 = x-a ]; then
	all=TRUE
fi
/bin/echo 'Extracting newacct.c'
sed 's/^X//' <<'//go.sysin dd *' >newacct.c
#ifndef lint
static char rcsid[] = "$Header: /usr/src/local/bin/RCS/newacct.c,v 1.5 85/09/02 18:23:24 bin Exp $";
#endif

X/*
 * New Account Request program
 *
 * Version 3.0, 2 Sep 1985, Chris Torek.
 *
 * Allows one to fill in an "entry form" for a new account.
 *
 % cc -O -o newacct newacct.c -lwinlib -ltermlib
 */

#include <stdio.h>
#include <local/window.h>
#include <pwd.h>
#include <grp.h>
#include <signal.h>
#include <ctype.h>

X/*
 * conffile contains configuration info in the form "keyword=[ \t]*value\n".
 * It can override all the other strings given below.
 */
char	conffile[] = "/usr/adm/newacct.conf";

char	mailcommand[200] = "/usr/ucb/mail -s 'new account' account-master";
char	homedir[50] = "/u";
char	defaultgroup[20] = "misc2";
char	defaultmachine[100];
char	machinenames[300];
char	dictionary[100] = "/usr/dict/words";
char	localdict[100] = "/usr/dict/local";
char	badpasswds[100] = "/usr/adm/badpasswds";

struct conf {
    char  *c_name;
    char  *c_value;
    int    c_size;
} confitems[] = {
#define ENTRY(n,s) n, s, sizeof s
    ENTRY ("mailcmd",		mailcommand),
    ENTRY ("home",		homedir),
    ENTRY ("group",		defaultgroup),
    ENTRY ("defaultmachine",	defaultmachine),
    ENTRY ("machines",		machinenames),
    ENTRY ("dictionary",	dictionary),
    ENTRY ("localdict",		localdict),
    ENTRY ("badpasswds",	badpasswds),
    0, 0, 0
#undef ENTRY
};

#define	ROWS		15	/* Number of rows required */
#define	COLS		76	/* Number of columns required */

int     Uid,
        Gid,
        Which;
Win    *MainWin,
       *ErrorWin;
char   *Machines[30];

extern int   errno;

int     VerifyLogin (), VerifyGroup (), VerifyPassword ();
int     VerifyMachine (), VerifyOK ();

#define	NFIELDS	(sizeof Fields / sizeof *Fields)

struct entry {
    char   *e_descr;		/* Description */
    char   *e_space;		/* Ptr to space for entry */
    int     e_size;		/* Size of entry (# chars) */
    int     e_rsize;		/* Real size (for passwd, which grows) */
    int     e_noecho;		/* True if should blank-type (for passwd) */
    int   (*e_verify) ();	/* Verification function: true => good */
} Fields[] = {
#define X_LOGIN		0
	"Login name:",		0,  8,  8, 0, VerifyLogin,
#define	X_FULLNAME	1
	"Full name:",		0, 20, 20, 0, VerifyOK,
#define	X_GROUP		2
	"Group:",		0,  8,  8, 0, VerifyGroup,
#define	X_PASSWD	3
	"Password:",		0,  8, 14, 1, VerifyPassword,
#define	X_OFFICE	4
	"Office:",		0, 10, 10, 0, VerifyOK,
#define	X_OPHONE	5
	"Office phone:",	0, 12, 12, 0, VerifyOK,
#define	X_HPHONE	6
	"Home phone:",		0, 12, 12, 0, VerifyOK,
#define	X_OTHER		7
	"Other info:",		0, 20, 20, 0, VerifyOK,
#define	X_FUND		8
	"Account:",		0, 20, 20, 0, VerifyOK,
#define	X_INCHARGE	9
	"Person in charge:",	0, 20, 20, 0, VerifyOK,
#define	X_MACHINE	10
	"Machine name:",	0, 10, 10, 0, VerifyMachine,
};

FILE *popen ();
struct passwd *getpwent ();
struct group *getgrnam ();
char *malloc (), *index (), *strcpy ();

#define	CTRL(c)		((c) & 0x1f)
#define	move(y,x)	WAcursor(MainWin, y, x)
#define	refresh()	(WRCurRow=MainWin->w_cursor.row, \
			 WRCurCol=MainWin->w_cursor.col, Wrefresh(0))

main (argc, argv)
int argc;
char **argv;
{
    register int    c,
                    i;
    register struct entry *ep;
    int     rrows,
            rcols;

    if (argc > 1)
	error (1, 0, "usage: %s\n", *argv);

    Configure ();
    i = strlen (homedir);
    if (i == 0 || homedir[i - 1] != '/') {
	homedir[i] = '/';
	homedir[i + 1] = 0;
    }
    MakeMachines ();
    if (Winit (0, 0)) {
	printf ("Sorry, your terminal won't run this program.\n");
	printf ("Here is your free consolation fortune:\n\n");
	fflush (stdout);
	execl ("/usr/games/fortune", "fortune", (char *)0);
	execl ("/usr/local/bin/fortune", "fortune", (char *)0);
	error (1, errno, "So much for \"fortune\"");
    }
    Wscreensize (&rrows, &rcols);
    if (rcols < COLS || rrows < ROWS) {
	Wcleanup ();
	printf ("Sorry, but your screen is too small.\n");
	exit (1);
    }
    if ((MainWin = Wopen (0, 0, 0, COLS, ROWS, 0, 0)) == 0) {
	Wcleanup ();
	printf ("Something is wrong (window open failed)\n");
	printf ("Contact the system administrator\n");
	exit (1);
    }
    Woncursor (MainWin, 0);
    ep = Fields;
    for (i = 0; i < NFIELDS; i++) {
	register char *p = malloc (ep -> e_rsize + 1);

	if (p == 0)
	    error (1, errno, "malloc failed - consult newacct guru");
	*p = 0;
	ep -> e_space = p;
	ep++;
    }
    WSetRealCursor++;
    center ("Please fill in this form");
    ep = Fields;
    for (i = 0; i < NFIELDS; i++) {
	move (i / 2 + 2, (i & 1) ? COLS / 2 : 0);
	Wputs (ep -> e_descr, MainWin);
	ep++;
    }
    move (8, 0);
    center ("^L Redraw screen        ^D Done, send message    ");
    move (9, 0);
    center ("^M Move to next entry   ^U Move to previous entry");
    move (10, 0);
    center ("Type to change, use ESC to cancel change");
    move (13, 0);
    center ("If you don't know what to enter for something, leave it blank");
    strcpy (Fields[X_GROUP].e_space, defaultgroup);
    Which = X_GROUP;
    sel (0);
    strcpy (Fields[X_MACHINE].e_space, defaultmachine);
    Which = X_MACHINE;
    sel (0);
    Which = 0;
    sel (1);
    for (;;) {
	refresh ();
	c = getchar ();
	if (ErrorWin)
	    Whide (ErrorWin);
	switch (c) {
	    case CTRL ('l'): 
		ScreenGarbaged++;
		break;
	    case '\r': 
	    case '\n': 
	    case CTRL ('n'): 
		sel (0);
		if (++Which >= NFIELDS)
		    Which = 0;
		sel (1);
		break;
	    case CTRL ('u'): 
	    case '\b': 
		sel (0);
		if (--Which < 0)
		    Which = NFIELDS - 1;
		sel (1);
		break;
	    case CTRL ('d'): 
	    case EOF: 
		save ();
		exit (0);
	    default: 
		if (c < ' ')
		    gripe ("That key means nothing");
		else {
		    ChangeIt (c);
		    sel (1);
		}
		break;
	}
    }
}

X/*
 * Center a string on the screen
 */
center (s)
register char *s;
{
    register int    nb;
    register char  *p = s;

    while (*p++);
    p--;
    nb = (COLS - (p - s)) / 2;
    while (--nb >= 0)
	Wputc (' ', MainWin);
    Wputs (s, MainWin);
}

X/*
 * Change one of the strings (the variable Which tells which one)
 */
ChangeIt (firstc)
int firstc;
{
    register int    c,
                    inlen = 0;
    register char  *s = Fields[Which].e_space;
    char    instr[81],
            remember[81];
    int     noecho,
            maxlen,
            legal;

    strcpy (remember, s);
    *s = 0;
    sel (1);
    Wsetmode (MainWin, WINVERSE);
    maxlen = Fields[Which].e_size;
    noecho = Fields[Which].e_noecho;
    for (c = firstc;; c = getchar ()) {
	if (ErrorWin)
	    Whide (ErrorWin);
	if (c == '\r' || c == '\n' || c == CTRL ('u') || c == EOF
		|| c == CTRL ('d')) {
	    strcpy (s, inlen ? instr : remember);
	    legal = (*Fields[Which].e_verify) (s);
	    if (c != EOF)
		ungetc (c, stdin);
	    if (legal)
		return 1;
	    if (c != EOF)
		c = getchar ();	/* un-ungetc */
	    strcpy (s, remember);
	}
	else if (c == 033) {
	    strcpy (s, remember);
	    return 0;
	}
	else if (c == '\b') {
	    if (inlen) {
		Wputs ("\b \b", MainWin);
		instr[--inlen] = 0;
	    }
	    else
		gripe ("Can't backspace - at left margin");
	}
	else if (c >= ' ' && c <= 0177 && c != ':' && c != ',' && c != ';') {
	    if (inlen >= maxlen)
		gripe ("Can't enter any more characters");
	    else {
		Wputc (noecho ? ' ' : c, MainWin);
		instr[inlen++] = c;
		instr[inlen] = 0;
	    }
	}
	else if (c == CTRL ('l'))
	    ScreenGarbaged++;
	refresh ();
    }
}

X/*
 * Mark or unmark the current string with inverse video
 */
sel (inv)
int inv;
{
    register char  *s = Fields[Which].e_space;
    register int    bl,
                    bl2;
    int     y,
            x;

    y = Which / 2 + 2;
    x = Which % 2 ? COLS / 2 + 18 : 18;
    move (y, x);
    Wsetmode (MainWin, inv ? WINVERSE : 0);
    bl = Fields[Which].e_rsize - strlen (s);
    bl2 = Fields[Which].e_rsize - Fields[Which].e_size;
    if (bl < 0)
	bl2 += bl;
    Wputs (s, MainWin);
    while (--bl >= 0)
	Wputc (' ', MainWin);
    Wsetmode (MainWin, 0);
    while (--bl2 >= 0)
	Wputc (' ', MainWin);
    move (y, x);
}

X/*
 * Save the results of the session.  Actually, send mail to someone.
 */
save () {
    register struct passwd *pw;
    register struct group  *g;
    register int    i = 1;
    register char  *s;
    char    remarks[1024],
            passwdentry[300];

    Wcleanup ();

 /* Find a unique userid and the group id */
    setpwent ();
    while ((pw = getpwent ()) != NULL) {
	if (pw -> pw_uid >= i)
	    i = pw -> pw_uid + 1;
    }
    setgrent ();
    g = getgrnam (Fields[X_GROUP].e_space);

 /* Make an entry for the passwd file */
    sprintf (passwdentry,
	    "%s:%s:%d:%d:%s,%s,%s,%s,%s:%s%s:/bin/csh",
	    Fields[X_LOGIN].e_space,
	    Fields[X_PASSWD].e_space,
	    i,
	    g ? g -> gr_gid : 0,
	    Fields[X_FULLNAME].e_space,
	    Fields[X_OFFICE].e_space,
	    Fields[X_OPHONE].e_space,
	    Fields[X_HPHONE].e_space,
	    Fields[X_OTHER].e_space,
	    homedir,
	    Fields[X_LOGIN].e_space);
    printf ("\
Oh, one other thing: please type in any additional\n\
information that could be helpful (e.g., why you are\n\
requesting this account), then enter a line consisting\n\
of a \".\" (period) on a line by itself.\n");
    fflush (stdout);
    s = remarks;
    while (fgets (s, sizeof remarks - (s - remarks), stdin)) {
	if (strcmp (s, ".\n") == 0)
	    break;
	s += strlen (s);
    }
    *s = 0;
    if ((i = fork ()) == 0) {	/* Child, send mail */
	register FILE *f = popen (mailcommand, "w");

	if (f == 0)
	    error (1, errno, "popen (\"%s\", \"w\") failed", mailcommand);
	fprintf (f, "\
%s Account Request:\n\
%s\n\
(group is %s, account %s, contact is %s)\n\
Remarks: %s\n",
		Fields[X_MACHINE].e_space,
		passwdentry,
		Fields[X_GROUP].e_space,
		Fields[X_FUND].e_space,
		Fields[X_INCHARGE].e_space,
		remarks);
	pclose (f);
	_exit (0);
    }
    if (i <= 0)
	error (1, errno, "fork");
    printf ("\nYour account will be ready in a few days.\n");
}

X/*
 * Field verification stuff
 */

VerifyOK () {
    return 1;			/* call it OK */
}

X/*
 * We may only bother checking name-in-use for local machine names.
 * This is a mistake if you are using several machines as a single
 * distributed system.  If you really don't like this, turn the
 * disabled code back on.
 */
VerifyLogin () {
    register int    i;
#ifdef notdef
    static char thishost[sizeof defaultmachine];

    if (thishost[0] == 0)
	(void) gethostname (thishost, sizeof thishost);
    if (strcmp (Fields[X_MACHINE].e_space, thishost))
	return 1;
#endif
    i = getpwnam (Fields[X_LOGIN].e_space) == 0;
    if (i == 0)
	gripe ("That name is in use");
    return i;
}

VerifyGroup () {
    register int    i = getgrnam (Fields[X_GROUP].e_space) != 0;

    if (i == 0)
	gripe ("There is no such group");
    return i;
}

VerifyMachine (p)
register char *p;
{
    register char **l = Machines;
    register char  *s;
    char    buf[BUFSIZ];

    while ((s = *l++) != 0)
	if (*s == *p && strcmp (s, p) == 0)
	    return 1;

    strcpy (buf, "No such machine.  Try one of:");
    p = buf + strlen (buf);
    l = Machines;
    while ((s = *l++) != 0) {
	*p++ = ' ';
	while ((*p = *s++) != 0)
	    p++;
    }
    *p++ = '.';
    *p = 0;
    gripe (buf);
    return 0;
}

VerifyPassword (p)
register char *p;
{
    register int    i,
                    c;
    register char  *np,
		   *sp;
    long    salt;
    char    saltc[2];
    static char  buf[BUFSIZ];

    if (!*p) {
	gripe ("You must specify a password");
	return 0;
    }
    if (strlen (p) < 6) {
	gripe ("Must be at least 6 characters");
	return 0;
    }
    if (alldigits (p)) {
	gripe ("Must have at least one non-digit");
	return 0;
    }
 /* Try login name */
    if (try (Fields[X_LOGIN].e_space, p)) {
tooeasy:
	gripe ("Too easy to guess");
	return 0;
    }

 /* Try full name, all pieces */
    strcpy (buf, Fields[X_FULLNAME].e_space);
    for (np = buf; np && *np;) {
	if ((sp = index (np, ' ')) != 0)
	    *sp = 0;
	if (try (np, p))
	    goto tooeasy;
	np = sp ? sp + 1 : 0;
    }

 /* Try word lists */
    if (lookup (p, dictionary) || lookup (p, localdict) ||
	    lookup (p, badpasswds))
	goto tooeasy;

    time (&salt);
    salt += getpid ();
    saltc[0] = salt & 077;
    saltc[1] = (salt >> 6) & 077;
    for (i = 0; i < 2; i++) {
	c = saltc[i] + '.';
	if (c >= '9')
	    c += 7;
	if (c > 'Z')
	    c += 6;
	saltc[i] = c;
    }
    strcpy (p, crypt (p, saltc));
}

alldigits (p)
register char *p;
{
    while (isdigit (*p++));
    return (*p == 0);
}

try (guess, pwd)
register char *guess;
register char *pwd;
{
    register int    c1,
                    c2;

    while (c1 = *guess++) {
	c2 = *pwd++;
	if (isupper (c1))
	    c1 = tolower (c1);
	if (isupper (c2))
	    c2 = tolower (c2);
	if (c1 != c2)
	    return 0;
    }
    return *pwd == 0;
}

X/*
 * Gripe about something the user did.
 */
gripe (s)
char *s;
{
    if (!ErrorWin) {
	ErrorWin = Wopen (1, 0, 11, COLS, 1, 0, 0);
	Woncursor (ErrorWin, 0);
	Wwrap (ErrorWin, 0);
    }
    else
	Wunhide (ErrorWin);
    Wsetmode (ErrorWin, 0);
    WAcursor (ErrorWin, 0, 0);
    Wclear (ErrorWin, 0);
    Wsetmode (ErrorWin, WINVERSE);
    Wputs (s, ErrorWin);
    Ding ();
}

X/*
 * Read the configuration file.
 */
Configure () {
    register FILE *cf;
    register char *p;
    register struct conf *c;
    register char *val;
    int lineno;
    char buf[BUFSIZ];

 /* first set up the default machine name */
    (void) gethostname (defaultmachine, sizeof defaultmachine);
    if ((cf = fopen (conffile, "r")) == 0)
	return;
    lineno = 0;
    while (fgets (buf, sizeof buf, cf)) {
	lineno++;
	if (buf[0] == '#')	/* comment */
	    continue;
	if ((p = index (buf, '\n')) != 0)
	    *p = 0;
	if (buf[0] == 0)	/* ignore blank lines */
	    continue;
	if ((p = index (buf, '=')) == 0) {
	    p = "malformed line";
badconf:
	    error (0, 0, "\"%s\", line %d: bad conf file (%s)", conffile,
		    lineno, p);
	    error (1, 0, "consult newacct guru");
	    /*NOTREACHED*/
	}
	*p = 0;
	val = p + 1;
	while (isspace (*val))
	    val++;
	for (c = confitems; c -> c_name; c++)
	    if (*c -> c_name == *buf && strcmp (c -> c_name, buf) == 0)
		goto found;
	*p = '=';
	p = "unrecognized keyword";
	goto badconf;
found:
	if (strlen (val) >= c -> c_size) {
	    *p = '=';
	    p = "value too long";
	    goto badconf;
	}
	(void) strcpy (c -> c_value, val);
    }
    (void) fclose (cf);
}

X/*
 * Turn the whitespace-separated list of machine names into a list
 * of pointers.
 */
MakeMachines () {
    register char *p, **l;

    l = Machines;
    p = machinenames;
    while (*p) {
	*l++ = p;
	while (*p) {		/* while there's more of this name */
	    if (isspace (*p)) {
		*p++ = 0;
		while (isspace (*p))
		    p++;
		break;		/* end there's more of this name */
	    }
	    p++;
	}
    }
    *l = 0;
}

FILE   *wordfile;

X/*
 * Perform a case independent binary search for target in the (sorted)
 * word list in file filenam.
 */
lookup (target, filenam)
char *target, *filenam; {
    register int    c;
    long    high,
            low,
            mid;
    char    key[10],
	    word[80];
#define CMP(s,t) ((*s) == (*t) ? strcmp (s, t) : (*s) - (*t))

    if (wordfile != NULL)
	fclose (wordfile);
    if ((wordfile = fopen (filenam, "r")) == NULL)
	return 0;
    strcpy (key, target);
    lower (key);
    low = 0;
    fseek (wordfile, 0L, 2);
    high = ftell (wordfile);
    for (;;) {
	mid = (high + low) >> 1;
	fseek (wordfile, mid, 0);
	do {
	    mid++;
	    c = getc (wordfile);
	} while (c != EOF && c != '\n');
	if (!getword (word))
	    break;
	c = CMP (key, word);
	if (c == 0)	/* found it */
	    return 1;
	if (c < 0) {	/* too far */
	    if (high == mid)
		break;	/* stop spinning the wheels */
	    high = mid;
	}
	else		/* not far enough */
	    low = mid;
    }
 /* at this point we've narrowed the range as much as we can; now
    search until either we find the word, or we go past the key. */
    fseek (wordfile, low, 0);
    for (;;) {
	if (!getword (word))
	    return (0);
	c = CMP (key, word);
	if (c < 0)
	    return 0;
	if (c == 0)
	    return 1;
    }
#undef CMP
}

X/*
 * Read a word and lowercasify it.
 */
getword (w)
char *w; {
    register char  *p = w;
    register int    c;

    while ((c = getc (wordfile)) != '\n') {
	if (c == EOF)
	    if (p == w)
		return 0;
	    else
		break;
	*p++ = c;
    }
    *p = 0;
    lower (w);
    return 1;
}

X/*
 * Lowercasify a word.
 */
lower (s)
register char *s; {
    register int c;

    while ((c = *s) != 0) {
	if (isupper (c))
	    *s = tolower (c);
	s++;
    }
}
//go.sysin dd *
if [ `wc -c < newacct.c` != 16480 ]; then
	made=FALSE
	/bin/echo 'error transmitting "newacct.c" --'
	/bin/echo 'length should be 16480, not' `wc -c < newacct.c`
else
	made=TRUE
fi
if [ $all = TRUE ]; then
	/bin/echo '	Changing owner to "bin"'
	/etc/chown bin newacct.c
else
	/bin/echo '	Original owner was "bin"'
fi
if [ $made = TRUE ]; then
	/bin/chmod 644 newacct.c
	/bin/echo -n '	'; /bin/ls -ld newacct.c
fi
/bin/echo 'Extracting newacct.conf'
sed 's/^X//' <<'//go.sysin dd *' >newacct.conf
# This file controls the operation of the newacct program.
# Keywords:
#	mailcmd home group defaultmachine machines dictionary
#	localdict badpasswds

mailcmd=/usr/ucb/mail -s 'account request' account-master
home=	/usr
group=	misc
machines=gymble gyre mimsy tove
//go.sysin dd *
if [ `wc -c < newacct.conf` != 264 ]; then
	made=FALSE
	/bin/echo 'error transmitting "newacct.conf" --'
	/bin/echo 'length should be 264, not' `wc -c < newacct.conf`
else
	made=TRUE
fi
if [ $all = TRUE ]; then
	/bin/echo '	Changing owner to "root"'
	/etc/chown root newacct.conf
else
	/bin/echo '	Original owner was "root"'
fi
if [ $made = TRUE ]; then
	/bin/chmod 644 newacct.conf
	/bin/echo -n '	'; /bin/ls -ld newacct.conf
fi
/bin/echo 'Extracting error.c'
sed 's/^X//' <<'//go.sysin dd *' >error.c
#include <stdio.h>

char *_argv0;			/* argv[0], set by C startup code */

X/*
 * error - University of Maryland specific (sigh)
 *
 * Useful for printing error messages.  Will print the program name
 * and (optionally) the system error associated with the values in
 * <errno.h>.
 *
 * Note that the type (and even the existence!) of ``arg'' is undefined.
 */
error(quit, e, fmt, arg)
	int quit;
	register int e;
	char *fmt;
{
	extern char *sys_errlist[];
	extern int sys_nerr;
	register char *p = _argv0;

	if (p != NULL) {
#ifdef optional
		char *s, *rindex();

		if ((s = rindex(p, '/')) != NULL)
			p = s + 1;
#endif
		(void) fprintf(stderr, "%s: ", p);
	}
	_doprnt(fmt, &arg, stderr);	/* magic */
	if (e > 0) {
		if (e < sys_nerr)
			(void) fprintf(stderr, ": %s", sys_errlist[e]);
		else
			(void) fprintf(stderr, ": unknown error number %d", e);
	}
	(void) putc('\n', stderr);
	(void) fflush(stderr);
	if (quit)
		exit(quit);
}
//go.sysin dd *
if [ `wc -c < error.c` != 934 ]; then
	made=FALSE
	/bin/echo 'error transmitting "error.c" --'
	/bin/echo 'length should be 934, not' `wc -c < error.c`
else
	made=TRUE
fi
if [ $all = TRUE ]; then
	/bin/echo '	Changing owner to "bin"'
	/etc/chown bin error.c
else
	/bin/echo '	Original owner was "bin"'
fi
if [ $made = TRUE ]; then
	/bin/chmod 664 error.c
	/bin/echo -n '	'; /bin/ls -ld error.c
fi
-- 
In-Real-Life: Chris Torek, Univ of MD Comp Sci Dept (+1 301 454 4251)
UUCP:	seismo!umcp-cs!chris
CSNet:	chris@umcp-cs		ARPA:	chris@maryland