1 /*	$NetBSD: postcat.c,v 1.4 2022/10/08 16:12:46 christos Exp $	*/
2 
3 /*++
4 /* NAME
5 /*	postcat 1
6 /* SUMMARY
7 /*	show Postfix queue file contents
8 /* SYNOPSIS
9 /*	\fBpostcat\fR [\fB-bdehnoqv\fR] [\fB-c \fIconfig_dir\fR] [\fIfiles\fR...]
10 /* DESCRIPTION
11 /*	The \fBpostcat\fR(1) command prints the contents of the
12 /*	named \fIfiles\fR in human-readable form. The files are
13 /*	expected to be in Postfix queue file format. If no \fIfiles\fR
14 /*	are specified on the command line, the program reads from
15 /*	standard input.
16 /*
17 /*	By default, \fBpostcat\fR(1) shows the envelope and message
18 /*	content, as if the options \fB-beh\fR were specified. To
19 /*	view message content only, specify \fB-bh\fR (Postfix 2.7
20 /*	and later).
21 /*
22 /*	Options:
23 /* .IP \fB-b\fR
24 /*	Show body content.  The \fB-b\fR option starts producing
25 /*	output at the first non-header line, and stops when the end
26 /*	of the message is reached.
27 /* .sp
28 /*	This feature is available in Postfix 2.7 and later.
29 /* .IP "\fB-c \fIconfig_dir\fR"
30 /*	The \fBmain.cf\fR configuration file is in the named directory
31 /*	instead of the default configuration directory.
32 /* .IP \fB-d\fR
33 /*	Print the decimal type of each record.
34 /* .IP \fB-e\fR
35 /*	Show message envelope content.
36 /* .sp
37 /*	This feature is available in Postfix 2.7 and later.
38 /* .IP \fB-h\fR
39 /*	Show message header content.  The \fB-h\fR option produces
40 /*	output from the beginning of the message up to, but not
41 /*	including, the first non-header line.
42 /* .sp
43 /*	This feature is available in Postfix 2.7 and later.
44 /* .IP \fB-o\fR
45 /*	Print the queue file offset of each record.
46 /* .IP \fB-q\fR
47 /*	Search the Postfix queue for the named \fIfiles\fR instead
48 /*	of taking the names literally.
49 /*
50 /*	This feature is available in Postfix 2.0 and later.
51 /* .IP \fB-r\fR
52 /*	Print records in file order, don't follow pointer records.
53 /*
54 /*	This feature is available in Postfix 3.7 and later.
55 /* .IP "\fB-s \fIoffset\fR"
56 /*	Skip to the specified queue file offset.
57 /*
58 /*	This feature is available in Postfix 3.7 and later.
59 /* .IP \fB-v\fR
60 /*	Enable verbose logging for debugging purposes. Multiple \fB-v\fR
61 /*	options make the software increasingly verbose.
62 /* DIAGNOSTICS
63 /*	Problems are reported to the standard error stream.
64 /* ENVIRONMENT
65 /* .ad
66 /* .fi
67 /* .IP \fBMAIL_CONFIG\fR
68 /*	Directory with Postfix configuration files.
69 /* CONFIGURATION PARAMETERS
70 /* .ad
71 /* .fi
72 /*	The following \fBmain.cf\fR parameters are especially relevant to
73 /*	this program.
74 /*
75 /*	The text below provides only a parameter summary. See
76 /*	\fBpostconf\fR(5) for more details including examples.
77 /* .IP "\fBconfig_directory (see 'postconf -d' output)\fR"
78 /*	The default location of the Postfix main.cf and master.cf
79 /*	configuration files.
80 /* .IP "\fBimport_environment (see 'postconf -d' output)\fR"
81 /*	The list of environment parameters that a privileged Postfix
82 /*	process will import from a non-Postfix parent process, or name=value
83 /*	environment overrides.
84 /* .IP "\fBqueue_directory (see 'postconf -d' output)\fR"
85 /*	The location of the Postfix top-level queue directory.
86 /* FILES
87 /*	/var/spool/postfix, Postfix queue directory
88 /* SEE ALSO
89 /*	postconf(5), Postfix configuration
90 /* LICENSE
91 /* .ad
92 /* .fi
93 /*	The Secure Mailer license must be distributed with this software.
94 /* AUTHOR(S)
95 /*	Wietse Venema
96 /*	IBM T.J. Watson Research
97 /*	P.O. Box 704
98 /*	Yorktown Heights, NY 10598, USA
99 /*
100 /*	Wietse Venema
101 /*	Google, Inc.
102 /*	111 8th Avenue
103 /*	New York, NY 10011, USA
104 /*--*/
105 
106 /* System library. */
107 
108 #include <sys_defs.h>
109 #include <sys/stat.h>
110 #include <sys/time.h>
111 #include <stdlib.h>
112 #include <unistd.h>
113 #include <time.h>
114 #include <fcntl.h>
115 #include <string.h>
116 #include <stdio.h>			/* sscanf() */
117 
118 /* Utility library. */
119 
120 #include <msg.h>
121 #include <vstream.h>
122 #include <vstring.h>
123 #include <msg_vstream.h>
124 #include <vstring_vstream.h>
125 #include <stringops.h>
126 #include <warn_stat.h>
127 #include <clean_env.h>
128 
129 /* Global library. */
130 
131 #include <record.h>
132 #include <rec_type.h>
133 #include <mail_queue.h>
134 #include <mail_conf.h>
135 #include <mail_params.h>
136 #include <mail_version.h>
137 #include <mail_proto.h>
138 #include <is_header.h>
139 #include <lex_822.h>
140 #include <mail_parm_split.h>
141 
142 /* Application-specific. */
143 
144 #define PC_FLAG_SEARCH_QUEUE	(1<<0)	/* search queue */
145 #define PC_FLAG_PRINT_OFFSET	(1<<1)	/* print record offsets */
146 #define PC_FLAG_PRINT_ENV	(1<<2)	/* print envelope records */
147 #define PC_FLAG_PRINT_HEADER	(1<<3)	/* print header records */
148 #define PC_FLAG_PRINT_BODY	(1<<4)	/* print body records */
149 #define PC_FLAG_PRINT_RTYPE_DEC	(1<<5)	/* print decimal record type */
150 #define PC_FLAG_PRINT_RTYPE_SYM	(1<<6)	/* print symbolic record type */
151 #define PC_FLAG_RAW		(1<<7)	/* don't follow pointers */
152 
153 #define PC_MASK_PRINT_TEXT	(PC_FLAG_PRINT_HEADER | PC_FLAG_PRINT_BODY)
154 #define PC_MASK_PRINT_ALL	(PC_FLAG_PRINT_ENV | PC_MASK_PRINT_TEXT)
155 
156  /*
157   * State machine.
158   */
159 #define PC_STATE_ENV	0		/* initial or extracted envelope */
160 #define PC_STATE_HEADER	1		/* primary header */
161 #define PC_STATE_BODY	2		/* other */
162 
163 off_t   start_offset = 0;
164 
165 #define STR	vstring_str
166 #define LEN	VSTRING_LEN
167 
168 /* postcat - visualize Postfix queue file contents */
169 
postcat(VSTREAM * fp,VSTRING * buffer,int flags)170 static void postcat(VSTREAM *fp, VSTRING *buffer, int flags)
171 {
172     int     prev_type = 0;
173     int     rec_type;
174     struct timeval tv;
175     time_t  time;
176     int     ch;
177     off_t   offset;
178     const char *error_text;
179     char   *attr_name;
180     char   *attr_value;
181     int     rec_flags = (msg_verbose ? REC_FLAG_NONE : REC_FLAG_DEFAULT);
182     int     state;			/* state machine, input type */
183     int     do_print;			/* state machine, output control */
184     long    data_offset;		/* state machine, read optimization */
185     long    data_size;			/* state machine, read optimization */
186 
187 #define TEXT_RECORD(rec_type) \
188 	    (rec_type == REC_TYPE_CONT || rec_type == REC_TYPE_NORM)
189 
190     /*
191      * Skip over or absorb some bytes.
192      */
193     if (start_offset > 0) {
194 	if (fp == VSTREAM_IN) {
195 	    for (offset = 0; offset < start_offset; offset++)
196 		if (VSTREAM_GETC(fp) == VSTREAM_EOF)
197 		    msg_fatal("%s: skip %ld bytes failed after %ld",
198 			      VSTREAM_PATH(fp), (long) start_offset,
199 			      (long) offset);
200 	} else {
201 	    if (vstream_fseek(fp, start_offset, SEEK_SET) < 0)
202 		msg_fatal("%s: seek to %ld: %m",
203 			  VSTREAM_PATH(fp), (long) start_offset);
204 	}
205     }
206 
207     /*
208      * See if this is a plausible file.
209      */
210     if (start_offset == 0 && (ch = VSTREAM_GETC(fp)) != VSTREAM_EOF) {
211 	if (!strchr(REC_TYPE_ENVELOPE, ch)) {
212 	    msg_warn("%s: input is not a valid queue file", VSTREAM_PATH(fp));
213 	    return;
214 	}
215 	vstream_ungetc(fp, ch);
216     }
217 
218     /*
219      * Other preliminaries.
220      */
221     if (start_offset == 0 && (flags & PC_FLAG_PRINT_ENV))
222 	vstream_printf("*** ENVELOPE RECORDS %s ***\n",
223 		       VSTREAM_PATH(fp));
224     state = PC_STATE_ENV;
225     do_print = (flags & PC_FLAG_PRINT_ENV);
226     data_offset = data_size = -1;
227 
228     /*
229      * Now look at the rest.
230      */
231     for (;;) {
232 	if (flags & PC_FLAG_PRINT_OFFSET)
233 	    offset = vstream_ftell(fp);
234 	rec_type = rec_get_raw(fp, buffer, 0, rec_flags);
235 	if (rec_type == REC_TYPE_ERROR)
236 	    msg_fatal("record read error");
237 	if (rec_type == REC_TYPE_EOF)
238 	    break;
239 
240 	/*
241 	 * First inspect records that have side effects on the (envelope,
242 	 * header, body) state machine or on the record reading order.
243 	 *
244 	 * XXX Comments marked "Optimization:" identify subtle code that will
245 	 * likely need to be revised when the queue file organization is
246 	 * changed.
247 	 */
248 #define PRINT_MARKER(flags, fp, offset, type, text) do { \
249     if ((flags) & PC_FLAG_PRINT_OFFSET) \
250 	vstream_printf("%9lu ", (unsigned long) (offset)); \
251     if (flags & PC_FLAG_PRINT_RTYPE_DEC) \
252 	vstream_printf("%3d ", (type)); \
253     vstream_printf("*** %s %s ***\n", (text), VSTREAM_PATH(fp)); \
254     vstream_fflush(VSTREAM_OUT); \
255 } while (0)
256 
257 #define PRINT_RECORD(flags, offset, type, value) do { \
258     if ((flags) & PC_FLAG_PRINT_OFFSET) \
259 	vstream_printf("%9lu ", (unsigned long) (offset)); \
260     if (flags & PC_FLAG_PRINT_RTYPE_DEC) \
261 	vstream_printf("%3d ", (type)); \
262     vstream_printf("%s: %s\n", rec_type_name(rec_type), (value)); \
263     vstream_fflush(VSTREAM_OUT); \
264 } while (0)
265 
266 	if (TEXT_RECORD(rec_type)) {
267 	    /* This is wrong when the message starts with whitespace. */
268 	    if (state == PC_STATE_HEADER && (flags & (PC_MASK_PRINT_TEXT))
269 		&& prev_type != REC_TYPE_CONT && TEXT_RECORD(rec_type)
270 	     && !(is_header(STR(buffer)) || IS_SPACE_TAB(STR(buffer)[0]))) {
271 		/* Update the state machine. */
272 		state = PC_STATE_BODY;
273 		do_print = (flags & PC_FLAG_PRINT_BODY);
274 		/* Optimization: terminate if nothing left to print. */
275 		if (do_print == 0 && (flags & PC_FLAG_PRINT_ENV) == 0)
276 		    break;
277 		/* Optimization: skip to extracted segment marker. */
278 		if (do_print == 0 && (flags & PC_FLAG_PRINT_ENV)
279 		    && data_offset >= 0 && data_size >= 0
280 		&& vstream_fseek(fp, data_offset + data_size, SEEK_SET) < 0)
281 		    msg_fatal("seek error: %m");
282 	    }
283 	    /* Optional output happens further down below. */
284 	} else if (rec_type == REC_TYPE_MESG) {
285 	    /* Sanity check. */
286 	    if (state != PC_STATE_ENV)
287 		msg_warn("%s: out-of-order message content marker",
288 			 VSTREAM_PATH(fp));
289 	    /* Optional output. */
290 	    if (flags & PC_FLAG_PRINT_ENV)
291 		PRINT_MARKER(flags, fp, offset, rec_type, "MESSAGE CONTENTS");
292 	    /* Optimization: skip to extracted segment marker. */
293 	    if ((flags & PC_MASK_PRINT_TEXT) == 0
294 		&& data_offset >= 0 && data_size >= 0
295 		&& vstream_fseek(fp, data_offset + data_size, SEEK_SET) < 0)
296 		msg_fatal("seek error: %m");
297 	    /* Update the state machine, even when skipping. */
298 	    state = PC_STATE_HEADER;
299 	    do_print = (flags & PC_FLAG_PRINT_HEADER);
300 	    continue;
301 	} else if (rec_type == REC_TYPE_XTRA) {
302 	    /* Sanity check. */
303 	    if (state != PC_STATE_HEADER && state != PC_STATE_BODY)
304 		msg_warn("%s: out-of-order extracted segment marker",
305 			 VSTREAM_PATH(fp));
306 	    /* Optional output (terminate preceding header/body line). */
307 	    if (do_print && prev_type == REC_TYPE_CONT)
308 		VSTREAM_PUTCHAR('\n');
309 	    if (flags & PC_FLAG_PRINT_ENV)
310 		PRINT_MARKER(flags, fp, offset, rec_type, "HEADER EXTRACTED");
311 	    /* Update the state machine. */
312 	    state = PC_STATE_ENV;
313 	    do_print = (flags & PC_FLAG_PRINT_ENV);
314 	    /* Optimization: terminate if nothing left to print. */
315 	    if (do_print == 0)
316 		break;
317 	    continue;
318 	} else if (rec_type == REC_TYPE_END) {
319 	    /* Sanity check. */
320 	    if (state != PC_STATE_ENV)
321 		msg_warn("%s: out-of-order message end marker",
322 			 VSTREAM_PATH(fp));
323 	    /* Optional output. */
324 	    if (flags & PC_FLAG_PRINT_ENV)
325 		PRINT_MARKER(flags, fp, offset, rec_type, "MESSAGE FILE END");
326 	    if (flags & PC_FLAG_RAW)
327 		continue;
328 	    /* Terminate the state machine. */
329 	    break;
330 	} else if (rec_type == REC_TYPE_PTR) {
331 	    /* Optional output. */
332 	    /* This record type is exposed only with '-v'. */
333 	    if (do_print)
334 		PRINT_RECORD(flags, offset, rec_type, STR(buffer));
335 	    /* Skip to the pointer's target record. */
336 	    if ((flags & PC_FLAG_RAW) == 0
337 		&& rec_goto(fp, STR(buffer)) == REC_TYPE_ERROR)
338 		msg_fatal("bad pointer record, or input is not seekable");
339 	    continue;
340 	} else if (rec_type == REC_TYPE_SIZE) {
341 	    /* Optional output (here before we update the state machine). */
342 	    if (do_print)
343 		PRINT_RECORD(flags, offset, rec_type, STR(buffer));
344 	    /* Read the message size/offset for the state machine optimizer. */
345 	    if (data_size >= 0 || data_offset >= 0) {
346 		msg_warn("file contains multiple size records");
347 	    } else {
348 		if (sscanf(STR(buffer), "%ld %ld", &data_size, &data_offset) != 2
349 		    || data_offset <= 0 || data_size <= 0)
350 		    msg_warn("invalid size record: %.100s", STR(buffer));
351 		/* Optimization: skip to the message header. */
352 		if ((flags & PC_FLAG_PRINT_ENV) == 0) {
353 		    if (vstream_fseek(fp, data_offset, SEEK_SET) < 0)
354 			msg_fatal("seek error: %m");
355 		    /* Update the state machine. */
356 		    state = PC_STATE_HEADER;
357 		    do_print = (flags & PC_FLAG_PRINT_HEADER);
358 		}
359 	    }
360 	    continue;
361 	}
362 
363 	/*
364 	 * Don't inspect side-effect-free records that aren't printed.
365 	 */
366 	if (do_print == 0)
367 	    continue;
368 	if (flags & PC_FLAG_PRINT_OFFSET)
369 	    vstream_printf("%9lu ", (unsigned long) offset);
370 	if (flags & PC_FLAG_PRINT_RTYPE_DEC)
371 	    vstream_printf("%3d ", rec_type);
372 	switch (rec_type) {
373 	case REC_TYPE_TIME:
374 	    REC_TYPE_TIME_SCAN(STR(buffer), tv);
375 	    time = tv.tv_sec;
376 	    vstream_printf("%s: %s", rec_type_name(rec_type),
377 			   asctime(localtime(&time)));
378 	    break;
379 	case REC_TYPE_WARN:
380 	    REC_TYPE_WARN_SCAN(STR(buffer), time);
381 	    vstream_printf("%s: %s", rec_type_name(rec_type),
382 			   asctime(localtime(&time)));
383 	    break;
384 	case REC_TYPE_CONT:			/* REC_TYPE_FILT collision */
385 	    if (state == PC_STATE_ENV)
386 		vstream_printf("%s: ", rec_type_name(rec_type));
387 	    else if (msg_verbose)
388 		vstream_printf("unterminated_text: ");
389 	    vstream_fwrite(VSTREAM_OUT, STR(buffer), LEN(buffer));
390 	    if (state == PC_STATE_ENV || msg_verbose
391 		|| (flags & PC_FLAG_PRINT_OFFSET) != 0) {
392 		rec_type = 0;
393 		VSTREAM_PUTCHAR('\n');
394 	    }
395 	    break;
396 	case REC_TYPE_NORM:
397 	    if (msg_verbose)
398 		vstream_printf("%s: ", rec_type_name(rec_type));
399 	    vstream_fwrite(VSTREAM_OUT, STR(buffer), LEN(buffer));
400 	    VSTREAM_PUTCHAR('\n');
401 	    break;
402 	case REC_TYPE_DTXT:
403 	    /* This record type is exposed only with '-v'. */
404 	    vstream_printf("%s: ", rec_type_name(rec_type));
405 	    vstream_fwrite(VSTREAM_OUT, STR(buffer), LEN(buffer));
406 	    VSTREAM_PUTCHAR('\n');
407 	    break;
408 	case REC_TYPE_ATTR:
409 	    error_text = split_nameval(STR(buffer), &attr_name, &attr_value);
410 	    if (error_text != 0) {
411 		msg_warn("%s: malformed attribute: %s: %.100s",
412 			 VSTREAM_PATH(fp), error_text, STR(buffer));
413 		break;
414 	    }
415 	    if (strcmp(attr_name, MAIL_ATTR_CREATE_TIME) == 0) {
416 		time = atol(attr_value);
417 		vstream_printf("%s: %s", MAIL_ATTR_CREATE_TIME,
418 			       asctime(localtime(&time)));
419 	    } else {
420 		vstream_printf("%s: %s=%s\n", rec_type_name(rec_type),
421 			       attr_name, attr_value);
422 	    }
423 	    break;
424 	default:
425 	    vstream_printf("%s: %s\n", rec_type_name(rec_type), STR(buffer));
426 	    break;
427 	}
428 	prev_type = rec_type;
429 
430 	/*
431 	 * In case the next record is broken.
432 	 */
433 	vstream_fflush(VSTREAM_OUT);
434     }
435 }
436 
437 /* usage - explain and terminate */
438 
usage(char * myname)439 static NORETURN usage(char *myname)
440 {
441     msg_fatal("usage: %s [-b (body text)] [-c config_dir] [-d (decimal record type)] [-e (envelope records)] [-h (header text)] [-q (access queue)] [-v] [file(s)...]",
442 	      myname);
443 }
444 
445 MAIL_VERSION_STAMP_DECLARE;
446 
main(int argc,char ** argv)447 int     main(int argc, char **argv)
448 {
449     VSTRING *buffer;
450     VSTREAM *fp;
451     int     ch;
452     int     fd;
453     struct stat st;
454     int     flags = 0;
455     static char *queue_names[] = {
456 	MAIL_QUEUE_MAILDROP,
457 	MAIL_QUEUE_INCOMING,
458 	MAIL_QUEUE_ACTIVE,
459 	MAIL_QUEUE_DEFERRED,
460 	MAIL_QUEUE_HOLD,
461 	MAIL_QUEUE_SAVED,
462 	0,
463     };
464     char  **cpp;
465     int     tries;
466     ARGV   *import_env;
467 
468     /*
469      * Fingerprint executables and core dumps.
470      */
471     MAIL_VERSION_STAMP_ALLOCATE;
472 
473     /*
474      * To minimize confusion, make sure that the standard file descriptors
475      * are open before opening anything else. XXX Work around for 44BSD where
476      * fstat can return EBADF on an open file descriptor.
477      */
478     for (fd = 0; fd < 3; fd++)
479 	if (fstat(fd, &st) == -1
480 	    && (close(fd), open("/dev/null", O_RDWR, 0)) != fd)
481 	    msg_fatal("open /dev/null: %m");
482 
483     /*
484      * Set up logging.
485      */
486     msg_vstream_init(argv[0], VSTREAM_ERR);
487 
488     /*
489      * Check the Postfix library version as soon as we enable logging.
490      */
491     MAIL_VERSION_CHECK;
492 
493     /*
494      * Parse JCL.
495      */
496     while ((ch = GETOPT(argc, argv, "bc:dehoqrs:v")) > 0) {
497 	switch (ch) {
498 	case 'b':
499 	    flags |= PC_FLAG_PRINT_BODY;
500 	    break;
501 	case 'c':
502 	    if (setenv(CONF_ENV_PATH, optarg, 1) < 0)
503 		msg_fatal("out of memory");
504 	    break;
505 	case 'd':
506 	    flags |= PC_FLAG_PRINT_RTYPE_DEC;
507 	    break;
508 	case 'e':
509 	    flags |= PC_FLAG_PRINT_ENV;
510 	    break;
511 	case 'h':
512 	    flags |= PC_FLAG_PRINT_HEADER;
513 	    break;
514 	case 'o':
515 	    flags |= PC_FLAG_PRINT_OFFSET;
516 	    break;
517 	case 'q':
518 	    flags |= PC_FLAG_SEARCH_QUEUE;
519 	    break;
520 	case 'r':
521 	    flags |= PC_FLAG_RAW;
522 	    break;
523 	case 's':
524 	    if (!alldig(optarg) || (start_offset = atol(optarg)) < 0)
525 		msg_fatal("bad offset: %s", optarg);
526 	    break;
527 	case 'v':
528 	    msg_verbose++;
529 	    break;
530 	default:
531 	    usage(argv[0]);
532 	}
533     }
534     if ((flags & PC_MASK_PRINT_ALL) == 0)
535 	flags |= PC_MASK_PRINT_ALL;
536 
537     /*
538      * Further initialization...
539      */
540     mail_conf_read();
541     import_env = mail_parm_split(VAR_IMPORT_ENVIRON, var_import_environ);
542     update_env(import_env->argv);
543     argv_free(import_env);
544 
545     /*
546      * Initialize.
547      */
548     buffer = vstring_alloc(10);
549 
550     /*
551      * If no file names are given, copy stdin.
552      */
553     if (argc == optind) {
554 	vstream_control(VSTREAM_IN,
555 			CA_VSTREAM_CTL_PATH("stdin"),
556 			CA_VSTREAM_CTL_END);
557 	postcat(VSTREAM_IN, buffer, flags);
558     }
559 
560     /*
561      * Copy the named queue files in the specified order.
562      */
563     else if (flags & PC_FLAG_SEARCH_QUEUE) {
564 	if (chdir(var_queue_dir))
565 	    msg_fatal("chdir %s: %m", var_queue_dir);
566 	while (optind < argc) {
567 	    if (!mail_queue_id_ok(argv[optind]))
568 		msg_fatal("bad mail queue ID: %s", argv[optind]);
569 	    for (fp = 0, tries = 0; fp == 0 && tries < 2; tries++)
570 		for (cpp = queue_names; fp == 0 && *cpp != 0; cpp++)
571 		    fp = mail_queue_open(*cpp, argv[optind], O_RDONLY, 0);
572 	    if (fp == 0)
573 		msg_fatal("open queue file %s: %m", argv[optind]);
574 	    postcat(fp, buffer, flags);
575 	    if (vstream_fclose(fp))
576 		msg_warn("close %s: %m", argv[optind]);
577 	    optind++;
578 	}
579     }
580 
581     /*
582      * Copy the named files in the specified order.
583      */
584     else {
585 	while (optind < argc) {
586 	    if ((fp = vstream_fopen(argv[optind], O_RDONLY, 0)) == 0)
587 		msg_fatal("open %s: %m", argv[optind]);
588 	    postcat(fp, buffer, flags);
589 	    if (vstream_fclose(fp))
590 		msg_warn("close %s: %m", argv[optind]);
591 	    optind++;
592 	}
593     }
594 
595     /*
596      * Clean up.
597      */
598     vstring_free(buffer);
599     exit(0);
600 }
601