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