Home | History | Annotate | Line # | Download | only in mail
      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