]> err.no Git - scalable-opengroupware.org/blobdiff - SoObjects/Mailer/SOGoMailObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1206 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SoObjects / Mailer / SOGoMailObject.m
index 68ec6326ad6925dc6f9e9cddffbb6bf55424ff7f..15d531375cae2514eab9b34b07fb3cd52bdd8bf5 100644 (file)
@@ -20,6 +20,7 @@
 */
 
 #import <Foundation/NSArray.h>
+#import <Foundation/NSCalendarDate.h>
 #import <Foundation/NSDictionary.h>
 #import <Foundation/NSEnumerator.h>
 #import <Foundation/NSString.h>
 #import <NGObjWeb/WOContext+SoObjects.h>
 #import <NGObjWeb/WOResponse.h>
 #import <NGObjWeb/NSException+HTTP.h>
-#import <NGExtensions/NGBase64Coding.h>
 #import <NGExtensions/NSNull+misc.h>
 #import <NGExtensions/NSObject+Logs.h>
-#import <NGExtensions/NGQuotedPrintableCoding.h>
 #import <NGExtensions/NSString+Encoding.h>
+#import <NGExtensions/NSString+misc.h>
 #import <NGImap4/NGImap4Connection.h>
 #import <NGImap4/NGImap4Envelope.h>
 #import <NGImap4/NGImap4EnvelopeAddress.h>
 #import <NGMail/NGMimeMessageParser.h>
 
+#import <SoObjects/SOGo/NSArray+Utilities.h>
 #import <SoObjects/SOGo/SOGoPermissions.h>
 #import <SoObjects/SOGo/SOGoUser.h>
+
+#import "NSData+Mail.h"
 #import "SOGoMailFolder.h"
 #import "SOGoMailAccount.h"
 #import "SOGoMailManager.h"
@@ -59,18 +62,11 @@ static BOOL debugOn            = NO;
 static BOOL debugBodyStructure = NO;
 static BOOL debugSoParts       = NO;
 
