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