xref: /netbsd-src/usr.bin/mail/format.c (revision c0179c282a5968435315a82f4128c61372c68fc3)
1 /*	$NetBSD: format.c,v 1.1 2006/10/31 22:36:37 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.1 2006/10/31 22:36:37 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 
53 
54 #define DEBUG(a)
55 
56 static void
57 check_bufsize(char **buf, size_t *bufsize, char **p, size_t cnt)
58 {
59 	char *q;
60 	if (*p + cnt < *buf + *bufsize)
61 		return;
62 	*bufsize *= 2;
63 	q = realloc(*buf, *bufsize);
64 	*p = q + (*p - *buf);
65 	*buf = q;
66 }
67 
68 static const char *
69 sfmtoff(const char **fmtbeg, const char *fmtch, off_t off)
70 {
71 	char *newfmt;	/* pointer to new format string */
72 	size_t len;	/* space for "lld" including '\0' */
73 	len = fmtch - *fmtbeg + sizeof(PRId64);
74 	newfmt = salloc(len);
75 	(void)strlcpy(newfmt, *fmtbeg, len - sizeof(sizeof(PRId64)) + 1);
76 	(void)strlcat(newfmt, PRId64, len);
77 	*fmtbeg = fmtch + 1;
78 	{
79 		char *p;
80 		char *q;
81 		(void)easprintf(&p, newfmt, off);
82 		q = savestr(p);
83 		free(p);
84 		return q;
85 	}
86 }
87 
88 static const char *
89 sfmtint(const char **fmtbeg, const char *fmtch, int num)
90 {
91 	char *newfmt;
92 	size_t len;
93 
94 	len = fmtch - *fmtbeg + 2;	/* space for 'd' and '\0' */
95 	newfmt = salloc(len);
96 	(void)strlcpy(newfmt, *fmtbeg, len);
97 	newfmt[len-2] = 'd';		/* convert to printf format */
98 
99 	*fmtbeg = fmtch + 1;
100 	{
101 		char *p;
102 		char *q;
103 		(void)easprintf(&p, newfmt, num);
104 		q = savestr(p);
105 		free(p);
106 		return q;
107 	}
108 }
109 
110 static const char *
111 sfmtstr(const char **fmtbeg, const char *fmtch, const char *str)
112 {
113 	char *newfmt;
114 	size_t len;
115 
116 	len = fmtch - *fmtbeg + 2;	/* space for 's' and '\0' */
117 	newfmt = salloc(len);
118 	(void)strlcpy(newfmt, *fmtbeg, len);
119 	newfmt[len-2] = 's';		/* convert to printf format */
120 
121 	*fmtbeg = fmtch + 1;
122 	{
123 		char *p;
124 		char *q;
125 		(void)easprintf(&p, newfmt, str ? str : "");
126 		q = savestr(p);
127 		free(p);
128 		return q;
129 	}
130 }
131 
132 static const char *
133 sfmtfield(const char **fmtbeg, const char *fmtch, struct message *mp)
134 {
135 	char *q;
136 	q = strchr(fmtch + 1, '?');
137 	if (q) {
138 		size_t len;
139 		char *p;
140 		const char *str;
141 		int skin_it;
142 
143 		skin_it = fmtch[1] == '-' ? 1 : 0;
144 		len = q - fmtch - skin_it;
145 		p = salloc(len + 1);
146 		(void)strlcpy(p, fmtch + skin_it + 1, len);
147 		str = sfmtstr(fmtbeg, fmtch, hfield(p, mp));
148 		if (skin_it)
149 			str = skin(__UNCONST(str));
150 		*fmtbeg = q + 1;
151 		return str;
152 	}
153 	return NULL;
154 }
155 
156 static const char *
157 sfmtflag(const char **fmtbeg, const char *fmtch, int flags)
158 {
159 	char disp[2];
160 	disp[0] = ' ';
161 	disp[1] = '\0';
162 	if (flags & MSAVED)
163 		disp[0] = '*';
164 	if (flags & MPRESERVE)
165 		disp[0] = 'P';
166 	if ((flags & (MREAD|MNEW)) == MNEW)
167 		disp[0] = 'N';
168 	if ((flags & (MREAD|MNEW)) == 0)
169 		disp[0] = 'U';
170 	if (flags & MBOX)
171 		disp[0] = 'M';
172 	return sfmtstr(fmtbeg, fmtch, disp);
173 }
174 
175 static const char *
176 login_name(const char *addr)
177 {
178 	char *p;
179 	p = strchr(addr, '@');
180 	if (p) {
181 		char *q;
182 		size_t len;
183 		len = p - addr + 1;
184 		q = salloc(len);
185 		(void)strlcpy(q, addr, len);
186 		return q;
187 	}
188 	return addr;
189 }
190 
191 static const char *
192 subformat(const char **src, struct message *mp, const char *addr,
193     const char *user, const char *subj, const char *gmtoff, const char *zone)
194 {
195 #define MP(a)	mp ? a : NULL
196 	const char *p;
197 
198 	p = *src;
199 	if (p[1] == '%') {
200 		*src += 2;
201 		return "%%";
202 	}
203 	for (p = *src; *p && !isalpha((unsigned char)*p) && *p != '?'; p++)
204 		continue;
205 
206 	switch (*p) {
207 	case '?':
208 		return MP(sfmtfield(src, p, mp));
209 	case 'J':
210  		return MP(sfmtint(src, p, (int)(mp->m_lines - mp->m_blines)));
211 	case 'K':
212  		return MP(sfmtint(src, p, (int)mp->m_blines));
213 	case 'L':
214  		return MP(sfmtint(src, p, (int)mp->m_lines));
215 	case 'N':
216 		return sfmtstr(src, p, user);
217 	case 'O':
218  		return MP(sfmtoff(src, p, mp->m_size));
219 	case 'P':
220  		return MP(sfmtstr(src, p, mp == dot ? ">" : " "));
221 	case 'Q':
222  		return MP(sfmtflag(src, p, mp->m_flag));
223 	case 'Z':
224 		*src = p + 1;
225 		return zone;
226 
227 	case 'f':
228 		return sfmtstr(src, p, addr);
229 	case 'i':
230 		if (mp == NULL && (mp = dot) == NULL)
231 			return NULL;
232  		return sfmtint(src, p, (mp - message) + 1);
233 	case 'n':
234 		return sfmtstr(src, p, login_name(addr));
235 	case 'q':
236 		return sfmtstr(src, p, subj);
237 	case 't':
238 		return sfmtint(src, p, msgCount);
239 	case 'z':
240 		*src = p + 1;
241 		return gmtoff;
242 	default:
243 		return NULL;
244 	}
245 #undef MP
246 }
247 
248 static const char *
249 snarf_comment(char **buf, char *bufend, const char *string)
250 {
251 	const char *p;
252 	char *q;
253 	char *qend;
254 	int clevel;
255 
256 	q    = buf ? *buf : NULL;
257 	qend = buf ? bufend : NULL;
258 
259 	clevel = 1;
260 	for (p = string + 1; *p != '\0'; p++) {
261 		DEBUG(("snarf_comment: %s\n", p));
262 		if (*p == '(') {
263 			clevel++;
264 			continue;
265 		}
266 		if (*p == ')') {
267 			if (--clevel == 0)
268 				break;
269 			continue;
270 		}
271 		if (*p == '\\' && p[1] != 0)
272 			p++;
273 
274 		if (q < qend)
275 			*q++ = *p;
276 	}
277 	if (buf) {
278 		*q = '\0';
279 		DEBUG(("snarf_comment: terminating: %s\n", *buf));
280 		*buf = q;
281 	}
282 	if (*p == '\0')
283 		p--;
284 	return p;
285 }
286 
287 static const char *
288 snarf_quote(char **buf, char *bufend, const char *string)
289 {
290 	const char *p;
291 	char *q;
292 	char *qend;
293 
294 	q    = buf ? *buf : NULL;
295 	qend = buf ? bufend : NULL;
296 
297 	for (p = string + 1; *p != '\0' && *p != '"'; p++) {
298 		DEBUG(("snarf_quote: %s\n", p));
299 		if (*p == '\\' && p[1] != '\0')
300 			p++;
301 
302 		if (q < qend)
303 			*q++ = *p;
304 	}
305 	if (buf) {
306 		*q = '\0';
307 		DEBUG(("snarf_quote: terminating: %s\n", *buf));
308 		*buf = q;
309 	}
310 	if (*p == '\0')
311 		p--;
312 	return p;
313 }
314 
315 /*
316  * Grab the comments, separating each by a space.
317  */
318 static char *
319 get_comments(char *name)
320 {
321 	char nbuf[BUFSIZ];
322 	const char *p;
323 	char *qend;
324 	char *q;
325 	char *lastq;
326 
327 	if (name == NULL)
328 		return(NULL);
329 
330 	p = name;
331 	q = nbuf;
332 	lastq = nbuf;
333 	qend = nbuf + sizeof(nbuf) - 1;
334 	for (p = skip_white(name); *p != '\0'; p++) {
335 		DEBUG(("get_comments: %s\n", p));
336 		switch (*p) {
337 		case '"': /* quoted-string ... skip it! */
338 			p = snarf_quote(NULL, NULL, p);
339 			break;
340 
341 		case '(':
342 			p = snarf_comment(&q, qend, p);
343 			lastq = q;
344 			if (q < qend) /* separate comments by space */
345 				*q++ = ' ';
346 			break;
347 
348 		default:
349 			break;
350 		}
351 	}
352 	*lastq = '\0';
353 	return savestr(nbuf);
354 }
355 
356 static char *
357 my_strptime(const char *buf, const char *fmtstr, struct tm *tm)
358 {
359 	char *tail;
360 	char zone[4];
361 
362 	zone[0] = '\0';
363 	tail = strptime(buf, fmtstr, tm);
364 	if (tail) {
365 		int len;
366 		if (sscanf(tail, " %3[A-Z] %n", zone, &len) == 1) {
367 			if (zone[0])
368 				tm->tm_zone = savestr(zone);
369 			tail += len;
370 		}
371 		tail = strptime(tail, " %Y ", tm);
372 	}
373 	return tail;
374 }
375 
376 /*
377  * Get the date and time info from the "Date:" line, parse it into a
378  * tm structure as much as possible.
379  *
380  * Note: We return the gmtoff as a string as "-0000" has special
381  * meaning.  See RFC 2822, sec 3.3.
382  */
383 static const char *
384 dateof(struct tm *tm, struct message *mp, int use_hl_date)
385 {
386 	char *tail;
387 	char *gmtoff;
388 	const char *date;
389 
390 	(void)memset(tm, 0, sizeof(*tm));
391 
392 	if (mp == NULL) {	/* use local time */
393 		char buf[6];	/* space for "+0000" */
394 		int hour;
395 		int min;
396 		time_t now;
397 		tzset();
398 		(void)time(&now);
399 		(void)localtime_r(&now, tm);
400 		min = (tm->tm_gmtoff / 60) % 60;
401 		hour = tm->tm_gmtoff / 3600;
402 		if (hour > 12)
403 			hour = 24 - hour;
404 		(void)snprintf(buf, sizeof(buf), "%+03d%02d", hour, min);
405 		return savestr(buf);
406 	}
407 	gmtoff = NULL;
408 	tail = NULL;
409 	/*
410 	 * See RFC 2822 sec 3.3 for date-time format used in
411 	 * the "Date:" field.
412 	 *
413 	 * Notes:
414 	 * 1) The 'day-of-week' and 'second' fields are optional so we
415 	 *    check 4 possibilities.  This could be optimized.
416 	 *
417 	 * 2) The timezone is frequently in a comment following the
418 	 *    zone offset.
419 	 *
420 	 * 3) The range for the time is 00:00 to 23:60 (for a leep
421 	 *    second), but I have seen this violated (e.g., Date: Tue,
422 	 *    24 Oct 2006 24:07:58 +0400) making strptime() fail.
423 	 *    Thus we fall back on the headline time which was written
424 	 *    locally when the message was received.  Of course, this
425 	 *    is not the same time as in the Date field.
426 	 */
427 	if (use_hl_date == 0 &&
428 	    (date = hfield("date", mp)) != NULL &&
429 	    ((tail = strptime(date, " %a, %d %b %Y %T ", tm)) != NULL ||
430 	     (tail = strptime(date,     " %d %b %Y %T ", tm)) != NULL ||
431 	     (tail = strptime(date, " %a, %d %b %Y %R ", tm)) != NULL ||
432 	     (tail = strptime(date,     " %d %b %Y %R ", tm)) != NULL)) {
433 		char *cp;
434 		if ((cp = strchr(tail, '(')) != NULL)
435 			tm->tm_zone = get_comments(cp);
436 		else
437 			tm->tm_zone = NULL;
438 		gmtoff = skin(tail);
439 	}
440 	else {
441 		/*
442 		 * The BSD and System V headline date formats differ
443 		 * and each have an optional timezone field between
444 		 * the time and date (see head.c).  Unfortunately,
445 		 * strptime(3) doesn't know about timezone fields, so
446 		 * we have to handle it ourselves.
447 		 *
448 		 * char ctype[]        = "Aaa Aaa O0 00:00:00 0000";
449 		 * char tmztype[]      = "Aaa Aaa O0 00:00:00 AAA 0000";
450 		 * char SysV_ctype[]   = "Aaa Aaa O0 00:00 0000";
451 		 * char SysV_tmztype[] = "Aaa Aaa O0 00:00 AAA 0000";
452 		 */
453 		struct headline hl;
454 		char headline[LINESIZE];
455 		char pbuf[BUFSIZ];
456 
457 		headline[0] = '\0';
458 		date = headline;
459 		(void)mail_readline(setinput(mp), headline, LINESIZE);
460 		parse(headline, &hl, pbuf);
461 		if (hl.l_date != NULL &&
462 		    (tail = my_strptime(hl.l_date, " %a %b %d %T ", tm)) == NULL &&
463 		    (tail = my_strptime(hl.l_date, " %a %b %d %R ", tm)) == NULL) {
464 			warnx("dateof: cannot determine date: %s", hl.l_date);
465 		}
466 	}
467 	/* tail will be NULL here if the mail file is empty, so don't
468 	 * check it. */
469 
470 	/* mark the zone and gmtoff info as invalid for strftime. */
471 	tm->tm_isdst = -1;
472 
473 	return gmtoff;
474 }
475 
476 /*
477  * Get the sender's address for display.  Let nameof() do this.
478  */
479 static const char *
480 addrof(struct message *mp)
481 {
482 	if (mp == NULL)
483 		return NULL;
484 
485 	return nameof(mp, 0);
486 }
487 
488 /************************************************************************
489  * The 'address' syntax - from rfc 2822:
490  *
491  * specials        =       "(" / ")" /     ; Special characters used in
492  *                         "<" / ">" /     ;  other parts of the syntax
493  *                         "[" / "]" /
494  *                         ":" / ";" /
495  *                         "@" / "\" /
496  *                         "," / "." /
497  *                         DQUOTE
498  * qtext           =       NO-WS-CTL /     ; Non white space controls
499  *                         %d33 /          ; The rest of the US-ASCII
500  *                         %d35-91 /       ;  characters not including "\"
501  *                         %d93-126        ;  or the quote character
502  * qcontent        =       qtext / quoted-pair
503  * quoted-string   =       [CFWS]
504  *                         DQUOTE *([FWS] qcontent) [FWS] DQUOTE
505  *                         [CFWS]
506  * atext           =       ALPHA / DIGIT / ; Any character except controls,
507  *                         "!" / "#" /     ;  SP, and specials.
508  *                         "$" / "%" /     ;  Used for atoms
509  *                         "&" / "'" /
510  *                         "*" / "+" /
511  *                         "-" / "/" /
512  *                         "=" / "?" /
513  *                         "^" / "_" /
514  *                         "`" / "{" /
515  *                         "|" / "}" /
516  *                         "~"
517  * atom            =       [CFWS] 1*atext [CFWS]
518  * word            =       atom / quoted-string
519  * phrase          =       1*word / obs-phrase
520  * display-name    =       phrase
521  * dtext           =       NO-WS-CTL /     ; Non white space controls
522  *                         %d33-90 /       ; The rest of the US-ASCII
523  *                         %d94-126        ;  characters not including "[",
524  *                                         ;  "]", or "\"
525  * dcontent        =       dtext / quoted-pair
526  * domain-literal  =       [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
527  * domain          =       dot-atom / domain-literal / obs-domain
528  * local-part      =       dot-atom / quoted-string / obs-local-part
529  * addr-spec       =       local-part "@" domain
530  * angle-addr      =       [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
531  * name-addr       =       [display-name] angle-addr
532  * mailbox         =       name-addr / addr-spec
533  * mailbox-list    =       (mailbox *("," mailbox)) / obs-mbox-list
534  * group           =       display-name ":" [mailbox-list / CFWS] ";"
535  *                         [CFWS]
536  * address         =       mailbox / group
537  ************************************************************************/
538 static char *
539 get_display_name(char *name)
540 {
541 	char nbuf[BUFSIZ];
542 	const char *p;
543 	char *q;
544 	char *qend;
545 	char *lastq;
546 	int quoted;
547 
548 	if (name == NULL)
549 		return(NULL);
550 
551 	q = nbuf;
552 	lastq = nbuf;
553 	qend = nbuf + sizeof(nbuf) - 1;	/* reserve space for '\0' */
554 	quoted = 0;
555 	for (p = skip_white(name); *p != '\0'; p++) {
556 		DEBUG(("get_display_name: %s\n", p));
557 		switch (*p) {
558 		case '"':	/* quoted-string */
559 			q = nbuf;
560 			p = snarf_quote(&q, qend, p);
561 			if (!quoted)
562 				lastq = q;
563 			quoted = 1;
564 			break;
565 
566 		case ':':	/* group */
567 		case '<':	/* angle-address */
568 			if (lastq == nbuf)
569 				return NULL;
570 			*lastq = '\0';	/* NULL termination */
571 			return(savestr(nbuf));
572 
573 		case '(':	/* comment - skip it! */
574 			p = snarf_comment(NULL, NULL, p);
575 			break;
576 
577 		default:
578 			if (!quoted && q < qend) {
579 				*q++ = *p;
580 				if (!isblank((unsigned char)*p)
581 				    /* && !is_specials((unsigned char)*p) */ )
582 					lastq = q;
583 			}
584 			break;
585 		}
586 	}
587 	return NULL;	/* no group or angle-address */
588 }
589 
590 /*
591  * See RFC 2822 sec 3.4 and 3.6.2.
592  */
593 static const char *
594 userof(struct message *mp)
595 {
596 	char *sender;
597 	char *dispname;
598 
599 	if (mp == NULL)
600 		return NULL;
601 
602 	if ((sender = hfield("from", mp)) != NULL ||
603 	    (sender = hfield("sender", mp)) != NULL)
604 		/*
605 		 * Try to get the display-name.  If one doesn't exist,
606 		 * then the best we can hope for is that the user's
607 		 * name is in the comments.
608 		 */
609 		if ((dispname = get_display_name(sender)) != NULL ||
610 		    (dispname = get_comments(sender)) != NULL)
611 			return dispname;
612 	return NULL;
613 }
614 
615 /*
616  * Grab the subject line.
617  */
618 static const char *
619 subjof(struct message *mp)
620 {
621 	const char *subj;
622 
623 	if (mp == NULL)
624 		return NULL;
625 
626 	if ((subj = hfield("subject", mp)) == NULL)
627 		subj = hfield("subj", mp);
628 	return subj;
629 }
630 
631 static char *
632 preformat(struct tm *tm, const char *oldfmt, struct message *mp, int use_hl_date)
633 {
634 	const char *gmtoff;
635 	const char *zone;
636 	const char *subj;
637 	const char *addr;
638 	const char *user;
639 	const char *p;
640 	char *q;
641 	char *newfmt;
642 	size_t fmtsize;
643 
644 	if (mp != NULL && (mp->m_flag & MDELETED) != 0)
645 		mp = NULL; /* deleted mail shouldn't show up! */
646 
647 	subj = subjof(mp);
648 	addr = addrof(mp);
649 	user = userof(mp);
650 	gmtoff = dateof(tm, mp, use_hl_date);
651 	zone = tm->tm_zone;
652 	fmtsize = LINESIZE;
653 	newfmt = malloc(fmtsize); /* so we can realloc() in check_bufsize() */
654 	q = newfmt;
655 	p = oldfmt;
656 	while (*p) {
657 		if (*p == '%') {
658 			const char *fp;
659 			fp = subformat(&p, mp, addr, user, subj, gmtoff, zone);
660 			if (fp) {
661 				size_t len;
662 				len = strlen(fp);
663 				check_bufsize(&newfmt, &fmtsize, &q, len);
664 				(void)strcpy(q, fp);
665 				q += len;
666 				continue;
667 			}
668 		}
669 		check_bufsize(&newfmt, &fmtsize, &q, 1);
670 		*q++ = *p++;
671 	}
672 	*q = '\0';
673 
674 	return newfmt;
675 }
676 
677 
678 /*
679  * If a format string begins with the USE_HL_DATE string, smsgprintf
680  * will use the headerline date rather than trying to extract the date
681  * from the Date field.
682  *
683  * Note: If a 'valid' date cannot be extracted from the Date field,
684  * then the headline date is used.
685  */
686 #define USE_HL_DATE "%??"
687 
688 PUBLIC char *
689 smsgprintf(const char *fmtstr, struct message *mp)
690 {
691 	struct tm tm;
692 	int use_hl_date;
693 	char *newfmt;
694 	char *buf;
695 	size_t bufsize;
696 
697 	if (strncmp(fmtstr, USE_HL_DATE, sizeof(USE_HL_DATE) - 1) != 0)
698 		use_hl_date = 0;
699 	else {
700 		use_hl_date = 1;
701 		fmtstr += sizeof(USE_HL_DATE) - 1;
702 	}
703 	bufsize = LINESIZE;
704 	buf = salloc(bufsize);
705 	newfmt = preformat(&tm, fmtstr, mp, use_hl_date);
706 	(void)strftime(buf, bufsize, newfmt, &tm);
707 	free(newfmt);	/* preformat() uses malloc()/realloc() */
708 	return buf;
709 }
710 
711 
712 PUBLIC void
713 fmsgprintf(FILE *fo, const char *fmtstr, struct message *mp)
714 {
715 	char *buf;
716 
717 	buf = smsgprintf(fmtstr, mp);
718 	(void)fprintf(fo, "%s\n", buf);	/* XXX - add the newline here? */
719 }
720