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