xref: /netbsd-src/usr.bin/mail/format.c (revision 6ace5b31d919be4c2917f6f665ab54f8812e9aea)
1 /*	$NetBSD: format.c,v 1.19 2024/10/06 19:31:26 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.19 2024/10/06 19:31:26 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 /*
287  * A simple routine to get around a lint warning.
288  */
289 static inline const char *
290 skip_fmt(const char **src, const char *p)
291 {
292 	*src = p;
293 	return NULL;
294 }
295 
296 static const char *
297 subformat(const char **src, struct message *mp, const char *addr,
298     const char *user, const char *subj, int tm_isdst)
299 {
300 #if 0
301 /* XXX - lint doesn't like this, hence skip_fmt(). */
302 #define MP(a)	mp ? a : (*src = (p + 1), NULL)
303 #else
304 #define MP(a)	mp ? a : skip_fmt(src, p + 1);
305 #endif
306 	const char *p;
307 
308 	p = *src;
309 	if (p[1] == '%') {
310 		*src += 2;
311 		return "%%";
312 	}
313 	for (p = *src; *p && !isalpha((unsigned char)*p) && *p != '?'; p++)
314 		continue;
315 
316 	switch (*p) {
317 	/*
318 	 * Our format extensions to strftime(3)
319 	 */
320 	case '?':
321 		return sfmtfield(src, p, mp);
322 	case 'J':
323 		return MP(sfmtint(src, p, (int)(mp->m_lines - mp->m_blines)));
324 	case 'K':
325  		return MP(sfmtint(src, p, (int)mp->m_blines));
326 	case 'L':
327  		return MP(sfmtint(src, p, (int)mp->m_lines));
328 	case 'N':
329 		return sfmtstr(src, p, user);
330 	case 'O':
331  		return MP(sfmtoff(src, p, mp->m_size));
332 	case 'P':
333  		return MP(sfmtstr(src, p, mp == dot ? ">" : " "));
334 	case 'Q':
335  		return MP(sfmtflag(src, p, mp));
336 	case 'f':
337 		return sfmtstr(src, p, addr);
338 	case 'i':
339  		return sfmtint(src, p, get_msgnum(mp));	/* '0' if mp == NULL */
340 	case 'n':
341 		return sfmtstr(src, p, login_name(addr));
342 	case 'q':
343 		return sfmtstr(src, p, subj);
344 	case 't':
345 		return sfmtint(src, p, get_msgCount());
346 
347 	/*
348 	 * strftime(3) special cases:
349 	 *
350 	 * When 'tm_isdst' was not determined (i.e., < 0), a C99
351 	 * compliant strftime(3) will output an empty string for the
352 	 * "%Z" and "%z" formats.  This messes up alignment so we
353 	 * handle these ourselves.
354 	 */
355 	case 'Z':
356 		if (tm_isdst < 0) {
357 			*src = p + 1;
358 			return "???";	/* XXX - not ideal */
359 		}
360 		return NULL;
361 	case 'z':
362 		if (tm_isdst < 0) {
363 			*src = p + 1;
364 			return "-0000";	/* consistent with RFC 2822 */
365 		}
366 		return NULL;
367 
368 	/* everything else is handled by strftime(3) */
369 	default:
370 		return NULL;
371 	}
372 #undef MP
373 }
374 
375 static const char *
376 snarf_comment(char **buf, char *bufend, const char *string)
377 {
378 	const char *p;
379 	char *q;
380 	char *qend;
381 	int clevel;
382 
383 	q    = buf ? *buf : NULL;
384 	qend = buf ? bufend : NULL;
385 
386 	clevel = 1;
387 	for (p = string + 1; *p != '\0'; p++) {
388 		DPRINTF(("snarf_comment: %s\n", p));
389 		if (*p == '(') {
390 			clevel++;
391 			continue;
392 		}
393 		if (*p == ')') {
394 			if (--clevel == 0)
395 				break;
396 			continue;
397 		}
398 		if (*p == '\\' && p[1] != 0)
399 			p++;
400 
401 		if (q < qend)
402 			*q++ = *p;
403 	}
404 	if (buf) {
405 		*q = '\0';
406 		DPRINTF(("snarf_comment: terminating: %s\n", *buf));
407 		*buf = q;
408 	}
409 	if (*p == '\0')
410 		p--;
411 	return p;
412 }
413 
414 static const char *
415 snarf_quote(char **buf, char *bufend, const char *string)
416 {
417 	const char *p;
418 	char *q;
419 	char *qend;
420 
421 	q    = buf ? *buf : NULL;
422 	qend = buf ? bufend : NULL;
423 
424 	for (p = string + 1; *p != '\0' && *p != '"'; p++) {
425 		DPRINTF(("snarf_quote: %s\n", p));
426 		if (*p == '\\' && p[1] != '\0')
427 			p++;
428 
429 		if (q < qend)
430 			*q++ = *p;
431 	}
432 	if (buf) {
433 		*q = '\0';
434 		DPRINTF(("snarf_quote: terminating: %s\n", *buf));
435 		*buf = q;
436 	}
437 	if (*p == '\0')
438 		p--;
439 	return p;
440 }
441 
442 /*
443  * Grab the comments, separating each by a space.
444  */
445 static char *
446 get_comments(char *name)
447 {
448 	char nbuf[LINESIZE];
449 	const char *p;
450 	char *qend;
451 	char *q;
452 	char *lastq;
453 
454 	if (name == NULL)
455 		return NULL;
456 
457 	p = name;
458 	q = nbuf;
459 	lastq = nbuf;
460 	qend = nbuf + sizeof(nbuf) - 1;
461 	for (p = skip_WSP(name); *p != '\0'; p++) {
462 		DPRINTF(("get_comments: %s\n", p));
463 		switch (*p) {
464 		case '"': /* quoted-string ... skip it! */
465 			p = snarf_quote(NULL, NULL, p);
466 			break;
467 
468 		case '(':
469 			p = snarf_comment(&q, qend, p);
470 			lastq = q;
471 			if (q < qend) /* separate comments by space */
472 				*q++ = ' ';
473 			break;
474 
475 		default:
476 			break;
477 		}
478 	}
479 	*lastq = '\0';
480 	return savestr(nbuf);
481 }
482 
483 /*
484  * Convert a possible obs_zone (see RFC 2822, sec 4.3) to a valid
485  * gmtoff string.
486  */
487 static const char *
488 convert_obs_zone(const char *obs_zone)
489 {
490 	static const struct obs_zone_tbl_s {
491 		const char *zone;
492 		const char *gmtoff;
493 	} obs_zone_tbl[] = {
494 		{"UT",	"+0000"},
495 		{"GMT",	"+0000"},
496 		{"EST",	"-0500"},
497 		{"EDT",	"-0400"},
498 		{"CST",	"-0600"},
499 		{"CDT",	"-0500"},
500 		{"MST",	"-0700"},
501 		{"MDT",	"-0600"},
502 		{"PST",	"-0800"},
503 		{"PDT",	"-0700"},
504 		{NULL,	NULL},
505 	};
506 	const struct obs_zone_tbl_s *zp;
507 
508 	if (obs_zone[0] == '+' || obs_zone[0] == '-')
509 		return obs_zone;
510 
511 	if (obs_zone[1] == 0) { /* possible military zones */
512 		/* be explicit here - avoid C extensions and ctype(3) */
513 		switch((unsigned char)obs_zone[0]) {
514 		case 'A': case 'B': case 'C': case 'D':	case 'E':
515 		case 'F': case 'G': case 'H': case 'I':
516 		case 'K': case 'L': case 'M': case 'N': case 'O':
517 		case 'P': case 'Q': case 'R': case 'S': case 'T':
518 		case 'U': case 'V': case 'W': case 'X': case 'Y':
519 		case 'Z':
520 		case 'a': case 'b': case 'c': case 'd':	case 'e':
521 		case 'f': case 'g': case 'h': case 'i':
522 		case 'k': case 'l': case 'm': case 'n': case 'o':
523 		case 'p': case 'q': case 'r': case 's': case 't':
524 		case 'u': case 'v': case 'w': case 'x': case 'y':
525 		case 'z':
526 			return "-0000";	/* See RFC 2822, sec 4.3 */
527 		default:
528 			return obs_zone;
529 		}
530 	}
531 	for (zp = obs_zone_tbl; zp->zone; zp++) {
532 		if (strcmp(obs_zone, zp->zone) == 0)
533 			return zp->gmtoff;
534 	}
535 	return obs_zone;
536 }
537 
538 /*
539  * Parse the 'Date:" field into a tm structure and return the gmtoff
540  * string or NULL on error.
541  */
542 static const char *
543 date_to_tm(char *date, struct tm *tm)
544 {
545 	/****************************************************************
546 	 * The header 'date-time' syntax - From RFC 2822 sec 3.3 and 4.3:
547 	 *
548 	 * date-time       =       [ day-of-week "," ] date FWS time [CFWS]
549 	 * day-of-week     =       ([FWS] day-name) / obs-day-of-week
550 	 * day-name        =       "Mon" / "Tue" / "Wed" / "Thu" /
551 	 *                         "Fri" / "Sat" / "Sun"
552 	 * date            =       day month year
553 	 * year            =       4*DIGIT / obs-year
554 	 * month           =       (FWS month-name FWS) / obs-month
555 	 * month-name      =       "Jan" / "Feb" / "Mar" / "Apr" /
556 	 *                         "May" / "Jun" / "Jul" / "Aug" /
557 	 *                         "Sep" / "Oct" / "Nov" / "Dec"
558 	 * day             =       ([FWS] 1*2DIGIT) / obs-day
559 	 * time            =       time-of-day FWS zone
560 	 * time-of-day     =       hour ":" minute [ ":" second ]
561 	 * hour            =       2DIGIT / obs-hour
562 	 * minute          =       2DIGIT / obs-minute
563 	 * second          =       2DIGIT / obs-second
564 	 * zone            =       (( "+" / "-" ) 4DIGIT) / obs-zone
565 	 *
566 	 * obs-day-of-week =       [CFWS] day-name [CFWS]
567 	 * obs-year        =       [CFWS] 2*DIGIT [CFWS]
568 	 * obs-month       =       CFWS month-name CFWS
569 	 * obs-day         =       [CFWS] 1*2DIGIT [CFWS]
570 	 * obs-hour        =       [CFWS] 2DIGIT [CFWS]
571 	 * obs-minute      =       [CFWS] 2DIGIT [CFWS]
572 	 * obs-second      =       [CFWS] 2DIGIT [CFWS]
573 	 ****************************************************************/
574 	/*
575 	 * For example, a typical date might look like:
576 	 *
577 	 * Date: Mon,  1 Oct 2007 05:38:10 +0000 (UTC)
578 	 */
579 	char *tail;
580 	char *p;
581 	struct tm tmp_tm;
582 	/*
583 	 * NOTE: Rather than depend on strptime(3) modifying only
584 	 * those fields specified in its format string, we use tmp_tm
585 	 * and copy the appropriate result to tm.  This is not
586 	 * required with the NetBSD strptime(3) implementation.
587 	 */
588 
589 	/* Check for an optional 'day-of-week' */
590 	if ((tail = strptime(date, " %a,", &tmp_tm)) == NULL)
591 		tail = date;
592 	else
593 		tm->tm_wday = tmp_tm.tm_wday;
594 
595 	/* Get the required 'day' and 'month' */
596 	if ((tail = strptime(tail, " %d %b", &tmp_tm)) == NULL)
597 		return NULL;
598 
599 	tm->tm_mday = tmp_tm.tm_mday;
600 	tm->tm_mon  = tmp_tm.tm_mon;
601 
602 	/* Check for 'obs-year' (2 digits) before 'year' (4 digits) */
603 	/* XXX - Portable?  This depends on strptime not scanning off
604 	 * trailing whitespace unless specified in the format string.
605 	 */
606 	if ((p = strptime(tail, " %y", &tmp_tm)) != NULL && is_WSP(*p))
607 		tail = p;
608 	else if ((tail = strptime(tail, " %Y", &tmp_tm)) == NULL)
609 		return NULL;
610 
611 	tm->tm_year = tmp_tm.tm_year;
612 
613 	/* Get the required 'hour' and 'minute' */
614 	if ((tail = strptime(tail, " %H:%M", &tmp_tm)) == NULL)
615 		return NULL;
616 
617 	tm->tm_hour = tmp_tm.tm_hour;
618 	tm->tm_min  = tmp_tm.tm_min;
619 
620 	/* Check for an optional 'seconds' field */
621 	if ((p = strptime(tail, ":%S", &tmp_tm)) != NULL) {
622 		tail = p;
623 		tm->tm_sec = tmp_tm.tm_sec;
624 	}
625 
626 	tail = skip_WSP(tail);
627 
628 	/*
629 	 * The timezone name is frequently in a comment following the
630 	 * zone offset.
631 	 *
632 	 * XXX - this will get overwritten later by timegm(3).
633 	 */
634 	if ((p = strchr(tail, '(')) != NULL)
635 		tm->tm_zone = get_comments(p);
636 	else
637 		tm->tm_zone = NULL;
638 
639 	/* what remains should be the gmtoff string */
640 	tail = skin(tail);
641 	return convert_obs_zone(tail);
642 }
643 
644 /*
645  * Parse the headline string into a tm structure.  Returns a pointer
646  * to first non-whitespace after the date or NULL on error.
647  *
648  * XXX - This needs to be consistent with isdate().
649  */
650 static char *
651 hl_date_to_tm(const char *buf, struct tm *tm)
652 {
653 	/****************************************************************
654 	 * The BSD and System V headline date formats differ
655 	 * and each have an optional timezone field between
656 	 * the time and date (see head.c).  Unfortunately,
657 	 * strptime(3) doesn't know about timezone fields, so
658 	 * we have to handle it ourselves.
659 	 *
660 	 * char ctype[]        = "Aaa Aaa O0 00:00:00 0000";
661 	 * char tmztype[]      = "Aaa Aaa O0 00:00:00 AAA 0000";
662 	 * char SysV_ctype[]   = "Aaa Aaa O0 00:00 0000";
663 	 * char SysV_tmztype[] = "Aaa Aaa O0 00:00 AAA 0000";
664 	 ****************************************************************/
665 	char *tail;
666 	char *p;
667 	char zone[4];
668 	struct tm tmp_tm; /* see comment in date_to_tm() */
669 	int len;
670 
671 	zone[0] = '\0';
672 	if ((tail = strptime(buf, " %a %b %d %H:%M", &tmp_tm)) == NULL)
673 		return NULL;
674 
675 	tm->tm_wday = tmp_tm.tm_wday;
676 	tm->tm_mday = tmp_tm.tm_mday;
677 	tm->tm_mon  = tmp_tm.tm_mon;
678 	tm->tm_hour = tmp_tm.tm_hour;
679 	tm->tm_min  = tmp_tm.tm_min;
680 
681 	/* Check for an optional 'seconds' field */
682 	if ((p = strptime(tail, ":%S", &tmp_tm)) != NULL) {
683 		tail = p;
684 		tm->tm_sec = tmp_tm.tm_sec;
685 	}
686 
687 	/* Grab an optional timezone name */
688 	/*
689 	 * XXX - Is the zone name always 3 characters as in isdate()?
690 	 */
691 	if (sscanf(tail, " %3[A-Z] %n", zone, &len) == 1) {
692 		if (zone[0])
693 			tm->tm_zone = savestr(zone);
694 		tail += len;
695 	}
696 
697 	/* Grab the required year field */
698 	tail = strptime(tail, " %Y ", &tmp_tm);
699 	tm->tm_year = tmp_tm.tm_year; /* save this even if it failed */
700 
701 	return tail;
702 }
703 
704 /*
705  * Get the date and time info from the "Date:" line, parse it into a
706  * tm structure as much as possible.
707  *
708  * Note: We return the gmtoff as a string as "-0000" has special
709  * meaning.  See RFC 2822, sec 3.3.
710  */
711 PUBLIC void
712 dateof(struct tm *tm, struct message *mp, int use_hl_date)
713 {
714 	static int tzinit = 0;
715 	char *date = NULL;
716 	const char *gmtoff;
717 
718 	(void)memset(tm, 0, sizeof(*tm));
719 
720 	/* Make sure the time zone info is initialized. */
721 	if (!tzinit) {
722 		tzinit = 1;
723 		tzset();
724 	}
725 	if (mp == NULL) {	/* use local time */
726 		time_t now;
727 		(void)time(&now);
728 		(void)localtime_r(&now, tm);
729 		return;
730 	}
731 
732 	/*
733 	 * See RFC 2822 sec 3.3 for date-time format used in
734 	 * the "Date:" field.
735 	 *
736 	 * NOTE: The range for the time is 00:00 to 23:60 (to allow
737 	 * for a leap second), but I have seen this violated making
738 	 * strptime() fail, e.g.,
739 	 *
740 	 *   Date: Tue, 24 Oct 2006 24:07:58 +0400
741 	 *
742 	 * In this case we (silently) fall back to the headline time
743 	 * which was written locally when the message was received.
744 	 * Of course, this is not the same time as in the Date field.
745 	 */
746 	if (use_hl_date == 0 &&
747 	    (date = hfield("date", mp)) != NULL &&
748 	    (gmtoff = date_to_tm(date, tm)) != NULL) {
749 		int hour;
750 		int min;
751 		char sign[2];
752 		struct tm save_tm;
753 
754 		/*
755 		 * Scan the gmtoff and use it to convert the time to a
756 		 * local time.
757 		 *
758 		 * Note: "-0000" means no valid zone info.  See
759 		 *       RFC 2822, sec 3.3.
760 		 *
761 		 * XXX - This is painful!  Is there a better way?
762 		 */
763 
764 		tm->tm_isdst = -1;	/* let timegm(3) determine tm_isdst */
765 		save_tm = *tm;		/* use this if we fail */
766 
767 		if (strcmp(gmtoff, "-0000") != 0 &&
768 		    sscanf(gmtoff, " %1[+-]%2d%2d ", sign, &hour, &min) == 3) {
769 			time_t otime;
770 
771 			if (sign[0] == '-') {
772 				tm->tm_hour += hour;
773 				tm->tm_min += min;
774 			}
775 			else {
776 				tm->tm_hour -= hour;
777 				tm->tm_min -= min;
778 			}
779 			if ((otime = timegm(tm)) == (time_t)-1 ||
780 			    localtime_r(&otime, tm) == NULL) {
781 				if (debug)
782 					warnx("cannot convert date: \"%s\"", date);
783 				*tm = save_tm;
784 			}
785 		}
786 		else {	/* Unable to do the conversion to local time. */
787 			*tm = save_tm;
788 		     /* tm->tm_isdst = -1; */ /* Set above */
789 			tm->tm_gmtoff = 0;
790 			tm->tm_zone = NULL;
791 		}
792 	}
793 	else {
794 		struct headline hl;
795 		char headline[LINESIZE];
796 		char pbuf[LINESIZE];
797 
798 		if (debug && use_hl_date == 0)
799 			warnx("invalid date: \"%s\"", date ? date : "<null>");
800 
801 		/*
802 		 * The headline is written locally so failures here
803 		 * should be seen (i.e., not conditional on 'debug').
804 		 */
805 		tm->tm_isdst = -1; /* let mktime(3) determine tm_isdst */
806 		headline[0] = '\0';
807 		(void)readline(setinput(mp), headline, (int)sizeof(headline), 0);
808 		parse(headline, &hl, pbuf);
809 		if (hl.l_date == NULL)
810 			warnx("invalid headline: `%s'", headline);
811 
812 		else if (hl_date_to_tm(hl.l_date, tm) == NULL ||
813 		    mktime(tm) == -1)
814 			warnx("invalid headline date: `%s'", hl.l_date);
815 	}
816 }
817 
818 /*
819  * Get the sender's address for display.  Let nameof() do this.
820  */
821 static const char *
822 addrof(struct message *mp)
823 {
824 	if (mp == NULL)
825 		return NULL;
826 
827 	return nameof(mp, 0);
828 }
829 
830 /************************************************************************
831  * The 'address' syntax - from RFC 2822:
832  *
833  * specials        =       "(" / ")" /     ; Special characters used in
834  *                         "<" / ">" /     ;  other parts of the syntax
835  *                         "[" / "]" /
836  *                         ":" / ";" /
837  *                         "@" / "\" /
838  *                         "," / "." /
839  *                         DQUOTE
840  * qtext           =       NO-WS-CTL /     ; Non white space controls
841  *                         %d33 /          ; The rest of the US-ASCII
842  *                         %d35-91 /       ;  characters not including "\"
843  *                         %d93-126        ;  or the quote character
844  * qcontent        =       qtext / quoted-pair
845  * quoted-string   =       [CFWS]
846  *                         DQUOTE *([FWS] qcontent) [FWS] DQUOTE
847  *                         [CFWS]
848  * atext           =       ALPHA / DIGIT / ; Any character except controls,
849  *                         "!" / "#" /     ;  SP, and specials.
850  *                         "$" / "%" /     ;  Used for atoms
851  *                         "&" / "'" /
852  *                         "*" / "+" /
853  *                         "-" / "/" /
854  *                         "=" / "?" /
855  *                         "^" / "_" /
856  *                         "`" / "{" /
857  *                         "|" / "}" /
858  *                         "~"
859  * atom            =       [CFWS] 1*atext [CFWS]
860  * word            =       atom / quoted-string
861  * phrase          =       1*word / obs-phrase
862  * display-name    =       phrase
863  * dtext           =       NO-WS-CTL /     ; Non white space controls
864  *                         %d33-90 /       ; The rest of the US-ASCII
865  *                         %d94-126        ;  characters not including "[",
866  *                                         ;  "]", or "\"
867  * dcontent        =       dtext / quoted-pair
868  * domain-literal  =       [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
869  * domain          =       dot-atom / domain-literal / obs-domain
870  * local-part      =       dot-atom / quoted-string / obs-local-part
871  * addr-spec       =       local-part "@" domain
872  * angle-addr      =       [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
873  * name-addr       =       [display-name] angle-addr
874  * mailbox         =       name-addr / addr-spec
875  * mailbox-list    =       (mailbox *("," mailbox)) / obs-mbox-list
876  * group           =       display-name ":" [mailbox-list / CFWS] ";"
877  *                         [CFWS]
878  * address         =       mailbox / group
879  ************************************************************************/
880 static char *
881 get_display_name(char *name)
882 {
883 	char nbuf[LINESIZE];
884 	const char *p;
885 	char *q;
886 	char *qend;
887 	char *lastq;
888 	int quoted;
889 
890 	if (name == NULL)
891 		return NULL;
892 
893 	q = nbuf;
894 	lastq = nbuf;
895 	qend = nbuf + sizeof(nbuf) - 1;	/* reserve space for '\0' */
896 	quoted = 0;
897 	for (p = skip_WSP(name); *p != '\0'; p++) {
898 		DPRINTF(("get_display_name: %s\n", p));
899 		switch (*p) {
900 		case '"':	/* quoted-string */
901 			q = nbuf;
902 			p = snarf_quote(&q, qend, p);
903 			if (!quoted)
904 				lastq = q;
905 			quoted = 1;
906 			break;
907 
908 		case ':':	/* group */
909 		case '<':	/* angle-address */
910 			if (lastq == nbuf)
911 				return NULL;
912 			*lastq = '\0';	/* NULL termination */
913 			return savestr(nbuf);
914 
915 		case '(':	/* comment - skip it! */
916 			p = snarf_comment(NULL, NULL, p);
917 			break;
918 
919 		default:
920 			if (!quoted && q < qend) {
921 				*q++ = *p;
922 				if (!is_WSP(*p)
923 				    /* && !is_specials((unsigned char)*p) */)
924 					lastq = q;
925 			}
926 			break;
927 		}
928 	}
929 	return NULL;	/* no group or angle-address */
930 }
931 
932 /*
933  * See RFC 2822 sec 3.4 and 3.6.2.
934  */
935 static const char *
936 userof(struct message *mp)
937 {
938 	char *sender;
939 	char *dispname;
940 
941 	if (mp == NULL)
942 		return NULL;
943 
944 	if ((sender = hfield("from", mp)) != NULL ||
945 	    (sender = hfield("sender", mp)) != NULL)
946 		/*
947 		 * Try to get the display-name.  If one doesn't exist,
948 		 * then the best we can hope for is that the user's
949 		 * name is in the comments.
950 		 */
951 		if ((dispname = get_display_name(sender)) != NULL ||
952 		    (dispname = get_comments(sender)) != NULL)
953 			return dispname;
954 	return NULL;
955 }
956 
957 /*
958  * Grab the subject line.
959  */
960 static const char *
961 subjof(struct message *mp)
962 {
963 	const char *subj;
964 
965 	if (mp == NULL)
966 		return NULL;
967 
968 	if ((subj = hfield("subject", mp)) == NULL)
969 		subj = hfield("subj", mp);
970 	return subj;
971 }
972 
973 /*
974  * Protect a string against strftime() conversion.
975  */
976 static const char*
977 protect(const char *str)
978 {
979 	char *p, *q;
980 	size_t size;
981 
982 	if (str == NULL || (size = strlen(str)) == 0)
983 		return str;
984 
985 	p = salloc(2 * size + 1);
986 	for (q = p; *str; str++) {
987 		*q = *str;
988 		if (*q++ == '%')
989 			*q++ = '%';
990 	}
991 	*q = '\0';
992 	return p;
993 }
994 
995 static char *
996 preformat(struct tm *tm, const char *oldfmt, struct message *mp, int use_hl_date)
997 {
998 	const char *subj;
999 	const char *addr;
1000 	const char *user;
1001 	const char *p;
1002 	char *q;
1003 	char *newfmt;
1004 	size_t fmtsize;
1005 
1006 	if (mp != NULL && (mp->m_flag & MDELETED) != 0)
1007 		mp = NULL; /* deleted mail shouldn't show up! */
1008 
1009 	subj = protect(subjof(mp));
1010 	addr = protect(addrof(mp));
1011 	user = protect(userof(mp));
1012 	dateof(tm, mp, use_hl_date);
1013 	fmtsize = LINESIZE;
1014 	newfmt = ecalloc(1, fmtsize); /* so we can realloc() in check_bufsize() */
1015 	q = newfmt;
1016 	p = oldfmt;
1017 	while (*p) {
1018 		if (*p == '%') {
1019 			const char *fp;
1020 			fp = subformat(&p, mp, addr, user, subj, tm->tm_isdst);
1021 			if (fp) {
1022 				size_t len;
1023 				len = strlen(fp);
1024 				check_bufsize(&newfmt, &fmtsize, &q, len);
1025 				(void)strcpy(q, fp);
1026 				q += len;
1027 				continue;
1028 			}
1029 		}
1030 		check_bufsize(&newfmt, &fmtsize, &q, 1);
1031 		*q++ = *p++;
1032 	}
1033 	*q = '\0';
1034 
1035 	return newfmt;
1036 }
1037 
1038 /*
1039  * If a format string begins with the USE_HL_DATE string, smsgprintf
1040  * will use the headerline date rather than trying to extract the date
1041  * from the Date field.
1042  *
1043  * Note: If a 'valid' date cannot be extracted from the Date field,
1044  * then the headline date is used.
1045  */
1046 #define USE_HL_DATE "%??"
1047 
1048 PUBLIC char *
1049 smsgprintf(const char *fmtstr, struct message *mp)
1050 {
1051 	struct tm tm;
1052 	int use_hl_date;
1053 	char *newfmt;
1054 	char *buf;
1055 	size_t bufsize;
1056 
1057 	if (strncmp(fmtstr, USE_HL_DATE, sizeof(USE_HL_DATE) - 1) != 0)
1058 		use_hl_date = 0;
1059 	else {
1060 		use_hl_date = 1;
1061 		fmtstr += sizeof(USE_HL_DATE) - 1;
1062 	}
1063 	bufsize = LINESIZE;
1064 	buf = salloc(bufsize);
1065 	newfmt = preformat(&tm, fmtstr, mp, use_hl_date);
1066 	(void)strftime(buf, bufsize, newfmt, &tm);
1067 	free(newfmt);	/* preformat() uses malloc()/realloc() */
1068 	return buf;
1069 }
1070 
1071 
1072 PUBLIC void
1073 fmsgprintf(FILE *fo, const char *fmtstr, struct message *mp)
1074 {
1075 	char *buf;
1076 
1077 	buf = smsgprintf(fmtstr, mp);
1078 	(void)fprintf(fo, "%s\n", buf);	/* XXX - add the newline here? */
1079 }
1080