1 /* $NetBSD: format.c,v 1.20 2025/07/09 16:59:54 rillig Exp $ */ 2 3 /*- 4 * Copyright (c) 2006 The NetBSD Foundation, Inc. 5 * All rights reserved. 6 * 7 * This code is derived from software contributed to The NetBSD Foundation 8 * by Anon Ymous. 9 * 10 * Redistribution and use in source and binary forms, with or without 11 * modification, are permitted provided that the following conditions 12 * are met: 13 * 1. Redistributions of source code must retain the above copyright 14 * notice, this list of conditions and the following disclaimer. 15 * 2. Redistributions in binary form must reproduce the above copyright 16 * notice, this list of conditions and the following disclaimer in the 17 * documentation and/or other materials provided with the distribution. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS 20 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 21 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 22 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS 23 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 * POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 #include <sys/cdefs.h> 33 #ifndef __lint__ 34 __RCSID("$NetBSD: format.c,v 1.20 2025/07/09 16:59:54 rillig Exp $"); 35 #endif /* not __lint__ */ 36 37 #include <time.h> 38 #include <stdio.h> 39 #include <util.h> 40 41 #include "def.h" 42 #include "extern.h" 43 #include "format.h" 44 #include "glob.h" 45 #include "thread.h" 46 47 #undef DEBUG 48 #ifdef DEBUG 49 #define DPRINTF(a) printf a 50 #else 51 #define DPRINTF(a) 52 #endif 53 54 static void 55 check_bufsize(char **buf, size_t *bufsize, char **p, size_t cnt) 56 { 57 size_t offset = (size_t)(*p - *buf); 58 59 /* enough buffer allocated already */ 60 if (cnt < *bufsize - offset) 61 return; 62 63 /* expand buffer till it's sufficient to handle the data */ 64 while (cnt >= *bufsize - offset) { 65 if (*bufsize > SIZE_MAX/2) 66 errx(1, "out of memory"); 67 *bufsize *= 2; 68 } 69 70 *buf = erealloc(*buf, *bufsize); 71 *p = *buf + offset; 72 } 73 74 static const char * 75 sfmtoff(const char **fmtbeg, const char *fmtch, off_t off) 76 { 77 char *newfmt; /* pointer to new format string */ 78 size_t len; /* space for "lld" including '\0' */ 79 char *p; 80 81 len = fmtch - *fmtbeg + sizeof(PRId64); 82 newfmt = salloc(len); 83 (void)strlcpy(newfmt, *fmtbeg, len - sizeof(PRId64) + 1); 84 (void)strlcat(newfmt, PRId64, len); 85 *fmtbeg = fmtch + 1; 86 (void)sasprintf(&p, newfmt, off); 87 return p; 88 } 89 90 static const char * 91 sfmtint(const char **fmtbeg, const char *fmtch, int num) 92 { 93 char *newfmt; 94 size_t len; 95 char *p; 96 97 len = fmtch - *fmtbeg + 2; /* space for 'd' and '\0' */ 98 newfmt = salloc(len); 99 (void)strlcpy(newfmt, *fmtbeg, len); 100 newfmt[len-2] = 'd'; /* convert to printf format */ 101 *fmtbeg = fmtch + 1; 102 (void)sasprintf(&p, newfmt, num); 103 return p; 104 } 105 106 static const char * 107 sfmtstr(const char **fmtbeg, const char *fmtch, const char *str) 108 { 109 char *newfmt; 110 size_t len; 111 char *p; 112 113 len = fmtch - *fmtbeg + 2; /* space for 's' and '\0' */ 114 newfmt = salloc(len); 115 (void)strlcpy(newfmt, *fmtbeg, len); 116 newfmt[len-2] = 's'; /* convert to printf format */ 117 *fmtbeg = fmtch + 1; 118 (void)sasprintf(&p, newfmt, str ? str : ""); 119 return p; 120 } 121 122 #ifdef THREAD_SUPPORT 123 static char* 124 sfmtdepth(char *str, int depth) 125 { 126 char *p; 127 if (*str == '\0') { 128 (void)sasprintf(&p, "%d", depth); 129 return p; 130 } 131 p = __UNCONST(""); 132 for (/*EMPTY*/; depth > 0; depth--) 133 (void)sasprintf(&p, "%s%s", p, str); 134 return p; 135 } 136 #endif 137 138 static const char * 139 sfmtfield(const char **fmtbeg, const char *fmtch, struct message *mp) 140 { 141 const char *q; 142 q = strchr(fmtch + 1, '?'); 143 if (q) { 144 size_t len; 145 char *p; 146 const char *str; 147 int skin_it; 148 #ifdef THREAD_SUPPORT 149 int depth; 150 #endif 151 if (mp == NULL) { 152 *fmtbeg = q + 1; 153 return NULL; 154 } 155 #ifdef THREAD_SUPPORT 156 depth = mp->m_depth; 157 #endif 158 skin_it = 0; 159 switch (fmtch[1]) { /* check the '?' modifier */ 160 #ifdef THREAD_SUPPORT 161 case '&': /* use the relative depth */ 162 depth -= thread_depth(); 163 /* FALLTHROUGH */ 164 case '*': /* use the absolute depth */ 165 len = q - fmtch - 1; 166 p = salloc(len); 167 (void)strlcpy(p, fmtch + 2, len); 168 p = sfmtdepth(p, depth); 169 break; 170 #endif 171 case '-': 172 skin_it = 1; 173 /* FALLTHROUGH */ 174 default: 175 len = q - fmtch - skin_it; 176 p = salloc(len); 177 (void)strlcpy(p, fmtch + skin_it + 1, len); 178 p = hfield(p, mp); 179 if (skin_it) 180 p = skin(p); 181 break; 182 } 183 str = sfmtstr(fmtbeg, fmtch, p); 184 *fmtbeg = q + 1; 185 return str; 186 } 187 return NULL; 188 } 189 190 struct flags_s { 191 int f_and; 192 int f_or; 193 int f_new; /* some message in the thread is new */ 194 int f_unread; /* some message in the thread is unread */ 195 }; 196 197 static void 198 get_and_or_flags(struct message *mp, struct flags_s *flags) 199 { 200 for (/*EMPTY*/; mp; mp = mp->m_flink) { 201 flags->f_and &= mp->m_flag; 202 flags->f_or |= mp->m_flag; 203 flags->f_new |= (mp->m_flag & (MREAD|MNEW)) == MNEW; 204 flags->f_unread |= (mp->m_flag & (MREAD|MNEW)) == 0; 205 get_and_or_flags(mp->m_clink, flags); 206 } 207 } 208 209 static const char * 210 sfmtflag(const char **fmtbeg, const char *fmtch, struct message *mp) 211 { 212 char disp[2]; 213 struct flags_s flags; 214 int is_thread; 215 216 if (mp == NULL) 217 return NULL; 218 219 is_thread = mp->m_clink != NULL; 220 disp[0] = is_thread ? '+' : ' '; 221 disp[1] = '\0'; 222 223 flags.f_and = mp->m_flag; 224 flags.f_or = mp->m_flag; 225 flags.f_new = 0; 226 flags.f_unread = 0; 227 #ifdef THREAD_SUPPORT 228 if (thread_hidden()) 229 get_and_or_flags(mp->m_clink, &flags); 230 #endif 231 232 if (flags.f_or & MTAGGED) 233 disp[0] = 't'; 234 if (flags.f_and & MTAGGED) 235 disp[0] = 'T'; 236 237 if (flags.f_or & MMODIFY) 238 disp[0] = 'e'; 239 if (flags.f_and & MMODIFY) 240 disp[0] = 'E'; 241 242 if (flags.f_or & MSAVED) 243 disp[0] = '&'; 244 if (flags.f_and & MSAVED) 245 disp[0] = '*'; 246 247 if (flags.f_or & MPRESERVE) 248 disp[0] = 'p'; 249 if (flags.f_and & MPRESERVE) 250 disp[0] = 'P'; 251 252 if (flags.f_unread) 253 disp[0] = 'u'; 254 if ((flags.f_or & (MREAD|MNEW)) == 0) 255 disp[0] = 'U'; 256 257 if (flags.f_new) 258 disp[0] = 'n'; 259 if ((flags.f_and & (MREAD|MNEW)) == MNEW) 260 disp[0] = 'N'; 261 262 if (flags.f_or & MBOX) 263 disp[0] = 'm'; 264 if (flags.f_and & MBOX) 265 disp[0] = 'M'; 266 267 return sfmtstr(fmtbeg, fmtch, disp); 268 } 269 270 static const char * 271 login_name(const char *addr) 272 { 273 const char *p; 274 p = strchr(addr, '@'); 275 if (p) { 276 char *q; 277 size_t len; 278 len = p - addr + 1; 279 q = salloc(len); 280 (void)strlcpy(q, addr, len); 281 return q; 282 } 283 return addr; 284 } 285 286 static const char * 287 subformat(const char **src, struct message *mp, const char *addr, 288 const char *user, const char *subj, int tm_isdst) 289 { 290 #define MP(a) mp ? a : (*src = (p + 1), NULL) 291 const char *p; 292 293 p = *src; 294 if (p[1] == '%') { 295 *src += 2; 296 return "%%"; 297 } 298 for (p = *src; *p && !isalpha((unsigned char)*p) && *p != '?'; p++) 299 continue; 300 301 switch (*p) { 302 /* 303 * Our format extensions to strftime(3) 304 */ 305 case '?': 306 return sfmtfield(src, p, mp); 307 case 'J': 308 return MP(sfmtint(src, p, (int)(mp->m_lines - mp->m_blines))); 309 case 'K': 310 return MP(sfmtint(src, p, (int)mp->m_blines)); 311 case 'L': 312 return MP(sfmtint(src, p, (int)mp->m_lines)); 313 case 'N': 314 return sfmtstr(src, p, user); 315 case 'O': 316 return MP(sfmtoff(src, p, mp->m_size)); 317 case 'P': 318 return MP(sfmtstr(src, p, mp == dot ? ">" : " ")); 319 case 'Q': 320 return MP(sfmtflag(src, p, mp)); 321 case 'f': 322 return sfmtstr(src, p, addr); 323 case 'i': 324 return sfmtint(src, p, get_msgnum(mp)); /* '0' if mp == NULL */ 325 case 'n': 326 return sfmtstr(src, p, login_name(addr)); 327 case 'q': 328 return sfmtstr(src, p, subj); 329 case 't': 330 return sfmtint(src, p, get_msgCount()); 331 332 /* 333 * strftime(3) special cases: 334 * 335 * When 'tm_isdst' was not determined (i.e., < 0), a C99 336 * compliant strftime(3) will output an empty string for the 337 * "%Z" and "%z" formats. This messes up alignment so we 338 * handle these ourselves. 339 */ 340 case 'Z': 341 if (tm_isdst < 0) { 342 *src = p + 1; 343 return "???"; /* XXX - not ideal */ 344 } 345 return NULL; 346 case 'z': 347 if (tm_isdst < 0) { 348 *src = p + 1; 349 return "-0000"; /* consistent with RFC 2822 */ 350 } 351 return NULL; 352 353 /* everything else is handled by strftime(3) */ 354 default: 355 return NULL; 356 } 357 #undef MP 358 } 359 360 static const char * 361 snarf_comment(char **buf, char *bufend, const char *string) 362 { 363 const char *p; 364 char *q; 365 char *qend; 366 int clevel; 367 368 q = buf ? *buf : NULL; 369 qend = buf ? bufend : NULL; 370 371 clevel = 1; 372 for (p = string + 1; *p != '\0'; p++) { 373 DPRINTF(("snarf_comment: %s\n", p)); 374 if (*p == '(') { 375 clevel++; 376 continue; 377 } 378 if (*p == ')') { 379 if (--clevel == 0) 380 break; 381 continue; 382 } 383 if (*p == '\\' && p[1] != 0) 384 p++; 385 386 if (q < qend) 387 *q++ = *p; 388 } 389 if (buf) { 390 *q = '\0'; 391 DPRINTF(("snarf_comment: terminating: %s\n", *buf)); 392 *buf = q; 393 } 394 if (*p == '\0') 395 p--; 396 return p; 397 } 398 399 static const char * 400 snarf_quote(char **buf, char *bufend, const char *string) 401 { 402 const char *p; 403 char *q; 404 char *qend; 405 406 q = buf ? *buf : NULL; 407 qend = buf ? bufend : NULL; 408 409 for (p = string + 1; *p != '\0' && *p != '"'; p++) { 410 DPRINTF(("snarf_quote: %s\n", p)); 411 if (*p == '\\' && p[1] != '\0') 412 p++; 413 414 if (q < qend) 415 *q++ = *p; 416 } 417 if (buf) { 418 *q = '\0'; 419 DPRINTF(("snarf_quote: terminating: %s\n", *buf)); 420 *buf = q; 421 } 422 if (*p == '\0') 423 p--; 424 return p; 425 } 426 427 /* 428 * Grab the comments, separating each by a space. 429 */ 430 static char * 431 get_comments(char *name) 432 { 433 char nbuf[LINESIZE]; 434 const char *p; 435 char *qend; 436 char *q; 437 char *lastq; 438 439 if (name == NULL) 440 return NULL; 441 442 p = name; 443 q = nbuf; 444 lastq = nbuf; 445 qend = nbuf + sizeof(nbuf) - 1; 446 for (p = skip_WSP(name); *p != '\0'; p++) { 447 DPRINTF(("get_comments: %s\n", p)); 448 switch (*p) { 449 case '"': /* quoted-string ... skip it! */ 450 p = snarf_quote(NULL, NULL, p); 451 break; 452 453 case '(': 454 p = snarf_comment(&q, qend, p); 455 lastq = q; 456 if (q < qend) /* separate comments by space */ 457 *q++ = ' '; 458 break; 459 460 default: 461 break; 462 } 463 } 464 *lastq = '\0'; 465 return savestr(nbuf); 466 } 467 468 /* 469 * Convert a possible obs_zone (see RFC 2822, sec 4.3) to a valid 470 * gmtoff string. 471 */ 472 static const char * 473 convert_obs_zone(const char *obs_zone) 474 { 475 static const struct obs_zone_tbl_s { 476 const char *zone; 477 const char *gmtoff; 478 } obs_zone_tbl[] = { 479 {"UT", "+0000"}, 480 {"GMT", "+0000"}, 481 {"EST", "-0500"}, 482 {"EDT", "-0400"}, 483 {"CST", "-0600"}, 484 {"CDT", "-0500"}, 485 {"MST", "-0700"}, 486 {"MDT", "-0600"}, 487 {"PST", "-0800"}, 488 {"PDT", "-0700"}, 489 {NULL, NULL}, 490 }; 491 const struct obs_zone_tbl_s *zp; 492 493 if (obs_zone[0] == '+' || obs_zone[0] == '-') 494 return obs_zone; 495 496 if (obs_zone[1] == 0) { /* possible military zones */ 497 /* be explicit here - avoid C extensions and ctype(3) */ 498 switch((unsigned char)obs_zone[0]) { 499 case 'A': case 'B': case 'C': case 'D': case 'E': 500 case 'F': case 'G': case 'H': case 'I': 501 case 'K': case 'L': case 'M': case 'N': case 'O': 502 case 'P': case 'Q': case 'R': case 'S': case 'T': 503 case 'U': case 'V': case 'W': case 'X': case 'Y': 504 case 'Z': 505 case 'a': case 'b': case 'c': case 'd': case 'e': 506 case 'f': case 'g': case 'h': case 'i': 507 case 'k': case 'l': case 'm': case 'n': case 'o': 508 case 'p': case 'q': case 'r': case 's': case 't': 509 case 'u': case 'v': case 'w': case 'x': case 'y': 510 case 'z': 511 return "-0000"; /* See RFC 2822, sec 4.3 */ 512 default: 513 return obs_zone; 514 } 515 } 516 for (zp = obs_zone_tbl; zp->zone; zp++) { 517 if (strcmp(obs_zone, zp->zone) == 0) 518 return zp->gmtoff; 519 } 520 return obs_zone; 521 } 522 523 /* 524 * Parse the 'Date:" field into a tm structure and return the gmtoff 525 * string or NULL on error. 526 */ 527 static const char * 528 date_to_tm(char *date, struct tm *tm) 529 { 530 /**************************************************************** 531 * The header 'date-time' syntax - From RFC 2822 sec 3.3 and 4.3: 532 * 533 * date-time = [ day-of-week "," ] date FWS time [CFWS] 534 * day-of-week = ([FWS] day-name) / obs-day-of-week 535 * day-name = "Mon" / "Tue" / "Wed" / "Thu" / 536 * "Fri" / "Sat" / "Sun" 537 * date = day month year 538 * year = 4*DIGIT / obs-year 539 * month = (FWS month-name FWS) / obs-month 540 * month-name = "Jan" / "Feb" / "Mar" / "Apr" / 541 * "May" / "Jun" / "Jul" / "Aug" / 542 * "Sep" / "Oct" / "Nov" / "Dec" 543 * day = ([FWS] 1*2DIGIT) / obs-day 544 * time = time-of-day FWS zone 545 * time-of-day = hour ":" minute [ ":" second ] 546 * hour = 2DIGIT / obs-hour 547 * minute = 2DIGIT / obs-minute 548 * second = 2DIGIT / obs-second 549 * zone = (( "+" / "-" ) 4DIGIT) / obs-zone 550 * 551 * obs-day-of-week = [CFWS] day-name [CFWS] 552 * obs-year = [CFWS] 2*DIGIT [CFWS] 553 * obs-month = CFWS month-name CFWS 554 * obs-day = [CFWS] 1*2DIGIT [CFWS] 555 * obs-hour = [CFWS] 2DIGIT [CFWS] 556 * obs-minute = [CFWS] 2DIGIT [CFWS] 557 * obs-second = [CFWS] 2DIGIT [CFWS] 558 ****************************************************************/ 559 /* 560 * For example, a typical date might look like: 561 * 562 * Date: Mon, 1 Oct 2007 05:38:10 +0000 (UTC) 563 */ 564 char *tail; 565 char *p; 566 struct tm tmp_tm; 567 /* 568 * NOTE: Rather than depend on strptime(3) modifying only 569 * those fields specified in its format string, we use tmp_tm 570 * and copy the appropriate result to tm. This is not 571 * required with the NetBSD strptime(3) implementation. 572 */ 573 574 /* Check for an optional 'day-of-week' */ 575 if ((tail = strptime(date, " %a,", &tmp_tm)) == NULL) 576 tail = date; 577 else 578 tm->tm_wday = tmp_tm.tm_wday; 579 580 /* Get the required 'day' and 'month' */ 581 if ((tail = strptime(tail, " %d %b", &tmp_tm)) == NULL) 582 return NULL; 583 584 tm->tm_mday = tmp_tm.tm_mday; 585 tm->tm_mon = tmp_tm.tm_mon; 586 587 /* Check for 'obs-year' (2 digits) before 'year' (4 digits) */ 588 /* XXX - Portable? This depends on strptime not scanning off 589 * trailing whitespace unless specified in the format string. 590 */ 591 if ((p = strptime(tail, " %y", &tmp_tm)) != NULL && is_WSP(*p)) 592 tail = p; 593 else if ((tail = strptime(tail, " %Y", &tmp_tm)) == NULL) 594 return NULL; 595 596 tm->tm_year = tmp_tm.tm_year; 597 598 /* Get the required 'hour' and 'minute' */ 599 if ((tail = strptime(tail, " %H:%M", &tmp_tm)) == NULL) 600 return NULL; 601 602 tm->tm_hour = tmp_tm.tm_hour; 603 tm->tm_min = tmp_tm.tm_min; 604 605 /* Check for an optional 'seconds' field */ 606 if ((p = strptime(tail, ":%S", &tmp_tm)) != NULL) { 607 tail = p; 608 tm->tm_sec = tmp_tm.tm_sec; 609 } 610 611 tail = skip_WSP(tail); 612 613 /* 614 * The timezone name is frequently in a comment following the 615 * zone offset. 616 * 617 * XXX - this will get overwritten later by timegm(3). 618 */ 619 if ((p = strchr(tail, '(')) != NULL) 620 tm->tm_zone = get_comments(p); 621 else 622 tm->tm_zone = NULL; 623 624 /* what remains should be the gmtoff string */ 625 tail = skin(tail); 626 return convert_obs_zone(tail); 627 } 628 629 /* 630 * Parse the headline string into a tm structure. Returns a pointer 631 * to first non-whitespace after the date or NULL on error. 632 * 633 * XXX - This needs to be consistent with isdate(). 634 */ 635 static char * 636 hl_date_to_tm(const char *buf, struct tm *tm) 637 { 638 /**************************************************************** 639 * The BSD and System V headline date formats differ 640 * and each have an optional timezone field between 641 * the time and date (see head.c). Unfortunately, 642 * strptime(3) doesn't know about timezone fields, so 643 * we have to handle it ourselves. 644 * 645 * char ctype[] = "Aaa Aaa O0 00:00:00 0000"; 646 * char tmztype[] = "Aaa Aaa O0 00:00:00 AAA 0000"; 647 * char SysV_ctype[] = "Aaa Aaa O0 00:00 0000"; 648 * char SysV_tmztype[] = "Aaa Aaa O0 00:00 AAA 0000"; 649 ****************************************************************/ 650 char *tail; 651 char *p; 652 char zone[4]; 653 struct tm tmp_tm; /* see comment in date_to_tm() */ 654 int len; 655 656 zone[0] = '\0'; 657 if ((tail = strptime(buf, " %a %b %d %H:%M", &tmp_tm)) == NULL) 658 return NULL; 659 660 tm->tm_wday = tmp_tm.tm_wday; 661 tm->tm_mday = tmp_tm.tm_mday; 662 tm->tm_mon = tmp_tm.tm_mon; 663 tm->tm_hour = tmp_tm.tm_hour; 664 tm->tm_min = tmp_tm.tm_min; 665 666 /* Check for an optional 'seconds' field */ 667 if ((p = strptime(tail, ":%S", &tmp_tm)) != NULL) { 668 tail = p; 669 tm->tm_sec = tmp_tm.tm_sec; 670 } 671 672 /* Grab an optional timezone name */ 673 /* 674 * XXX - Is the zone name always 3 characters as in isdate()? 675 */ 676 if (sscanf(tail, " %3[A-Z] %n", zone, &len) == 1) { 677 if (zone[0]) 678 tm->tm_zone = savestr(zone); 679 tail += len; 680 } 681 682 /* Grab the required year field */ 683 tail = strptime(tail, " %Y ", &tmp_tm); 684 tm->tm_year = tmp_tm.tm_year; /* save this even if it failed */ 685 686 return tail; 687 } 688 689 /* 690 * Get the date and time info from the "Date:" line, parse it into a 691 * tm structure as much as possible. 692 * 693 * Note: We return the gmtoff as a string as "-0000" has special 694 * meaning. See RFC 2822, sec 3.3. 695 */ 696 PUBLIC void 697 dateof(struct tm *tm, struct message *mp, int use_hl_date) 698 { 699 static int tzinit = 0; 700 char *date = NULL; 701 const char *gmtoff; 702 703 (void)memset(tm, 0, sizeof(*tm)); 704 705 /* Make sure the time zone info is initialized. */ 706 if (!tzinit) { 707 tzinit = 1; 708 tzset(); 709 } 710 if (mp == NULL) { /* use local time */ 711 time_t now; 712 (void)time(&now); 713 (void)localtime_r(&now, tm); 714 return; 715 } 716 717 /* 718 * See RFC 2822 sec 3.3 for date-time format used in 719 * the "Date:" field. 720 * 721 * NOTE: The range for the time is 00:00 to 23:60 (to allow 722 * for a leap second), but I have seen this violated making 723 * strptime() fail, e.g., 724 * 725 * Date: Tue, 24 Oct 2006 24:07:58 +0400 726 * 727 * In this case we (silently) fall back to the headline time 728 * which was written locally when the message was received. 729 * Of course, this is not the same time as in the Date field. 730 */ 731 if (use_hl_date == 0 && 732 (date = hfield("date", mp)) != NULL && 733 (gmtoff = date_to_tm(date, tm)) != NULL) { 734 int hour; 735 int min; 736 char sign[2]; 737 struct tm save_tm; 738 739 /* 740 * Scan the gmtoff and use it to convert the time to a 741 * local time. 742 * 743 * Note: "-0000" means no valid zone info. See 744 * RFC 2822, sec 3.3. 745 * 746 * XXX - This is painful! Is there a better way? 747 */ 748 749 tm->tm_isdst = -1; /* let timegm(3) determine tm_isdst */ 750 save_tm = *tm; /* use this if we fail */ 751 752 if (strcmp(gmtoff, "-0000") != 0 && 753 sscanf(gmtoff, " %1[+-]%2d%2d ", sign, &hour, &min) == 3) { 754 time_t otime; 755 756 if (sign[0] == '-') { 757 tm->tm_hour += hour; 758 tm->tm_min += min; 759 } 760 else { 761 tm->tm_hour -= hour; 762 tm->tm_min -= min; 763 } 764 if ((otime = timegm(tm)) == (time_t)-1 || 765 localtime_r(&otime, tm) == NULL) { 766 if (debug) 767 warnx("cannot convert date: \"%s\"", date); 768 *tm = save_tm; 769 } 770 } 771 else { /* Unable to do the conversion to local time. */ 772 *tm = save_tm; 773 /* tm->tm_isdst = -1; */ /* Set above */ 774 tm->tm_gmtoff = 0; 775 tm->tm_zone = NULL; 776 } 777 } 778 else { 779 struct headline hl; 780 char headline[LINESIZE]; 781 char pbuf[LINESIZE]; 782 783 if (debug && use_hl_date == 0) 784 warnx("invalid date: \"%s\"", date ? date : "<null>"); 785 786 /* 787 * The headline is written locally so failures here 788 * should be seen (i.e., not conditional on 'debug'). 789 */ 790 tm->tm_isdst = -1; /* let mktime(3) determine tm_isdst */ 791 headline[0] = '\0'; 792 (void)readline(setinput(mp), headline, (int)sizeof(headline), 0); 793 parse(headline, &hl, pbuf); 794 if (hl.l_date == NULL) 795 warnx("invalid headline: `%s'", headline); 796 797 else if (hl_date_to_tm(hl.l_date, tm) == NULL || 798 mktime(tm) == -1) 799 warnx("invalid headline date: `%s'", hl.l_date); 800 } 801 } 802 803 /* 804 * Get the sender's address for display. Let nameof() do this. 805 */ 806 static const char * 807 addrof(struct message *mp) 808 { 809 if (mp == NULL) 810 return NULL; 811 812 return nameof(mp, 0); 813 } 814 815 /************************************************************************ 816 * The 'address' syntax - from RFC 2822: 817 * 818 * specials = "(" / ")" / ; Special characters used in 819 * "<" / ">" / ; other parts of the syntax 820 * "[" / "]" / 821 * ":" / ";" / 822 * "@" / "\" / 823 * "," / "." / 824 * DQUOTE 825 * qtext = NO-WS-CTL / ; Non white space controls 826 * %d33 / ; The rest of the US-ASCII 827 * %d35-91 / ; characters not including "\" 828 * %d93-126 ; or the quote character 829 * qcontent = qtext / quoted-pair 830 * quoted-string = [CFWS] 831 * DQUOTE *([FWS] qcontent) [FWS] DQUOTE 832 * [CFWS] 833 * atext = ALPHA / DIGIT / ; Any character except controls, 834 * "!" / "#" / ; SP, and specials. 835 * "$" / "%" / ; Used for atoms 836 * "&" / "'" / 837 * "*" / "+" / 838 * "-" / "/" / 839 * "=" / "?" / 840 * "^" / "_" / 841 * "`" / "{" / 842 * "|" / "}" / 843 * "~" 844 * atom = [CFWS] 1*atext [CFWS] 845 * word = atom / quoted-string 846 * phrase = 1*word / obs-phrase 847 * display-name = phrase 848 * dtext = NO-WS-CTL / ; Non white space controls 849 * %d33-90 / ; The rest of the US-ASCII 850 * %d94-126 ; characters not including "[", 851 * ; "]", or "\" 852 * dcontent = dtext / quoted-pair 853 * domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS] 854 * domain = dot-atom / domain-literal / obs-domain 855 * local-part = dot-atom / quoted-string / obs-local-part 856 * addr-spec = local-part "@" domain 857 * angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr 858 * name-addr = [display-name] angle-addr 859 * mailbox = name-addr / addr-spec 860 * mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list 861 * group = display-name ":" [mailbox-list / CFWS] ";" 862 * [CFWS] 863 * address = mailbox / group 864 ************************************************************************/ 865 static char * 866 get_display_name(char *name) 867 { 868 char nbuf[LINESIZE]; 869 const char *p; 870 char *q; 871 char *qend; 872 char *lastq; 873 int quoted; 874 875 if (name == NULL) 876 return NULL; 877 878 q = nbuf; 879 lastq = nbuf; 880 qend = nbuf + sizeof(nbuf) - 1; /* reserve space for '\0' */ 881 quoted = 0; 882 for (p = skip_WSP(name); *p != '\0'; p++) { 883 DPRINTF(("get_display_name: %s\n", p)); 884 switch (*p) { 885 case '"': /* quoted-string */ 886 q = nbuf; 887 p = snarf_quote(&q, qend, p); 888 if (!quoted) 889 lastq = q; 890 quoted = 1; 891 break; 892 893 case ':': /* group */ 894 case '<': /* angle-address */ 895 if (lastq == nbuf) 896 return NULL; 897 *lastq = '\0'; /* NULL termination */ 898 return savestr(nbuf); 899 900 case '(': /* comment - skip it! */ 901 p = snarf_comment(NULL, NULL, p); 902 break; 903 904 default: 905 if (!quoted && q < qend) { 906 *q++ = *p; 907 if (!is_WSP(*p) 908 /* && !is_specials((unsigned char)*p) */) 909 lastq = q; 910 } 911 break; 912 } 913 } 914 return NULL; /* no group or angle-address */ 915 } 916 917 /* 918 * See RFC 2822 sec 3.4 and 3.6.2. 919 */ 920 static const char * 921 userof(struct message *mp) 922 { 923 char *sender; 924 char *dispname; 925 926 if (mp == NULL) 927 return NULL; 928 929 if ((sender = hfield("from", mp)) != NULL || 930 (sender = hfield("sender", mp)) != NULL) 931 /* 932 * Try to get the display-name. If one doesn't exist, 933 * then the best we can hope for is that the user's 934 * name is in the comments. 935 */ 936 if ((dispname = get_display_name(sender)) != NULL || 937 (dispname = get_comments(sender)) != NULL) 938 return dispname; 939 return NULL; 940 } 941 942 /* 943 * Grab the subject line. 944 */ 945 static const char * 946 subjof(struct message *mp) 947 { 948 const char *subj; 949 950 if (mp == NULL) 951 return NULL; 952 953 if ((subj = hfield("subject", mp)) == NULL) 954 subj = hfield("subj", mp); 955 return subj; 956 } 957 958 /* 959 * Protect a string against strftime() conversion. 960 */ 961 static const char* 962 protect(const char *str) 963 { 964 char *p, *q; 965 size_t size; 966 967 if (str == NULL || (size = strlen(str)) == 0) 968 return str; 969 970 p = salloc(2 * size + 1); 971 for (q = p; *str; str++) { 972 *q = *str; 973 if (*q++ == '%') 974 *q++ = '%'; 975 } 976 *q = '\0'; 977 return p; 978 } 979 980 static char * 981 preformat(struct tm *tm, const char *oldfmt, struct message *mp, int use_hl_date) 982 { 983 const char *subj; 984 const char *addr; 985 const char *user; 986 const char *p; 987 char *q; 988 char *newfmt; 989 size_t fmtsize; 990 991 if (mp != NULL && (mp->m_flag & MDELETED) != 0) 992 mp = NULL; /* deleted mail shouldn't show up! */ 993 994 subj = protect(subjof(mp)); 995 addr = protect(addrof(mp)); 996 user = protect(userof(mp)); 997 dateof(tm, mp, use_hl_date); 998 fmtsize = LINESIZE; 999 newfmt = ecalloc(1, fmtsize); /* so we can realloc() in check_bufsize() */ 1000 q = newfmt; 1001 p = oldfmt; 1002 while (*p) { 1003 if (*p == '%') { 1004 const char *fp; 1005 fp = subformat(&p, mp, addr, user, subj, tm->tm_isdst); 1006 if (fp) { 1007 size_t len; 1008 len = strlen(fp); 1009 check_bufsize(&newfmt, &fmtsize, &q, len); 1010 (void)strcpy(q, fp); 1011 q += len; 1012 continue; 1013 } 1014 } 1015 check_bufsize(&newfmt, &fmtsize, &q, 1); 1016 *q++ = *p++; 1017 } 1018 *q = '\0'; 1019 1020 return newfmt; 1021 } 1022 1023 /* 1024 * If a format string begins with the USE_HL_DATE string, smsgprintf 1025 * will use the headerline date rather than trying to extract the date 1026 * from the Date field. 1027 * 1028 * Note: If a 'valid' date cannot be extracted from the Date field, 1029 * then the headline date is used. 1030 */ 1031 #define USE_HL_DATE "%??" 1032 1033 PUBLIC char * 1034 smsgprintf(const char *fmtstr, struct message *mp) 1035 { 1036 struct tm tm; 1037 int use_hl_date; 1038 char *newfmt; 1039 char *buf; 1040 size_t bufsize; 1041 1042 if (strncmp(fmtstr, USE_HL_DATE, sizeof(USE_HL_DATE) - 1) != 0) 1043 use_hl_date = 0; 1044 else { 1045 use_hl_date = 1; 1046 fmtstr += sizeof(USE_HL_DATE) - 1; 1047 } 1048 bufsize = LINESIZE; 1049 buf = salloc(bufsize); 1050 newfmt = preformat(&tm, fmtstr, mp, use_hl_date); 1051 (void)strftime(buf, bufsize, newfmt, &tm); 1052 free(newfmt); /* preformat() uses malloc()/realloc() */ 1053 return buf; 1054 } 1055 1056 1057 PUBLIC void 1058 fmsgprintf(FILE *fo, const char *fmtstr, struct message *mp) 1059 { 1060 char *buf; 1061 1062 buf = smsgprintf(fmtstr, mp); 1063 (void)fprintf(fo, "%s\n", buf); /* XXX - add the newline here? */ 1064 } 1065