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