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