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