lex.c revision 77274
1214979Sdes/*
298937Sdes * Copyright (c) 1980, 1993
398937Sdes *	The Regents of the University of California.  All rights reserved.
498937Sdes *
598937Sdes * Redistribution and use in source and binary forms, with or without
698937Sdes * modification, are permitted provided that the following conditions
798937Sdes * are met:
898937Sdes * 1. Redistributions of source code must retain the above copyright
998937Sdes *    notice, this list of conditions and the following disclaimer.
1098937Sdes * 2. Redistributions in binary form must reproduce the above copyright
1198937Sdes *    notice, this list of conditions and the following disclaimer in the
1298937Sdes *    documentation and/or other materials provided with the distribution.
1398937Sdes * 3. All advertising materials mentioning features or use of this software
14164146Sdes *    must display the following acknowledgement:
1598937Sdes *	This product includes software developed by the University of
1698937Sdes *	California, Berkeley and its contributors.
1798937Sdes * 4. Neither the name of the University nor the names of its contributors
1898937Sdes *    may be used to endorse or promote products derived from this software
1998937Sdes *    without specific prior written permission.
2098937Sdes *
2198937Sdes * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
2298937Sdes * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
2398937Sdes * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
2498937Sdes * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
2598937Sdes * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
2698937Sdes * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
2798937Sdes * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28204861Sdes * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
2998937Sdes * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
3098937Sdes * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
3199060Sdes * SUCH DAMAGE.
32113908Sdes *
3398937Sdes * $FreeBSD: head/usr.bin/mail/lex.c 77274 2001-05-27 20:26:22Z mikeh $
3498937Sdes */
3598937Sdes
3698937Sdes#ifndef lint
3798937Sdes#if 0
3898937Sdesstatic char sccsid[] = "@(#)lex.c	8.1 (Berkeley) 6/6/93";
39204861Sdes#endif
4098937Sdesstatic const char rcsid[] =
4198937Sdes  "$FreeBSD: head/usr.bin/mail/lex.c 77274 2001-05-27 20:26:22Z mikeh $";
4298937Sdes#endif /* not lint */
4398937Sdes
4498937Sdes#include "rcv.h"
4598937Sdes#include <errno.h>
4698937Sdes#include <fcntl.h>
4798937Sdes#include "extern.h"
4898937Sdes
49162852Sdes/*
50146998Sdes * Mail -- a mail program
5198937Sdes *
52124208Sdes * Lexical processing of commands.
5398937Sdes */
5498937Sdes
5598937Sdesconst char	*prompt = "& ";
56113908Sdes
5798937Sdesextern const struct cmd cmdtab[];
5898937Sdesextern const char *version;
5998937Sdes
6098937Sdes/*
6198937Sdes * Set up editing on the given file name.
6298937Sdes * If the first character of name is %, we are considered to be
6398937Sdes * editing the file, otherwise we are reading our mail which has
6498937Sdes * signficance for mbox and so forth.
65204861Sdes */
6698937Sdesint
67162852Sdessetfile(name)
68126274Sdes	char *name;
69126274Sdes{
70126274Sdes	FILE *ibuf;
71162852Sdes	int i, fd;
72180750Sdes	struct stat stb;
73137015Sdes	char isedit = *name != '%';
74137015Sdes	char *who = name[1] ? name + 1 : myname;
75204861Sdes	char tempname[PATHSIZE];
76204861Sdes	static int shudclob;
77204861Sdes
7898937Sdes	if ((name = expand(name)) == NULL)
79113908Sdes		return (-1);
80197670Sdes
81204861Sdes	if ((ibuf = Fopen(name, "r")) == NULL) {
8298937Sdes		if (!isedit && errno == ENOENT)
83113908Sdes			goto nomail;
84137015Sdes		warn("%s", name);
85113908Sdes		return (-1);
86113908Sdes	}
87113908Sdes
88189006Sdes	if (fstat(fileno(ibuf), &stb) < 0) {
89137015Sdes		warn("fstat");
90124208Sdes		(void)Fclose(ibuf);
91124208Sdes		return (-1);
92146998Sdes	}
93197670Sdes
94204861Sdes	if (S_ISDIR(stb.st_mode) || !S_ISREG(stb.st_mode)) {
9598937Sdes		(void)Fclose(ibuf);
96204861Sdes		errno = S_ISDIR(stb.st_mode) ? EISDIR : EINVAL;
97204861Sdes		warn("%s", name);
9898937Sdes		return (-1);
9998937Sdes	}
10098937Sdes
10198937Sdes	/*
10298937Sdes	 * Looks like all will be well.  We must now relinquish our
10398937Sdes	 * hold on the current set of stuff.  Must hold signals
104113908Sdes	 * while we are reading the new file, else we will ruin
105113908Sdes	 * the message[] data structure.
106113908Sdes	 */
107113908Sdes
108113908Sdes	holdsigs();
109113908Sdes	if (shudclob)
110113908Sdes		quit();
111113908Sdes
112113908Sdes	/*
113113908Sdes	 * Copy the messages into /tmp
114180750Sdes	 * and set pointers.
115113908Sdes	 */
116162852Sdes
117113908Sdes	readonly = 0;
118113908Sdes	if ((i = open(name, 1)) < 0)
119113908Sdes		readonly++;
12098937Sdes	else
121113908Sdes		(void)close(i);
12298937Sdes	if (shudclob) {
123124208Sdes		(void)fclose(itf);
12498937Sdes		(void)fclose(otf);
125124208Sdes	}
126124208Sdes	shudclob = 1;
127124208Sdes	edit = isedit;
12898937Sdes	strlcpy(prevfile, mailname, sizeof(prevfile));
12998937Sdes	if (name != mailname)
13098937Sdes		strlcpy(mailname, name, sizeof(mailname));
13198937Sdes	mailsize = fsize(ibuf);
13298937Sdes	(void)snprintf(tempname, sizeof(tempname),
13398937Sdes	    "%s/mail.RxXXXXXXXXXX", tmpdir);
13498937Sdes	if ((fd = mkstemp(tempname)) == -1 || (otf = fdopen(fd, "w")) == NULL)
13598937Sdes		err(1, "%s", tempname);
13698937Sdes	(void)fcntl(fileno(otf), F_SETFD, 1);
13798937Sdes	if ((itf = fopen(tempname, "r")) == NULL)
13898937Sdes		err(1, "%s", tempname);
13998937Sdes	(void)fcntl(fileno(itf), F_SETFD, 1);
14098937Sdes	(void)rm(tempname);
14198937Sdes	setptr(ibuf);
14298937Sdes	setmsize(msgCount);
14398937Sdes	(void)Fclose(ibuf);
14498937Sdes	relsesigs();
145180744Sdes	sawcom = 0;
14698937Sdes	if (!edit && msgCount == 0) {
147113908Sdesnomail:
148157016Sdes		fprintf(stderr, "No mail for %s\n", who);
14998937Sdes		return (-1);
15098937Sdes	}
151126274Sdes	return (0);
15298937Sdes}
153204861Sdes
154204861Sdesint	*msgvec;
15598937Sdesint	reset_on_stop;			/* do a reset() if stopped */
15698937Sdes
157126274Sdes/*
15898937Sdes * Interpret user commands one by one.  If standard input is not a tty,
159204861Sdes * print no prompt.
160197670Sdes */
16198937Sdesvoid
162204861Sdescommands()
163214979Sdes{
164204861Sdes	int n, eofloop = 0;
165197670Sdes	char linebuf[LINESIZE];
166197670Sdes
16798937Sdes	if (!sourcing) {
168180746Sdes		if (signal(SIGINT, SIG_IGN) != SIG_IGN)
169180746Sdes			(void)signal(SIGINT, intr);
17098937Sdes		if (signal(SIGHUP, SIG_IGN) != SIG_IGN)
171126274Sdes			(void)signal(SIGHUP, hangup);
172146998Sdes		(void)signal(SIGTSTP, stop);
17398937Sdes		(void)signal(SIGTTOU, stop);
17498937Sdes		(void)signal(SIGTTIN, stop);
17598937Sdes	}
17698937Sdes	setexit();
17798937Sdes	for (;;) {
17898937Sdes		/*
17998937Sdes		 * Print the prompt, if needed.  Clear out
18098937Sdes		 * string space, and flush the output.
18198937Sdes		 */
18298937Sdes		if (!sourcing && value("interactive") != NULL) {
18398937Sdes			reset_on_stop = 1;
18498937Sdes			printf("%s", prompt);
18598937Sdes		}
18698937Sdes		(void)fflush(stdout);
18798937Sdes		sreset();
188124208Sdes		/*
18998937Sdes		 * Read a line of commands from the current input
19098937Sdes		 * and handle end of file specially.
19198937Sdes		 */
19298937Sdes		n = 0;
19398937Sdes		for (;;) {
19498937Sdes			if (readline(input, &linebuf[n], LINESIZE - n) < 0) {
19598937Sdes				if (n == 0)
19698937Sdes					n = -1;
197124208Sdes				break;
198124208Sdes			}
199124208Sdes			if ((n = strlen(linebuf)) == 0)
200124208Sdes				break;
201124208Sdes			n--;
202149749Sdes			if (linebuf[n] != '\\')
203124208Sdes				break;
204124208Sdes			linebuf[n++] = ' ';
205124208Sdes		}
206124208Sdes		reset_on_stop = 0;
207126274Sdes		if (n < 0) {
208146998Sdes				/* eof */
20998937Sdes			if (loading)
21098937Sdes				break;
211124208Sdes			if (sourcing) {
212126274Sdes				unstack();
213162852Sdes				continue;
214162852Sdes			}
215162852Sdes			if (value("interactive") != NULL &&
21698937Sdes			    value("ignoreeof") != NULL &&
21798937Sdes			    ++eofloop < 25) {
218137015Sdes				printf("Use \"quit\" to quit.\n");
219137015Sdes				continue;
220137015Sdes			}
22198937Sdes			break;
222124208Sdes		}
22398937Sdes		eofloop = 0;
22498937Sdes		if (execute(linebuf, 0))
225124208Sdes			break;
22698937Sdes	}
227124208Sdes}
228124208Sdes
22998937Sdes/*
23098937Sdes * Execute a single command.
23198937Sdes * Command functions return 0 for success, 1 for error, and -1
23298937Sdes * for abort.  A 1 or -1 aborts a load or source.  A -1 aborts
23398937Sdes * the interactive command loop.
23498937Sdes * Contxt is non-zero if called while composing mail.
23598937Sdes */
23698937Sdesint
23798937Sdesexecute(linebuf, contxt)
23898937Sdes	char linebuf[];
239126274Sdes	int contxt;
24098937Sdes{
241146998Sdes	char word[LINESIZE];
242146998Sdes	char *arglist[MAXARGC];
243146998Sdes	const struct cmd *com;
24498937Sdes	char *cp, *cp2;
245106121Sdes	int c, muvec[2];
246106121Sdes	int e = 1;
24799060Sdes
248204861Sdes	/*
24998937Sdes	 * Strip the white space away from the beginning
25098937Sdes	 * of the command, then scan out a word, which
25198937Sdes	 * consists of anything except digits and white space.
25298937Sdes	 *
25398937Sdes	 * Handle ! escapes differently to get the correct
25498937Sdes	 * lexical conventions.
25598937Sdes	 */
256106121Sdes
257207311Sdes	for (cp = linebuf; isspace(*cp); cp++)
258207311Sdes		;
259207311Sdes	if (*cp == '!') {
260207311Sdes		if (sourcing) {
261207311Sdes			printf("Can't \"!\" while sourcing\n");
262207311Sdes			goto out;
263207311Sdes		}
26498937Sdes		shell(cp+1);
265207311Sdes		return (0);
26698937Sdes	}
267207311Sdes	cp2 = word;
268207311Sdes	while (*cp != '\0' && strchr(" \t0123456789$^.:/-+*'\"", *cp) == NULL)
269207311Sdes		*cp2++ = *cp++;
270207311Sdes	*cp2 = '\0';
27198937Sdes
27298937Sdes	/*
27398937Sdes	 * Look up the command; if not found, bitch.
27498937Sdes	 * Normally, a blank command would map to the
27598937Sdes	 * first command in the table; while sourcing,
27698937Sdes	 * however, we ignore blank lines to eliminate
277180750Sdes	 * confusion.
27898937Sdes	 */
27998937Sdes
28098937Sdes	if (sourcing && *word == '\0')
28199060Sdes		return (0);
28298937Sdes	com = lex(word);
28398937Sdes	if (com == NULL) {
284113908Sdes		printf("Unknown command: \"%s\"\n", word);
285113908Sdes		goto out;
28698937Sdes	}
287204861Sdes
28898937Sdes	/*
289106121Sdes	 * See if we should execute the command -- if a conditional
29098937Sdes	 * we always execute it, otherwise, check the state of cond.
291106121Sdes	 */
292146998Sdes
293146998Sdes	if ((com->c_argtype & F) == 0)
29498937Sdes		if ((cond == CRCV && !rcvmode) || (cond == CSEND && rcvmode))
29598937Sdes			return (0);
29698937Sdes
297106121Sdes	/*
29898937Sdes	 * Process the arguments to the command, depending
29998937Sdes	 * on the type he expects.  Default to an error.
30098937Sdes	 * If we are sourcing an interactive command, it's
30198937Sdes	 * an error.
302106121Sdes	 */
30398937Sdes
30498937Sdes	if (!rcvmode && (com->c_argtype & M) == 0) {
30598937Sdes		printf("May not execute \"%s\" while sending\n",
30698937Sdes		    com->c_name);
307146998Sdes		goto out;
30898937Sdes	}
30998937Sdes	if (sourcing && com->c_argtype & I) {
31098937Sdes		printf("May not execute \"%s\" while sourcing\n",
31198937Sdes		    com->c_name);
31298937Sdes		goto out;
31398937Sdes	}
314106121Sdes	if (readonly && com->c_argtype & W) {
31598937Sdes		printf("May not execute \"%s\" -- message file is read only\n",
31698937Sdes		   com->c_name);
31798937Sdes		goto out;
31898937Sdes	}
31998937Sdes	if (contxt && com->c_argtype & R) {
32098937Sdes		printf("Cannot recursively invoke \"%s\"\n", com->c_name);
32198937Sdes		goto out;
32298937Sdes	}
32398937Sdes	switch (com->c_argtype & ~(F|P|I|M|T|W|R)) {
32498937Sdes	case MSGLIST:
32598937Sdes		/*
326106121Sdes		 * A message list defaulting to nearest forward
32798937Sdes		 * legal message.
32898937Sdes		 */
32998937Sdes		if (msgvec == 0) {
33098937Sdes			printf("Illegal use of \"message list\"\n");
33198937Sdes			break;
33298937Sdes		}
33398937Sdes		if ((c = getmsglist(cp, msgvec, com->c_msgflag)) < 0)
33498937Sdes			break;
33598937Sdes		if (c  == 0) {
33698937Sdes			*msgvec = first(com->c_msgflag, com->c_msgmask);
33798937Sdes			msgvec[1] = 0;
33898937Sdes		}
33998937Sdes		if (*msgvec == 0) {
34098937Sdes			printf("No applicable messages\n");
34198937Sdes			break;
34298937Sdes		}
34398937Sdes		e = (*com->c_func)(msgvec);
34498937Sdes		break;
34598937Sdes
34698937Sdes	case NDMLIST:
34798937Sdes		/*
34898937Sdes		 * A message list with no defaults, but no error
34998937Sdes		 * if none exist.
35098937Sdes		 */
35198937Sdes		if (msgvec == 0) {
35298937Sdes			printf("Illegal use of \"message list\"\n");
35398937Sdes			break;
35498937Sdes		}
35598937Sdes		if (getmsglist(cp, msgvec, com->c_msgflag) < 0)
35698937Sdes			break;
35798937Sdes		e = (*com->c_func)(msgvec);
35898937Sdes		break;
35998937Sdes
36098937Sdes	case STRLIST:
361126274Sdes		/*
36298937Sdes		 * Just the straight string, with
36398937Sdes		 * leading blanks removed.
36498937Sdes		 */
36598937Sdes		while (isspace(*cp))
36698937Sdes			cp++;
36798937Sdes		e = (*com->c_func)(cp);
36898937Sdes		break;
36998937Sdes
37098937Sdes	case RAWLIST:
37198937Sdes		/*
37298937Sdes		 * A vector of strings, in shell style.
373204861Sdes		 */
37498937Sdes		if ((c = getrawlist(cp, arglist,
37598937Sdes		    sizeof(arglist) / sizeof(*arglist))) < 0)
37698937Sdes			break;
37798937Sdes		if (c < com->c_minargs) {
37898937Sdes			printf("%s requires at least %d arg(s)\n",
37998937Sdes			    com->c_name, com->c_minargs);
38098937Sdes			break;
38198937Sdes		}
38298937Sdes		if (c > com->c_maxargs) {
38398937Sdes			printf("%s takes no more than %d arg(s)\n",
38498937Sdes			    com->c_name, com->c_maxargs);
38598937Sdes			break;
386204861Sdes		}
38798937Sdes		e = (*com->c_func)(arglist);
388124208Sdes		break;
389180746Sdes
390124208Sdes	case NOLIST:
391124208Sdes		/*
392124208Sdes		 * Just the constant zero, for exiting,
393146998Sdes		 * eg.
394137015Sdes		 */
395124208Sdes		e = (*com->c_func)(0);
396124208Sdes		break;
397124208Sdes
398124208Sdes	default:
399124208Sdes		errx(1, "Unknown argtype");
400204861Sdes	}
401124208Sdes
402124208Sdesout:
403124208Sdes	/*
404180746Sdes	 * Exit the current source file on
405180746Sdes	 * error.
406180750Sdes	 */
407180750Sdes	if (e) {
408124208Sdes		if (e < 0)
409124208Sdes			return (1);
410124208Sdes		if (loading)
411124208Sdes			return (1);
412124208Sdes		if (sourcing)
413124208Sdes			unstack();
414124208Sdes		return (0);
415126274Sdes	}
416124208Sdes	if (value("autoprint") != NULL && com->c_argtype & P)
417124208Sdes		if ((dot->m_flag & MDELETED) == 0) {
418124208Sdes			muvec[0] = dot - &message[0] + 1;
419124208Sdes			muvec[1] = 0;
420124208Sdes			type(muvec);
421204861Sdes		}
422124208Sdes	if (!sourcing && (com->c_argtype & T) == 0)
423124208Sdes		sawcom = 1;
424124208Sdes	return (0);
425180746Sdes}
426180746Sdes
427180750Sdes/*
428180750Sdes * Set the size of the message vector used to construct argument
429124208Sdes * lists to message list functions.
430180750Sdes */
431124208Sdesvoid
432162852Sdessetmsize(sz)
433162852Sdes	int sz;
434162852Sdes{
435124208Sdes
436146998Sdes	if (msgvec != 0)
437124208Sdes		(void)free(msgvec);
438124208Sdes	msgvec = calloc((unsigned)(sz + 1), sizeof(*msgvec));
439137015Sdes}
440146998Sdes
441146998Sdes/*
442146998Sdes * Find the correct command in the command table corresponding
443146998Sdes * to the passed command "word"
444146998Sdes */
445146998Sdes
446146998Sdes__const struct cmd *
447146998Sdeslex(word)
448146998Sdes	char word[];
449137015Sdes{
450137015Sdes	const struct cmd *cp;
451137015Sdes
452137015Sdes	/*
453137015Sdes	 * ignore trailing chars after `#'
454	 *
455	 * lines with beginning `#' are comments
456	 * spaces before `#' are ignored in execute()
457	 */
458
459	if (*word == '#')
460	    *(word+1) = '\0';
461
462
463	for (cp = &cmdtab[0]; cp->c_name != NULL; cp++)
464		if (isprefix(word, cp->c_name))
465			return (cp);
466	return (NULL);
467}
468
469/*
470 * Determine if as1 is a valid prefix of as2.
471 * Return true if yep.
472 */
473int
474isprefix(as1, as2)
475	const char *as1, *as2;
476{
477	const char *s1, *s2;
478
479	s1 = as1;
480	s2 = as2;
481	while (*s1++ == *s2)
482		if (*s2++ == '\0')
483			return (1);
484	return (*--s1 == '\0');
485}
486
487/*
488 * The following gets called on receipt of an interrupt.  This is
489 * to abort printout of a command, mainly.
490 * Dispatching here when command() is inactive crashes rcv.
491 * Close all open files except 0, 1, 2, and the temporary.
492 * Also, unstack all source files.
493 */
494
495int	inithdr;			/* am printing startup headers */
496
497/*ARGSUSED*/
498void
499intr(s)
500	int s;
501{
502
503	noreset = 0;
504	if (!inithdr)
505		sawcom++;
506	inithdr = 0;
507	while (sourcing)
508		unstack();
509
510	close_all_files();
511
512	if (image >= 0) {
513		(void)close(image);
514		image = -1;
515	}
516	fprintf(stderr, "Interrupt\n");
517	reset(0);
518}
519
520/*
521 * When we wake up after ^Z, reprint the prompt.
522 */
523void
524stop(s)
525	int s;
526{
527	sig_t old_action = signal(s, SIG_DFL);
528
529	(void)sigsetmask(sigblock(0) & ~sigmask(s));
530	(void)kill(0, s);
531	(void)sigblock(sigmask(s));
532	(void)signal(s, old_action);
533	if (reset_on_stop) {
534		reset_on_stop = 0;
535		reset(0);
536	}
537}
538
539/*
540 * Branch here on hangup signal and simulate "exit".
541 */
542/*ARGSUSED*/
543void
544hangup(s)
545	int s;
546{
547
548	/* nothing to do? */
549	exit(1);
550}
551
552/*
553 * Announce the presence of the current Mail version,
554 * give the message count, and print a header listing.
555 */
556void
557announce()
558{
559	int vec[2], mdot;
560
561	mdot = newfileinfo();
562	vec[0] = mdot;
563	vec[1] = 0;
564	dot = &message[mdot - 1];
565	if (msgCount > 0 && value("noheader") == NULL) {
566		inithdr++;
567		headers(vec);
568		inithdr = 0;
569	}
570}
571
572/*
573 * Announce information about the file we are editing.
574 * Return a likely place to set dot.
575 */
576int
577newfileinfo()
578{
579	struct message *mp;
580	int u, n, mdot, d, s;
581	char fname[PATHSIZE+1], zname[PATHSIZE+1], *ename;
582
583	for (mp = &message[0]; mp < &message[msgCount]; mp++)
584		if (mp->m_flag & MNEW)
585			break;
586	if (mp >= &message[msgCount])
587		for (mp = &message[0]; mp < &message[msgCount]; mp++)
588			if ((mp->m_flag & MREAD) == 0)
589				break;
590	if (mp < &message[msgCount])
591		mdot = mp - &message[0] + 1;
592	else
593		mdot = 1;
594	s = d = 0;
595	for (mp = &message[0], n = 0, u = 0; mp < &message[msgCount]; mp++) {
596		if (mp->m_flag & MNEW)
597			n++;
598		if ((mp->m_flag & MREAD) == 0)
599			u++;
600		if (mp->m_flag & MDELETED)
601			d++;
602		if (mp->m_flag & MSAVED)
603			s++;
604	}
605	ename = mailname;
606	if (getfold(fname, sizeof(fname) - 1) >= 0) {
607		strcat(fname, "/");
608		if (strncmp(fname, mailname, strlen(fname)) == 0) {
609			(void)snprintf(zname, sizeof(zname), "+%s",
610			    mailname + strlen(fname));
611			ename = zname;
612		}
613	}
614	printf("\"%s\": ", ename);
615	if (msgCount == 1)
616		printf("1 message");
617	else
618		printf("%d messages", msgCount);
619	if (n > 0)
620		printf(" %d new", n);
621	if (u-n > 0)
622		printf(" %d unread", u);
623	if (d > 0)
624		printf(" %d deleted", d);
625	if (s > 0)
626		printf(" %d saved", s);
627	if (readonly)
628		printf(" [Read only]");
629	printf("\n");
630	return (mdot);
631}
632
633/*
634 * Print the current version number.
635 */
636
637/*ARGSUSED*/
638int
639pversion(e)
640	int e;
641{
642
643	printf("Version %s\n", version);
644	return (0);
645}
646
647/*
648 * Load a file of user definitions.
649 */
650void
651load(name)
652	char *name;
653{
654	FILE *in, *oldin;
655
656	if ((in = Fopen(name, "r")) == NULL)
657		return;
658	oldin = input;
659	input = in;
660	loading = 1;
661	sourcing = 1;
662	commands();
663	loading = 0;
664	sourcing = 0;
665	input = oldin;
666	(void)Fclose(in);
667}
668