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