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