xref: /netbsd-src/usr.bin/mail/format.c (revision deb6f0161a9109e7de9b519dc8dfb9478668dcdd)
1 /*	$NetBSD: format.c,v 1.15 2009/04/11 14:22:32 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.15 2009/04/11 14:22:32 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 
54 static void
55 check_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 
66 static const char *
67 sfmtoff(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 
82 static const char *
83 sfmtint(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 
98 static const char *
99 sfmtstr(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
115 static char*
116 sfmtdepth(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 
130 static const char *
131 sfmtfield(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 
182 struct 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 
189 static void
190 get_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 
201 static const char *
202 sfmtflag(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 
262 static const char *
263 login_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  */
281 static inline const char *
282 skip_fmt(const char **src, const char *p)
283 {
284 	*src = p;
285 	return NULL;
286 }
287 
288 static const char *
289 subformat(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 
367 static const char *
368 snarf_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 
406 static const char *
407 snarf_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  */
437 static char *
438 get_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  */
479 static const char *
480 convert_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  */
534 static const char *
535 date_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  */
642 static char *
643 hl_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  */
703 PUBLIC void
704 dateof(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  */
813 static const char *
814 addrof(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  ************************************************************************/
872 static char *
873 get_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  */
927 static const char *
928 userof(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  */
952 static const char *
953 subjof(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  */
968 static const char*
969 protect(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 
987 static char *
988 preformat(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 
1040 PUBLIC char *
1041 smsgprintf(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 
1064 PUBLIC void
1065 fmsgprintf(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