]> err.no Git - sope/blob - sope-mime/NGMime/NGMimeRFC822DateHeaderFieldParser.m
Add libxml2-dev to libsope-xml4.7-dev deps
[sope] / sope-mime / NGMime / NGMimeRFC822DateHeaderFieldParser.m
1 /*
2   Copyright (C) 2000-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
6   SOPE is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with SOPE; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21
22 #include "NGMimeHeaderFieldParser.h"
23 #include "NGMimeHeaderFields.h"
24 #include "NGMimeUtilities.h"
25 #include "common.h"
26 #include <string.h>
27
28 @implementation NGMimeRFC822DateHeaderFieldParser
29
30 static Class CalDateClass = Nil;
31 static NSTimeZone *gmt   = nil;
32 static NSTimeZone *gmt01 = nil;
33 static NSTimeZone *gmt02 = nil;
34 static NSTimeZone *gmt03 = nil;
35 static NSTimeZone *gmt04 = nil;
36 static NSTimeZone *gmt05 = nil;
37 static NSTimeZone *gmt06 = nil;
38 static NSTimeZone *gmt07 = nil;
39 static NSTimeZone *gmt08 = nil;
40 static NSTimeZone *gmt09 = nil;
41 static NSTimeZone *gmt10 = nil;
42 static NSTimeZone *gmt11 = nil;
43 static NSTimeZone *gmt12 = nil;
44 static NSTimeZone *gmt0530 = nil;
45 static NSTimeZone *gmtM01 = nil;
46 static NSTimeZone *gmtM02 = nil;
47 static NSTimeZone *gmtM03 = nil;
48 static NSTimeZone *gmtM04 = nil;
49 static NSTimeZone *gmtM05 = nil;
50 static NSTimeZone *gmtM06 = nil;
51 static NSTimeZone *gmtM07 = nil;
52 static NSTimeZone *gmtM08 = nil;
53 static NSTimeZone *gmtM09 = nil;
54 static NSTimeZone *gmtM10 = nil;
55 static NSTimeZone *gmtM11 = nil;
56 static NSTimeZone *gmtM12 = nil;
57 static NSTimeZone *gmtM13 = nil;
58 static NSTimeZone *gmtM14 = nil;
59 static NSTimeZone *met    = nil;
60
61 + (int)version {
62   return 2;
63 }
64 + (void)initialize {
65   static BOOL didInit = NO;
66   Class TzClass;
67   if (didInit) return;
68   didInit = YES;
69   
70   CalDateClass = [NSCalendarDate class];
71   
72   /* timezones which were actually used in a maillist mailbox */
73   TzClass = [NSTimeZone class];
74   gmt    = [[TzClass timeZoneWithName:@"GMT"] retain];
75   met    = [[TzClass timeZoneWithName:@"MET"] retain];
76   gmt01  = [[TzClass timeZoneForSecondsFromGMT:  1 * (60 * 60)] retain];
77   gmt02  = [[TzClass timeZoneForSecondsFromGMT:  2 * (60 * 60)] retain];
78   gmt03  = [[TzClass timeZoneForSecondsFromGMT:  3 * (60 * 60)] retain];
79   gmt04  = [[TzClass timeZoneForSecondsFromGMT:  4 * (60 * 60)] retain];
80   gmt05  = [[TzClass timeZoneForSecondsFromGMT:  5 * (60 * 60)] retain];
81   gmt06  = [[TzClass timeZoneForSecondsFromGMT:  6 * (60 * 60)] retain];
82   gmt07  = [[TzClass timeZoneForSecondsFromGMT:  7 * (60 * 60)] retain];
83   gmt08  = [[TzClass timeZoneForSecondsFromGMT:  8 * (60 * 60)] retain];
84   gmt09  = [[TzClass timeZoneForSecondsFromGMT:  9 * (60 * 60)] retain];
85   gmt10  = [[TzClass timeZoneForSecondsFromGMT: 10 * (60 * 60)] retain];
86   gmt11  = [[TzClass timeZoneForSecondsFromGMT: 11 * (60 * 60)] retain];
87   gmt12  = [[TzClass timeZoneForSecondsFromGMT: 12 * (60 * 60)] retain];
88   gmtM01 = [[TzClass timeZoneForSecondsFromGMT: -1 * (60 * 60)] retain];
89   gmtM02 = [[TzClass timeZoneForSecondsFromGMT: -2 * (60 * 60)] retain];
90   gmtM03 = [[TzClass timeZoneForSecondsFromGMT: -3 * (60 * 60)] retain];
91   gmtM04 = [[TzClass timeZoneForSecondsFromGMT: -4 * (60 * 60)] retain];
92   gmtM05 = [[TzClass timeZoneForSecondsFromGMT: -5 * (60 * 60)] retain];
93   gmtM06 = [[TzClass timeZoneForSecondsFromGMT: -6 * (60 * 60)] retain];
94   gmtM07 = [[TzClass timeZoneForSecondsFromGMT: -7 * (60 * 60)] retain];
95   gmtM08 = [[TzClass timeZoneForSecondsFromGMT: -8 * (60 * 60)] retain];
96   gmtM09 = [[TzClass timeZoneForSecondsFromGMT: -9 * (60 * 60)] retain];
97   gmtM10 = [[TzClass timeZoneForSecondsFromGMT:-10 * (60 * 60)] retain];
98   gmtM11 = [[TzClass timeZoneForSecondsFromGMT:-11 * (60 * 60)] retain];
99   gmtM12 = [[TzClass timeZoneForSecondsFromGMT:-12 * (60 * 60)] retain];
100   gmtM13 = [[TzClass timeZoneForSecondsFromGMT:-13 * (60 * 60)] retain];
101   gmtM14 = [[TzClass timeZoneForSecondsFromGMT:-14 * (60 * 60)] retain];
102   
103   gmt0530 = [[TzClass timeZoneForSecondsFromGMT:5 * (60*60) + (30*60)] retain];
104 }
105
106 /* 
107    All the date formats are more or less the same. If they start with a char
108    those can be skipped to the first digit (since it is the weekday name that
109    is unnecessary for date construction).
110    
111    TODO: use an own parser for that.
112 */
113
114 static int parseMonthOfYear(unsigned char *s, unsigned int len) {
115   /*
116     This one is *extremely* forgiving, it only checks what is
117     necessary for the set below. This should work for both, English
118     and German.
119     
120     English: Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
121              J    F    M    A    M    J    J    A    S    O    N    D
122   */
123   if (len < 3) {
124     NSLog(@"RFC822 Parser: cannot process month name: '%s'", s);
125     return 0;
126   }
127   switch (toupper(*s)) {
128   case 'A': // April, August
129     if (toupper(s[1]) == 'P') return 4; // Apr
130     return 8; // Aug
131   case 'D': return 12; // Dec
132   case 'F': return  2; // Feb
133   case 'J': // Jan, Jun, Jul
134     if (toupper(s[1]) == 'A') return 1; // Jan
135     if (toupper(s[2]) == 'N') return 6; // Jun
136     return 7; // Jul
137   case 'M': // Mar, May
138     if (toupper(s[2]) == 'Y' || toupper(s[2]) == 'I') // May or Mai (German ;-)
139       return 5;
140     return 3; // Mar
141   case 'N': return 11; // Nov
142   case 'O': return 10; // Oct
143   case 'S': return  9; // Sep
144   default:
145     NSLog(@"RFC822 Parser: cannot process month name: '%s'", s);
146     return 0;
147   }
148 }
149
150 static NSTimeZone *parseTimeZone(unsigned char *s, unsigned int len) {
151   /*
152     WARNING: failed to parse RFC822 timezone: '+0530' \
153              (value='Tue, 13 Jul 2004 21:39:28 +0530')
154     TODO: this is because libFoundation doesn't accept 'GMT+0530' as input.
155   */
156   char       *p = (char *)s;
157   NSTimeZone *tz;
158   NSString   *ts;
159   
160   if (len == 0) 
161     return nil;
162   
163   if (*s == '+' || *s == '-') {
164     if (len == 3) {
165       if (p[1] == '0' && p[2] == '0') // '+00' or '-00'
166         return gmt;
167       if (*s == '+') {
168         if (p[1] == '0' && p[2] == '1') // '+01'
169           return gmt01;
170         if (p[1] == '0' && p[2] == '2') // '+02'
171           return gmt02;
172       }
173     }
174     else if (len == 5) {
175       if (p[3] == '0' && p[4] == '0' && p[1] == '0') { // '?0x00'
176         if (p[2] == '0') // '+0000'
177           return gmt;
178         
179         if (*s == '+') {
180           if (p[2] == '1') return gmt01; // '+0100'
181           if (p[2] == '2') return gmt02; // '+0200'
182           if (p[2] == '3') return gmt03; // '+0300'
183           if (p[2] == '4') return gmt04; // '+0400'
184           if (p[2] == '5') return gmt05; // '+0500'
185           if (p[2] == '6') return gmt06; // '+0600'
186           if (p[2] == '7') return gmt07; // '+0700'
187           if (p[2] == '8') return gmt08; // '+0800'
188           if (p[2] == '9') return gmt09; // '+0900'
189         }
190         else if (*s == '-') {
191           if (p[2] == '1') return gmtM01; // '-0100'
192           if (p[2] == '2') return gmtM02; // '-0200'
193           if (p[2] == '3') return gmtM03; // '-0300'
194           if (p[2] == '4') return gmtM04; // '-0400'
195           if (p[2] == '5') return gmtM05; // '-0500'
196           if (p[2] == '6') return gmtM06; // '-0600'
197           if (p[2] == '7') return gmtM07; // '-0700'
198           if (p[2] == '8') return gmtM08; // '-0800'
199           if (p[2] == '9') return gmtM09; // '-0900'
200         }
201       }
202       else if (p[3] == '0' && p[4] == '0' && p[1] == '1') { // "?1x00"
203         if (*s == '+') {
204           if (p[2] == '0') return gmt10; // '+1000'
205           if (p[2] == '1') return gmt11; // '+1100'
206           if (p[2] == '2') return gmt12; // '+1200'
207         }
208         else if (*s == '-') {
209           if (p[2] == '0') return gmtM10; // '-1000'
210           if (p[2] == '1') return gmtM11; // '-1100'
211           if (p[2] == '2') return gmtM12; // '-1200'
212           if (p[2] == '3') return gmtM13; // '-1300'
213           if (p[2] == '4') return gmtM14; // '-1400'
214         }
215       }
216       
217       /* special case for GMT+0530 */
218       if (strncmp((char *)s, "+0530", 5) == 0)
219         return gmt0530;
220     }
221     else if (len == 7) {
222       /*
223         "MultiMail" submits timezones like this: 
224           "Tue, 9 Mar 2004 9:43:00 -05-500",
225         don't know what the "-500" trailer is supposed to mean? Apparently 
226         Thunderbird just uses the "-05", so do we.
227       */
228       
229       if (isdigit(p[1]) && isdigit(p[2]) && (p[3] == '-'||p[3] == '+')) {
230         unsigned char tmp[8];
231         
232         strncpy((char *)tmp, p, 3);
233         tmp[3] = '0';
234         tmp[4] = '0';
235         tmp[5] = '\0';
236         return parseTimeZone(tmp, 5);
237       }
238     }
239   }
240   else if (*s == '0') {
241     if (len == 2) { // '00'
242       if (p[1] == '0') return gmt;
243       if (p[1] == '1') return gmt01;
244       if (p[1] == '2') return gmt02;
245     }
246     else if (len == 4) {
247       if (p[2] == '0' && p[3] == '0') { // '0x00'
248         if (p[1] == '0') return gmt;
249         if (p[1] == '1') return gmt01;
250         if (p[1] == '2') return gmt02;
251       }
252     }
253   }
254   else if (len == 3) {
255     if (strcasecmp((char *)s, "GMT") == 0) return gmt;
256     if (strcasecmp((char *)s, "UTC") == 0) return gmt;
257     if (strcasecmp((char *)s, "MET") == 0) return met;
258     if (strcasecmp((char *)s, "CET") == 0) return met;
259   }
260   
261   if (isalpha(*s)) {
262     ts = [[NSString alloc] initWithCString:(char *)s length:len];
263   }
264   else {
265     char buf[len + 5];
266     
267     buf[0] = 'G'; buf[1] = 'M'; buf[2] = 'T';
268     if (*s == '+' || *s == '-') {
269       strcpy(&(buf[3]), (char *)s);
270     }
271     else {
272       buf[3] = '+';
273       strcpy(&(buf[4]), (char *)s);
274     }
275     ts = [[NSString alloc] initWithCString:buf];
276   }
277 #if 1
278   NSLog(@"%s: RFC822 TZ Parser: expensive: '%@'", __PRETTY_FUNCTION__, ts);
279 #endif
280   tz = [NSTimeZone timeZoneWithAbbreviation:ts];
281   [ts release];
282   return tz;
283 }
284
285 - (id)parseValue:(id)_data ofHeaderField:(NSString *)_field {
286   // TODO: use UNICODE
287   NSCalendarDate *date       = nil;
288   unsigned char  buf[256];
289   unsigned char  *bytes = buf, *pe;
290   unsigned       length = 0;
291   NSTimeZone     *tz = nil;
292   char  dayOfMonth, monthOfYear, hour, minute, second;
293   short year;
294   BOOL  flag;
295   
296   if ((length = [_data cStringLength]) > 254) {
297     [self logWithFormat:
298             @"header field value to large for date parsing: '%@'(%i)",
299             _data, length];
300     length = 254;
301   }
302   
303   [_data getCString:(char *)buf maxLength:length];
304   buf[length] = '\0';
305   
306   /* remove leading chars (skip to first digit, the day of the month) */
307   while (length > 0 && (!isdigit(*bytes))) {
308     bytes++;
309     length--;
310   }
311   
312   if (length == 0) {
313     NSLog(@"WARNING(%s): empty value for header field %@ ..",
314           __PRETTY_FUNCTION__, _field);
315     return [CalDateClass date];
316   }
317   
318   // TODO: should be a category on NSCalendarDate
319   // TODO: optimize much further!
320   //   first part: '16 Jun 2002'
321   //   snd   part: '12:28[:11]'
322   //   trd   part: 'GMT' '+0000' '(MET)' '(+0200)'
323
324   /* defaults for early aborts */
325   tz     = gmt;
326   second = 0;
327   minute = 0;
328   
329   /* parse day of month */
330   
331   for (pe = bytes; isdigit(*pe); pe++)
332     ;
333   if (*pe == 0) goto failed;
334   *pe = '\0';
335   dayOfMonth = atoi((char *)bytes);
336   bytes = pe + 1;
337   
338   /* parse month-abbrev (should be English, could be other langs) */
339   
340   while (!isalpha(*bytes)) { /* go to first char */
341     if (*bytes == '\0') goto failed;
342     bytes++;
343   }
344   for (pe = bytes; isalpha(*pe); pe++) /* find end of string */
345     ;
346   if (*pe == 0) goto failed;
347   *pe = '\0';
348   if ((monthOfYear = parseMonthOfYear(bytes, (pe - bytes))) == 0) {
349     [self logWithFormat:@"WARNING(%s): cannot parse month in date: %@",
350             __PRETTY_FUNCTION__, _data];
351   }
352   bytes = pe + 1;
353   
354   /* parse year */
355   
356   while (!isdigit(*bytes)) { /* go to first digit */
357     if (*bytes == '\0') goto failed;
358     bytes++;
359   }
360   for (pe = bytes; isdigit(*pe); pe++) /* find end of number */
361     ;
362   if (*pe == 0) goto failed;
363   *pe = '\0';
364   year = atoi((char *)bytes);
365   bytes = pe + 1;
366   if (year >= 70 && year < 135) // Y2K
367     year += 1900;
368   else if (year >= 0 && year < 70) // Y2K
369     year += 2000;
370   
371 #if LIB_FOUNDATION_LIBRARY
372   if (year > 2030) {
373     NSLog(@"ERROR(%s): got invalid year in date header %d: '%s'",
374           __PRETTY_FUNCTION__, year, buf);
375     year = 2000; /* no choice is good ..., maybe return nil? */
376   }
377 #endif
378   
379   /* parse hour */
380   
381   while (!isdigit(*bytes)) { /* go to first digit */
382     if (*bytes == '\0') goto failed;
383     bytes++;
384   }
385   for (pe = bytes; isdigit(*pe); pe++) /* find end of number */
386     ;
387   flag = (*pe == 0);
388   *pe = '\0';
389   hour = bytes != pe ? atoi((char *)bytes) : 0;
390   if (flag) goto finished; // this is: '12\0'
391   bytes = pe + 1;
392   
393   /* parse minute */
394   
395   while (!isdigit(*bytes)) { /* go to first digit */
396     if (*bytes == '\0') goto finished; // this is: '12  \0'
397     bytes++;
398   }
399   for (pe = bytes; isdigit(*pe); pe++) /* find end of number */
400     ;
401   flag = (*pe == 0);
402   *pe = '\0';
403   minute = bytes != pe ? atoi((char *)bytes) : 0;
404   if (flag) goto finished; // this is: '12:23\0'
405   bytes = pe + 1;
406   
407   /* parse second - if available '13:13:23' vs '12:23\0' or '12:12 (MET)' */
408   
409   while (isspace(*bytes)) /* skip spaces */
410     bytes++;
411   if (*bytes == 0) goto finished; // this is: '12:23   \0'
412   if (isdigit(*bytes) || *bytes == ':') {
413     /* parse second */
414     while (!isdigit(*bytes)) { /* go to first digit, skip the ':' */
415       if (*bytes == '\0') goto finished;
416       bytes++;
417     }
418     
419     for (pe = bytes; isdigit(*pe); pe++) /* find end of number */
420       ;
421     flag = (*pe == 0);
422     *pe = '\0';
423     second = bytes != pe ? atoi((char *)bytes) : 0;
424     if (flag) goto finished; // this is: '12:23:12\0'
425     bytes = pe + 1;
426   }
427   
428   /* parse timezone: 'GMT' '+0000' '(MET)' '(+0200)' */
429   // TODO: do we need to parse: "-0700 (PDT)" as "PDT"?
430   
431   while (isspace(*bytes) || *bytes == '(') /* skip spaces */
432     bytes++;
433   if (*bytes == 0) goto finished; // this is: '12:23:12 \0' or '12:12 ('
434   
435   for (pe = bytes; isalnum(*pe) || *pe == '-' || *pe == '+'; pe++)
436     ;
437   *pe = '\0';
438   if ((tz = parseTimeZone(bytes, (pe - bytes))) == nil) {
439     [self logWithFormat:
440             @"WARNING: failed to parse RFC822 timezone: '%s' (value='%@')",
441             bytes, _data];
442     tz = gmt;
443   }
444   
445   /* construct and return */
446  finished:  
447   date = [CalDateClass dateWithYear:year month:monthOfYear day:dayOfMonth
448                        hour:hour minute:minute second:second
449                        timeZone:tz];
450   if (date == nil) goto failed;
451
452 #if 0  
453   printf("parsed '%s' to date: %s\n", 
454          [_data cString], [[date description] cString]);
455   //[self logWithFormat:@"parsed '%@' to date: %@", _data, date];
456 #endif
457   return date;
458   
459  failed:
460   // TODO: 'Sun, May 18 2003 14:20:55 -0700' - why does this fail?
461   [self logWithFormat:@"WARNING: failed to parse RFC822 date field: '%@'",
462           _data];
463   return nil;
464 }
465
466 @end /* NGMimeRFC822DateHeaderFieldParser */