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