]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoMailObject.m
f6969e9721fb02db61eebcf26b1bb92f0dc26260
[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 - (NSData *) mailHeaderData
300 {
301   return [[self fetchCoreInfos] valueForKey: @"header"];
302 }
303
304 - (BOOL) hasMailHeaderInCoreInfos
305 {
306   return [[self mailHeaderData] length] > 0 ? YES : NO;
307 }
308
309 - (id) mailHeaderPart
310 {
311   NGMimeMessageParser *parser;
312   NSData *data;
313   
314   if (headerPart != nil)
315     return [headerPart isNotNull] ? headerPart : nil;
316   
317   if ([(data = [self mailHeaderData]) length] == 0)
318     return nil;
319   
320   // TODO: do we need to set some delegate method which stops parsing the body?
321   parser = [[NGMimeMessageParser alloc] init];
322   headerPart = [[parser parsePartFromData:data] retain];
323   [parser release]; parser = nil;
324
325   if (headerPart == nil) {
326     headerPart = [[NSNull null] retain];
327     return nil;
328   }
329   return headerPart;
330 }
331
332 - (NSDictionary *) mailHeaders
333 {
334   if (!headers)
335     headers = [[[self mailHeaderPart] headers] copy];
336
337   return headers;
338 }
339
340 - (id) lookupInfoForBodyPart: (id) _path
341 {
342   NSEnumerator *pe;
343   NSString *p;
344   id info;
345
346   if (![_path isNotNull])
347     return nil;
348   
349   if ((info = [self bodyStructure]) == nil) {
350     [self errorWithFormat: @"got no body part structure!"];
351     return nil;
352   }
353
354   /* ensure array argument */
355   
356   if ([_path isKindOfClass:[NSString class]]) {
357     if ([_path length] == 0)
358       return info;
359     
360     _path = [_path componentsSeparatedByString: @"."];
361   }
362   
363   /* 
364      For each path component, eg 1,1,3 
365      
366      Remember that we need special processing for message/rfc822 which maps the
367      namespace of multiparts directly into the main namespace.
368      
369      TODO(hh): no I don't remember, please explain in more detail!
370   */
371   pe = [_path objectEnumerator];
372   while ((p = [pe nextObject]) != nil && [info isNotNull]) {
373     unsigned idx;
374     NSArray  *parts;
375     NSString *mt;
376     
377     [self debugWithFormat: @"check PATH: %@", p];
378     idx = [p intValue] - 1;
379
380     parts = [info valueForKey: @"parts"];
381     mt = [[info valueForKey: @"type"] lowercaseString];
382     if ([mt isEqualToString: @"message"]) {
383       /* we have special behaviour for message types */
384       id body;
385       
386       if ((body = [info valueForKey: @"body"]) != nil) {
387         mt = [body valueForKey: @"type"];
388         if ([mt isEqualToString: @"multipart"])
389           parts = [body valueForKey: @"parts"];
390         else
391           parts = [NSArray arrayWithObject:body];
392       }
393     }
394     
395     if (idx >= [parts count]) {
396       [self errorWithFormat:
397               @"body part index out of bounds(idx=%d vs count=%d): %@", 
398               (idx + 1), [parts count], info];
399       return nil;
400     }
401     info = [parts objectAtIndex:idx];
402   }
403   return [info isNotNull] ? info : nil;
404 }
405
406 /* content */
407
408 - (NSData *) content
409 {
410   NSData *content;
411   id     result, fullResult;
412   
413   fullResult = [self fetchParts: [NSArray arrayWithObject: @"RFC822"]];
414   if (fullResult == nil)
415     return nil;
416   
417   if ([fullResult isKindOfClass: [NSException class]])
418     return fullResult;
419   
420   /* extract fetch result */
421   
422   result = [fullResult valueForKey: @"fetch"];
423   if (![result isKindOfClass:[NSArray class]]) {
424     [self logWithFormat:
425             @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", 
426             fullResult];
427     return [NSException exceptionWithHTTPStatus:500 /* server error */
428                         reason: @"unexpected IMAP4 result"];
429   }
430   if ([result count] == 0)
431     return nil;
432   
433   result = [result objectAtIndex:0];
434   
435   /* extract message */
436   
437   if ((content = [result valueForKey: @"message"]) == nil) {
438     [self logWithFormat:
439             @"ERROR: unexpected IMAP4 result (missing 'message'): %@", 
440             result];
441     return [NSException exceptionWithHTTPStatus:500 /* server error */
442                         reason: @"unexpected IMAP4 result"];
443   }
444   
445   return [[content copy] autorelease];
446 }
447
448 - (NSString *) davContentType
449 {
450   return @"message/rfc822";
451 }
452
453 - (NSString *) contentAsString
454 {
455   id s;
456   NSData *content;
457
458   content = [self content];
459   if (content)
460     {
461       if ([content isKindOfClass: [NSData class]])
462         {
463           s = [[NSString alloc] initWithData: content
464                                 encoding: NSISOLatin1StringEncoding];
465           if (s)
466             [s autorelease];
467           else
468             [self logWithFormat:
469                     @"ERROR: could not convert data of length %d to string", 
470                   [content length]];
471         }
472       else
473         s = content;
474     }
475   else
476     s = nil;
477
478   return s;
479 }
480
481 /* bulk fetching of plain/text content */
482
483 // - (BOOL) shouldFetchPartOfType: (NSString *) _type
484 //                     subtype: (NSString *) _subtype
485 // {
486 //   /*
487 //     This method decides which parts are 'prefetched' for display. Those are
488 //     usually text parts (the set is currently hardcoded in this method ...).
489 //   */
490 //   _type    = [_type    lowercaseString];
491 //   _subtype = [_subtype lowercaseString];
492   
493 //   return (([_type isEqualToString: @"text"]
494 //            && ([_subtype isEqualToString: @"plain"]
495 //                || [_subtype isEqualToString: @"html"]
496 //                || [_subtype isEqualToString: @"calendar"]))
497 //           || ([_type isEqualToString: @"application"]
498 //               && ([_subtype isEqualToString: @"pgp-signature"]
499 //                   || [_subtype hasPrefix: @"x-vnd.kolab."])));
500 // }
501
502 - (void) addRequiredKeysOfStructure: (NSDictionary *) info
503                                path: (NSString *) p
504                             toArray: (NSMutableArray *) keys
505                       acceptedTypes: (NSArray *) types
506 {
507   /* 
508      This is used to collect the set of IMAP4 fetch-keys required to fetch
509      the basic parts of the body structure. That is, to fetch all parts which
510      are displayed 'inline' in a single IMAP4 fetch.
511      
512      The method calls itself recursively to walk the body structure.
513   */
514   NSArray *parts;
515   unsigned i, count;
516   NSString *k;
517   id body;
518   NSString *sp, *mimeType;
519   id childInfo;
520
521   mimeType = [[NSString stringWithFormat: @"%@/%@",
522                         [info valueForKey: @"type"],
523                         [info valueForKey: @"subtype"]]
524                lowercaseString];
525   if ([types containsObject: mimeType])
526     {
527       if ([p length] > 0)
528         k = [NSString stringWithFormat: @"body[%@]", p];
529       else
530         {
531           /*
532             for some reason we need to add ".TEXT" for plain text stuff on root
533             entities?
534             TODO: check with HTML
535           */
536           k = @"body[text]";
537         }
538       [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
539                                      mimeType, @"mimeType", nil]];
540     }
541
542   parts = [info objectForKey: @"parts"];
543   count = [parts count];
544   for (i = 0; i < count; i++)
545     {
546       sp = (([p length] > 0)
547             ? [p stringByAppendingFormat: @".%d", i + 1]
548             : [NSString stringWithFormat: @"%d", i + 1]);
549       
550       childInfo = [parts objectAtIndex: i];
551       
552       [self addRequiredKeysOfStructure: childInfo
553             path: sp toArray: keys
554             acceptedTypes: types];
555     }
556       
557   /* check body */
558   body = [info objectForKey: @"body"];
559   if (body)
560     {
561       sp = [[body valueForKey: @"type"] lowercaseString];
562       if ([sp isEqualToString: @"multipart"])
563         sp = p;
564       else
565         sp = [p length] > 0 ? [p stringByAppendingString: @".1"] : @"1";
566       [self addRequiredKeysOfStructure: body
567             path: sp toArray: keys
568             acceptedTypes: types];
569     }
570 }
571
572 - (NSArray *) plainTextContentFetchKeys
573 {
574   /*
575     The name is not 100% correct. The method returns all body structure fetch
576     keys which are marked by the -shouldFetchPartOfType:subtype: method.
577   */
578   NSMutableArray *ma;
579   NSArray *types;
580
581   types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
582                    @"text/calendar", @"application/pgp-signature", nil];
583   ma = [NSMutableArray arrayWithCapacity: 4];
584   [self addRequiredKeysOfStructure: [self bodyStructure]
585         path: @"" toArray: ma acceptedTypes: types];
586
587   return ma;
588 }
589
590 - (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
591 {
592   // TODO: is the name correct or does it also fetch other parts?
593   NSMutableDictionary *flatContents;
594   unsigned i, count;
595   id result;
596   
597   [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
598   
599   result = [self fetchParts: [_fetchKeys objectsForKey: @"key"]];
600   result = [result valueForKey: @"RawResponse"]; // hackish
601   
602   // Note: -valueForKey: doesn't work!
603   result = [(NSDictionary *)result objectForKey: @"fetch"]; 
604   
605   count        = [_fetchKeys count];
606   flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
607   for (i = 0; i < count; i++) {
608     NSString *key;
609     NSData   *data;
610     
611     key  = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
612     data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] 
613                             objectForKey: @"data"];
614     
615     if (![data isNotNull]) {
616       [self errorWithFormat: @"got no data for key: %@", key];
617       continue;
618     }
619     
620     if ([key isEqualToString: @"body[text]"])
621       key = @""; // see key collector for explanation (TODO: where?)
622     else if ([key hasPrefix: @"body["]) {
623       NSRange r;
624       
625       key = [key substringFromIndex:5];
626       r   = [key rangeOfString: @"]"];
627       if (r.length > 0)
628         key = [key substringToIndex:r.location];
629     }
630     [flatContents setObject:data forKey:key];
631   }
632   return flatContents;
633 }
634
635 - (NSDictionary *) fetchPlainTextParts
636 {
637   return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
638 }
639
640 /* convert parts to strings */
641 - (NSString *) stringForData: (NSData *) _data
642                     partInfo: (NSDictionary *) _info
643 {
644   NSString *charset, *s;
645   NSData *mailData;
646   
647   if ([_data isNotNull])
648     {
649       mailData
650         = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
651
652       charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
653       if (![charset length])
654         {
655           s = [[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding];
656           [s autorelease];
657         }
658       else
659         s = [NSString stringWithData: mailData
660                       usingEncodingNamed: charset];
661     }
662   else
663     s = nil;
664
665   return s;
666 }
667
668 - (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
669 {
670   NSMutableDictionary *md;
671   NSDictionary *info;
672   NSEnumerator *keys;
673   NSString     *key, *s;
674
675   md = [NSMutableDictionary dictionaryWithCapacity:4];
676   keys = [_datas keyEnumerator];
677   while ((key = [keys nextObject]))
678     {
679       info = [self lookupInfoForBodyPart: key];
680       s = [self stringForData: [_datas objectForKey:key] partInfo: info];
681       if (s)
682         [md setObject: s forKey: key];
683     }
684
685   return md;
686 }
687
688 - (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
689 {
690   /*
691     The fetched parts are NSData objects, this method converts them into
692     NSString objects based on the information inside the bodystructure.
693     
694     The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
695     The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
696   */
697   NSDictionary *datas;
698   
699   if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
700     return nil;
701   if ([datas isKindOfClass:[NSException class]])
702     return datas;
703   
704   return [self stringifyTextParts:datas];
705 }
706
707 /* flags */
708
709 - (NSException *) addFlags: (id) _flags
710 {
711   return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
712 }
713
714 - (NSException *) removeFlags: (id) _flags
715 {
716   return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
717 }
718
719 /* permissions */
720
721 - (BOOL) isDeletionAllowed
722 {
723   NSArray *parentAcl;
724   NSString *login;
725
726   login = [[context activeUser] login];
727   parentAcl = [[self container] aclsForUser: login];
728
729   return [parentAcl containsObject: SOGoRole_ObjectEraser];
730 }
731
732 /* name lookup */
733
734 - (id) lookupImap4BodyPartKey: (NSString *) _key
735                     inContext: (id) _ctx
736 {
737   // TODO: we might want to check for existence prior controller creation
738   Class clazz;
739   NSArray *parts;
740   int part;
741   NSDictionary *partDesc;
742   NSString *mimeType;
743
744   parts = [[self bodyStructure] objectForKey: @"parts"];
745   part = [_key intValue] - 1;
746   if (part > -1 && part < [parts count])
747     {
748       partDesc = [parts objectAtIndex: part];
749       mimeType = [[partDesc keysWithFormat: @"%{type}/%{subtype}"] lowercaseString];
750       clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
751                                 inContext: _ctx];
752     }
753   else
754     clazz = Nil;
755
756   return [clazz objectWithName:_key inContainer: self];
757 }
758
759 - (id) lookupName: (NSString *) _key
760         inContext: (id) _ctx
761           acquire: (BOOL) _flag
762 {
763   id obj;
764   
765   /* first check attributes directly bound to the application */
766   if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
767     return obj;
768   
769   /* lookup body part */
770   
771   if ([self isBodyPartKey:_key inContext:_ctx]) {
772     if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
773       if (debugSoParts)
774         [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
775       return obj;
776     }
777   }
778   
779   /* return 404 to stop acquisition */
780   return [NSException exceptionWithHTTPStatus:404 /* Not Found */
781                       reason: @"Did not find mail method or part-reference!"];
782 }
783
784 /* WebDAV */
785
786 - (BOOL) davIsCollection
787 {
788   /* while a mail has child objects, it should appear as a file in WebDAV */
789   return NO;
790 }
791
792 - (id) davContentLength
793 {
794   return [[self fetchCoreInfos] valueForKey: @"size"];
795 }
796
797 - (NSDate *) davCreationDate
798 {
799   // TODO: use INTERNALDATE once NGImap4 supports that
800   return nil;
801 }
802
803 - (NSDate *) davLastModified
804 {
805   return [self davCreationDate];
806 }
807
808 - (NSException *) davMoveToTargetObject: (id) _target
809                                 newName: (NSString *) _name
810                               inContext: (id)_ctx
811 {
812   [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
813         _name, _target];
814   return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
815                       reason: @"not implemented"];
816 }
817
818 - (NSException *) davCopyToTargetObject: (id) _target
819                                 newName: (NSString *) _name
820                               inContext: (id)_ctx
821 {
822   /* 
823      Note: this is special because we create SOGoMailObject's even if they do
824            not exist (for performance reasons).
825
826      Also: we cannot really take a target resource, the ID will be assigned by
827            the IMAP4 server.
828            We even cannot return a 'location' header instead because IMAP4
829            doesn't tell us the new ID.
830   */
831   NSURL *destImap4URL;
832   
833   destImap4URL = ([_name length] == 0)
834     ? [[_target container] imap4URL]
835     : [_target imap4URL];
836   
837   return [[self mailManager] copyMailURL:[self imap4URL] 
838                              toFolderURL:destImap4URL
839                              password:[self imap4Password]];
840 }
841
842 /* actions */
843
844 - (id) GETAction: (id) _ctx
845 {
846   NSException *error;
847   WOResponse  *r;
848   NSData      *content;
849   
850   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
851     /* check whether the mail still exists */
852     if (![self doesMailExist]) {
853       return [NSException exceptionWithHTTPStatus:404 /* Not Found */
854                           reason: @"mail was deleted"];
855     }
856     return error; /* return 304 or 416 */
857   }
858   
859   content = [self content];
860   if ([content isKindOfClass:[NSException class]])
861     return content;
862   if (content == nil) {
863     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
864                         reason: @"did not find IMAP4 message"];
865   }
866   
867   r = [(WOContext *)_ctx response];
868   [r setHeader: @"message/rfc822" forKey: @"content-type"];
869   [r setContent:content];
870   return r;
871 }
872
873 /* operations */
874
875 - (NSException *) trashInContext: (id) _ctx
876 {
877   /*
878     Trashing is three actions:
879     a) copy to trash folder
880     b) mark mail as deleted
881     c) expunge folder
882     
883     In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
884     the ID used in the trash folder.
885   */
886   SOGoMailFolder *trashFolder;
887   NSException    *error;
888
889   // TODO: check for safe HTTP method
890   
891   trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
892   if ([trashFolder isKindOfClass:[NSException class]])
893     return (NSException *)trashFolder;
894   if (![trashFolder isNotNull]) {
895     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
896                         reason: @"Did not find Trash folder!"];
897   }
898   [trashFolder flushMailCaches];
899
900   /* a) copy */
901   
902   error = [self davCopyToTargetObject:trashFolder
903                 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
904                 inContext:_ctx];
905   if (error != nil) return error;
906   
907   /* b) mark deleted */
908   
909   error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
910   if (error != nil) return error;
911
912   [container markForExpunge];
913
914   [self flushMailCaches];
915   
916   return nil;
917 }
918
919 - (NSException *) copyToFolderNamed: (NSString *) folderName
920                           inContext: (id)_ctx
921 {
922   SOGoMailAccounts *destFolder;
923   NSEnumerator *folders;
924   NSString *currentFolderName, *reason;
925
926   // TODO: check for safe HTTP method
927
928   destFolder = [self mailAccountsFolder];
929   folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
930   currentFolderName = [folders nextObject];
931   currentFolderName = [folders nextObject];
932
933   while (currentFolderName)
934     {
935       destFolder = [destFolder lookupName: currentFolderName
936                                inContext: _ctx
937                                acquire: NO];
938       if ([destFolder isKindOfClass: [NSException class]])
939         return (NSException *) destFolder;
940       currentFolderName = [folders nextObject];
941     }
942
943   if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
944         && [destFolder isNotNull]))
945     {
946       reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
947                          folderName];
948       return [NSException exceptionWithHTTPStatus:500 /* Server Error */
949                           reason: reason];
950     }
951   [destFolder flushMailCaches];
952
953   /* a) copy */
954
955   return [self davCopyToTargetObject: destFolder
956                newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
957                inContext:_ctx];
958 }
959
960 - (NSException *) moveToFolderNamed: (NSString *) folderName
961                           inContext: (id)_ctx
962 {
963   NSException    *error;
964
965   if (![self copyToFolderNamed: folderName
966              inContext: _ctx])
967     {
968       /* b) mark deleted */
969   
970       error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
971       if (error != nil) return error;
972
973       [self flushMailCaches];
974     }
975   
976   return nil;
977 }
978
979 - (NSException *) delete
980 {
981   /* 
982      Note: delete is different to DELETEAction: for mails! The 'delete' runs
983            either flags a message as deleted or moves it to the Trash while
984            the DELETEAction: really deletes a message (by flagging it as
985            deleted _AND_ performing an expunge).
986   */
987   // TODO: copy to Trash folder
988   NSException *error;
989
990   // TODO: check for safe HTTP method
991   
992   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
993   return error;
994 }
995
996 - (id) DELETEAction: (id) _ctx
997 {
998   NSException *error;
999   
1000   // TODO: ensure safe HTTP method
1001   
1002   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
1003   if (error != nil) return error;
1004   
1005   error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
1006   if (error != nil) return error; // TODO: unflag as deleted?
1007   
1008   return [NSNumber numberWithBool:YES]; /* delete was successful */
1009 }
1010
1011 /* some mail classification */
1012
1013 - (BOOL) isKolabObject
1014 {
1015   NSDictionary *h;
1016   
1017   if ((h = [self mailHeaders]) != nil)
1018     return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
1019   
1020   // TODO: we could check the body structure?
1021   
1022   return NO;
1023 }
1024
1025 - (BOOL) isMailingListMail
1026 {
1027   NSDictionary *h;
1028   
1029   if ((h = [self mailHeaders]) == nil)
1030     return NO;
1031   
1032   return [[h objectForKey: @"list-id"] isNotEmpty];
1033 }
1034
1035 - (BOOL) isVirusScanned
1036 {
1037   NSDictionary *h;
1038   
1039   if ((h = [self mailHeaders]) == nil)
1040     return NO;
1041   
1042   if (![[h objectForKey: @"x-virus-status"]  isNotEmpty]) return NO;
1043   if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
1044   return YES;
1045 }
1046
1047 - (NSString *) scanListHeaderValue: (id) _value
1048                 forFieldWithPrefix: (NSString *) _prefix
1049 {
1050   /* Note: not very tolerant on embedded commands and <> */
1051   // TODO: does not really belong here, should be a header-field-parser
1052   NSRange r;
1053   
1054   if (![_value isNotEmpty])
1055     return nil;
1056   
1057   if ([_value isKindOfClass:[NSArray class]]) {
1058     NSEnumerator *e;
1059     id value;
1060
1061     e = [_value objectEnumerator];
1062     while ((value = [e nextObject]) != nil) {
1063       value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1064       if (value != nil) return value;
1065     }
1066     return nil;
1067   }
1068   
1069   if (![_value isKindOfClass:[NSString class]])
1070     return nil;
1071   
1072   /* check for commas in string values */
1073   r = [_value rangeOfString: @","];
1074   if (r.length > 0) {
1075     return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1076                  forFieldWithPrefix:_prefix];
1077   }
1078
1079   /* value qualifies */
1080   if (![(NSString *)_value hasPrefix:_prefix])
1081     return nil;
1082   
1083   /* unquote */
1084   if ([_value characterAtIndex:0] == '<') {
1085     r = [_value rangeOfString: @">"];
1086     _value = (r.length == 0)
1087       ? [_value substringFromIndex:1]
1088       : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1089   }
1090
1091   return _value;
1092 }
1093
1094 - (NSString *) mailingListArchiveURL
1095 {
1096   return [self scanListHeaderValue:
1097                  [[self mailHeaders] objectForKey: @"list-archive"]
1098                forFieldWithPrefix: @"<http://"];
1099 }
1100
1101 - (NSString *) mailingListSubscribeURL
1102 {
1103   return [self scanListHeaderValue:
1104                  [[self mailHeaders] objectForKey: @"list-subscribe"]
1105                forFieldWithPrefix: @"<http://"];
1106 }
1107
1108 - (NSString *) mailingListUnsubscribeURL
1109 {
1110   return [self scanListHeaderValue:
1111                  [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1112                forFieldWithPrefix: @"<http://"];
1113 }
1114
1115 /* etag support */
1116
1117 - (id) davEntityTag
1118 {
1119   /*
1120     Note: There is one thing which *can* change for an existing message,
1121           those are the IMAP4 flags (and annotations, which we do not use).
1122           Since we don't render the flags, it should be OK, if this changes
1123           we must embed the flagging into the etag.
1124   */
1125   return mailETag;
1126 }
1127
1128 - (int) zlGenerationCount
1129 {
1130   return 0; /* mails never change */
1131 }
1132
1133 /* Outlook mail tagging */
1134
1135 - (NSString *) outlookMessageClass
1136 {
1137   NSString *type;
1138   
1139   if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
1140     if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
1141       return @"IPM.Contact";
1142     if ([type isEqualToString: @"application/x-vnd.kolab.task"])
1143       return @"IPM.Task";
1144     if ([type isEqualToString: @"application/x-vnd.kolab.event"])
1145       return @"IPM.Appointment";
1146     if ([type isEqualToString: @"application/x-vnd.kolab.note"])
1147       return @"IPM.Note";
1148     if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
1149       return @"IPM.Journal";
1150   }
1151   
1152   return @"IPM.Message"; /* email, default class */
1153 }
1154
1155 - (NSArray *) aclsForUser: (NSString *) uid
1156 {
1157   return [container aclsForUser: uid];
1158 }
1159
1160 /* debugging */
1161
1162 - (BOOL) isDebuggingEnabled
1163 {
1164   return debugOn;
1165 }
1166
1167 @end /* SOGoMailObject */