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