102d688afc86e1d8791a191ce5b5eed558194f5c
[notmuch] / date.c
1 /* date.c - Date-parsing utility for the notmuch mail system.
2  *
3  *  Copyright © 2000-2009 Jeffrey Stedfast
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ 
17  */
18
19 /* This code was originally written by from Jeffrey Stedfast
20  * as part of his GMime library (http://spruce.sourceforge.net/gmime/)
21  *
22  * Carl Worth <cworth@cworth.org> imported it into notmuch and removed
23  * some glib-isms.
24  */
25
26 #include "notmuch-private.h"
27
28 #include <time.h>
29
30 #ifndef FALSE
31 #define FALSE 0
32 #endif
33
34 #ifndef TRUE
35 #define TRUE 1
36 #endif
37
38 #define d(x)
39
40 #define GMIME_FOLD_PREENCODED  (GMIME_FOLD_LEN / 2)
41
42 /* date parser macros */
43 #define NUMERIC_CHARS          "1234567890"
44 #define WEEKDAY_CHARS          "SundayMondayTuesdayWednesdayThursdayFridaySaturday"
45 #define MONTH_CHARS            "JanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecember"
46 #define TIMEZONE_ALPHA_CHARS   "UTCGMTESTEDTCSTCDTMSTPSTPDTZAMNY()"
47 #define TIMEZONE_NUMERIC_CHARS "-+1234567890"
48 #define TIME_CHARS             "1234567890:"
49
50 #define DATE_TOKEN_NON_NUMERIC          (1 << 0)
51 #define DATE_TOKEN_NON_WEEKDAY          (1 << 1)
52 #define DATE_TOKEN_NON_MONTH            (1 << 2)
53 #define DATE_TOKEN_NON_TIME             (1 << 3)
54 #define DATE_TOKEN_HAS_COLON            (1 << 4)
55 #define DATE_TOKEN_NON_TIMEZONE_ALPHA   (1 << 5)
56 #define DATE_TOKEN_NON_TIMEZONE_NUMERIC (1 << 6)
57 #define DATE_TOKEN_HAS_SIGN             (1 << 7)
58
59 static unsigned char gmime_datetok_table[256] = {
60         128,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
61         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
62         111,111,111,111,111,111,111,111, 79, 79,111,175,111,175,111,111,
63          38, 38, 38, 38, 38, 38, 38, 38, 38, 38,119,111,111,111,111,111,
64         111, 75,111, 79, 75, 79,105, 79,111,111,107,111,111, 73, 75,107,
65          79,111,111, 73, 77, 79,111,109,111, 79, 79,111,111,111,111,111,
66         111,105,107,107,109,105,111,107,105,105,111,111,107,107,105,105,
67         107,111,105,105,105,105,107,111,111,105,111,111,111,111,111,111,
68         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
69         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
70         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
71         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
72         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
73         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
74         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
75         111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,111,
76 };
77
78 /* hrm, is there a library for this stuff? */
79 static struct {
80         char *name;
81         int offset;
82 } tz_offsets [] = {
83         { "UT", 0 },
84         { "GMT", 0 },
85         { "EST", -500 },        /* these are all US timezones.  bloody yanks */
86         { "EDT", -400 },
87         { "CST", -600 },
88         { "CDT", -500 },
89         { "MST", -700 },
90         { "MDT", -600 },
91         { "PST", -800 },
92         { "PDT", -700 },
93         { "Z", 0 },
94         { "A", -100 },
95         { "M", -1200 },
96         { "N", 100 },
97         { "Y", 1200 },
98 };
99
100 static char *tm_months[] = {
101         "Jan", "Feb", "Mar", "Apr", "May", "Jun",
102         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
103 };
104
105 static char *tm_days[] = {
106         "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
107 };
108
109 /* This is where it gets ugly... */
110
111 typedef struct _date_token {
112         struct _date_token *next;
113         unsigned char mask;
114         const char *start;
115         size_t len;
116 } date_token;
117
118 #define date_token_free(tok) g_slice_free (date_token, tok)
119 #define date_token_new() g_slice_new (date_token)
120
121 static date_token *
122 datetok (const char *date)
123 {
124         date_token *tokens = NULL, *token, *tail = (date_token *) &tokens;
125         const char *start, *end;
126         unsigned char mask;
127         
128         start = date;
129         while (*start) {
130                 /* kill leading whitespace */
131                 while (*start == ' ' || *start == '\t')
132                         start++;
133                 
134                 if (*start == '\0')
135                         break;
136                 
137                 mask = gmime_datetok_table[(unsigned char) *start];
138                 
139                 /* find the end of this token */
140                 end = start + 1;
141                 while (*end && !strchr ("-/,\t\r\n ", *end))
142                         mask |= gmime_datetok_table[(unsigned char) *end++];
143                 
144                 if (end != start) {
145                         token = date_token_new ();
146                         token->next = NULL;
147                         token->start = start;
148                         token->len = end - start;
149                         token->mask = mask;
150                         
151                         tail->next = token;
152                         tail = token;
153                 }
154                 
155                 if (*end)
156                         start = end + 1;
157                 else
158                         break;
159         }
160         
161         return tokens;
162 }
163
164 static int
165 decode_int (const char *in, size_t inlen)
166 {
167         register const char *inptr;
168         int sign = 1, val = 0;
169         const char *inend;
170         
171         inptr = in;
172         inend = in + inlen;
173         
174         if (*inptr == '-') {
175                 sign = -1;
176                 inptr++;
177         } else if (*inptr == '+')
178                 inptr++;
179         
180         for ( ; inptr < inend; inptr++) {
181                 if (!(*inptr >= '0' && *inptr <= '9'))
182                         return -1;
183                 else
184                         val = (val * 10) + (*inptr - '0');
185         }
186         
187         val *= sign;
188         
189         return val;
190 }
191
192 #if 0
193 static int
194 get_days_in_month (int month, int year)
195 {
196         switch (month) {
197         case 1:
198         case 3:
199         case 5:
200         case 7:
201         case 8:
202         case 10:
203         case 12:
204                 return 31;
205         case 4:
206         case 6:
207         case 9:
208         case 11:
209                 return 30;
210         case 2:
211                 if (g_date_is_leap_year (year))
212                         return 29;
213                 else
214                         return 28;
215         default:
216                 return 0;
217         }
218 }
219 #endif
220
221 static int
222 get_wday (const char *in, size_t inlen)
223 {
224         int wday;
225
226         if (in == NULL)
227                 return -1;
228         
229         if (inlen < 3)
230                 return -1;
231         
232         for (wday = 0; wday < 7; wday++) {
233                 if (!g_ascii_strncasecmp (in, tm_days[wday], 3))
234                         return wday;
235         }
236         
237         return -1;  /* unknown week day */
238 }
239
240 static int
241 get_mday (const char *in, size_t inlen)
242 {
243         int mday;
244         
245         if (in == NULL)
246                 return -1;
247         
248         mday = decode_int (in, inlen);
249         
250         if (mday < 0 || mday > 31)
251                 mday = -1;
252         
253         return mday;
254 }
255
256 static int
257 get_month (const char *in, size_t inlen)
258 {
259         int i;
260         
261         if (in == NULL)
262                 return -1;
263         
264         if (inlen < 3)
265                 return -1;
266         
267         for (i = 0; i < 12; i++) {
268                 if (!g_ascii_strncasecmp (in, tm_months[i], 3))
269                         return i;
270         }
271         
272         return -1;  /* unknown month */
273 }
274
275 static int
276 get_year (const char *in, size_t inlen)
277 {
278         int year;
279         
280         if (in == NULL)
281                 return -1;
282         
283         if ((year = decode_int (in, inlen)) == -1)
284                 return -1;
285         
286         if (year < 100)
287                 year += (year < 70) ? 2000 : 1900;
288         
289         if (year < 1969)
290                 return -1;
291         
292         return year;
293 }
294
295 static int
296 get_time (const char *in, size_t inlen, int *hour, int *min, int *sec)
297 {
298         register const char *inptr;
299         int *val, colons = 0;
300         const char *inend;
301         
302         *hour = *min = *sec = 0;
303         
304         inend = in + inlen;
305         val = hour;
306         for (inptr = in; inptr < inend; inptr++) {
307                 if (*inptr == ':') {
308                         colons++;
309                         switch (colons) {
310                         case 1:
311                                 val = min;
312                                 break;
313                         case 2:
314                                 val = sec;
315                                 break;
316                         default:
317                                 return FALSE;
318                         }
319                 } else if (!(*inptr >= '0' && *inptr <= '9'))
320                         return FALSE;
321                 else
322                         *val = (*val * 10) + (*inptr - '0');
323         }
324         
325         return TRUE;
326 }
327
328 static int
329 get_tzone (date_token **token)
330 {
331         const char *inptr, *inend;
332         size_t inlen;
333         int i, t;
334         
335         for (i = 0; *token && i < 2; *token = (*token)->next, i++) {
336                 inptr = (*token)->start;
337                 inlen = (*token)->len;
338                 inend = inptr + inlen;
339                 
340                 if (*inptr == '+' || *inptr == '-') {
341                         return decode_int (inptr, inlen);
342                 } else {
343                         if (*inptr == '(') {
344                                 inptr++;
345                                 if (*(inend - 1) == ')')
346                                         inlen -= 2;
347                                 else
348                                         inlen--;
349                         }
350                         
351                         for (t = 0; t < 15; t++) {
352                                 size_t len = strlen (tz_offsets[t].name);
353                                 
354                                 if (len != inlen)
355                                         continue;
356                                 
357                                 if (!strncmp (inptr, tz_offsets[t].name, len))
358                                         return tz_offsets[t].offset;
359                         }
360                 }
361         }
362         
363         return -1;
364 }
365
366 static time_t
367 mktime_utc (struct tm *tm)
368 {
369         time_t tt;
370         long tz;
371         
372         tm->tm_isdst = -1;
373         tt = mktime (tm);
374         
375 #if defined (G_OS_WIN32)
376         _get_timezone (&tz);
377         if (tm->tm_isdst > 0) {
378                 int dst;
379                 
380                 _get_dstbias (&dst);
381                 tz += dst;
382         }
383 #elif defined (HAVE_TM_GMTOFF)
384         tz = -tm->tm_gmtoff;
385 #elif defined (HAVE_TIMEZONE)
386         if (tm->tm_isdst > 0) {
387 #if defined (HAVE_ALTZONE)
388                 tz = altzone;
389 #else /* !defined (HAVE_ALTZONE) */
390                 tz = (timezone - 3600);
391 #endif
392         } else {
393                 tz = timezone;
394         }
395 #elif defined (HAVE__TIMEZONE)
396         tz = _timezone;
397 #else
398 #error Neither HAVE_TIMEZONE nor HAVE_TM_GMTOFF defined. Rerun autoheader, autoconf, etc.
399 #endif
400         
401         return tt - tz;
402 }
403
404 static time_t
405 parse_rfc822_date (date_token *tokens, int *tzone)
406 {
407         int hour, min, sec, offset, n;
408         date_token *token;
409         struct tm tm;
410         time_t t;
411         
412         if (tokens == NULL)
413                 return 0;
414         
415         token = tokens;
416         
417         memset ((void *) &tm, 0, sizeof (struct tm));
418         
419         if ((n = get_wday (token->start, token->len)) != -1) {
420                 /* not all dates may have this... */
421                 tm.tm_wday = n;
422                 token = token->next;
423         }
424         
425         /* get the mday */
426         if (!token || (n = get_mday (token->start, token->len)) == -1)
427                 return (time_t) 0;
428         
429         tm.tm_mday = n;
430         token = token->next;
431         
432         /* get the month */
433         if (!token || (n = get_month (token->start, token->len)) == -1)
434                 return (time_t) 0;
435         
436         tm.tm_mon = n;
437         token = token->next;
438         
439         /* get the year */
440         if (!token || (n = get_year (token->start, token->len)) == -1)
441                 return (time_t) 0;
442         
443         tm.tm_year = n - 1900;
444         token = token->next;
445         
446         /* get the hour/min/sec */
447         if (!token || !get_time (token->start, token->len, &hour, &min, &sec))
448                 return (time_t) 0;
449         
450         tm.tm_hour = hour;
451         tm.tm_min = min;
452         tm.tm_sec = sec;
453         token = token->next;
454         
455         /* get the timezone */
456         if (!token || (n = get_tzone (&token)) == -1) {
457                 /* I guess we assume tz is GMT? */
458                 offset = 0;
459         } else {
460                 offset = n;
461         }
462         
463         t = mktime_utc (&tm);
464         
465         /* t is now GMT of the time we want, but not offset by the timezone ... */
466         
467         /* this should convert the time to the GMT equiv time */
468         t -= ((offset / 100) * 60 * 60) + (offset % 100) * 60;
469         
470         if (tzone)
471                 *tzone = offset;
472         
473         return t;
474 }
475
476
477 #define date_token_mask(t)  (((date_token *) t)->mask)
478 #define is_numeric(t)       ((date_token_mask (t) & DATE_TOKEN_NON_NUMERIC) == 0)
479 #define is_weekday(t)       ((date_token_mask (t) & DATE_TOKEN_NON_WEEKDAY) == 0)
480 #define is_month(t)         ((date_token_mask (t) & DATE_TOKEN_NON_MONTH) == 0)
481 #define is_time(t)          (((date_token_mask (t) & DATE_TOKEN_NON_TIME) == 0) && (date_token_mask (t) & DATE_TOKEN_HAS_COLON))
482 #define is_tzone_alpha(t)   ((date_token_mask (t) & DATE_TOKEN_NON_TIMEZONE_ALPHA) == 0)
483 #define is_tzone_numeric(t) (((date_token_mask (t) & DATE_TOKEN_NON_TIMEZONE_NUMERIC) == 0) && (date_token_mask (t) & DATE_TOKEN_HAS_SIGN))
484 #define is_tzone(t)         (is_tzone_alpha (t) || is_tzone_numeric (t))
485
486 static time_t
487 parse_broken_date (date_token *tokens, int *tzone)
488 {
489         int got_wday, got_month, got_tzone;
490         int hour, min, sec, offset, n;
491         date_token *token;
492         struct tm tm;
493         time_t t;
494         
495         memset ((void *) &tm, 0, sizeof (struct tm));
496         got_wday = got_month = got_tzone = FALSE;
497         offset = 0;
498         
499         token = tokens;
500         while (token) {
501                 if (is_weekday (token) && !got_wday) {
502                         if ((n = get_wday (token->start, token->len)) != -1) {
503                                 d(printf ("weekday; "));
504                                 got_wday = TRUE;
505                                 tm.tm_wday = n;
506                                 goto next;
507                         }
508                 }
509                 
510                 if (is_month (token) && !got_month) {
511                         if ((n = get_month (token->start, token->len)) != -1) {
512                                 d(printf ("month; "));
513                                 got_month = TRUE;
514                                 tm.tm_mon = n;
515                                 goto next;
516                         }
517                 }
518                 
519                 if (is_time (token) && !tm.tm_hour && !tm.tm_min && !tm.tm_sec) {
520                         if (get_time (token->start, token->len, &hour, &min, &sec)) {
521                                 d(printf ("time; "));
522                                 tm.tm_hour = hour;
523                                 tm.tm_min = min;
524                                 tm.tm_sec = sec;
525                                 goto next;
526                         }
527                 }
528                 
529                 if (is_tzone (token) && !got_tzone) {
530                         date_token *t = token;
531                         
532                         if ((n = get_tzone (&t)) != -1) {
533                                 d(printf ("tzone; "));
534                                 got_tzone = TRUE;
535                                 offset = n;
536                                 goto next;
537                         }
538                 }
539                 
540                 if (is_numeric (token)) {
541                         if (token->len == 4 && !tm.tm_year) {
542                                 if ((n = get_year (token->start, token->len)) != -1) {
543                                         d(printf ("year; "));
544                                         tm.tm_year = n - 1900;
545                                         goto next;
546                                 }
547                         } else {
548                                 /* Note: assumes MM-DD-YY ordering if '0 < MM < 12' holds true */
549                                 if (!got_month && token->next && is_numeric (token->next)) {
550                                         if ((n = decode_int (token->start, token->len)) > 12) {
551                                                 goto mday;
552                                         } else if (n > 0) {
553                                                 d(printf ("mon; "));
554                                                 got_month = TRUE;
555                                                 tm.tm_mon = n - 1;
556                                         }
557                                         goto next;
558                                 } else if (!tm.tm_mday && (n = get_mday (token->start, token->len)) != -1) {
559                                 mday:
560                                         d(printf ("mday; "));
561                                         tm.tm_mday = n;
562                                         goto next;
563                                 } else if (!tm.tm_year) {
564                                         if ((n = get_year (token->start, token->len)) != -1) {
565                                                 d(printf ("2-digit year; "));
566                                                 tm.tm_year = n - 1900;
567                                         }
568                                         goto next;
569                                 }
570                         }
571                 }
572                 
573                 d(printf ("???; "));
574                 
575         next:
576                 
577                 token = token->next;
578         }
579         
580         d(printf ("\n"));
581         
582         t = mktime_utc (&tm);
583         
584         /* t is now GMT of the time we want, but not offset by the timezone ... */
585         
586         /* this should convert the time to the GMT equiv time */
587         t -= ((offset / 100) * 60 * 60) + (offset % 100) * 60;
588         
589         if (tzone)
590                 *tzone = offset;
591         
592         return t;
593 }
594
595 #if 0
596 static void
597 gmime_datetok_table_init (void)
598 {
599         int i;
600         
601         memset (gmime_datetok_table, 0, sizeof (gmime_datetok_table));
602         
603         for (i = 0; i < 256; i++) {
604                 if (!strchr (NUMERIC_CHARS, i))
605                         gmime_datetok_table[i] |= DATE_TOKEN_NON_NUMERIC;
606                 
607                 if (!strchr (WEEKDAY_CHARS, i))
608                         gmime_datetok_table[i] |= DATE_TOKEN_NON_WEEKDAY;
609                 
610                 if (!strchr (MONTH_CHARS, i))
611                         gmime_datetok_table[i] |= DATE_TOKEN_NON_MONTH;
612                 
613                 if (!strchr (TIME_CHARS, i))
614                         gmime_datetok_table[i] |= DATE_TOKEN_NON_TIME;
615                 
616                 if (!strchr (TIMEZONE_ALPHA_CHARS, i))
617                         gmime_datetok_table[i] |= DATE_TOKEN_NON_TIMEZONE_ALPHA;
618                 
619                 if (!strchr (TIMEZONE_NUMERIC_CHARS, i))
620                         gmime_datetok_table[i] |= DATE_TOKEN_NON_TIMEZONE_NUMERIC;
621                 
622                 if (((char) i) == ':')
623                         gmime_datetok_table[i] |= DATE_TOKEN_HAS_COLON;
624                 
625                 if (strchr ("+-", i))
626                         gmime_datetok_table[i] |= DATE_TOKEN_HAS_SIGN;
627         }
628         
629         printf ("static unsigned char gmime_datetok_table[256] = {");
630         for (i = 0; i < 256; i++) {
631                 if (i % 16 == 0)
632                         printf ("\n\t");
633                 printf ("%3d,", gmime_datetok_table[i]);
634         }
635         printf ("\n};\n");
636 }
637 #endif
638
639
640 /**
641  * g_mime_utils_header_decode_date:
642  * @str: input date string
643  * @tz_offset: timezone offset
644  *
645  * Decodes the rfc822 date string and saves the GMT offset into
646  * @tz_offset if non-NULL.
647  *
648  * Returns: the time_t representation of the date string specified by
649  * @str or (time_t) %0 on error. If @tz_offset is non-NULL, the value
650  * of the timezone offset will be stored.
651  **/
652 time_t
653 g_mime_utils_header_decode_date (const char *str, int *tz_offset)
654 {
655         date_token *token, *tokens;
656         time_t date;
657         
658         if (!(tokens = datetok (str))) {
659                 if (tz_offset)
660                         *tz_offset = 0;
661                 
662                 return (time_t) 0;
663         }
664         
665         if (!(date = parse_rfc822_date (tokens, tz_offset)))
666                 date = parse_broken_date (tokens, tz_offset);
667         
668         /* cleanup */
669         while (tokens) {
670                 token = tokens;
671                 tokens = tokens->next;
672                 date_token_free (token);
673         }
674         
675         return date;
676 }