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