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