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