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 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 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 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