-+ (int)version {
-  return [super version] + 0 /* v1 */;
-}
-
-+ (void)initialize {
++ (void) initialize
+{
   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
   
-  NSAssert2([super version] == 1,
-            @"invalid superclass (%@) version %i !",
-            NSStringFromClass([self superclass]), [super version]);
-  
-  if ((fetchHeader = ([ud boolForKey:@"SOGoDoNotFetchMailHeader"] ? NO : YES)))
+  if ((fetchHeader = ([ud boolForKey: @"SOGoDoNotFetchMailHeader"] ? NO : YES)))
     NSLog(@"Note: fetching full mail header.");
   else
     NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
@@ -79,7 +75,7 @@ static BOOL debugSoParts       = NO;
   /* Note: "BODY" actually returns the structure! */
   if (fetchHeader) {
     coreInfoKeys = [[NSArray alloc] initWithObjects:
-                                     @"FLAGS", @"ENVELOPE", @"BODY",
+                                     @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
                                      @"RFC822.SIZE",
                                      @"RFC822.HEADER",
                                      // not yet supported: @"INTERNALDATE",
@@ -87,14 +83,14 @@ static BOOL debugSoParts       = NO;
   }
   else {
     coreInfoKeys = [[NSArray alloc] initWithObjects:
-                                     @"FLAGS", @"ENVELOPE", @"BODY",
+                                     @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
                                      @"RFC822.SIZE",
                                      // not yet supported: @"INTERNALDATE",
                                    nil];
   }
 
-  if (![[ud objectForKey:@"SOGoMailDisableETag"] boolValue]) {
-    mailETag = [[NSString alloc] initWithFormat:@"\"imap4url_%d_%d_%03d\"",
+  if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) {
+    mailETag = [[NSString alloc] initWithFormat: @"\"imap4url_%d_%d_%03d\"",
                                 UIX_MAILER_MAJOR_VERSION,
                                 UIX_MAILER_MINOR_VERSION,
                                 UIX_MAILER_SUBMINOR_VERSION];
@@ -105,17 +101,19 @@ static BOOL debugSoParts       = NO;
     NSLog(@"Note(SOGoMailObject): etag caching disabled!");
 }
 
-- (void)dealloc {
-  [self->headers    release];
-  [self->headerPart release];
-  [self->coreInfos  release];
+- (void) dealloc
+{
+  [headers    release];
+  [headerPart release];
+  [coreInfos  release];
   [super dealloc];
 }
 
 /* IMAP4 */
 
-- (NSString *)relativeImap4Name {
-  return [[self nameInContainer] stringByDeletingPathExtension];
+- (NSString *) relativeImap4Name
+{
+  return [nameInContainer stringByDeletingPathExtension];
 }
 
 /* hierarchy */
@@ -126,24 +124,25 @@ static BOOL debugSoParts       = NO;
 
 /* part hierarchy */
 
-- (NSString *)keyExtensionForPart:(id)_partInfo {
+- (NSString *) keyExtensionForPart: (id) _partInfo
+{
   NSString *mt, *st;
   
   if (_partInfo == nil)
     return nil;
   
-  mt = [_partInfo valueForKey:@"type"];
-  st = [[_partInfo valueForKey:@"subtype"] lowercaseString];
-  if ([mt isEqualToString:@"text"]) {
-    if ([st isEqualToString:@"plain"])    return @".txt";
-    if ([st isEqualToString:@"html"])     return @".html";
-    if ([st isEqualToString:@"calendar"]) return @".ics";
-    if ([st isEqualToString:@"x-vcard"])  return @".vcf";
+  mt = [_partInfo valueForKey: @"type"];
+  st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
+  if ([mt isEqualToString: @"text"]) {
+    if ([st isEqualToString: @"plain"])    return @".txt";
+    if ([st isEqualToString: @"html"])     return @".html";
+    if ([st isEqualToString: @"calendar"]) return @".ics";
+    if ([st isEqualToString: @"x-vcard"])  return @".vcf";
   }
-  else if ([mt isEqualToString:@"image"])
+  else if ([mt isEqualToString: @"image"])
     return [@"." stringByAppendingString:st];
-  else if ([mt isEqualToString:@"application"]) {
-    if ([st isEqualToString:@"pgp-signature"])
+  else if ([mt isEqualToString: @"application"]) {
+    if ([st isEqualToString: @"pgp-signature"])
       return @".asc";
   }
   
@@ -156,7 +155,7 @@ static BOOL debugSoParts       = NO;
   NSArray *parts;
   unsigned i, count;
   
-  parts = [[self bodyStructure] valueForKey:@"parts"];
+  parts = [[self bodyStructure] valueForKey: @"parts"];
   if (![parts isNotNull]) 
     return nil;
   if ((count = [parts count]) == 0)
@@ -168,7 +167,7 @@ static BOOL debugSoParts       = NO;
     BOOL hasParts;
     
     part     = [parts objectAtIndex:i];
-    hasParts = [part valueForKey:@"parts"] != nil ? YES:NO;
+    hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
     if ((hasParts && !_withParts) || (_withParts && !hasParts))
       continue;
 
@@ -176,28 +175,32 @@ static BOOL debugSoParts       = NO;
       ma = [NSMutableArray arrayWithCapacity:count - i];
     
     ext = [self keyExtensionForPart:part];
-    key = [[NSString alloc] initWithFormat:@"%d%@", i + 1, ext?ext:@""];
+    key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ext?ext: @""];
     [ma addObject:key];
     [key release];
   }
   return ma;
 }
 
-- (NSArray *)toOneRelationshipKeys {
+- (NSArray *) toOneRelationshipKeys
+{
   return [self relationshipKeysWithParts:NO];
 }
-- (NSArray *)toManyRelationshipKeys {
+
+- (NSArray *) toManyRelationshipKeys
+{
   return [self relationshipKeysWithParts:YES];
 }
 
 /* message */
 
-- (id)fetchParts:(NSArray *)_parts {
+- (id) fetchParts: (NSArray *) _parts
+{
   // TODO: explain what it does
   /*
     Called by -fetchPlainTextParts:
   */
-  return [[self imap4Connection] fetchURL:[self imap4URL] parts:_parts];
+  return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts];
 }
 
 /* core infos */
@@ -206,102 +209,128 @@ static BOOL debugSoParts       = NO;
   static NSArray *existsKey = nil;
   id msgs;
   
-  if (self->coreInfos != nil) /* if we have coreinfos, we can use them */
-    return [self->coreInfos isNotNull];
+  if (coreInfos != nil) /* if we have coreinfos, we can use them */
+    return [coreInfos isNotNull];
   
   /* otherwise fetch something really simple */
   
   if (existsKey == nil) /* we use size, other suggestions? */
-    existsKey = [[NSArray alloc] initWithObjects:@"RFC822.SIZE", nil];
+    existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
   
   msgs = [self fetchParts:existsKey]; // returns dict
-  msgs = [msgs valueForKey:@"fetch"];
+  msgs = [msgs valueForKey: @"fetch"];
   return [msgs count] > 0 ? YES : NO;
 }
 
-- (id)fetchCoreInfos {
+- (id) fetchCoreInfos
+{
   id msgs;
-  
-  if (self->coreInfos != nil)
-    return [self->coreInfos isNotNull] ? self->coreInfos : nil;
-  
-#if 0 // TODO: old code, why was it using clientObject??
-  msgs = [[self clientObject] fetchParts:coreInfoKeys]; // returns dict
-#else
-  msgs = [self fetchParts:coreInfoKeys]; // returns dict
-#endif
-  if (heavyDebug) [self logWithFormat:@"M: %@", msgs];
-  msgs = [msgs valueForKey:@"fetch"];
-  if ([msgs count] == 0)
-    return nil;
-  
-  self->coreInfos = [[msgs objectAtIndex:0] retain];
-  return self->coreInfos;
+
+  if (!coreInfos)
+    {  
+      msgs = [self fetchParts: coreInfoKeys]; // returns dict
+      if (heavyDebug)
+       [self logWithFormat: @"M: %@", msgs];
+      msgs = [msgs valueForKey: @"fetch"];
+      if ([msgs count] > 0)
+       coreInfos = [msgs objectAtIndex: 0];
+      [coreInfos retain];
+    }
+
+  return coreInfos;
 }
 
-- (id)bodyStructure {
+- (id) bodyStructure
+{
   id body;
 
-  body = [[self fetchCoreInfos] valueForKey:@"body"];
+  body = [[self fetchCoreInfos] valueForKey: @"body"];
   if (debugBodyStructure)
-    [self logWithFormat:@"BODY: %@", body];
+    [self logWithFormat: @"BODY: %@", body];
+
   return body;
 }
 
-- (NGImap4Envelope *)envelope {
-  return [[self fetchCoreInfos] valueForKey:@"envelope"];
+- (NGImap4Envelope *) envelope
+{
+  return [[self fetchCoreInfos] valueForKey: @"envelope"];
 }
-- (NSString *)subject {
+
+- (NSString *) subject
+{
   return [[self envelope] subject];
 }
-- (NSCalendarDate *)date {
-  return [[self envelope] date];
+
+- (NSCalendarDate *) date
+{
+  NSTimeZone *userTZ;
+  NSCalendarDate *date;
+
+  userTZ = [[context activeUser] timeZone];
+  date = [[self envelope] date];
+  [date setTimeZone: userTZ];
+
+  return date;
 }
-- (NSArray *)fromEnvelopeAddresses {
+
+- (NSArray *) fromEnvelopeAddresses
+{
   return [[self envelope] from];
 }
-- (NSArray *)toEnvelopeAddresses {
+
+- (NSArray *) toEnvelopeAddresses
+{
   return [[self envelope] to];
 }
-- (NSArray *)ccEnvelopeAddresses {
+
+- (NSArray *) ccEnvelopeAddresses
+{
   return [[self envelope] cc];
 }
 
-- (NSData *)mailHeaderData {
-  return [[self fetchCoreInfos] valueForKey:@"header"];
+- (NSData *) mailHeaderData
+{
+  return [[self fetchCoreInfos] valueForKey: @"header"];
 }
-- (BOOL)hasMailHeaderInCoreInfos {
+
+- (BOOL) hasMailHeaderInCoreInfos
+{
   return [[self mailHeaderData] length] > 0 ? YES : NO;
 }
 
-- (id)mailHeaderPart {
+- (id) mailHeaderPart
+{
   NGMimeMessageParser *parser;
   NSData *data;
   
-  if (self->headerPart != nil)
-    return [self->headerPart isNotNull] ? self->headerPart : nil;
+  if (headerPart != nil)
+    return [headerPart isNotNull] ? headerPart : nil;
   
   if ([(data = [self mailHeaderData]) length] == 0)
     return nil;
   
   // TODO: do we need to set some delegate method which stops parsing the body?
   parser = [[NGMimeMessageParser alloc] init];
-  self->headerPart = [[parser parsePartFromData:data] retain];
+  headerPart = [[parser parsePartFromData:data] retain];
   [parser release]; parser = nil;
 
-  if (self->headerPart == nil) {
-    self->headerPart = [[NSNull null] retain];
+  if (headerPart == nil) {
+    headerPart = [[NSNull null] retain];
     return nil;
   }
-  return self->headerPart;
+  return headerPart;
 }
-- (NSDictionary *)mailHeaders {
-  if (self->headers == nil)
-    self->headers = [[[self mailHeaderPart] headers] copy];
-  return self->headers;
+
+- (NSDictionary *) mailHeaders
+{
+  if (!headers)
+    headers = [[[self mailHeaderPart] headers] copy];
+
+  return headers;
 }
 
-- (id)lookupInfoForBodyPart:(id)_path {
+- (id) lookupInfoForBodyPart: (id) _path
+{
   NSEnumerator *pe;
   NSString *p;
   id info;
@@ -310,7 +339,7 @@ static BOOL debugSoParts       = NO;
     return nil;
   
   if ((info = [self bodyStructure]) == nil) {
-    [self errorWithFormat:@"got no body part structure!"];
+    [self errorWithFormat: @"got no body part structure!"];
     return nil;
   }
 
@@ -320,7 +349,7 @@ static BOOL debugSoParts       = NO;
     if ([_path length] == 0)
       return info;
     
-    _path = [_path componentsSeparatedByString:@"."];
+    _path = [_path componentsSeparatedByString: @"."];
   }
   
   /* 
@@ -337,19 +366,19 @@ static BOOL debugSoParts       = NO;
     NSArray  *parts;
     NSString *mt;
     
-    [self debugWithFormat:@"check PATH: %@", p];
+    [self debugWithFormat: @"check PATH: %@", p];
     idx = [p intValue] - 1;
 
-    parts = [info valueForKey:@"parts"];
-    mt = [[info valueForKey:@"type"] lowercaseString];
-    if ([mt isEqualToString:@"message"]) {
+    parts = [info valueForKey: @"parts"];
+    mt = [[info valueForKey: @"type"] lowercaseString];
+    if ([mt isEqualToString: @"message"]) {
       /* we have special behaviour for message types */
       id body;
       
-      if ((body = [info valueForKey:@"body"]) != nil) {
-       mt = [body valueForKey:@"type"];
-       if ([mt isEqualToString:@"multipart"])
-         parts = [body valueForKey:@"parts"];
+      if ((body = [info valueForKey: @"body"]) != nil) {
+       mt = [body valueForKey: @"type"];
+       if ([mt isEqualToString: @"multipart"])
+         parts = [body valueForKey: @"parts"];
        else
          parts = [NSArray arrayWithObject:body];
       }
@@ -368,26 +397,27 @@ static BOOL debugSoParts       = NO;
 
 /* content */
 
-- (NSData *)content {
+- (NSData *) content
+{
   NSData *content;
   id     result, fullResult;
   
-  fullResult = [self fetchParts:[NSArray arrayWithObject:@"RFC822"]];
+  fullResult = [self fetchParts: [NSArray arrayWithObject: @"RFC822"]];
   if (fullResult == nil)
     return nil;
   
-  if ([fullResult isKindOfClass:[NSException class]])
+  if ([fullResult isKindOfClass: [NSException class]])
     return fullResult;
   
   /* extract fetch result */
   
-  result = [fullResult valueForKey:@"fetch"];
+  result = [fullResult valueForKey: @"fetch"];
   if (![result isKindOfClass:[NSArray class]]) {
     [self logWithFormat:
            @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", 
            fullResult];
     return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"unexpected IMAP4 result"];
+                       reason: @"unexpected IMAP4 result"];
   }
   if ([result count] == 0)
     return nil;
@@ -396,59 +426,75 @@ static BOOL debugSoParts       = NO;
   
   /* extract message */
   
-  if ((content = [result valueForKey:@"message"]) == nil) {
+  if ((content = [result valueForKey: @"message"]) == nil) {
     [self logWithFormat:
            @"ERROR: unexpected IMAP4 result (missing 'message'): %@", 
            result];
     return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"unexpected IMAP4 result"];
+                       reason: @"unexpected IMAP4 result"];
   }
   
   return [[content copy] autorelease];
 }
 
-- (NSString *)contentAsString {
-  NSString *s;
-  NSData *content;
-  
-  if ((content = [self content]) == nil)
-    return nil;
-  if ([content isKindOfClass:[NSException class]])
-    return (id)content;
-  
-  s = [[NSString alloc] initWithData:content 
-                       encoding:NSISOLatin1StringEncoding];
-  if (s == nil) {
-    [self logWithFormat:
-           @"ERROR: could not convert data of length %d to string", 
-           [content length]];
-    return nil;
-  }
-  return [s autorelease];
+- (NSString *) davContentType
+{
+  return @"message/rfc822";
 }
 
-/* bulk fetching of plain/text content */
+- (NSString *) contentAsString
+{
+  id s;
+  NSData *content;
 
-- (BOOL)shouldFetchPartOfType:(NSString *)_type subtype:(NSString *)_subtype {
-  /*
-    This method decides which parts are 'prefetched' for display. Those are
-    usually text parts (the set is currently hardcoded in this method ...).
-  */
-  _type    = [_type    lowercaseString];
-  _subtype = [_subtype lowercaseString];
-  
-  return (([_type isEqualToString:@"text"]
-           && ([_subtype isEqualToString:@"plain"]
-               || [_subtype isEqualToString:@"html"]
-               || [_subtype isEqualToString:@"calendar"]))
-          || ([_type isEqualToString:@"application"]
-              && ([_subtype isEqualToString:@"pgp-signature"]
-                  || [_subtype hasPrefix:@"x-vnd.kolab."])));
+  content = [self content];
+  if (content)
+    {
+      if ([content isKindOfClass: [NSData class]])
+       {
+         s = [[NSString alloc] initWithData: content
+                               encoding: NSISOLatin1StringEncoding];
+         if (s)
+           [s autorelease];
+         else
+           [self logWithFormat:
+                   @"ERROR: could not convert data of length %d to string", 
+                 [content length]];
+       }
+      else
+       s = content;
+    }
+  else
+    s = nil;
+
+  return s;
 }
 
-- (void)addRequiredKeysOfStructure:(id)_info path:(NSString *)_p
-  toArray:(NSMutableArray *)_keys
-  recurse:(BOOL)_recurse
+/* bulk fetching of plain/text content */
+
+// - (BOOL) shouldFetchPartOfType: (NSString *) _type
+//                    subtype: (NSString *) _subtype
+// {
+//   /*
+//     This method decides which parts are 'prefetched' for display. Those are
+//     usually text parts (the set is currently hardcoded in this method ...).
+//   */
+//   _type    = [_type    lowercaseString];
+//   _subtype = [_subtype lowercaseString];
+  
+//   return (([_type isEqualToString: @"text"]
+//            && ([_subtype isEqualToString: @"plain"]
+//                || [_subtype isEqualToString: @"html"]
+//                || [_subtype isEqualToString: @"calendar"]))
+//           || ([_type isEqualToString: @"application"]
+//               && ([_subtype isEqualToString: @"pgp-signature"]
+//                   || [_subtype hasPrefix: @"x-vnd.kolab."])));
+// }
+
+- (void) addRequiredKeysOfStructure: (NSDictionary *) info
+                              path: (NSString *) p
+                           toArray: (NSMutableArray *) keys
+                     acceptedTypes: (NSArray *) types
 {
   /* 
      This is used to collect the set of IMAP4 fetch-keys required to fetch
@@ -457,92 +503,96 @@ static BOOL debugSoParts       = NO;
      
      The method calls itself recursively to walk the body structure.
   */
-  NSArray  *parts;
+  NSArray *parts;
   unsigned i, count;
-  BOOL fetchPart;
+  NSString *k;
   id body;
-  
-  /* Note: if the part itself doesn't qualify, we still check subparts */
-  fetchPart = [self shouldFetchPartOfType:[_info valueForKey:@"type"]
-                   subtype:[_info valueForKey:@"subtype"]];
-  if (fetchPart) {
-    NSString *k;
-    
-    if ([_p length] > 0) {
-      k = [[@"body[" stringByAppendingString:_p] stringByAppendingString:@"]"];
+  NSString *sp, *mimeType;
+  id childInfo;
+
+  mimeType = [[NSString stringWithFormat: @"%@/%@",
+                       [info valueForKey: @"type"],
+                       [info valueForKey: @"subtype"]]
+              lowercaseString];
+  if ([types containsObject: mimeType])
+    {
+      if ([p length] > 0)
+       k = [NSString stringWithFormat: @"body[%@]", p];
+      else
+       {
+         /*
+           for some reason we need to add ".TEXT" for plain text stuff on root
+           entities?
+           TODO: check with HTML
+         */
+         k = @"body[text]";
+       }
+      [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
+                                    mimeType, @"mimeType", nil]];
     }
-    else {
-      /*
-       for some reason we need to add ".TEXT" for plain text stuff on root
-       entities?
-       TODO: check with HTML
-      */
-      k = @"body[text]";
+
+  parts = [info objectForKey: @"parts"];
+  count = [parts count];
+  for (i = 0; i < count; i++)
+    {
+      sp = (([p length] > 0)
+           ? [p stringByAppendingFormat: @".%d", i + 1]
+           : [NSString stringWithFormat: @"%d", i + 1]);
+      
+      childInfo = [parts objectAtIndex: i];
+      
+      [self addRequiredKeysOfStructure: childInfo
+           path: sp toArray: keys
+           acceptedTypes: types];
     }
-    [_keys addObject:k];
-  }
-  
-  if (!_recurse)
-    return;
-  
-  /* recurse */
-  
-  parts = [(NSDictionary *)_info objectForKey:@"parts"];
-  for (i = 0, count = [parts count]; i < count; i++) {
-    NSString *sp;
-    id childInfo;
-    
-    sp = ([_p length] > 0)
-      ? [_p stringByAppendingFormat:@".%d", i + 1]
-      : [NSString stringWithFormat:@"%d", i + 1];
-    
-    childInfo = [parts objectAtIndex:i];
-    
-    [self addRequiredKeysOfStructure:childInfo path:sp toArray:_keys
-         recurse:YES];
-  }
-  
+      
   /* check body */
-  
-  if ((body = [(NSDictionary *)_info objectForKey:@"body"]) != nil) {
-    NSString *sp;
-
-    sp = [[body valueForKey:@"type"] lowercaseString];
-    if ([sp isEqualToString:@"multipart"])
-      sp = _p;
-    else
-      sp = [_p length] > 0 ? [_p stringByAppendingString:@".1"] : @"1";
-    [self addRequiredKeysOfStructure:body path:sp toArray:_keys
-         recurse:YES];
-  }
+  body = [info objectForKey: @"body"];
+  if (body)
+    {
+      sp = [[body valueForKey: @"type"] lowercaseString];
+      if ([sp isEqualToString: @"multipart"])
+       sp = p;
+      else
+       sp = [p length] > 0 ? [p stringByAppendingString: @".1"] : @"1";
+      [self addRequiredKeysOfStructure: body
+           path: sp toArray: keys
+           acceptedTypes: types];
+    }
 }
 
-- (NSArray *)plainTextContentFetchKeys {
+- (NSArray *) plainTextContentFetchKeys
+{
   /*
     The name is not 100% correct. The method returns all body structure fetch
     keys which are marked by the -shouldFetchPartOfType:subtype: method.
   */
   NSMutableArray *ma;
-  
-  ma = [NSMutableArray arrayWithCapacity:4];
-  [self addRequiredKeysOfStructure:[[self clientObject] bodyStructure]
-       path:@"" toArray:ma recurse:YES];
+  NSArray *types;
+
+  types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
+                  @"text/calendar", @"application/pgp-signature", nil];
+  ma = [NSMutableArray arrayWithCapacity: 4];
+  [self addRequiredKeysOfStructure: [self bodyStructure]
+       path: @"" toArray: ma acceptedTypes: types];
+
   return ma;
 }
 
-- (NSDictionary *)fetchPlainTextParts:(NSArray *)_fetchKeys {
+- (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
+{
   // TODO: is the name correct or does it also fetch other parts?
   NSMutableDictionary *flatContents;
   unsigned i, count;
   id result;
   
-  [self debugWithFormat:@"fetch keys: %@", _fetchKeys];
+  [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
   
-  result = [self fetchParts:_fetchKeys];
-  result = [result valueForKey:@"RawResponse"]; // hackish
+  result = [self fetchParts: [_fetchKeys objectsForKey: @"key"]];
+  result = [result valueForKey: @"RawResponse"]; // hackish
   
   // Note: -valueForKey: doesn't work!
-  result = [(NSDictionary *)result objectForKey:@"fetch"]; 
+  result = [(NSDictionary *)result objectForKey: @"fetch"]; 
   
   count        = [_fetchKeys count];
   flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
@@ -550,22 +600,22 @@ static BOOL debugSoParts       = NO;
     NSString *key;
     NSData   *data;
     
-    key  = [_fetchKeys objectAtIndex:i];
+    key  = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
     data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] 
-                           objectForKey:@"data"];
+                           objectForKey: @"data"];
     
     if (![data isNotNull]) {
-      [self errorWithFormat:@"got no data for key: %@", key];
+      [self errorWithFormat: @"got no data for key: %@", key];
       continue;
     }
     
-    if ([key isEqualToString:@"body[text]"])
+    if ([key isEqualToString: @"body[text]"])
       key = @""; // see key collector for explanation (TODO: where?)
-    else if ([key hasPrefix:@"body["]) {
+    else if ([key hasPrefix: @"body["]) {
       NSRange r;
       
       key = [key substringFromIndex:5];
-      r   = [key rangeOfString:@"]"];
+      r   = [key rangeOfString: @"]"];
       if (r.length > 0)
        key = [key substringToIndex:r.location];
     }
@@ -574,63 +624,61 @@ static BOOL debugSoParts       = NO;
   return flatContents;
 }
 
-- (NSDictionary *)fetchPlainTextParts {
-  return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
+- (NSDictionary *) fetchPlainTextParts
+{
+  return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
 }
 
 /* convert parts to strings */
-
-- (NSString *)stringForData:(NSData *)_data partInfo:(NSDictionary *)_info
+- (NSString *) stringForData: (NSData *) _data
+                   partInfo: (NSDictionary *) _info
 {
-  NSString *charset, *encoding, *s;
+  NSString *charset, *s;
   NSData *mailData;
   
-  if (![_data isNotNull])
-    return nil;
-
-  s = nil;
-
-  encoding = [[_info objectForKey:@"encoding"] lowercaseString];
-
-  if ([encoding isEqualToString: @"7bit"]
-      || [encoding isEqualToString: @"8bit"])
-    mailData = _data;
-  else if ([encoding isEqualToString: @"base64"])
-    mailData = [_data dataByDecodingBase64];
-  else if ([encoding isEqualToString: @"quoted-printable"])
-    mailData = [_data dataByDecodingQuotedPrintable];
-  
-  charset = [[_info valueForKey:@"parameterList"] valueForKey: @"charset"];
-  if (![charset length])
+  if ([_data isNotNull])
     {
-      s = [[NSString alloc] initWithData:mailData encoding:NSUTF8StringEncoding];
-      [s autorelease];
+      mailData
+       = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
+
+      charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
+      if (![charset length])
+       {
+         s = [[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding];
+         [s autorelease];
+       }
+      else
+       s = [NSString stringWithData: mailData
+                     usingEncodingNamed: charset];
     }
   else
-    s = [NSString stringWithData: mailData
-                  usingEncodingNamed: charset];
+    s = nil;
 
   return s;
 }
 
-- (NSDictionary *)stringifyTextParts:(NSDictionary *)_datas {
+- (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
+{
   NSMutableDictionary *md;
+  NSDictionary *info;
   NSEnumerator *keys;
-  NSString     *key;
-  
-  md   = [NSMutableDictionary dictionaryWithCapacity:4];
+  NSString     *key, *s;
+
+  md = [NSMutableDictionary dictionaryWithCapacity:4];
   keys = [_datas keyEnumerator];
-  while ((key = [keys nextObject]) != nil) {
-    NSDictionary *info;
-    NSString *s;
-    
-    info = [self lookupInfoForBodyPart:key];
-    if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
-      [md setObject:s forKey:key];
-  }
+  while ((key = [keys nextObject]))
+    {
+      info = [self lookupInfoForBodyPart: key];
+      s = [self stringForData: [_datas objectForKey:key] partInfo: info];
+      if (s)
+       [md setObject: s forKey: key];
+    }
+
   return md;
 }
-- (NSDictionary *)fetchPlainTextStrings:(NSArray *)_fetchKeys {
+
+- (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
+{
   /*
     The fetched parts are NSData objects, this method converts them into
     NSString objects based on the information inside the bodystructure.
@@ -650,11 +698,14 @@ static BOOL debugSoParts       = NO;
 
 /* flags */
 
-- (NSException *)addFlags:(id)_flags {
-  return [[self imap4Connection] addFlags:_flags toURL:[self imap4URL]];
+- (NSException *) addFlags: (id) _flags
+{
+  return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
 }
-- (NSException *)removeFlags:(id)_flags {
-  return [[self imap4Connection] removeFlags:_flags toURL:[self imap4URL]];
+
+- (NSException *) removeFlags: (id) _flags
+{
+  return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
 }
 
 /* permissions */
@@ -667,20 +718,26 @@ static BOOL debugSoParts       = NO;
   login = [[context activeUser] login];
   parentAcl = [[self container] aclsForUser: login];
 
-  return [parentAcl containsObject: SOGoMailRole_MessageEraser];
+  return [parentAcl containsObject: SOGoRole_ObjectEraser];
 }
 
 /* name lookup */
 
-- (id)lookupImap4BodyPartKey:(NSString *)_key inContext:(id)_ctx {
+- (id) lookupImap4BodyPartKey: (NSString *) _key
+                   inContext: (id) _ctx
+{
   // TODO: we might want to check for existence prior controller creation
   Class clazz;
   
   clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
-  return [[[clazz alloc] initWithName:_key inContainer:self] autorelease];
+
+  return [clazz objectWithName:_key inContainer: self];
 }
 
-- (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
+- (id) lookupName: (NSString *) _key
+       inContext: (id) _ctx
+         acquire: (BOOL) _flag
+{
   id obj;
   
   /* first check attributes directly bound to the application */
@@ -692,46 +749,53 @@ static BOOL debugSoParts       = NO;
   if ([self isBodyPartKey:_key inContext:_ctx]) {
     if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
       if (debugSoParts) 
-       [self logWithFormat:@"mail looked up part %@: %@", _key, obj];
+       [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
       return obj;
     }
   }
   
   /* return 404 to stop acquisition */
   return [NSException exceptionWithHTTPStatus:404 /* Not Found */
-                     reason:@"Did not find mail method or part-reference!"];
+                     reason: @"Did not find mail method or part-reference!"];
 }
 
 /* WebDAV */
 
-- (BOOL)davIsCollection {
+- (BOOL) davIsCollection
+{
   /* while a mail has child objects, it should appear as a file in WebDAV */
   return NO;
 }
 
-- (id)davContentLength {
-  return [[self fetchCoreInfos] valueForKey:@"size"];
+- (id) davContentLength
+{
+  return [[self fetchCoreInfos] valueForKey: @"size"];
 }
 
-- (NSDate *)davCreationDate {
+- (NSDate *) davCreationDate
+{
   // TODO: use INTERNALDATE once NGImap4 supports that
   return nil;
 }
-- (NSDate *)davLastModified {
+
+- (NSDate *) davLastModified
+{
   return [self davCreationDate];
 }
 
-- (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
-  inContext:(id)_ctx
+- (NSException *) davMoveToTargetObject: (id) _target
+                               newName: (NSString *) _name
+                             inContext: (id)_ctx
 {
-  [self logWithFormat:@"TODO: should move mail as '%@' to: %@",
+  [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
        _name, _target];
-  return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
-                     reason:@"not implemented"];
+  return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
+                     reason: @"not implemented"];
 }
 
-- (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
-  inContext:(id)_ctx
+- (NSException *) davCopyToTargetObject: (id) _target
+                               newName: (NSString *) _name
+                             inContext: (id)_ctx
 {
   /* 
      Note: this is special because we create SOGoMailObject's even if they do
@@ -755,7 +819,8 @@ static BOOL debugSoParts       = NO;
 
 /* actions */
 
-- (id)GETAction:(id)_ctx {
+- (id) GETAction: (id) _ctx
+{
   NSException *error;
   WOResponse  *r;
   NSData      *content;
@@ -764,7 +829,7 @@ static BOOL debugSoParts       = NO;
     /* check whether the mail still exists */
     if (![self doesMailExist]) {
       return [NSException exceptionWithHTTPStatus:404 /* Not Found */
-                         reason:@"mail was deleted"];
+                         reason: @"mail was deleted"];
     }
     return error; /* return 304 or 416 */
   }
@@ -774,18 +839,19 @@ static BOOL debugSoParts       = NO;
     return content;
   if (content == nil) {
     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
-                       reason:@"did not find IMAP4 message"];
+                       reason: @"did not find IMAP4 message"];
   }
   
   r = [(WOContext *)_ctx response];
-  [r setHeader:@"message/rfc822" forKey:@"content-type"];
+  [r setHeader: @"message/rfc822" forKey: @"content-type"];
   [r setContent:content];
   return r;
 }
 
 /* operations */
 
-- (NSException *)trashInContext:(id)_ctx {
+- (NSException *) trashInContext: (id) _ctx
+{
   /*
     Trashing is three actions:
     a) copy to trash folder
@@ -805,47 +871,35 @@ static BOOL debugSoParts       = NO;
     return (NSException *)trashFolder;
   if (![trashFolder isNotNull]) {
     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
-                       reason:@"Did not find Trash folder!"];
+                       reason: @"Did not find Trash folder!"];
   }
   [trashFolder flushMailCaches];
 
   /* a) copy */
   
   error = [self davCopyToTargetObject:trashFolder
-               newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
+               newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
                inContext:_ctx];
   if (error != nil) return error;
   
   /* b) mark deleted */
   
-  error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
+  error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
   if (error != nil) return error;
-  
-  /* c) expunge */
 
-  error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
-  if (error != nil) return error; // TODO: unflag as deleted?
+  [container markForExpunge];
+
   [self flushMailCaches];
   
   return nil;
 }
 
-- (NSException *) moveToFolderNamed: (NSString *) folderName
+- (NSException *) copyToFolderNamed: (NSString *) folderName
                           inContext: (id)_ctx
 {
-  /*
-    Trashing is three actions:
-    a) copy to trash folder
-    b) mark mail as deleted
-    c) expunge folder
-    
-    In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
-    the ID used in the trash folder.
-  */
   SOGoMailAccounts *destFolder;
   NSEnumerator *folders;
   NSString *currentFolderName, *reason;
-  NSException    *error;
 
   // TODO: check for safe HTTP method
 
@@ -875,27 +929,33 @@ static BOOL debugSoParts       = NO;
   [destFolder flushMailCaches];
 
   /* a) copy */
-  
-  error = [self davCopyToTargetObject: destFolder
-               newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
-               inContext:_ctx];
-  if (error != nil) return error;
 
-  /* b) mark deleted */
-  
-  error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
-  if (error != nil) return error;
+  return [self davCopyToTargetObject: destFolder
+              newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
+              inContext:_ctx];
+}
+
+- (NSException *) moveToFolderNamed: (NSString *) folderName
+                          inContext: (id)_ctx
+{
+  NSException    *error;
+
+  if (![self copyToFolderNamed: folderName
+            inContext: _ctx])
+    {
+      /* b) mark deleted */
   
-  /* c) expunge */
+      error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
+      if (error != nil) return error;
 
-  error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
-  if (error != nil) return error; // TODO: unflag as deleted?
-  [self flushMailCaches];
+      [self flushMailCaches];
+    }
   
   return nil;
 }
 
-- (NSException *)delete {
+- (NSException *) delete
+{
   /* 
      Note: delete is different to DELETEAction: for mails! The 'delete' runs
            either flags a message as deleted or moves it to the Trash while
@@ -910,7 +970,9 @@ static BOOL debugSoParts       = NO;
   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
   return error;
 }
-- (id)DELETEAction:(id)_ctx {
+
+- (id) DELETEAction: (id) _ctx
+{
   NSException *error;
   
   // TODO: ensure safe HTTP method
@@ -926,39 +988,42 @@ static BOOL debugSoParts       = NO;
 
 /* some mail classification */
 
-- (BOOL)isKolabObject {
+- (BOOL) isKolabObject
+{
   NSDictionary *h;
   
   if ((h = [self mailHeaders]) != nil)
-    return [[h objectForKey:@"x-kolab-type"] isNotEmpty];
+    return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
   
   // TODO: we could check the body structure?
   
   return NO;
 }
 
-- (BOOL)isMailingListMail {
+- (BOOL) isMailingListMail
+{
   NSDictionary *h;
   
   if ((h = [self mailHeaders]) == nil)
     return NO;
   
-  return [[h objectForKey:@"list-id"] isNotEmpty];
+  return [[h objectForKey: @"list-id"] isNotEmpty];
 }
 
-- (BOOL)isVirusScanned {
+- (BOOL) isVirusScanned
+{
   NSDictionary *h;
   
   if ((h = [self mailHeaders]) == nil)
     return NO;
   
-  if (![[h objectForKey:@"x-virus-status"]  isNotEmpty]) return NO;
-  if (![[h objectForKey:@"x-virus-scanned"] isNotEmpty]) return NO;
+  if (![[h objectForKey: @"x-virus-status"]  isNotEmpty]) return NO;
+  if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
   return YES;
 }
 
-- (NSString *)scanListHeaderValue:(id)_value
-  forFieldWithPrefix:(NSString *)_prefix
+- (NSString *) scanListHeaderValue: (id) _value
+               forFieldWithPrefix: (NSString *) _prefix
 {
   /* Note: not very tolerant on embedded commands and <> */
   // TODO: does not really belong here, should be a header-field-parser
@@ -983,9 +1048,9 @@ static BOOL debugSoParts       = NO;
     return nil;
   
   /* check for commas in string values */
-  r = [_value rangeOfString:@","];
+  r = [_value rangeOfString: @","];
   if (r.length > 0) {
-    return [self scanListHeaderValue:[_value componentsSeparatedByString:@","]
+    return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
                 forFieldWithPrefix:_prefix];
   }
 
@@ -995,7 +1060,7 @@ static BOOL debugSoParts       = NO;
   
   /* unquote */
   if ([_value characterAtIndex:0] == '<') {
-    r = [_value rangeOfString:@">"];
+    r = [_value rangeOfString: @">"];
     _value = (r.length == 0)
       ? [_value substringFromIndex:1]
       : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
@@ -1004,25 +1069,31 @@ static BOOL debugSoParts       = NO;
   return _value;
 }
 
-- (NSString *)mailingListArchiveURL {
+- (NSString *) mailingListArchiveURL
+{
   return [self scanListHeaderValue:
-                [[self mailHeaders] objectForKey:@"list-archive"]
-              forFieldWithPrefix:@"<http://"];
+                [[self mailHeaders] objectForKey: @"list-archive"]
+              forFieldWithPrefix: @"<http://"];
 }
-- (NSString *)mailingListSubscribeURL {
+
+- (NSString *) mailingListSubscribeURL
+{
   return [self scanListHeaderValue:
-                [[self mailHeaders] objectForKey:@"list-subscribe"]
-              forFieldWithPrefix:@"<http://"];
+                [[self mailHeaders] objectForKey: @"list-subscribe"]
+              forFieldWithPrefix: @"<http://"];
 }
-- (NSString *)mailingListUnsubscribeURL {
+
+- (NSString *) mailingListUnsubscribeURL
+{
   return [self scanListHeaderValue:
-                [[self mailHeaders] objectForKey:@"list-unsubscribe"]
-              forFieldWithPrefix:@"<http://"];
+                [[self mailHeaders] objectForKey: @"list-unsubscribe"]
+              forFieldWithPrefix: @"<http://"];
 }
 
 /* etag support */
 
-- (id)davEntityTag {
+- (id) davEntityTag
+{
   /*
     Note: There is one thing which *can* change for an existing message,
           those are the IMAP4 flags (and annotations, which we do not use).
@@ -1031,34 +1102,43 @@ static BOOL debugSoParts       = NO;
   */
   return mailETag;
 }
-- (int)zlGenerationCount {
+
+- (int) zlGenerationCount
+{
   return 0; /* mails never change */
 }
 
 /* Outlook mail tagging */
 
-- (NSString *)outlookMessageClass {
+- (NSString *) outlookMessageClass
+{
   NSString *type;
   
-  if ((type = [[self mailHeaders] objectForKey:@"x-kolab-type"]) != nil) {
-    if ([type isEqualToString:@"application/x-vnd.kolab.contact"])
+  if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
+    if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
       return @"IPM.Contact";
-    if ([type isEqualToString:@"application/x-vnd.kolab.task"])
+    if ([type isEqualToString: @"application/x-vnd.kolab.task"])
       return @"IPM.Task";
-    if ([type isEqualToString:@"application/x-vnd.kolab.event"])
+    if ([type isEqualToString: @"application/x-vnd.kolab.event"])
       return @"IPM.Appointment";
-    if ([type isEqualToString:@"application/x-vnd.kolab.note"])
+    if ([type isEqualToString: @"application/x-vnd.kolab.note"])
       return @"IPM.Note";
-    if ([type isEqualToString:@"application/x-vnd.kolab.journal"])
+    if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
       return @"IPM.Journal";
   }
   
   return @"IPM.Message"; /* email, default class */
 }
 
+- (NSArray *) aclsForUser: (NSString *) uid
+{
+  return [container aclsForUser: uid];
+}
+
 /* debugging */
 
-- (BOOL)isDebuggingEnabled {
+- (BOOL) isDebuggingEnabled
+{
   return debugOn;
 }