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