]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoMailObject.m
69f1457a0853997d9a071e98a8c3ff2d2308f19a
[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       return YES;
423
424     if ([_subtype isEqualToString:@"calendar"]) /* we also fetch calendars */
425       return YES;
426   }
427
428   if ([_type isEqualToString:@"application"]) {
429     if ([_subtype isEqualToString:@"pgp-signature"])
430       return YES;
431     if ([_subtype hasPrefix:@"x-vnd.kolab."])
432       return YES;
433   }
434   
435   return NO;
436 }
437
438 - (void)addRequiredKeysOfStructure:(id)_info path:(NSString *)_p
439   toArray:(NSMutableArray *)_keys
440   recurse:(BOOL)_recurse
441 {
442   /* 
443      This is used to collect the set of IMAP4 fetch-keys required to fetch
444      the basic parts of the body structure. That is, to fetch all parts which
445      are displayed 'inline' in a single IMAP4 fetch.
446      
447      The method calls itself recursively to walk the body structure.
448   */
449   NSArray  *parts;
450   unsigned i, count;
451   BOOL fetchPart;
452   id body;
453   
454   /* Note: if the part itself doesn't qualify, we still check subparts */
455   fetchPart = [self shouldFetchPartOfType:[_info valueForKey:@"type"]
456                     subtype:[_info valueForKey:@"subtype"]];
457   if (fetchPart) {
458     NSString *k;
459     
460     if ([_p length] > 0) {
461       k = [[@"body[" stringByAppendingString:_p] stringByAppendingString:@"]"];
462     }
463     else {
464       /*
465         for some reason we need to add ".TEXT" for plain text stuff on root
466         entities?
467         TODO: check with HTML
468       */
469       k = @"body[text]";
470     }
471     [_keys addObject:k];
472   }
473   
474   if (!_recurse)
475     return;
476   
477   /* recurse */
478   
479   parts = [(NSDictionary *)_info objectForKey:@"parts"];
480   for (i = 0, count = [parts count]; i < count; i++) {
481     NSString *sp;
482     id childInfo;
483     
484     sp = ([_p length] > 0)
485       ? [_p stringByAppendingFormat:@".%d", i + 1]
486       : [NSString stringWithFormat:@"%d", i + 1];
487     
488     childInfo = [parts objectAtIndex:i];
489     
490     [self addRequiredKeysOfStructure:childInfo path:sp toArray:_keys
491           recurse:YES];
492   }
493   
494   /* check body */
495   
496   if ((body = [(NSDictionary *)_info objectForKey:@"body"]) != nil) {
497     NSString *sp;
498
499     sp = [[body valueForKey:@"type"] lowercaseString];
500     if ([sp isEqualToString:@"multipart"])
501       sp = _p;
502     else
503       sp = [_p length] > 0 ? [_p stringByAppendingString:@".1"] : @"1";
504     [self addRequiredKeysOfStructure:body path:sp toArray:_keys
505           recurse:YES];
506   }
507 }
508
509 - (NSArray *)plainTextContentFetchKeys {
510   /*
511     The name is not 100% correct. The method returns all body structure fetch
512     keys which are marked by the -shouldFetchPartOfType:subtype: method.
513   */
514   NSMutableArray *ma;
515   
516   ma = [NSMutableArray arrayWithCapacity:4];
517   [self addRequiredKeysOfStructure:[[self clientObject] bodyStructure]
518         path:@"" toArray:ma recurse:YES];
519   return ma;
520 }
521
522 - (NSDictionary *)fetchPlainTextParts:(NSArray *)_fetchKeys {
523   // TODO: is the name correct or does it also fetch other parts?
524   NSMutableDictionary *flatContents;
525   unsigned i, count;
526   id result;
527   
528   [self debugWithFormat:@"fetch keys: %@", _fetchKeys];
529   
530   result = [self fetchParts:_fetchKeys];
531   result = [result valueForKey:@"RawResponse"]; // hackish
532   
533   // Note: -valueForKey: doesn't work!
534   result = [(NSDictionary *)result objectForKey:@"fetch"]; 
535   
536   count        = [_fetchKeys count];
537   flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
538   for (i = 0; i < count; i++) {
539     NSString *key;
540     NSData   *data;
541     
542     key  = [_fetchKeys objectAtIndex:i];
543     data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] 
544                             objectForKey:@"data"];
545     
546     if (![data isNotNull]) {
547       [self errorWithFormat:@"got no data for key: %@", key];
548       continue;
549     }
550     
551     if ([key isEqualToString:@"body[text]"])
552       key = @""; // see key collector for explanation (TODO: where?)
553     else if ([key hasPrefix:@"body["]) {
554       NSRange r;
555       
556       key = [key substringFromIndex:5];
557       r   = [key rangeOfString:@"]"];
558       if (r.length > 0)
559         key = [key substringToIndex:r.location];
560     }
561     [flatContents setObject:data forKey:key];
562   }
563   return flatContents;
564 }
565
566 - (NSDictionary *)fetchPlainTextParts {
567   return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
568 }
569
570 /* convert parts to strings */
571
572 - (NSString *)stringForData:(NSData *)_data partInfo:(NSDictionary *)_info {
573   NSString *charset;
574   NSString *s;
575   
576   if (![_data isNotNull])
577     return nil;
578
579   s = nil;
580   
581   charset = [[_info valueForKey:@"parameterList"] valueForKey:@"charset"];
582   if ([charset isNotNull] && [charset length] > 0)
583     s = [NSString stringWithData:_data usingEncodingNamed:charset];
584   
585   if (s == nil) { /* no charset provided, fall back to UTF-8 */
586     s = [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding];
587     s = [s autorelease];
588   }
589   
590   return s;
591 }
592
593 - (NSDictionary *)stringifyTextParts:(NSDictionary *)_datas {
594   NSMutableDictionary *md;
595   NSEnumerator *keys;
596   NSString     *key;
597   
598   md   = [NSMutableDictionary dictionaryWithCapacity:4];
599   keys = [_datas keyEnumerator];
600   while ((key = [keys nextObject]) != nil) {
601     NSDictionary *info;
602     NSString *s;
603     
604     info = [self lookupInfoForBodyPart:key];
605     if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
606       [md setObject:s forKey:key];
607   }
608   return md;
609 }
610 - (NSDictionary *)fetchPlainTextStrings:(NSArray *)_fetchKeys {
611   /*
612     The fetched parts are NSData objects, this method converts them into
613     NSString objects based on the information inside the bodystructure.
614     
615     The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
616     The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
617   */
618   NSDictionary *datas;
619   
620   if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
621     return nil;
622   if ([datas isKindOfClass:[NSException class]])
623     return datas;
624   
625   return [self stringifyTextParts:datas];
626 }
627
628 /* flags */
629
630 - (NSException *)addFlags:(id)_flags {
631   return [[self imap4Connection] addFlags:_flags toURL:[self imap4URL]];
632 }
633 - (NSException *)removeFlags:(id)_flags {
634   return [[self imap4Connection] removeFlags:_flags toURL:[self imap4URL]];
635 }
636
637 /* permissions */
638
639 - (BOOL)isDeletionAllowed {
640   return [[self container] isDeleteAndExpungeAllowed];
641 }
642
643 /* name lookup */
644
645 - (id)lookupImap4BodyPartKey:(NSString *)_key inContext:(id)_ctx {
646   // TODO: we might want to check for existence prior controller creation
647   Class clazz;
648   
649   clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
650   return [[[clazz alloc] initWithName:_key inContainer:self] autorelease];
651 }
652
653 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
654   id obj;
655   
656   /* first check attributes directly bound to the application */
657   if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
658     return obj;
659   
660   /* lookup body part */
661   
662   if ([self isBodyPartKey:_key inContext:_ctx]) {
663     if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
664       if (debugSoParts) 
665         [self logWithFormat:@"mail looked up part %@: %@", _key, obj];
666       return obj;
667     }
668   }
669   
670   /* return 404 to stop acquisition */
671   return [NSException exceptionWithHTTPStatus:404 /* Not Found */
672                       reason:@"Did not find mail method or part-reference!"];
673 }
674
675 /* WebDAV */
676
677 - (BOOL)davIsCollection {
678   /* while a mail has child objects, it should appear as a file in WebDAV */
679   return NO;
680 }
681
682 - (id)davContentLength {
683   return [[self fetchCoreInfos] valueForKey:@"size"];
684 }
685
686 - (NSDate *)davCreationDate {
687   // TODO: use INTERNALDATE once NGImap4 supports that
688   return nil;
689 }
690 - (NSDate *)davLastModified {
691   return [self davCreationDate];
692 }
693
694 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
695   inContext:(id)_ctx
696 {
697   [self logWithFormat:@"TODO: should move mail as '%@' to: %@",
698         _name, _target];
699   return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
700                       reason:@"not implemented"];
701 }
702
703 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
704   inContext:(id)_ctx
705 {
706   /* 
707      Note: this is special because we create SOGoMailObject's even if they do
708            not exist (for performance reasons).
709
710      Also: we cannot really take a target resource, the ID will be assigned by
711            the IMAP4 server.
712            We even cannot return a 'location' header instead because IMAP4
713            doesn't tell us the new ID.
714   */
715   NSURL *destImap4URL;
716   
717   destImap4URL = ([_name length] == 0)
718     ? [[_target container] imap4URL]
719     : [_target imap4URL];
720   
721   return [[self mailManager] copyMailURL:[self imap4URL] 
722                              toFolderURL:destImap4URL
723                              password:[self imap4Password]];
724 }
725
726 /* actions */
727
728 - (id)GETAction:(id)_ctx {
729   NSException *error;
730   WOResponse  *r;
731   NSData      *content;
732   
733   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
734     /* check whether the mail still exists */
735     if (![self doesMailExist]) {
736       return [NSException exceptionWithHTTPStatus:404 /* Not Found */
737                           reason:@"mail was deleted"];
738     }
739     return error; /* return 304 or 416 */
740   }
741   
742   content = [self content];
743   if ([content isKindOfClass:[NSException class]])
744     return content;
745   if (content == nil) {
746     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
747                         reason:@"did not find IMAP4 message"];
748   }
749   
750   r = [(WOContext *)_ctx response];
751   [r setHeader:@"message/rfc822" forKey:@"content-type"];
752   [r setContent:content];
753   return r;
754 }
755
756 /* operations */
757
758 - (NSException *)trashInContext:(id)_ctx {
759   /*
760     Trashing is three actions:
761     a) copy to trash folder
762     b) mark mail as deleted
763     c) expunge folder
764     
765     In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
766     the ID used in the trash folder.
767   */
768   SOGoMailFolder *trashFolder;
769   NSException    *error;
770
771   // TODO: check for safe HTTP method
772   
773   trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
774   if ([trashFolder isKindOfClass:[NSException class]])
775     return (NSException *)trashFolder;
776   if (![trashFolder isNotNull]) {
777     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
778                         reason:@"Did not find Trash folder!"];
779   }
780   [trashFolder flushMailCaches];
781
782   /* a) copy */
783   
784   error = [self davCopyToTargetObject:trashFolder
785                 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
786                 inContext:_ctx];
787   if (error != nil) return error;
788   
789   /* b) mark deleted */
790   
791   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
792   if (error != nil) return error;
793   
794   /* c) expunge */
795
796   error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
797   if (error != nil) return error; // TODO: unflag as deleted?
798   [self flushMailCaches];
799   
800   return nil;
801 }
802
803 - (NSException *)delete {
804   /* 
805      Note: delete is different to DELETEAction: for mails! The 'delete' runs
806            either flags a message as deleted or moves it to the Trash while
807            the DELETEAction: really deletes a message (by flagging it as
808            deleted _AND_ performing an expunge).
809   */
810   // TODO: copy to Trash folder
811   NSException *error;
812
813   // TODO: check for safe HTTP method
814   
815   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
816   return error;
817 }
818 - (id)DELETEAction:(id)_ctx {
819   NSException *error;
820   
821   // TODO: ensure safe HTTP method
822   
823   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
824   if (error != nil) return error;
825   
826   error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
827   if (error != nil) return error; // TODO: unflag as deleted?
828   
829   return [NSNumber numberWithBool:YES]; /* delete was successful */
830 }
831
832 /* some mail classification */
833
834 - (BOOL)isKolabObject {
835   NSDictionary *h;
836   
837   if ((h = [self mailHeaders]) != nil)
838     return [[h objectForKey:@"x-kolab-type"] isNotEmpty];
839   
840   // TODO: we could check the body structure?
841   
842   return NO;
843 }
844
845 - (BOOL)isMailingListMail {
846   NSDictionary *h;
847   
848   if ((h = [self mailHeaders]) == nil)
849     return NO;
850   
851   return [[h objectForKey:@"list-id"] isNotEmpty];
852 }
853
854 - (BOOL)isVirusScanned {
855   NSDictionary *h;
856   
857   if ((h = [self mailHeaders]) == nil)
858     return NO;
859   
860   if (![[h objectForKey:@"x-virus-status"]  isNotEmpty]) return NO;
861   if (![[h objectForKey:@"x-virus-scanned"] isNotEmpty]) return NO;
862   return YES;
863 }
864
865 - (NSString *)scanListHeaderValue:(id)_value
866   forFieldWithPrefix:(NSString *)_prefix
867 {
868   /* Note: not very tolerant on embedded commands and <> */
869   // TODO: does not really belong here, should be a header-field-parser
870   NSRange r;
871   
872   if (![_value isNotEmpty])
873     return nil;
874   
875   if ([_value isKindOfClass:[NSArray class]]) {
876     NSEnumerator *e;
877     id value;
878
879     e = [_value objectEnumerator];
880     while ((value = [e nextObject]) != nil) {
881       value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
882       if (value != nil) return value;
883     }
884     return nil;
885   }
886   
887   if (![_value isKindOfClass:[NSString class]])
888     return nil;
889   
890   /* check for commas in string values */
891   r = [_value rangeOfString:@","];
892   if (r.length > 0) {
893     return [self scanListHeaderValue:[_value componentsSeparatedByString:@","]
894                  forFieldWithPrefix:_prefix];
895   }
896
897   /* value qualifies */
898   if (![(NSString *)_value hasPrefix:_prefix])
899     return nil;
900   
901   /* unquote */
902   if ([_value characterAtIndex:0] == '<') {
903     r = [_value rangeOfString:@">"];
904     _value = (r.length == 0)
905       ? [_value substringFromIndex:1]
906       : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
907   }
908
909   return _value;
910 }
911
912 - (NSString *)mailingListArchiveURL {
913   return [self scanListHeaderValue:
914                  [[self mailHeaders] objectForKey:@"list-archive"]
915                forFieldWithPrefix:@"<http://"];
916 }
917 - (NSString *)mailingListSubscribeURL {
918   return [self scanListHeaderValue:
919                  [[self mailHeaders] objectForKey:@"list-subscribe"]
920                forFieldWithPrefix:@"<http://"];
921 }
922 - (NSString *)mailingListUnsubscribeURL {
923   return [self scanListHeaderValue:
924                  [[self mailHeaders] objectForKey:@"list-unsubscribe"]
925                forFieldWithPrefix:@"<http://"];
926 }
927
928 /* etag support */
929
930 - (id)davEntityTag {
931   /*
932     Note: There is one thing which *can* change for an existing message,
933           those are the IMAP4 flags (and annotations, which we do not use).
934           Since we don't render the flags, it should be OK, if this changes
935           we must embed the flagging into the etag.
936   */
937   return mailETag;
938 }
939 - (int)zlGenerationCount {
940   return 0; /* mails never change */
941 }
942
943 /* Outlook mail tagging */
944
945 - (NSString *)outlookMessageClass {
946   NSString *type;
947   
948   if ((type = [[self mailHeaders] objectForKey:@"x-kolab-type"]) != nil) {
949     if ([type isEqualToString:@"application/x-vnd.kolab.contact"])
950       return @"IPM.Contact";
951     if ([type isEqualToString:@"application/x-vnd.kolab.task"])
952       return @"IPM.Task";
953     if ([type isEqualToString:@"application/x-vnd.kolab.event"])
954       return @"IPM.Appointment";
955     if ([type isEqualToString:@"application/x-vnd.kolab.note"])
956       return @"IPM.Note";
957     if ([type isEqualToString:@"application/x-vnd.kolab.journal"])
958       return @"IPM.Journal";
959   }
960   
961   return @"IPM.Message"; /* email, default class */
962 }
963
964 /* debugging */
965
966 - (BOOL)isDebuggingEnabled {
967   return debugOn;
968 }
969
970 @end /* SOGoMailObject */