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