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