7fdbb7101cf4d447ec649f225ef512cbc5c84af8
[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) free (tok)
119 #define date_token_new() malloc (sizeof (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 #define HAVE_TIMEZONE
367
368 static time_t
369 mktime_utc (struct tm *tm)
370 {
371         time_t tt;
372         long tz;
373         
374         tm->tm_isdst = -1;
375         tt = mktime (tm);
376         
377 #if defined (G_OS_WIN32)
378         _get_timezone (&tz);
379         if (tm->tm_isdst > 0) {
380                 int dst;
381                 
382                 _get_dstbias (&dst);
383                 tz += dst;
384         }
385 #elif defined (HAVE_TM_GMTOFF)
386         tz = -tm->tm_gmtoff;
387 #elif defined (HAVE_TIMEZONE)
388         if (tm->tm_isdst > 0) {
389 #if defined (HAVE_ALTZONE)
390                 tz = altzone;
391 #else /* !defined (HAVE_ALTZONE) */
392                 tz = (timezone - 3600);
393 #endif
394         } else {
395                 tz = timezone;
396         }
397 #elif defined (HAVE__TIMEZONE)
398         tz = _timezone;
399 #else
400 #error Neither HAVE_TIMEZONE nor HAVE_TM_GMTOFF defined. Rerun autoheader, autoconf, etc.
401 #endif
402         
403         return tt - tz;
404 }
405
406 static time_t
407 parse_rfc822_date (date_token *tokens, int *tzone)
408 {
409         int hour, min, sec, offset, n;
410         date_token *token;
411         struct tm tm;
412         time_t t;
413         
414         if (tokens == NULL)
415                 return 0;
416         
417         token = tokens;
418         
419         memset ((void *) &tm, 0, sizeof (struct tm));
420         
421         if ((n = get_wday (token->start, token->len)) != -1) {
422                 /* not all dates may have this... */
423                 tm.tm_wday = n;
424                 token = token->next;
425         }
426         
427         /* get the mday */
428         if (!token || (n = get_mday (token->start, token->len)) == -1)
429                 return (time_t) 0;
430         
431         tm.tm_mday = n;
432         token = token->next;
433         
434         /* get the month */
435         if (!token || (n = get_month (token->start, token->len)) == -1)
436                 return (time_t) 0;
437         
438         tm.tm_mon = n;
439         token = token->next;
440         
441         /* get the year */
442         if (!token || (n = get_year (token->start, token->len)) == -1)
443                 return (time_t) 0;
444         
445         tm.tm_year = n - 1900;
446         token = token->next;
447         
448         /* get the hour/min/sec */
449         if (!token || !get_time (token->start, token->len, &hour, &min, &sec))
450                 return (time_t) 0;
451         
452         tm.tm_hour = hour;
453         tm.tm_min = min;
454         tm.tm_sec = sec;
455         token = token->next;
456         
457         /* get the timezone */
458         if (!token || (n = get_tzone (&token)) == -1) {
459                 /* I guess we assume tz is GMT? */
460                 offset = 0;
461         } else {
462                 offset = n;
463         }
464         
465         t = mktime_utc (&tm);
466         
467         /* t is now GMT of the time we want, but not offset by the timezone ... */
468         
469         /* this should convert the time to the GMT equiv time */
470         t -= ((offset / 100) * 60 * 60) + (offset % 100) * 60;
471         
472         if (tzone)
473                 *tzone = offset;
474         
475         return t;
476 }
477
478
479 #define date_token_mask(t)  (((date_token *) t)->mask)
480 #define is_numeric(t)       ((date_token_mask (t) & DATE_TOKEN_NON_NUMERIC) == 0)
481 #define is_weekday(t)       ((date_token_mask (t) & DATE_TOKEN_NON_WEEKDAY) == 0)
482 #define is_month(t)         ((date_token_mask (t) & DATE_TOKEN_NON_MONTH) == 0)
483 #define is_time(t)          (((date_token_mask (t) & DATE_TOKEN_NON_TIME) == 0) && (date_token_mask (t) & DATE_TOKEN_HAS_COLON))
484 #define is_tzone_alpha(t)   ((date_token_mask (t) & DATE_TOKEN_NON_TIMEZONE_ALPHA) == 0)
485 #define is_tzone_numeric(t) (((date_token_mask (t) & DATE_TOKEN_NON_TIMEZONE_NUMERIC) == 0) && (date_token_mask (t) & DATE_TOKEN_HAS_SIGN))
486 #define is_tzone(t)         (is_tzone_alpha (t) || is_tzone_numeric (t))
487
488 static time_t
489 parse_broken_date (date_token *tokens, int *tzone)
490 {
491         int got_wday, got_month, got_tzone;
492         int hour, min, sec, offset, n;
493         date_token *token;
494         struct tm tm;
495         time_t t;
496         
497         memset ((void *) &tm, 0, sizeof (struct tm));
498         got_wday = got_month = got_tzone = FALSE;
499         offset = 0;
500         
501         token = tokens;
502         while (token) {
503                 if (is_weekday (token) && !got_wday) {
504                         if ((n = get_wday (token->start, token->len)) != -1) {
505                                 d(printf ("weekday; "));
506                                 got_wday = TRUE;
507                                 tm.tm_wday = n;
508                                 goto next;
509                         }
510                 }
511                 
512                 if (is_month (token) && !got_month) {
513                         if ((n = get_month (token->start, token->len)) != -1) {
514                                 d(printf ("month; "));
515                                 got_month = TRUE;
516                                 tm.tm_mon = n;
517                                 goto next;
518                         }
519                 }
520                 
521                 if (is_time (token) && !tm.tm_hour && !tm.tm_min && !tm.tm_sec) {
522                         if (get_time (token->start, token->len, &hour, &min, &sec)) {
523                                 d(printf ("time; "));
524                                 tm.tm_hour = hour;
525                                 tm.tm_min = min;
526                                 tm.tm_sec = sec;
527                                 goto next;
528                         }
529                 }
530                 
531                 if (is_tzone (token) && !got_tzone) {
532                         date_token *t = token;
533                         
534                         if ((n = get_tzone (&t)) != -1) {
535                                 d(printf ("tzone; "));
536                                 got_tzone = TRUE;
537                                 offset = n;
538                                 goto next;
539                         }
540                 }
541                 
542                 if (is_numeric (token)) {
543                         if (token->len == 4 && !tm.tm_year) {
544                                 if ((n = get_year (token->start, token->len)) != -1) {
545                                         d(printf ("year; "));
546                                         tm.tm_year = n - 1900;
547                                         goto next;
548                                 }
549                         } else {
550                                 /* Note: assumes MM-DD-YY ordering if '0 < MM < 12' holds true */
551                                 if (!got_month && token->next && is_numeric (token->next)) {
552                                         if ((n = decode_int (token->start, token->len)) > 12) {
553                                                 goto mday;
554                                         } else if (n > 0) {
555                                                 d(printf ("mon; "));
556                                                 got_month = TRUE;
557                                                 tm.tm_mon = n - 1;
558                                         }
559                                         goto next;
560                                 } else if (!tm.tm_mday && (n = get_mday (token->start, token->len)) != -1) {
561                                 mday:
562                                         d(printf ("mday; "));
563                                         tm.tm_mday = n;
564                                         goto next;
565                                 } else if (!tm.tm_year) {
566                                         if ((n = get_year (token->start, token->len)) != -1) {
567                                                 d(printf ("2-digit year; "));
568                                                 tm.tm_year = n - 1900;
569                                         }
570                                         goto next;
571                                 }
572                         }
573                 }
574                 
575                 d(printf ("???; "));
576                 
577         next:
578                 
579                 token = token->next;
580         }
581         
582         d(printf ("\n"));
583         
584         t = mktime_utc (&tm);
585         
586         /* t is now GMT of the time we want, but not offset by the timezone ... */
587         
588         /* this should convert the time to the GMT equiv time */
589         t -= ((offset / 100) * 60 * 60) + (offset % 100) * 60;
590         
591         if (tzone)
592                 *tzone = offset;
593         
594         return t;
595 }
596
597 #if 0
598 static void
599 gmime_datetok_table_init (void)
600 {
601         int i;
602         
603         memset (gmime_datetok_table, 0, sizeof (gmime_datetok_table));
604         
605         for (i = 0; i < 256; i++) {
606                 if (!strchr (NUMERIC_CHARS, i))
607                         gmime_datetok_table[i] |= DATE_TOKEN_NON_NUMERIC;
608                 
609                 if (!strchr (WEEKDAY_CHARS, i))
610                         gmime_datetok_table[i] |= DATE_TOKEN_NON_WEEKDAY;
611                 
612                 if (!strchr (MONTH_CHARS, i))
613                         gmime_datetok_table[i] |= DATE_TOKEN_NON_MONTH;
614                 
615                 if (!strchr (TIME_CHARS, i))
616                         gmime_datetok_table[i] |= DATE_TOKEN_NON_TIME;
617                 
618                 if (!strchr (TIMEZONE_ALPHA_CHARS, i))
619                         gmime_datetok_table[i] |= DATE_TOKEN_NON_TIMEZONE_ALPHA;
620                 
621                 if (!strchr (TIMEZONE_NUMERIC_CHARS, i))
622                         gmime_datetok_table[i] |= DATE_TOKEN_NON_TIMEZONE_NUMERIC;
623                 
624                 if (((char) i) == ':')
625                         gmime_datetok_table[i] |= DATE_TOKEN_HAS_COLON;
626                 
627                 if (strchr ("+-", i))
628                         gmime_datetok_table[i] |= DATE_TOKEN_HAS_SIGN;
629         }
630         
631         printf ("static unsigned char gmime_datetok_table[256] = {");
632         for (i = 0; i < 256; i++) {
633                 if (i % 16 == 0)
634                         printf ("\n\t");
635                 printf ("%3d,", gmime_datetok_table[i]);
636         }
637         printf ("\n};\n");
638 }
639 #endif
640
641 time_t
642 notmuch_parse_date (const char *str, int *tz_offset)
643 {
644         date_token *token, *tokens;
645         time_t date;
646         
647         if (!(tokens = datetok (str))) {
648                 if (tz_offset)
649                         *tz_offset = 0;
650                 
651                 return (time_t) 0;
652         }
653         
654         if (!(date = parse_rfc822_date (tokens, tz_offset)))
655                 date = parse_broken_date (tokens, tz_offset);
656         
657         /* cleanup */
658         while (tokens) {
659                 token = tokens;
660                 tokens = tokens->next;
661                 date_token_free (token);
662         }
663         
664         return date;
665 }