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