]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoMailObject.m
a28d752e68c9c36caf38d96aa393faff4775461c
[scalable-opengroupware.org] / SoObjects / Mailer / SOGoMailObject.m
1 /*
2   Copyright (C) 2004-2005 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
22 #import <Foundation/NSArray.h>
23 #import <Foundation/NSCalendarDate.h>
24 #import <Foundation/NSDictionary.h>
25 #import <Foundation/NSEnumerator.h>
26 #import <Foundation/NSString.h>
27 #import <Foundation/NSUserDefaults.h>
28 #import <Foundation/NSValue.h>
29
30 #import <NGObjWeb/WOContext.h>
31 #import <NGObjWeb/WOContext+SoObjects.h>
32 #import <NGObjWeb/WOResponse.h>
33 #import <NGObjWeb/NSException+HTTP.h>
34 #import <NGExtensions/NSNull+misc.h>
35 #import <NGExtensions/NSObject+Logs.h>
36 #import <NGExtensions/NSString+Encoding.h>
37 #import <NGExtensions/NSString+misc.h>
38 #import <NGImap4/NGImap4Connection.h>
39 #import <NGImap4/NGImap4Envelope.h>
40 #import <NGImap4/NGImap4EnvelopeAddress.h>
41 #import <NGMail/NGMimeMessageParser.h>
42
43 #import <SoObjects/SOGo/NSArray+Utilities.h>
44 #import <SoObjects/SOGo/NSDictionary+Utilities.h>
45 #import <SoObjects/SOGo/SOGoPermissions.h>
46 #import <SoObjects/SOGo/SOGoUser.h>
47
48 #import "NSString+Mail.h"
49 #import "NSData+Mail.h"
50 #import "SOGoMailFolder.h"
51 #import "SOGoMailAccount.h"
52 #import "SOGoMailAccounts.h"
53 #import "SOGoMailManager.h"
54 #import "SOGoMailBodyPart.h"
55
56 #import "SOGoMailObject.h"
57
58 @implementation SOGoMailObject
59
60 static NSArray  *coreInfoKeys = nil;
61 static NSString *mailETag = nil;
62 static BOOL heavyDebug         = NO;
63 static BOOL fetchHeader        = YES;
64 static BOOL debugOn            = NO;
65 static BOOL debugBodyStructure = NO;
66 static BOOL debugSoParts       = NO;
67
68 + (void) initialize
69 {
70   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
71   
72   if ((fetchHeader = ([ud boolForKey: @"SOGoDoNotFetchMailHeader"] ? NO : YES)))
73     NSLog(@"Note: fetching full mail header.");
74   else
75     NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
76   
77   /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
78   /* Note: "BODY" actually returns the structure! */
79   if (fetchHeader) {
80     coreInfoKeys = [[NSArray alloc] initWithObjects:
81                                       @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
82                                       @"RFC822.SIZE",
83                                       @"RFC822.HEADER",
84                                       // not yet supported: @"INTERNALDATE",
85                                     nil];
86   }
87   else {
88     coreInfoKeys = [[NSArray alloc] initWithObjects:
89                                       @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
90                                       @"RFC822.SIZE",
91                                       // not yet supported: @"INTERNALDATE",
92                                     nil];
93   }
94
95   if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) {
96     mailETag = [[NSString alloc] initWithFormat: @"\"imap4url_%d_%d_%03d\"",
97                                  UIX_MAILER_MAJOR_VERSION,
98                                  UIX_MAILER_MINOR_VERSION,
99                                  UIX_MAILER_SUBMINOR_VERSION];
100     NSLog(@"Note(SOGoMailObject): using constant etag for mail parts: '%@'", 
101           mailETag);
102   }
103   else
104     NSLog(@"Note(SOGoMailObject): etag caching disabled!");
105 }
106
107 - (void) dealloc
108 {
109   [headers    release];
110   [headerPart release];
111   [coreInfos  release];
112   [super dealloc];
113 }
114
115 /* IMAP4 */
116
117 - (NSString *) relativeImap4Name
118 {
119   return [nameInContainer stringByDeletingPathExtension];
120 }
121
122 /* hierarchy */
123
124 - (SOGoMailObject *)mailObject {
125   return self;
126 }
127
128 /* part hierarchy */
129
130 - (NSString *) keyExtensionForPart: (id) _partInfo
131 {
132   NSString *mt, *st;
133   
134   if (_partInfo == nil)
135     return nil;
136   
137   mt = [_partInfo valueForKey: @"type"];
138   st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
139   if ([mt isEqualToString: @"text"]) {
140     if ([st isEqualToString: @"plain"])    return @".txt";
141     if ([st isEqualToString: @"html"])     return @".html";
142     if ([st isEqualToString: @"calendar"]) return @".ics";
143     if ([st isEqualToString: @"x-vcard"])  return @".vcf";
144   }
145   else if ([mt isEqualToString: @"image"])
146     return [@"." stringByAppendingString:st];
147   else if ([mt isEqualToString: @"application"]) {
148     if ([st isEqualToString: @"pgp-signature"])
149       return @".asc";
150   }
151   
152   return nil;
153 }
154
155 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
156   /* should return non-multipart children */
157   NSMutableArray *ma;
158   NSArray *parts;
159   unsigned i, count;
160   
161   parts = [[self bodyStructure] valueForKey: @"parts"];
162   if (![parts isNotNull]) 
163     return nil;
164   if ((count = [parts count]) == 0)
165     return nil;
166   
167   for (i = 0, ma = nil; i < count; i++) {
168     NSString *key, *ext;
169     id   part;
170     BOOL hasParts;
171     
172     part     = [parts objectAtIndex:i];
173     hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
174     if ((hasParts && !_withParts) || (_withParts && !hasParts))
175       continue;
176
177     if (ma == nil)
178       ma = [NSMutableArray arrayWithCapacity:count - i];
179     
180     ext = [self keyExtensionForPart:part];
181     key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ext?ext: @""];
182     [ma addObject:key];
183     [key release];
184   }
185   return ma;
186 }
187
188 - (NSArray *) toOneRelationshipKeys
189 {
190   return [self relationshipKeysWithParts:NO];
191 }
192
193 - (NSArray *) toManyRelationshipKeys
194 {
195   return [self relationshipKeysWithParts:YES];
196 }
197
198 /* message */
199
200 - (id) fetchParts: (NSArray *) _parts
201 {
202   // TODO: explain what it does
203   /*
204     Called by -fetchPlainTextParts:
205   */
206   return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts];
207 }
208
209 /* core infos */
210
211 - (BOOL)doesMailExist {
212   static NSArray *existsKey = nil;
213   id msgs;
214   
215   if (coreInfos != nil) /* if we have coreinfos, we can use them */
216     return [coreInfos isNotNull];
217   
218   /* otherwise fetch something really simple */
219   
220   if (existsKey == nil) /* we use size, other suggestions? */
221     existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
222   
223   msgs = [self fetchParts:existsKey]; // returns dict
224   msgs = [msgs valueForKey: @"fetch"];
225   return [msgs count] > 0 ? YES : NO;
226 }
227
228 - (id) fetchCoreInfos
229 {
230   id msgs;
231
232   if (!coreInfos)
233     {  
234       msgs = [self fetchParts: coreInfoKeys]; // returns dict
235       if (heavyDebug)
236         [self logWithFormat: @"M: %@", msgs];
237       msgs = [msgs valueForKey: @"fetch"];
238       if ([msgs count] > 0)
239         coreInfos = [msgs objectAtIndex: 0];
240       [coreInfos retain];
241     }
242
243   return coreInfos;
244 }
245
246 - (id) bodyStructure
247 {
248   id body;
249
250   body = [[self fetchCoreInfos] valueForKey: @"body"];
251   if (debugBodyStructure)
252     [self logWithFormat: @"BODY: %@", body];
253
254   return body;
255 }
256
257 - (NGImap4Envelope *) envelope
258 {
259   return [[self fetchCoreInfos] valueForKey: @"envelope"];
260 }
261
262 - (NSString *) subject
263 {
264   return [[self envelope] subject];
265 }
266
267 - (NSString *) decodedSubject
268 {
269   return [[self subject] decodedSubject];
270 }
271
272 - (NSCalendarDate *) date
273 {
274   NSTimeZone *userTZ;
275   NSCalendarDate *date;
276
277   userTZ = [[context activeUser] timeZone];
278   date = [[self envelope] date];
279   [date setTimeZone: userTZ];
280
281   return date;
282 }
283
284 - (NSArray *) fromEnvelopeAddresses
285 {
286   return [[self envelope] from];
287 }
288
289 - (NSArray *) toEnvelopeAddresses
290 {
291   return [[self envelope] to];
292 }
293
294 - (NSArray *) ccEnvelopeAddresses
295 {
296   return [[self envelope] cc];
297 }
298
299 - (NSArray *) replyToEnvelopeAddresses
300 {
301   return [[self envelope] replyTo];
302 }
303
304 - (NSData *) mailHeaderData
305 {
306   return [[self fetchCoreInfos] valueForKey: @"header"];
307 }
308
309 - (BOOL) hasMailHeaderInCoreInfos
310 {
311   return [[self mailHeaderData] length] > 0 ? YES : NO;
312 }
313
314 - (id) mailHeaderPart
315 {
316   NGMimeMessageParser *parser;
317   NSData *data;
318   
319   if (headerPart != nil)
320     return [headerPart isNotNull] ? headerPart : nil;
321   
322   if ([(data = [self mailHeaderData]) length] == 0)
323     return nil;
324   
325   // TODO: do we need to set some delegate method which stops parsing the body?
326   parser = [[NGMimeMessageParser alloc] init];
327   headerPart = [[parser parsePartFromData:data] retain];
328   [parser release]; parser = nil;
329
330   if (headerPart == nil) {
331     headerPart = [[NSNull null] retain];
332     return nil;
333   }
334   return headerPart;
335 }
336
337 - (NSDictionary *) mailHeaders
338 {
339   if (!headers)
340     headers = [[[self mailHeaderPart] headers] copy];
341
342   return headers;
343 }
344
345 - (id) lookupInfoForBodyPart: (id) _path
346 {
347   NSEnumerator *pe;
348   NSString *p;
349   id info;
350
351   if (![_path isNotNull])
352     return nil;
353   
354   if ((info = [self bodyStructure]) == nil) {
355     [self errorWithFormat: @"got no body part structure!"];
356     return nil;
357   }
358
359   /* ensure array argument */
360   
361   if ([_path isKindOfClass:[NSString class]]) {
362     if ([_path length] == 0)
363       return info;
364     
365     _path = [_path componentsSeparatedByString: @"."];
366   }
367   
368   /* 
369      For each path component, eg 1,1,3 
370      
371      Remember that we need special processing for message/rfc822 which maps the
372      namespace of multiparts directly into the main namespace.
373      
374      TODO(hh): no I don't remember, please explain in more detail!
375   */
376   pe = [_path objectEnumerator];
377   while ((p = [pe nextObject]) != nil && [info isNotNull]) {
378     unsigned idx;
379     NSArray  *parts;
380     NSString *mt;
381     
382     [self debugWithFormat: @"check PATH: %@", p];
383     idx = [p intValue] - 1;
384
385     parts = [info valueForKey: @"parts"];
386     mt = [[info valueForKey: @"type"] lowercaseString];
387     if ([mt isEqualToString: @"message"]) {
388       /* we have special behaviour for message types */
389       id body;
390       
391       if ((body = [info valueForKey: @"body"]) != nil) {
392         mt = [body valueForKey: @"type"];
393         if ([mt isEqualToString: @"multipart"])
394           parts = [body valueForKey: @"parts"];
395         else
396           parts = [NSArray arrayWithObject:body];
397       }
398     }
399     
400     if (idx >= [parts count]) {
401       [self errorWithFormat:
402               @"body part index out of bounds(idx=%d vs count=%d): %@", 
403               (idx + 1), [parts count], info];
404       return nil;
405     }
406     info = [parts objectAtIndex:idx];
407   }
408   return [info isNotNull] ? info : nil;
409 }
410
411 /* content */
412
413 - (NSData *) content
414 {
415   NSData *content;
416   id     result, fullResult;
417   
418   fullResult = [self fetchParts: [NSArray arrayWithObject: @"RFC822"]];
419   if (fullResult == nil)
420     return nil;
421   
422   if ([fullResult isKindOfClass: [NSException class]])
423     return fullResult;
424   
425   /* extract fetch result */
426   
427   result = [fullResult valueForKey: @"fetch"];
428   if (![result isKindOfClass:[NSArray class]]) {
429     [self logWithFormat:
430             @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", 
431             fullResult];
432     return [NSException exceptionWithHTTPStatus:500 /* server error */
433                         reason: @"unexpected IMAP4 result"];
434   }
435   if ([result count] == 0)
436     return nil;
437   
438   result = [result objectAtIndex:0];
439   
440   /* extract message */
441   
442   if ((content = [result valueForKey: @"message"]) == nil) {
443     [self logWithFormat:
444             @"ERROR: unexpected IMAP4 result (missing 'message'): %@", 
445             result];
446     return [NSException exceptionWithHTTPStatus:500 /* server error */
447                         reason: @"unexpected IMAP4 result"];
448   }
449   
450   return [[content copy] autorelease];
451 }
452
453 - (NSString *) davContentType
454 {
455   return @"message/rfc822";
456 }
457
458 - (NSString *) contentAsString
459 {
460   id s;
461   NSData *content;
462
463   content = [self content];
464   if (content)
465     {
466       if ([content isKindOfClass: [NSData class]])
467         {
468 #warning we ignore the charset here?
469           s = [[NSString alloc] initWithData: content
470                                 encoding: NSISOLatin1StringEncoding];
471           if (s)
472             [s autorelease];
473           else
474             [self logWithFormat:
475                     @"ERROR: could not convert data of length %d to string", 
476                   [content length]];
477         }
478       else
479         s = content;
480     }
481   else
482     s = nil;
483
484   return s;
485 }
486
487 /* bulk fetching of plain/text content */
488
489 // - (BOOL) shouldFetchPartOfType: (NSString *) _type
490 //                     subtype: (NSString *) _subtype
491 // {
492 //   /*
493 //     This method decides which parts are 'prefetched' for display. Those are
494 //     usually text parts (the set is currently hardcoded in this method ...).
495 //   */
496 //   _type    = [_type    lowercaseString];
497 //   _subtype = [_subtype lowercaseString];
498   
499 //   return (([_type isEqualToString: @"text"]
500 //            && ([_subtype isEqualToString: @"plain"]
501 //                || [_subtype isEqualToString: @"html"]
502 //                || [_subtype isEqualToString: @"calendar"]))
503 //           || ([_type isEqualToString: @"application"]
504 //               && ([_subtype isEqualToString: @"pgp-signature"]
505 //                   || [_subtype hasPrefix: @"x-vnd.kolab."])));
506 // }
507
508 - (void) addRequiredKeysOfStructure: (NSDictionary *) info
509                                path: (NSString *) p
510                             toArray: (NSMutableArray *) keys
511                       acceptedTypes: (NSArray *) types
512 {
513   /* 
514      This is used to collect the set of IMAP4 fetch-keys required to fetch
515      the basic parts of the body structure. That is, to fetch all parts which
516      are displayed 'inline' in a single IMAP4 fetch.
517      
518      The method calls itself recursively to walk the body structure.
519   */
520   NSArray *parts;
521   unsigned i, count;
522   NSString *k;
523   id body;
524   NSString *sp, *mimeType;
525   id childInfo;
526
527   mimeType = [[NSString stringWithFormat: @"%@/%@",
528                         [info valueForKey: @"type"],
529                         [info valueForKey: @"subtype"]]
530                lowercaseString];
531   if ([types containsObject: mimeType])
532     {
533       if ([p length] > 0)
534         k = [NSString stringWithFormat: @"body[%@]", p];
535       else
536         {
537           /*
538             for some reason we need to add ".TEXT" for plain text stuff on root
539             entities?
540             TODO: check with HTML
541           */
542           k = @"body[text]";
543         }
544       [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
545                                      mimeType, @"mimeType", nil]];
546     }
547
548   parts = [info objectForKey: @"parts"];
549   count = [parts count];
550   for (i = 0; i < count; i++)
551     {
552       sp = (([p length] > 0)
553             ? [p stringByAppendingFormat: @".%d", i + 1]
554             : [NSString stringWithFormat: @"%d", i + 1]);
555       
556       childInfo = [parts objectAtIndex: i];
557       
558       [self addRequiredKeysOfStructure: childInfo
559             path: sp toArray: keys
560             acceptedTypes: types];
561     }
562       
563   /* check body */
564   body = [info objectForKey: @"body"];
565   if (body)
566     {
567       sp = [[body valueForKey: @"type"] lowercaseString];
568       if ([sp isEqualToString: @"multipart"])
569         sp = p;
570       else
571         sp = [p length] > 0 ? [p stringByAppendingString: @".1"] : @"1";
572       [self addRequiredKeysOfStructure: body
573             path: sp toArray: keys
574             acceptedTypes: types];
575     }
576 }
577
578 - (NSArray *) plainTextContentFetchKeys
579 {
580   /*
581     The name is not 100% correct. The method returns all body structure fetch
582     keys which are marked by the -shouldFetchPartOfType:subtype: method.
583   */
584   NSMutableArray *ma;
585   NSArray *types;
586
587   types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
588                    @"text/calendar", @"application/ics",
589                    @"application/pgp-signature", nil];
590   ma = [NSMutableArray arrayWithCapacity: 4];
591   [self addRequiredKeysOfStructure: [self bodyStructure]
592         path: @"" toArray: ma acceptedTypes: types];
593
594   return ma;
595 }
596
597 - (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
598 {
599   // TODO: is the name correct or does it also fetch other parts?
600   NSMutableDictionary *flatContents;
601   unsigned i, count;
602   id result;
603   
604   [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
605   
606   result = [self fetchParts: [_fetchKeys objectsForKey: @"key"]];
607   result = [result valueForKey: @"RawResponse"]; // hackish
608   
609   // Note: -valueForKey: doesn't work!
610   result = [(NSDictionary *)result objectForKey: @"fetch"]; 
611   
612   count        = [_fetchKeys count];
613   flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
614   for (i = 0; i < count; i++) {
615     NSString *key;
616     NSData   *data;
617     
618     key  = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
619     data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] 
620                             objectForKey: @"data"];
621     
622     if (![data isNotNull]) {
623       [self errorWithFormat: @"got no data for key: %@", key];
624       continue;
625     }
626     
627     if ([key isEqualToString: @"body[text]"])
628       key = @""; // see key collector for explanation (TODO: where?)
629     else if ([key hasPrefix: @"body["]) {
630       NSRange r;
631       
632       key = [key substringFromIndex:5];
633       r   = [key rangeOfString: @"]"];
634       if (r.length > 0)
635         key = [key substringToIndex:r.location];
636     }
637     [flatContents setObject:data forKey:key];
638   }
639   return flatContents;
640 }
641
642 - (NSDictionary *) fetchPlainTextParts
643 {
644   return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
645 }
646
647 /* convert parts to strings */
648 - (NSString *) stringForData: (NSData *) _data
649                     partInfo: (NSDictionary *) _info
650 {
651   NSString *charset, *s;
652   NSData *mailData;
653   
654   if ([_data isNotNull])
655     {
656       mailData
657         = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
658
659       charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
660       if (![charset length])
661         {
662           s = nil;
663         }
664       else
665         {
666           s = [NSString stringWithData: mailData usingEncodingNamed: charset];
667         }
668       
669       // If it has failed, we try at least using UTF-8. Normally, this can NOT fail.
670       // Unfortunately, it seems to fail under GNUstep so we try latin1 if that's
671       // the case
672       if (!s)
673         s = [[[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding] autorelease];
674       
675       if (!s)
676         s = [[[NSString alloc] initWithData: mailData encoding: NSISOLatin1StringEncoding] autorelease];
677     }
678   else
679     s = nil;
680
681   return s;
682 }
683
684 - (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
685 {
686   NSMutableDictionary *md;
687   NSDictionary *info;
688   NSEnumerator *keys;
689   NSString     *key, *s;
690
691   md = [NSMutableDictionary dictionaryWithCapacity:4];
692   keys = [_datas keyEnumerator];
693   while ((key = [keys nextObject]))
694     {
695       info = [self lookupInfoForBodyPart: key];
696       s = [self stringForData: [_datas objectForKey:key] partInfo: info];
697       if (s)
698         [md setObject: s forKey: key];
699     }
700
701   return md;
702 }
703
704 - (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
705 {
706   /*
707     The fetched parts are NSData objects, this method converts them into
708     NSString objects based on the information inside the bodystructure.
709     
710     The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
711     The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
712   */
713   NSDictionary *datas;
714   
715   if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
716     return nil;
717   if ([datas isKindOfClass:[NSException class]])
718     return datas;
719   
720   return [self stringifyTextParts:datas];
721 }
722
723 /* flags */
724
725 - (NSException *) addFlags: (id) _flags
726 {
727   return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
728 }
729
730 - (NSException *) removeFlags: (id) _flags
731 {
732   return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
733 }
734
735 /* permissions */
736
737 - (BOOL) isDeletionAllowed
738 {
739   NSArray *parentAcl;
740   NSString *login;
741
742   login = [[context activeUser] login];
743   parentAcl = [[self container] aclsForUser: login];
744
745   return [parentAcl containsObject: SOGoRole_ObjectEraser];
746 }
747
748 /* name lookup */
749
750 - (id) lookupImap4BodyPartKey: (NSString *) _key
751                     inContext: (id) _ctx
752 {
753   // TODO: we might want to check for existence prior controller creation
754   Class clazz;
755   NSArray *parts;
756   int part;
757   NSDictionary *partDesc;
758   NSString *mimeType;
759
760   parts = [[self bodyStructure] objectForKey: @"parts"];
761
762   /* We don't have parts here but we're trying to download the message's
763      content that could be an image/jpeg, as an example */
764   if ([parts count] == 0 && ![_key intValue])
765     {
766       partDesc = [self bodyStructure];
767       _key = @"1";
768     }
769   else
770     {
771       part = [_key intValue] - 1;
772       if (part > -1 && part < [parts count])
773         partDesc = [parts objectAtIndex: part];
774       else
775         partDesc = nil;
776     }
777
778   if (partDesc)
779     {
780       mimeType = [[partDesc keysWithFormat: @"%{type}/%{subtype}"] lowercaseString];
781       clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
782                                 inContext: _ctx];
783     }
784   else
785     clazz = Nil;
786     
787   return [clazz objectWithName:_key inContainer: self];
788 }
789
790 - (id) lookupName: (NSString *) _key
791         inContext: (id) _ctx
792           acquire: (BOOL) _flag
793 {
794   id obj;
795   
796   /* first check attributes directly bound to the application */
797   if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
798     return obj;
799   
800   /* lookup body part */
801   
802   if ([self isBodyPartKey:_key inContext:_ctx]) {
803     if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
804       if (debugSoParts)
805         [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
806       return obj;
807     }
808   }
809   
810   /* return 404 to stop acquisition */
811   return [NSException exceptionWithHTTPStatus:404 /* Not Found */
812                       reason: @"Did not find mail method or part-reference!"];
813 }
814
815 /* WebDAV */
816
817 - (BOOL) davIsCollection
818 {
819   /* while a mail has child objects, it should appear as a file in WebDAV */
820   return NO;
821 }
822
823 - (id) davContentLength
824 {
825   return [[self fetchCoreInfos] valueForKey: @"size"];
826 }
827
828 - (NSDate *) davCreationDate
829 {
830   // TODO: use INTERNALDATE once NGImap4 supports that
831   return nil;
832 }
833
834 - (NSDate *) davLastModified
835 {
836   return [self davCreationDate];
837 }
838
839 - (NSException *) davMoveToTargetObject: (id) _target
840                                 newName: (NSString *) _name
841                               inContext: (id)_ctx
842 {
843   [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
844         _name, _target];
845   return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
846                       reason: @"not implemented"];
847 }
848
849 - (NSException *) davCopyToTargetObject: (id) _target
850                                 newName: (NSString *) _name
851                               inContext: (id)_ctx
852 {
853   /* 
854      Note: this is special because we create SOGoMailObject's even if they do
855            not exist (for performance reasons).
856
857      Also: we cannot really take a target resource, the ID will be assigned by
858            the IMAP4 server.
859            We even cannot return a 'location' header instead because IMAP4
860            doesn't tell us the new ID.
861   */
862   NSURL *destImap4URL;
863   
864   destImap4URL = ([_name length] == 0)
865     ? [[_target container] imap4URL]
866     : [_target imap4URL];
867   
868   return [[self mailManager] copyMailURL:[self imap4URL] 
869                              toFolderURL:destImap4URL
870                              password:[self imap4Password]];
871 }
872
873 /* actions */
874
875 - (id) GETAction: (id) _ctx
876 {
877   NSException *error;
878   WOResponse  *r;
879   NSData      *content;
880   
881   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
882     /* check whether the mail still exists */
883     if (![self doesMailExist]) {
884       return [NSException exceptionWithHTTPStatus:404 /* Not Found */
885                           reason: @"mail was deleted"];
886     }
887     return error; /* return 304 or 416 */
888   }
889   
890   content = [self content];
891   if ([content isKindOfClass:[NSException class]])
892     return content;
893   if (content == nil) {
894     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
895                         reason: @"did not find IMAP4 message"];
896   }
897   
898   r = [(WOContext *)_ctx response];
899   [r setHeader: @"message/rfc822" forKey: @"content-type"];
900   [r setContent:content];
901   return r;
902 }
903
904 /* operations */
905
906 - (NSException *) trashInContext: (id) _ctx
907 {
908   /*
909     Trashing is three actions:
910     a) copy to trash folder
911     b) mark mail as deleted
912     c) expunge folder
913     
914     In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
915     the ID used in the trash folder.
916   */
917   SOGoMailFolder *trashFolder;
918   NSException    *error;
919
920   // TODO: check for safe HTTP method
921   
922   trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
923   if ([trashFolder isKindOfClass:[NSException class]])
924     return (NSException *)trashFolder;
925   if (![trashFolder isNotNull]) {
926     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
927                         reason: @"Did not find Trash folder!"];
928   }
929   [trashFolder flushMailCaches];
930
931   /* a) copy */
932   
933   error = [self davCopyToTargetObject:trashFolder
934                 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
935                 inContext:_ctx];
936   if (error != nil) return error;
937   
938   /* b) mark deleted */
939   
940   error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
941   if (error != nil) return error;
942
943   [container markForExpunge];
944
945   [self flushMailCaches];
946   
947   return nil;
948 }
949
950 - (NSException *) copyToFolderNamed: (NSString *) folderName
951                           inContext: (id)_ctx
952 {
953   SOGoMailFolder *destFolder;
954   NSEnumerator *folders;
955   NSString *currentFolderName, *reason;
956
957   // TODO: check for safe HTTP method
958
959   destFolder = (SOGoMailFolder *) [self mailAccountsFolder];
960   folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
961   currentFolderName = [folders nextObject];
962   currentFolderName = [folders nextObject];
963
964   while (currentFolderName)
965     {
966       destFolder = [destFolder lookupName: currentFolderName
967                                inContext: _ctx
968                                acquire: NO];
969       if ([destFolder isKindOfClass: [NSException class]])
970         return (NSException *) destFolder;
971       currentFolderName = [folders nextObject];
972     }
973
974   if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
975         && [destFolder isNotNull]))
976     {
977       reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
978                          folderName];
979       return [NSException exceptionWithHTTPStatus:500 /* Server Error */
980                           reason: reason];
981     }
982   [destFolder flushMailCaches];
983
984   /* a) copy */
985
986   return [self davCopyToTargetObject: destFolder
987                newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
988                inContext:_ctx];
989 }
990
991 - (NSException *) moveToFolderNamed: (NSString *) folderName
992                           inContext: (id)_ctx
993 {
994   NSException    *error;
995
996   if (![self copyToFolderNamed: folderName
997              inContext: _ctx])
998     {
999       /* b) mark deleted */
1000   
1001       error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
1002       if (error != nil) return error;
1003
1004       [self flushMailCaches];
1005     }
1006   
1007   return nil;
1008 }
1009
1010 - (NSException *) delete
1011 {
1012   /* 
1013      Note: delete is different to DELETEAction: for mails! The 'delete' runs
1014            either flags a message as deleted or moves it to the Trash while
1015            the DELETEAction: really deletes a message (by flagging it as
1016            deleted _AND_ performing an expunge).
1017   */
1018   // TODO: copy to Trash folder
1019   NSException *error;
1020
1021   // TODO: check for safe HTTP method
1022   
1023   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
1024   return error;
1025 }
1026
1027 - (id) DELETEAction: (id) _ctx
1028 {
1029   NSException *error;
1030   
1031   // TODO: ensure safe HTTP method
1032   
1033   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
1034   if (error != nil) return error;
1035   
1036   error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
1037   if (error != nil) return error; // TODO: unflag as deleted?
1038   
1039   return [NSNumber numberWithBool:YES]; /* delete was successful */
1040 }
1041
1042 /* some mail classification */
1043
1044 - (BOOL) isKolabObject
1045 {
1046   NSDictionary *h;
1047   
1048   if ((h = [self mailHeaders]) != nil)
1049     return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
1050   
1051   // TODO: we could check the body structure?
1052   
1053   return NO;
1054 }
1055
1056 - (BOOL) isMailingListMail
1057 {
1058   NSDictionary *h;
1059   
1060   if ((h = [self mailHeaders]) == nil)
1061     return NO;
1062   
1063   return [[h objectForKey: @"list-id"] isNotEmpty];
1064 }
1065
1066 - (BOOL) isVirusScanned
1067 {
1068   NSDictionary *h;
1069   
1070   if ((h = [self mailHeaders]) == nil)
1071     return NO;
1072   
1073   if (![[h objectForKey: @"x-virus-status"]  isNotEmpty]) return NO;
1074   if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
1075   return YES;
1076 }
1077
1078 - (NSString *) scanListHeaderValue: (id) _value
1079                 forFieldWithPrefix: (NSString *) _prefix
1080 {
1081   /* Note: not very tolerant on embedded commands and <> */
1082   // TODO: does not really belong here, should be a header-field-parser
1083   NSRange r;
1084   
1085   if (![_value isNotEmpty])
1086     return nil;
1087   
1088   if ([_value isKindOfClass:[NSArray class]]) {
1089     NSEnumerator *e;
1090     id value;
1091
1092     e = [_value objectEnumerator];
1093     while ((value = [e nextObject]) != nil) {
1094       value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1095       if (value != nil) return value;
1096     }
1097     return nil;
1098   }
1099   
1100   if (![_value isKindOfClass:[NSString class]])
1101     return nil;
1102   
1103   /* check for commas in string values */
1104   r = [_value rangeOfString: @","];
1105   if (r.length > 0) {
1106     return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1107                  forFieldWithPrefix:_prefix];
1108   }
1109
1110   /* value qualifies */
1111   if (![(NSString *)_value hasPrefix:_prefix])
1112     return nil;
1113   
1114   /* unquote */
1115   if ([_value characterAtIndex:0] == '<') {
1116     r = [_value rangeOfString: @">"];
1117     _value = (r.length == 0)
1118       ? [_value substringFromIndex:1]
1119       : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1120   }
1121
1122   return _value;
1123 }
1124
1125 - (NSString *) mailingListArchiveURL
1126 {
1127   return [self scanListHeaderValue:
1128                  [[self mailHeaders] objectForKey: @"list-archive"]
1129                forFieldWithPrefix: @"<http://"];
1130 }
1131
1132 - (NSString *) mailingListSubscribeURL
1133 {
1134   return [self scanListHeaderValue:
1135                  [[self mailHeaders] objectForKey: @"list-subscribe"]
1136                forFieldWithPrefix: @"<http://"];
1137 }
1138
1139 - (NSString *) mailingListUnsubscribeURL
1140 {
1141   return [self scanListHeaderValue:
1142                  [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1143                forFieldWithPrefix: @"<http://"];
1144 }
1145
1146 /* etag support */
1147
1148 - (id) davEntityTag
1149 {
1150   /*
1151     Note: There is one thing which *can* change for an existing message,
1152           those are the IMAP4 flags (and annotations, which we do not use).
1153           Since we don't render the flags, it should be OK, if this changes
1154           we must embed the flagging into the etag.
1155   */
1156   return mailETag;
1157 }
1158
1159 - (int) zlGenerationCount
1160 {
1161   return 0; /* mails never change */
1162 }
1163
1164 /* Outlook mail tagging */
1165
1166 - (NSString *) outlookMessageClass
1167 {
1168   NSString *type;
1169   
1170   if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
1171     if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
1172       return @"IPM.Contact";
1173     if ([type isEqualToString: @"application/x-vnd.kolab.task"])
1174       return @"IPM.Task";
1175     if ([type isEqualToString: @"application/x-vnd.kolab.event"])
1176       return @"IPM.Appointment";
1177     if ([type isEqualToString: @"application/x-vnd.kolab.note"])
1178       return @"IPM.Note";
1179     if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
1180       return @"IPM.Journal";
1181   }
1182   
1183   return @"IPM.Message"; /* email, default class */
1184 }
1185
1186 - (NSArray *) aclsForUser: (NSString *) uid
1187 {
1188   return [container aclsForUser: uid];
1189 }
1190
1191 /* debugging */
1192
1193 - (BOOL) isDebuggingEnabled
1194 {
1195   return debugOn;
1196 }
1197
1198 @end /* SOGoMailObject */