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