xref: /freebsd/usr.bin/mail/lex.c (revision 0957b409)
1 /*-
2  * SPDX-License-Identifier: BSD-3-Clause
3  *
4  * Copyright (c) 1980, 1993
5  *	The Regents of the University of California.  All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  * 3. Neither the name of the University nor the names of its contributors
16  *    may be used to endorse or promote products derived from this software
17  *    without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
20  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
23  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29  * SUCH DAMAGE.
30  */
31 
32 #ifndef lint
33 #if 0
34 static char sccsid[] = "@(#)lex.c	8.2 (Berkeley) 4/20/95";
35 #endif
36 #endif /* not lint */
37 #include <sys/cdefs.h>
38 __FBSDID("$FreeBSD$");
39 
40 #include "rcv.h"
41 #include <errno.h>
42 #include <fcntl.h>
43 #include "extern.h"
44 
45 /*
46  * Mail -- a mail program
47  *
48  * Lexical processing of commands.
49  */
50 
51 static const char	*prompt = "& ";
52 
53 extern const struct cmd cmdtab[];
54 extern const char *version;
55 
56 /*
57  * Set up editing on the given file name.
58  * If the first character of name is %, we are considered to be
59  * editing the file, otherwise we are reading our mail which has
60  * signficance for mbox and so forth.
61  *
62  * If the -e option is being passed to mail, this function has a
63  * tri-state return code: -1 on error, 0 on no mail, 1 if there is
64  * mail.
65  */
66 int
67 setfile(char *name)
68 {
69 	FILE *ibuf;
70 	int checkmode, i, fd;
71 	struct stat stb;
72 	char isedit = *name != '%' || getuserid(myname) != getuid();
73 	char *who = name[1] ? name + 1 : myname;
74 	char tempname[PATHSIZE];
75 	static int shudclob;
76 
77 	checkmode = value("checkmode") != NULL;
78 	if ((name = expand(name)) == NULL)
79 		return (-1);
80 
81 	if ((ibuf = Fopen(name, "r")) == NULL) {
82 		if (!isedit && errno == ENOENT)
83 			goto nomail;
84 		warn("%s", name);
85 		return (-1);
86 	}
87 
88 	if (fstat(fileno(ibuf), &stb) < 0) {
89 		warn("fstat");
90 		(void)Fclose(ibuf);
91 		return (-1);
92 	}
93 
94 	if (S_ISDIR(stb.st_mode) || !S_ISREG(stb.st_mode)) {
95 		(void)Fclose(ibuf);
96 		errno = S_ISDIR(stb.st_mode) ? EISDIR : EINVAL;
97 		warn("%s", name);
98 		return (-1);
99 	}
100 
101 	/*
102 	 * Looks like all will be well.  We must now relinquish our
103 	 * hold on the current set of stuff.  Must hold signals
104 	 * while we are reading the new file, else we will ruin
105 	 * the message[] data structure.
106 	 */
107 
108 	holdsigs();
109 	if (shudclob)
110 		quit();
111 
112 	/*
113 	 * Copy the messages into /tmp
114 	 * and set pointers.
115 	 */
116 
117 	readonly = 0;
118 	if ((i = open(name, 1)) < 0)
119 		readonly++;
120 	else
121 		(void)close(i);
122 	if (shudclob) {
123 		(void)fclose(itf);
124 		(void)fclose(otf);
125 	}
126 	shudclob = 1;
127 	edit = isedit;
128 	strlcpy(prevfile, mailname, sizeof(prevfile));
129 	if (name != mailname)
130 		strlcpy(mailname, name, sizeof(mailname));
131 	mailsize = fsize(ibuf);
132 	(void)snprintf(tempname, sizeof(tempname),
133 	    "%s/mail.RxXXXXXXXXXX", tmpdir);
134 	if ((fd = mkstemp(tempname)) == -1 || (otf = fdopen(fd, "w")) == NULL)
135 		err(1, "%s", tempname);
136 	(void)fcntl(fileno(otf), F_SETFD, 1);
137 	if ((itf = fopen(tempname, "r")) == NULL)
138 		err(1, "%s", tempname);
139 	(void)fcntl(fileno(itf), F_SETFD, 1);
140 	(void)rm(tempname);
141 	setptr(ibuf, 0);
142 	setmsize(msgCount);
143 	/*
144 	 * New mail may have arrived while we were reading
145 	 * the mail file, so reset mailsize to be where
146 	 * we really are in the file...
147 	 */
148 	mailsize = ftello(ibuf);
149 	(void)Fclose(ibuf);
150 	relsesigs();
151 	sawcom = 0;
152 
153 	if ((checkmode || !edit) && msgCount == 0) {
154 nomail:
155 		if (!checkmode) {
156 			fprintf(stderr, "No mail for %s\n", who);
157 			return (-1);
158 		} else
159 			return (0);
160 	}
161 	return (checkmode ? 1 : 0);
162 }
163 
164 /*
165  * Incorporate any new mail that has arrived since we first
166  * started reading mail.
167  */
168 int
169 incfile(void)
170 {
171 	off_t newsize;
172 	int omsgCount = msgCount;
173 	FILE *ibuf;
174 
175 	ibuf = Fopen(mailname, "r");
176 	if (ibuf == NULL)
177 		return (-1);
178 	holdsigs();
179 	newsize = fsize(ibuf);
180 	if (newsize == 0)
181 		return (-1);		/* mail box is now empty??? */
182 	if (newsize < mailsize)
183 		return (-1);		/* mail box has shrunk??? */
184 	if (newsize == mailsize)
185 		return (0);		/* no new mail */
186 	setptr(ibuf, mailsize);
187 	setmsize(msgCount);
188 	mailsize = ftello(ibuf);
189 	(void)Fclose(ibuf);
190 	relsesigs();
191 	return (msgCount - omsgCount);
192 }
193 
194 static int	*msgvec;
195 static int	reset_on_stop;		/* do a reset() if stopped */
196 
197 /*
198  * Interpret user commands one by one.  If standard input is not a tty,
199  * print no prompt.
200  */
201 void
202 commands(void)
203 {
204 	int n, eofloop = 0;
205 	char linebuf[LINESIZE];
206 
207 	if (!sourcing) {
208 		if (signal(SIGINT, SIG_IGN) != SIG_IGN)
209 			(void)signal(SIGINT, intr);
210 		if (signal(SIGHUP, SIG_IGN) != SIG_IGN)
211 			(void)signal(SIGHUP, hangup);
212 		(void)signal(SIGTSTP, stop);
213 		(void)signal(SIGTTOU, stop);
214 		(void)signal(SIGTTIN, stop);
215 	}
216 	setexit();
217 	for (;;) {
218 		/*
219 		 * Print the prompt, if needed.  Clear out
220 		 * string space, and flush the output.
221 		 */
222 		if (!sourcing && value("interactive") != NULL) {
223 			if ((value("autoinc") != NULL) && (incfile() > 0))
224 				printf("New mail has arrived.\n");
225 			reset_on_stop = 1;
226 			printf("%s", prompt);
227 		}
228 		(void)fflush(stdout);
229 		sreset();
230 		/*
231 		 * Read a line of commands from the current input
232 		 * and handle end of file specially.
233 		 */
234 		n = 0;
235 		for (;;) {
236 			if (readline(input, &linebuf[n], LINESIZE - n) < 0) {
237 				if (n == 0)
238 					n = -1;
239 				break;
240 			}
241 			if ((n = strlen(linebuf)) == 0)
242 				break;
243 			n--;
244 			if (linebuf[n] != '\\')
245 				break;
246 			linebuf[n++] = ' ';
247 		}
248 		reset_on_stop = 0;
249 		if (n < 0) {
250 				/* eof */
251 			if (loading)
252 				break;
253 			if (sourcing) {
254 				unstack();
255 				continue;
256 			}
257 			if (value("interactive") != NULL &&
258 			    value("ignoreeof") != NULL &&
259 			    ++eofloop < 25) {
260 				printf("Use \"quit\" to quit.\n");
261 				continue;
262 			}
263 			break;
264 		}
265 		eofloop = 0;
266 		if (execute(linebuf, 0))
267 			break;
268 	}
269 }
270 
271 /*
272  * Execute a single command.
273  * Command functions return 0 for success, 1 for error, and -1
274  * for abort.  A 1 or -1 aborts a load or source.  A -1 aborts
275  * the interactive command loop.
276  * Contxt is non-zero if called while composing mail.
277  */
278 int
279 execute(char linebuf[], int contxt)
280 {
281 	char word[LINESIZE];
282 	char *arglist[MAXARGC];
283 	const struct cmd *com;
284 	char *cp, *cp2;
285 	int c, muvec[2];
286 	int e = 1;
287 
288 	/*
289 	 * Strip the white space away from the beginning
290 	 * of the command, then scan out a word, which
291 	 * consists of anything except digits and white space.
292 	 *
293 	 * Handle ! escapes differently to get the correct
294 	 * lexical conventions.
295 	 */
296 
297 	for (cp = linebuf; isspace((unsigned char)*cp); cp++)
298 		;
299 	if (*cp == '!') {
300 		if (sourcing) {
301 			printf("Can't \"!\" while sourcing\n");
302 			goto out;
303 		}
304 		shell(cp+1);
305 		return (0);
306 	}
307 	cp2 = word;
308 	while (*cp != '\0' && strchr(" \t0123456789$^.:/-+*'\"", *cp) == NULL)
309 		*cp2++ = *cp++;
310 	*cp2 = '\0';
311 
312 	/*
313 	 * Look up the command; if not found, bitch.
314 	 * Normally, a blank command would map to the
315 	 * first command in the table; while sourcing,
316 	 * however, we ignore blank lines to eliminate
317 	 * confusion.
318 	 */
319 
320 	if (sourcing && *word == '\0')
321 		return (0);
322 	com = lex(word);
323 	if (com == NULL) {
324 		printf("Unknown command: \"%s\"\n", word);
325 		goto out;
326 	}
327 
328 	/*
329 	 * See if we should execute the command -- if a conditional
330 	 * we always execute it, otherwise, check the state of cond.
331 	 */
332 
333 	if ((com->c_argtype & F) == 0)
334 		if ((cond == CRCV && !rcvmode) || (cond == CSEND && rcvmode))
335 			return (0);
336 
337 	/*
338 	 * Process the arguments to the command, depending
339 	 * on the type he expects.  Default to an error.
340 	 * If we are sourcing an interactive command, it's
341 	 * an error.
342 	 */
343 
344 	if (!rcvmode && (com->c_argtype & M) == 0) {
345 		printf("May not execute \"%s\" while sending\n",
346 		    com->c_name);
347 		goto out;
348 	}
349 	if (sourcing && com->c_argtype & I) {
350 		printf("May not execute \"%s\" while sourcing\n",
351 		    com->c_name);
352 		goto out;
353 	}
354 	if (readonly && com->c_argtype & W) {
355 		printf("May not execute \"%s\" -- message file is read only\n",
356 		   com->c_name);
357 		goto out;
358 	}
359 	if (contxt && com->c_argtype & R) {
360 		printf("Cannot recursively invoke \"%s\"\n", com->c_name);
361 		goto out;
362 	}
363 	switch (com->c_argtype & ~(F|P|I|M|T|W|R)) {
364 	case MSGLIST:
365 		/*
366 		 * A message list defaulting to nearest forward
367 		 * legal message.
368 		 */
369 		if (msgvec == 0) {
370 			printf("Illegal use of \"message list\"\n");
371 			break;
372 		}
373 		if ((c = getmsglist(cp, msgvec, com->c_msgflag)) < 0)
374 			break;
375 		if (c  == 0) {
376 			*msgvec = first(com->c_msgflag, com->c_msgmask);
377 			msgvec[1] = 0;
378 		}
379 		if (*msgvec == 0) {
380 			printf("No applicable messages\n");
381 			break;
382 		}
383 		e = (*com->c_func)(msgvec);
384 		break;
385 
386 	case NDMLIST:
387 		/*
388 		 * A message list with no defaults, but no error
389 		 * if none exist.
390 		 */
391 		if (msgvec == 0) {
392 			printf("Illegal use of \"message list\"\n");
393 			break;
394 		}
395 		if (getmsglist(cp, msgvec, com->c_msgflag) < 0)
396 			break;
397 		e = (*com->c_func)(msgvec);
398 		break;
399 
400 	case STRLIST:
401 		/*
402 		 * Just the straight string, with
403 		 * leading blanks removed.
404 		 */
405 		while (isspace((unsigned char)*cp))
406 			cp++;
407 		e = (*com->c_func)(cp);
408 		break;
409 
410 	case RAWLIST:
411 		/*
412 		 * A vector of strings, in shell style.
413 		 */
414 		if ((c = getrawlist(cp, arglist,
415 		    sizeof(arglist) / sizeof(*arglist))) < 0)
416 			break;
417 		if (c < com->c_minargs) {
418 			printf("%s requires at least %d arg(s)\n",
419 			    com->c_name, com->c_minargs);
420 			break;
421 		}
422 		if (c > com->c_maxargs) {
423 			printf("%s takes no more than %d arg(s)\n",
424 			    com->c_name, com->c_maxargs);
425 			break;
426 		}
427 		e = (*com->c_func)(arglist);
428 		break;
429 
430 	case NOLIST:
431 		/*
432 		 * Just the constant zero, for exiting,
433 		 * eg.
434 		 */
435 		e = (*com->c_func)(0);
436 		break;
437 
438 	default:
439 		errx(1, "Unknown argtype");
440 	}
441 
442 out:
443 	/*
444 	 * Exit the current source file on
445 	 * error.
446 	 */
447 	if (e) {
448 		if (e < 0)
449 			return (1);
450 		if (loading)
451 			return (1);
452 		if (sourcing)
453 			unstack();
454 		return (0);
455 	}
456 	if (com == NULL)
457 		return (0);
458 	if (value("autoprint") != NULL && com->c_argtype & P)
459 		if ((dot->m_flag & MDELETED) == 0) {
460 			muvec[0] = dot - &message[0] + 1;
461 			muvec[1] = 0;
462 			type(muvec);
463 		}
464 	if (!sourcing && (com->c_argtype & T) == 0)
465 		sawcom = 1;
466 	return (0);
467 }
468 
469 /*
470  * Set the size of the message vector used to construct argument
471  * lists to message list functions.
472  */
473 void
474 setmsize(int sz)
475 {
476 
477 	if (msgvec != NULL)
478 		(void)free(msgvec);
479 	msgvec = calloc((unsigned)(sz + 1), sizeof(*msgvec));
480 }
481 
482 /*
483  * Find the correct command in the command table corresponding
484  * to the passed command "word"
485  */
486 
487 const struct cmd *
488 lex(char word[])
489 {
490 	const struct cmd *cp;
491 
492 	/*
493 	 * ignore trailing chars after `#'
494 	 *
495 	 * lines with beginning `#' are comments
496 	 * spaces before `#' are ignored in execute()
497 	 */
498 
499 	if (*word == '#')
500 	    *(word+1) = '\0';
501 
502 
503 	for (cp = &cmdtab[0]; cp->c_name != NULL; cp++)
504 		if (isprefix(word, cp->c_name))
505 			return (cp);
506 	return (NULL);
507 }
508 
509 /*
510  * Determine if as1 is a valid prefix of as2.
511  * Return true if yep.
512  */
513 int
514 isprefix(const char *as1, const char *as2)
515 {
516 	const char *s1, *s2;
517 
518 	s1 = as1;
519 	s2 = as2;
520 	while (*s1++ == *s2)
521 		if (*s2++ == '\0')
522 			return (1);
523 	return (*--s1 == '\0');
524 }
525 
526 /*
527  * The following gets called on receipt of an interrupt.  This is
528  * to abort printout of a command, mainly.
529  * Dispatching here when command() is inactive crashes rcv.
530  * Close all open files except 0, 1, 2, and the temporary.
531  * Also, unstack all source files.
532  */
533 
534 static int	inithdr;		/* am printing startup headers */
535 
536 void
537 intr(int s __unused)
538 {
539 
540 	noreset = 0;
541 	if (!inithdr)
542 		sawcom++;
543 	inithdr = 0;
544 	while (sourcing)
545 		unstack();
546 
547 	close_all_files();
548 
549 	if (image >= 0) {
550 		(void)close(image);
551 		image = -1;
552 	}
553 	fprintf(stderr, "Interrupt\n");
554 	reset(0);
555 }
556 
557 /*
558  * When we wake up after ^Z, reprint the prompt.
559  */
560 void
561 stop(int s)
562 {
563 	sig_t old_action = signal(s, SIG_DFL);
564 	sigset_t nset;
565 
566 	(void)sigemptyset(&nset);
567 	(void)sigaddset(&nset, s);
568 	(void)sigprocmask(SIG_UNBLOCK, &nset, NULL);
569 	(void)kill(0, s);
570 	(void)sigprocmask(SIG_BLOCK, &nset, NULL);
571 	(void)signal(s, old_action);
572 	if (reset_on_stop) {
573 		reset_on_stop = 0;
574 		reset(0);
575 	}
576 }
577 
578 /*
579  * Branch here on hangup signal and simulate "exit".
580  */
581 void
582 hangup(int s __unused)
583 {
584 
585 	/* nothing to do? */
586 	exit(1);
587 }
588 
589 /*
590  * Announce the presence of the current Mail version,
591  * give the message count, and print a header listing.
592  */
593 void
594 announce(void)
595 {
596 	int vec[2], mdot;
597 
598 	mdot = newfileinfo(0);
599 	vec[0] = mdot;
600 	vec[1] = 0;
601 	dot = &message[mdot - 1];
602 	if (msgCount > 0 && value("noheader") == NULL) {
603 		inithdr++;
604 		headers(vec);
605 		inithdr = 0;
606 	}
607 }
608 
609 /*
610  * Announce information about the file we are editing.
611  * Return a likely place to set dot.
612  */
613 int
614 newfileinfo(int omsgCount)
615 {
616 	struct message *mp;
617 	int u, n, mdot, d, s;
618 	char fname[PATHSIZE+1], zname[PATHSIZE+1], *ename;
619 
620 	for (mp = &message[omsgCount]; mp < &message[msgCount]; mp++)
621 		if (mp->m_flag & MNEW)
622 			break;
623 	if (mp >= &message[msgCount])
624 		for (mp = &message[omsgCount]; mp < &message[msgCount]; mp++)
625 			if ((mp->m_flag & MREAD) == 0)
626 				break;
627 	if (mp < &message[msgCount])
628 		mdot = mp - &message[0] + 1;
629 	else
630 		mdot = omsgCount + 1;
631 	s = d = 0;
632 	for (mp = &message[0], n = 0, u = 0; mp < &message[msgCount]; mp++) {
633 		if (mp->m_flag & MNEW)
634 			n++;
635 		if ((mp->m_flag & MREAD) == 0)
636 			u++;
637 		if (mp->m_flag & MDELETED)
638 			d++;
639 		if (mp->m_flag & MSAVED)
640 			s++;
641 	}
642 	ename = mailname;
643 	if (getfold(fname, sizeof(fname) - 1) >= 0) {
644 		strcat(fname, "/");
645 		if (strncmp(fname, mailname, strlen(fname)) == 0) {
646 			(void)snprintf(zname, sizeof(zname), "+%s",
647 			    mailname + strlen(fname));
648 			ename = zname;
649 		}
650 	}
651 	printf("\"%s\": ", ename);
652 	if (msgCount == 1)
653 		printf("1 message");
654 	else
655 		printf("%d messages", msgCount);
656 	if (n > 0)
657 		printf(" %d new", n);
658 	if (u-n > 0)
659 		printf(" %d unread", u);
660 	if (d > 0)
661 		printf(" %d deleted", d);
662 	if (s > 0)
663 		printf(" %d saved", s);
664 	if (readonly)
665 		printf(" [Read only]");
666 	printf("\n");
667 	return (mdot);
668 }
669 
670 /*
671  * Print the current version number.
672  */
673 
674 int
675 pversion(int e __unused)
676 {
677 
678 	printf("Version %s\n", version);
679 	return (0);
680 }
681 
682 /*
683  * Load a file of user definitions.
684  */
685 void
686 load(char *name)
687 {
688 	FILE *in, *oldin;
689 
690 	if ((in = Fopen(name, "r")) == NULL)
691 		return;
692 	oldin = input;
693 	input = in;
694 	loading = 1;
695 	sourcing = 1;
696 	commands();
697 	loading = 0;
698 	sourcing = 0;
699 	input = oldin;
700 	(void)Fclose(in);
701 }
702