xref: /freebsd/usr.bin/mail/collect.c (revision eb69d1f144a6fcc765d1b9d44a5ae8082353e70b)
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[] = "@(#)collect.c	8.2 (Berkeley) 4/19/94";
35 #endif
36 #endif /* not lint */
37 #include <sys/cdefs.h>
38 __FBSDID("$FreeBSD$");
39 
40 /*
41  * Mail -- a mail program
42  *
43  * Collect input from standard input, handling
44  * ~ escapes.
45  */
46 
47 #include "rcv.h"
48 #include <fcntl.h>
49 #include "extern.h"
50 
51 /*
52  * Read a message from standard input and return a read file to it
53  * or NULL on error.
54  */
55 
56 /*
57  * The following hokiness with global variables is so that on
58  * receipt of an interrupt signal, the partial message can be salted
59  * away on dead.letter.
60  */
61 
62 static	sig_t	saveint;		/* Previous SIGINT value */
63 static	sig_t	savehup;		/* Previous SIGHUP value */
64 static	sig_t	savetstp;		/* Previous SIGTSTP value */
65 static	sig_t	savettou;		/* Previous SIGTTOU value */
66 static	sig_t	savettin;		/* Previous SIGTTIN value */
67 static	FILE	*collf;			/* File for saving away */
68 static	int	hadintr;		/* Have seen one SIGINT so far */
69 
70 static	jmp_buf	colljmp;		/* To get back to work */
71 static	int	colljmp_p;		/* whether to long jump */
72 static	jmp_buf	collabort;		/* To end collection with error */
73 
74 FILE *
75 collect(struct header *hp, int printheaders)
76 {
77 	FILE *fbuf;
78 	int lc, cc, escape, eofcount, fd, c, t;
79 	char linebuf[LINESIZE], tempname[PATHSIZE], *cp, getsub;
80 	sigset_t nset;
81 	int longline, lastlong, rc;	/* So we don't make 2 or more lines
82 					   out of a long input line. */
83 
84 	collf = NULL;
85 	/*
86 	 * Start catching signals from here, but we're still die on interrupts
87 	 * until we're in the main loop.
88 	 */
89 	(void)sigemptyset(&nset);
90 	(void)sigaddset(&nset, SIGINT);
91 	(void)sigaddset(&nset, SIGHUP);
92 	(void)sigprocmask(SIG_BLOCK, &nset, NULL);
93 	if ((saveint = signal(SIGINT, SIG_IGN)) != SIG_IGN)
94 		(void)signal(SIGINT, collint);
95 	if ((savehup = signal(SIGHUP, SIG_IGN)) != SIG_IGN)
96 		(void)signal(SIGHUP, collhup);
97 	savetstp = signal(SIGTSTP, collstop);
98 	savettou = signal(SIGTTOU, collstop);
99 	savettin = signal(SIGTTIN, collstop);
100 	if (setjmp(collabort) || setjmp(colljmp)) {
101 		(void)rm(tempname);
102 		goto err;
103 	}
104 	(void)sigprocmask(SIG_UNBLOCK, &nset, NULL);
105 
106 	noreset++;
107 	(void)snprintf(tempname, sizeof(tempname),
108 	    "%s/mail.RsXXXXXXXXXX", tmpdir);
109 	if ((fd = mkstemp(tempname)) == -1 ||
110 	    (collf = Fdopen(fd, "w+")) == NULL) {
111 		warn("%s", tempname);
112 		goto err;
113 	}
114 	(void)rm(tempname);
115 
116 	/*
117 	 * If we are going to prompt for a subject,
118 	 * refrain from printing a newline after
119 	 * the headers (since some people mind).
120 	 */
121 	t = GTO|GSUBJECT|GCC|GNL;
122 	getsub = 0;
123 	if (hp->h_subject == NULL && value("interactive") != NULL &&
124 	    (value("ask") != NULL || value("asksub") != NULL))
125 		t &= ~GNL, getsub++;
126 	if (printheaders) {
127 		puthead(hp, stdout, t);
128 		(void)fflush(stdout);
129 	}
130 	if ((cp = value("escape")) != NULL)
131 		escape = *cp;
132 	else
133 		escape = ESCAPE;
134 	eofcount = 0;
135 	hadintr = 0;
136 	longline = 0;
137 
138 	if (!setjmp(colljmp)) {
139 		if (getsub)
140 			grabh(hp, GSUBJECT);
141 	} else {
142 		/*
143 		 * Come here for printing the after-signal message.
144 		 * Duplicate messages won't be printed because
145 		 * the write is aborted if we get a SIGTTOU.
146 		 */
147 cont:
148 		if (hadintr) {
149 			(void)fflush(stdout);
150 			fprintf(stderr,
151 			"\n(Interrupt -- one more to kill letter)\n");
152 		} else {
153 			printf("(continue)\n");
154 			(void)fflush(stdout);
155 		}
156 	}
157 	for (;;) {
158 		colljmp_p = 1;
159 		c = readline(stdin, linebuf, LINESIZE);
160 		colljmp_p = 0;
161 		if (c < 0) {
162 			if (value("interactive") != NULL &&
163 			    value("ignoreeof") != NULL && ++eofcount < 25) {
164 				printf("Use \".\" to terminate letter\n");
165 				continue;
166 			}
167 			break;
168 		}
169 		lastlong = longline;
170 		longline = c == LINESIZE - 1;
171 		eofcount = 0;
172 		hadintr = 0;
173 		if (linebuf[0] == '.' && linebuf[1] == '\0' &&
174 		    value("interactive") != NULL && !lastlong &&
175 		    (value("dot") != NULL || value("ignoreeof") != NULL))
176 			break;
177 		if (linebuf[0] != escape || value("interactive") == NULL ||
178 		    lastlong) {
179 			if (putline(collf, linebuf, !longline) < 0)
180 				goto err;
181 			continue;
182 		}
183 		c = linebuf[1];
184 		switch (c) {
185 		default:
186 			/*
187 			 * On double escape, just send the single one.
188 			 * Otherwise, it's an error.
189 			 */
190 			if (c == escape) {
191 				if (putline(collf, &linebuf[1], !longline) < 0)
192 					goto err;
193 				else
194 					break;
195 			}
196 			printf("Unknown tilde escape.\n");
197 			break;
198 		case 'C':
199 			/*
200 			 * Dump core.
201 			 */
202 			core();
203 			break;
204 		case '!':
205 			/*
206 			 * Shell escape, send the balance of the
207 			 * line to sh -c.
208 			 */
209 			shell(&linebuf[2]);
210 			break;
211 		case ':':
212 		case '_':
213 			/*
214 			 * Escape to command mode, but be nice!
215 			 */
216 			execute(&linebuf[2], 1);
217 			goto cont;
218 		case '.':
219 			/*
220 			 * Simulate end of file on input.
221 			 */
222 			goto out;
223 		case 'q':
224 			/*
225 			 * Force a quit of sending mail.
226 			 * Act like an interrupt happened.
227 			 */
228 			hadintr++;
229 			collint(SIGINT);
230 			exit(1);
231 		case 'x':
232 			/*
233 			 * Exit, do not save in dead.letter.
234 			 */
235 			goto err;
236 		case 'h':
237 			/*
238 			 * Grab a bunch of headers.
239 			 */
240 			grabh(hp, GTO|GSUBJECT|GCC|GBCC);
241 			goto cont;
242 		case 't':
243 			/*
244 			 * Add to the To list.
245 			 */
246 			hp->h_to = cat(hp->h_to, extract(&linebuf[2], GTO));
247 			break;
248 		case 's':
249 			/*
250 			 * Set the Subject line.
251 			 */
252 			cp = &linebuf[2];
253 			while (isspace((unsigned char)*cp))
254 				cp++;
255 			hp->h_subject = savestr(cp);
256 			break;
257 		case 'R':
258 			/*
259 			 * Set the Reply-To line.
260 			 */
261 			cp = &linebuf[2];
262 			while (isspace((unsigned char)*cp))
263 				cp++;
264 			hp->h_replyto = savestr(cp);
265 			break;
266 		case 'c':
267 			/*
268 			 * Add to the CC list.
269 			 */
270 			hp->h_cc = cat(hp->h_cc, extract(&linebuf[2], GCC));
271 			break;
272 		case 'b':
273 			/*
274 			 * Add to the BCC list.
275 			 */
276 			hp->h_bcc = cat(hp->h_bcc, extract(&linebuf[2], GBCC));
277 			break;
278 		case 'i':
279 		case 'A':
280 		case 'a':
281 			/*
282 			 * Insert named variable in message.
283 			 */
284 			switch(c) {
285 				case 'i':
286 					cp = &linebuf[2];
287 					while(isspace((unsigned char)*cp))
288 						cp++;
289 					break;
290 				case 'a':
291 					cp = "sign";
292 					break;
293 				case 'A':
294 					cp = "Sign";
295 					break;
296 				default:
297 					goto err;
298 			}
299 
300 			if(*cp != '\0' && (cp = value(cp)) != NULL) {
301 				printf("%s\n", cp);
302 				if(putline(collf, cp, 1) < 0)
303 					goto err;
304 			}
305 
306 			break;
307 		case 'd':
308 			/*
309 			 * Read in the dead letter file.
310 			 */
311 			if (strlcpy(linebuf + 2, getdeadletter(),
312 				sizeof(linebuf) - 2)
313 			    >= sizeof(linebuf) - 2) {
314 				printf("Line buffer overflow\n");
315 				break;
316 			}
317 			/* FALLTHROUGH */
318 		case 'r':
319 		case '<':
320 			/*
321 			 * Invoke a file:
322 			 * Search for the file name,
323 			 * then open it and copy the contents to collf.
324 			 */
325 			cp = &linebuf[2];
326 			while (isspace((unsigned char)*cp))
327 				cp++;
328 			if (*cp == '\0') {
329 				printf("Interpolate what file?\n");
330 				break;
331 			}
332 			cp = expand(cp);
333 			if (cp == NULL)
334 				break;
335 			if (*cp == '!') {
336 				/*
337 				 * Insert stdout of command.
338 				 */
339 				char *sh;
340 				int nullfd, tempfd, rc;
341 				char tempname2[PATHSIZE];
342 
343 				if ((nullfd = open(_PATH_DEVNULL, O_RDONLY, 0))
344 				    == -1) {
345 					warn(_PATH_DEVNULL);
346 					break;
347 				}
348 
349 				(void)snprintf(tempname2, sizeof(tempname2),
350 				    "%s/mail.ReXXXXXXXXXX", tmpdir);
351 				if ((tempfd = mkstemp(tempname2)) == -1 ||
352 				    (fbuf = Fdopen(tempfd, "w+")) == NULL) {
353 					warn("%s", tempname2);
354 					break;
355 				}
356 				(void)unlink(tempname2);
357 
358 				if ((sh = value("SHELL")) == NULL)
359 					sh = _PATH_CSHELL;
360 
361 				rc = run_command(sh, 0, nullfd, fileno(fbuf),
362 				    "-c", cp+1, NULL);
363 
364 				close(nullfd);
365 
366 				if (rc < 0) {
367 					(void)Fclose(fbuf);
368 					break;
369 				}
370 
371 				if (fsize(fbuf) == 0) {
372 					fprintf(stderr,
373 					    "No bytes from command \"%s\"\n",
374 					    cp+1);
375 					(void)Fclose(fbuf);
376 					break;
377 				}
378 
379 				rewind(fbuf);
380 			} else if (isdir(cp)) {
381 				printf("%s: Directory\n", cp);
382 				break;
383 			} else if ((fbuf = Fopen(cp, "r")) == NULL) {
384 				warn("%s", cp);
385 				break;
386 			}
387 			printf("\"%s\" ", cp);
388 			(void)fflush(stdout);
389 			lc = 0;
390 			cc = 0;
391 			while ((rc = readline(fbuf, linebuf, LINESIZE)) >= 0) {
392 				if (rc != LINESIZE - 1)
393 					lc++;
394 				if ((t = putline(collf, linebuf,
395 					 rc != LINESIZE - 1)) < 0) {
396 					(void)Fclose(fbuf);
397 					goto err;
398 				}
399 				cc += t;
400 			}
401 			(void)Fclose(fbuf);
402 			printf("%d/%d\n", lc, cc);
403 			break;
404 		case 'w':
405 			/*
406 			 * Write the message on a file.
407 			 */
408 			cp = &linebuf[2];
409 			while (*cp == ' ' || *cp == '\t')
410 				cp++;
411 			if (*cp == '\0') {
412 				fprintf(stderr, "Write what file!?\n");
413 				break;
414 			}
415 			if ((cp = expand(cp)) == NULL)
416 				break;
417 			rewind(collf);
418 			exwrite(cp, collf, 1);
419 			break;
420 		case 'm':
421 		case 'M':
422 		case 'f':
423 		case 'F':
424 			/*
425 			 * Interpolate the named messages, if we
426 			 * are in receiving mail mode.  Does the
427 			 * standard list processing garbage.
428 			 * If ~f is given, we don't shift over.
429 			 */
430 			if (forward(linebuf + 2, collf, tempname, c) < 0)
431 				goto err;
432 			goto cont;
433 		case '?':
434 			if ((fbuf = Fopen(_PATH_TILDE, "r")) == NULL) {
435 				warn("%s", _PATH_TILDE);
436 				break;
437 			}
438 			while ((t = getc(fbuf)) != EOF)
439 				(void)putchar(t);
440 			(void)Fclose(fbuf);
441 			break;
442 		case 'p':
443 			/*
444 			 * Print out the current state of the
445 			 * message without altering anything.
446 			 */
447 			rewind(collf);
448 			printf("-------\nMessage contains:\n");
449 			puthead(hp, stdout, GTO|GSUBJECT|GCC|GBCC|GNL);
450 			while ((t = getc(collf)) != EOF)
451 				(void)putchar(t);
452 			goto cont;
453 		case '|':
454 			/*
455 			 * Pipe message through command.
456 			 * Collect output as new message.
457 			 */
458 			rewind(collf);
459 			mespipe(collf, &linebuf[2]);
460 			goto cont;
461 		case 'v':
462 		case 'e':
463 			/*
464 			 * Edit the current message.
465 			 * 'e' means to use EDITOR
466 			 * 'v' means to use VISUAL
467 			 */
468 			rewind(collf);
469 			mesedit(collf, c);
470 			goto cont;
471 		}
472 	}
473 	goto out;
474 err:
475 	if (collf != NULL) {
476 		(void)Fclose(collf);
477 		collf = NULL;
478 	}
479 out:
480 	if (collf != NULL)
481 		rewind(collf);
482 	noreset--;
483 	(void)sigprocmask(SIG_BLOCK, &nset, NULL);
484 	(void)signal(SIGINT, saveint);
485 	(void)signal(SIGHUP, savehup);
486 	(void)signal(SIGTSTP, savetstp);
487 	(void)signal(SIGTTOU, savettou);
488 	(void)signal(SIGTTIN, savettin);
489 	(void)sigprocmask(SIG_UNBLOCK, &nset, NULL);
490 	return (collf);
491 }
492 
493 /*
494  * Write a file, ex-like if f set.
495  */
496 int
497 exwrite(char name[], FILE *fp, int f)
498 {
499 	FILE *of;
500 	int c, lc;
501 	long cc;
502 	struct stat junk;
503 
504 	if (f) {
505 		printf("\"%s\" ", name);
506 		(void)fflush(stdout);
507 	}
508 	if (stat(name, &junk) >= 0 && S_ISREG(junk.st_mode)) {
509 		if (!f)
510 			fprintf(stderr, "%s: ", name);
511 		fprintf(stderr, "File exists\n");
512 		return (-1);
513 	}
514 	if ((of = Fopen(name, "w")) == NULL) {
515 		warn((char *)NULL);
516 		return (-1);
517 	}
518 	lc = 0;
519 	cc = 0;
520 	while ((c = getc(fp)) != EOF) {
521 		cc++;
522 		if (c == '\n')
523 			lc++;
524 		(void)putc(c, of);
525 		if (ferror(of)) {
526 			warnx("%s", name);
527 			(void)Fclose(of);
528 			return (-1);
529 		}
530 	}
531 	(void)Fclose(of);
532 	printf("%d/%ld\n", lc, cc);
533 	(void)fflush(stdout);
534 	return (0);
535 }
536 
537 /*
538  * Edit the message being collected on fp.
539  * On return, make the edit file the new temp file.
540  */
541 void
542 mesedit(FILE *fp, int c)
543 {
544 	sig_t sigint = signal(SIGINT, SIG_IGN);
545 	FILE *nf = run_editor(fp, (off_t)-1, c, 0);
546 
547 	if (nf != NULL) {
548 		(void)fseeko(nf, (off_t)0, SEEK_END);
549 		collf = nf;
550 		(void)Fclose(fp);
551 	}
552 	(void)signal(SIGINT, sigint);
553 }
554 
555 /*
556  * Pipe the message through the command.
557  * Old message is on stdin of command;
558  * New message collected from stdout.
559  * Sh -c must return 0 to accept the new message.
560  */
561 void
562 mespipe(FILE *fp, char cmd[])
563 {
564 	FILE *nf;
565 	int fd;
566 	sig_t sigint = signal(SIGINT, SIG_IGN);
567 	char *sh, tempname[PATHSIZE];
568 
569 	(void)snprintf(tempname, sizeof(tempname),
570 	    "%s/mail.ReXXXXXXXXXX", tmpdir);
571 	if ((fd = mkstemp(tempname)) == -1 ||
572 	    (nf = Fdopen(fd, "w+")) == NULL) {
573 		warn("%s", tempname);
574 		goto out;
575 	}
576 	(void)rm(tempname);
577 	/*
578 	 * stdin = current message.
579 	 * stdout = new message.
580 	 */
581 	if ((sh = value("SHELL")) == NULL)
582 		sh = _PATH_CSHELL;
583 	if (run_command(sh,
584 	    0, fileno(fp), fileno(nf), "-c", cmd, NULL) < 0) {
585 		(void)Fclose(nf);
586 		goto out;
587 	}
588 	if (fsize(nf) == 0) {
589 		fprintf(stderr, "No bytes from \"%s\" !?\n", cmd);
590 		(void)Fclose(nf);
591 		goto out;
592 	}
593 	/*
594 	 * Take new files.
595 	 */
596 	(void)fseeko(nf, (off_t)0, SEEK_END);
597 	collf = nf;
598 	(void)Fclose(fp);
599 out:
600 	(void)signal(SIGINT, sigint);
601 }
602 
603 /*
604  * Interpolate the named messages into the current
605  * message, preceding each line with a tab.
606  * Return a count of the number of characters now in
607  * the message, or -1 if an error is encountered writing
608  * the message temporary.  The flag argument is 'm' if we
609  * should shift over and 'f' if not.
610  */
611 int
612 forward(char ms[], FILE *fp, char *fn, int f)
613 {
614 	int *msgvec;
615 	struct ignoretab *ig;
616 	char *tabst;
617 
618 	msgvec = (int *)salloc((msgCount+1) * sizeof(*msgvec));
619 	if (msgvec == NULL)
620 		return (0);
621 	if (getmsglist(ms, msgvec, 0) < 0)
622 		return (0);
623 	if (*msgvec == 0) {
624 		*msgvec = first(0, MMNORM);
625 		if (*msgvec == 0) {
626 			printf("No appropriate messages\n");
627 			return (0);
628 		}
629 		msgvec[1] = 0;
630 	}
631 	if (f == 'f' || f == 'F')
632 		tabst = NULL;
633 	else if ((tabst = value("indentprefix")) == NULL)
634 		tabst = "\t";
635 	ig = isupper((unsigned char)f) ? NULL : ignore;
636 	printf("Interpolating:");
637 	for (; *msgvec != 0; msgvec++) {
638 		struct message *mp = message + *msgvec - 1;
639 
640 		touch(mp);
641 		printf(" %d", *msgvec);
642 		if (sendmessage(mp, fp, ig, tabst) < 0) {
643 			warnx("%s", fn);
644 			return (-1);
645 		}
646 	}
647 	printf("\n");
648 	return (0);
649 }
650 
651 /*
652  * Print (continue) when continued after ^Z.
653  */
654 /*ARGSUSED*/
655 void
656 collstop(int s)
657 {
658 	sig_t old_action = signal(s, SIG_DFL);
659 	sigset_t nset;
660 
661 	(void)sigemptyset(&nset);
662 	(void)sigaddset(&nset, s);
663 	(void)sigprocmask(SIG_UNBLOCK, &nset, NULL);
664 	(void)kill(0, s);
665 	(void)sigprocmask(SIG_BLOCK, &nset, NULL);
666 	(void)signal(s, old_action);
667 	if (colljmp_p) {
668 		colljmp_p = 0;
669 		hadintr = 0;
670 		longjmp(colljmp, 1);
671 	}
672 }
673 
674 /*
675  * On interrupt, come here to save the partial message in ~/dead.letter.
676  * Then jump out of the collection loop.
677  */
678 /*ARGSUSED*/
679 void
680 collint(int s __unused)
681 {
682 	/*
683 	 * the control flow is subtle, because we can be called from ~q.
684 	 */
685 	if (!hadintr) {
686 		if (value("ignore") != NULL) {
687 			printf("@");
688 			(void)fflush(stdout);
689 			clearerr(stdin);
690 			return;
691 		}
692 		hadintr = 1;
693 		longjmp(colljmp, 1);
694 	}
695 	rewind(collf);
696 	if (value("nosave") == NULL)
697 		savedeadletter(collf);
698 	longjmp(collabort, 1);
699 }
700 
701 /*ARGSUSED*/
702 void
703 collhup(int s __unused)
704 {
705 	rewind(collf);
706 	savedeadletter(collf);
707 	/*
708 	 * Let's pretend nobody else wants to clean up,
709 	 * a true statement at this time.
710 	 */
711 	exit(1);
712 }
713 
714 void
715 savedeadletter(FILE *fp)
716 {
717 	FILE *dbuf;
718 	int c;
719 	char *cp;
720 
721 	if (fsize(fp) == 0)
722 		return;
723 	cp = getdeadletter();
724 	c = umask(077);
725 	dbuf = Fopen(cp, "a");
726 	(void)umask(c);
727 	if (dbuf == NULL)
728 		return;
729 	while ((c = getc(fp)) != EOF)
730 		(void)putc(c, dbuf);
731 	(void)Fclose(dbuf);
732 	rewind(fp);
733 }
734