]> err.no Git - scalable-opengroupware.org/blobdiff - SoObjects/Mailer/SOGoDraftObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1205 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SoObjects / Mailer / SOGoDraftObject.m
index e8411e99caccadfb49b1f4f0e6b39be55a25e425..2cc71010e49324d027058eefafd6d656707c3bd9 100644 (file)
   02111-1307, USA.
 */
 
-#include "SOGoDraftObject.h"
-#include <SoObjects/SOGo/WOContext+Agenor.h>
-#include <NGMail/NGMimeMessage.h>
-#include <NGMail/NGMimeMessageGenerator.h>
-#include <NGMail/NGSendMail.h>
-#include <NGMime/NGMimeBodyPart.h>
-#include <NGMime/NGMimeFileData.h>
-#include <NGMime/NGMimeMultipartBody.h>
-#include <NGMime/NGMimeType.h>
-#include <NGImap4/NGImap4Envelope.h>
-#include <NGImap4/NGImap4EnvelopeAddress.h>
-#include <NGExtensions/NSFileManager+Extensions.h>
-#include "common.h"
+#import <Foundation/NSArray.h>
+#import <Foundation/NSAutoreleasePool.h>
+#import <Foundation/NSDictionary.h>
+#import <Foundation/NSKeyValueCoding.h>
+#import <Foundation/NSURL.h>
+#import <Foundation/NSUserDefaults.h>
+#import <Foundation/NSValue.h>
+
+#import <NGObjWeb/NSException+HTTP.h>
+#import <NGObjWeb/SoObject+SoDAV.h>
+#import <NGObjWeb/WOContext+SoObjects.h>
+#import <NGObjWeb/WORequest+So.h>
+#import <NGObjWeb/WOResponse.h>
+#import <NGExtensions/NGBase64Coding.h>
+#import <NGExtensions/NSFileManager+Extensions.h>
+#import <NGExtensions/NGHashMap.h>
+#import <NGExtensions/NSNull+misc.h>
+#import <NGExtensions/NSObject+Logs.h>
+#import <NGExtensions/NGQuotedPrintableCoding.h>
+#import <NGExtensions/NSString+misc.h>
+#import <NGImap4/NGImap4Connection.h>
+#import <NGImap4/NGImap4Client.h>
+#import <NGImap4/NGImap4Envelope.h>
+#import <NGImap4/NGImap4EnvelopeAddress.h>
+#import <NGMail/NGMimeMessage.h>
+#import <NGMail/NGMimeMessageGenerator.h>
+#import <NGMime/NGMimeBodyPart.h>
+#import <NGMime/NGMimeFileData.h>
+#import <NGMime/NGMimeMultipartBody.h>
+#import <NGMime/NGMimeType.h>
+#import <NGMime/NGMimeHeaderFieldGenerator.h>
+
+#import <SoObjects/SOGo/NSArray+Utilities.h>
+#import <SoObjects/SOGo/NSCalendarDate+SOGo.h>
+#import <SoObjects/SOGo/NSString+Utilities.h>
+#import <SoObjects/SOGo/SOGoMailer.h>
+#import <SoObjects/SOGo/SOGoUser.h>
+
+#import "NSData+Mail.h"
+#import "SOGoMailAccount.h"
+#import "SOGoMailFolder.h"
+#import "SOGoMailObject.h"
+#import "SOGoMailObject+Draft.h"
+
+#import "SOGoDraftObject.h"
+
+static NSString *contentTypeValue = @"text/plain; charset=utf-8";
+static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", 
+                                @"from", @"replyTo",
+                                nil};
 
 @implementation SOGoDraftObject
 
@@ -41,173 +78,534 @@ static NSString    *userAgent      = @"SOGoMail 1.0";
 static BOOL        draftDeleteDisabled = NO; // for debugging
 static BOOL        debugOn = NO;
 static BOOL        showTextAttachmentsInline  = NO;
-static NSString    *fromInternetSuffixPattern = nil;
 
-+ (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]);
-  
   /* Note: be aware of the charset issues before enabling this! */
-  showTextAttachmentsInline = [ud boolForKey:@"SOGoShowTextAttachmentsInline"];
+  showTextAttachmentsInline = [ud boolForKey: @"SOGoShowTextAttachmentsInline"];
   
-  if ((draftDeleteDisabled = [ud boolForKey:@"SOGoNoDraftDeleteAfterSend"]))
+  if ((draftDeleteDisabled = [ud boolForKey: @"SOGoNoDraftDeleteAfterSend"]))
     NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
   
-  fromInternetSuffixPattern = [ud stringForKey:@"SOGoInternetMailSuffix"];
-  if ([fromInternetSuffixPattern length] == 0)
-    NSLog(@"Note: no 'SOGoInternetMailSuffix' is configured.");
-  else {
-    fromInternetSuffixPattern =
-      [@"\n" stringByAppendingString:fromInternetSuffixPattern];
-  }
-  
-  TextPlainType  = [[NGMimeType mimeType:@"text"      subType:@"plain"]  copy];
-  MultiMixedType = [[NGMimeType mimeType:@"multipart" subType:@"mixed"]  copy];
+  TextPlainType  = [[NGMimeType mimeType: @"text" subType: @"plain"]  copy];
+  MultiMixedType = [[NGMimeType mimeType: @"multipart" subType: @"mixed"]  copy];
 }
 
-- (void)dealloc {
-  [self->envelope release];
-  [self->info release];
-  [self->path release];
+- (id) init
+{
+  if ((self = [super init]))
+    {
+      IMAP4ID = -1;
+      headers = [NSMutableDictionary new];
+      text = @"";
+      sourceURL = nil;
+      sourceFlag = nil;
+      inReplyTo = nil;
+    }
+
+  return self;
+}
+
+- (void) dealloc
+{
+  [headers release];
+  [text release];
+  [envelope release];
+  [path release];
+  [sourceURL release];
+  [sourceFlag release];
+  [inReplyTo release];
   [super dealloc];
 }
 
 /* draft folder functionality */
 
-- (NSFileManager *)spoolFileManager {
-  return [[self container] spoolFileManager];
-}
-- (NSString *)userSpoolFolderPath {
+- (NSString *) userSpoolFolderPath
+{
   return [[self container] userSpoolFolderPath];
 }
-- (BOOL)_ensureUserSpoolFolderPath {
-  return [[self container] _ensureUserSpoolFolderPath];
-}
 
 /* draft object functionality */
 
-- (NSString *)draftFolderPath {
-  if (self->path != nil)
-    return self->path;
-  
-  self->path = [[[self userSpoolFolderPath] stringByAppendingPathComponent:
-                                             [self nameInContainer]] copy];
-  return self->path;
+- (NSString *) draftFolderPath
+{
+  if (!path)
+    {
+      path = [[self userSpoolFolderPath] stringByAppendingPathComponent:
+                                          nameInContainer];
+      [path retain];
+    }
+
+  return path;
 }
-- (BOOL)_ensureDraftFolderPath {
+
+- (BOOL) _ensureDraftFolderPath
+{
   NSFileManager *fm;
+
+  fm = [NSFileManager defaultManager];
   
-  if (![self _ensureUserSpoolFolderPath])
-    return NO;
-  
-  if ((fm = [self spoolFileManager]) == nil) {
-    [self errorWithFormat:@"missing spool file manager!"];
-    return NO;
-  }
-  return [fm createDirectoriesAtPath:[self draftFolderPath] attributes:nil];
+  return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
+             attributes: nil]
+         && [fm createDirectoriesAtPath: [self draftFolderPath]
+                attributes:nil]);
 }
 
-- (NSString *)infoPath {
-  return [[self draftFolderPath] 
-               stringByAppendingPathComponent:@".info.plist"];
+- (NSString *) infoPath
+{
+  return [[self draftFolderPath]
+          stringByAppendingPathComponent: @".info.plist"];
 }
 
 /* contents */
 
-- (NSException *)storeInfo:(NSDictionary *)_info {
-  if (_info == nil) {
-    return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"got no info to write for draft!"];
-  }
-  if (![self _ensureDraftFolderPath]) {
-    [self errorWithFormat:@"could not create folder for draft: '%@'",
+- (void) setHeaders: (NSDictionary *) newHeaders
+{
+  id headerValue;
+  unsigned int count;
+
+  for (count = 0; count < 7; count++)
+    {
+      headerValue = [newHeaders objectForKey: headerKeys[count]];
+      if (headerValue)
+       [headers setObject: headerValue
+                forKey: headerKeys[count]];
+      else
+       [headers removeObjectForKey: headerKeys[count]];
+    }
+}
+
+- (NSDictionary *) headers
+{
+  return headers;
+}
+
+- (void) setText: (NSString *) newText
+{
+  ASSIGN (text, newText);
+}
+
+- (NSString *) text
+{
+  return text;
+}
+
+- (void) setInReplyTo: (NSString *) newInReplyTo
+{
+  ASSIGN (inReplyTo, newInReplyTo);
+}
+
+- (void) setSourceURL: (NSString *) newSourceURL
+{
+  ASSIGN (sourceURL, newSourceURL);
+}
+
+- (void) setSourceFlag: (NSString *) newSourceFlag
+{
+  ASSIGN (sourceFlag, newSourceFlag);
+}
+
+- (NSException *) storeInfo
+{
+  NSMutableDictionary *infos;
+  NSException *error;
+
+  if ([self _ensureDraftFolderPath])
+    {
+      infos = [NSMutableDictionary new];
+      [infos setObject: headers forKey: @"headers"];
+      if (text)
+       [infos setObject: text forKey: @"text"];
+      if (inReplyTo)
+       [infos setObject: inReplyTo forKey: @"inReplyTo"];
+      if (IMAP4ID > -1)
+       [infos setObject: [NSNumber numberWithInt: IMAP4ID]
+              forKey: @"IMAP4ID"];
+      if (sourceURL && sourceFlag)
+       {
+         [infos setObject: sourceURL forKey: @"sourceURL"];
+         [infos setObject: sourceFlag forKey: @"sourceFlag"];
+       }
+
+      if ([infos writeToFile: [self infoPath] atomically:YES])
+       error = nil;
+      else
+       {
+         [self errorWithFormat: @"could not write info: '%@'",
+               [self infoPath]];
+         error = [NSException exceptionWithHTTPStatus:500 /* server error */
+                              reason: @"could not write draft info!"];
+       }
+
+      [infos release];
+    }
+  else
+    {
+      [self errorWithFormat: @"could not create folder for draft: '%@'",
             [self draftFolderPath]];
-    return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"could not create folder for draft!"];
-  }
-  if (![_info writeToFile:[self infoPath] atomically:YES]) {
-    [self errorWithFormat:@"could not write info: '%@'", [self infoPath]];
-    return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"could not write draft info!"];
-  }
-  
-  /* reset info cache */
-  [self->info release]; self->info = nil;
-  
-  return nil /* everything is excellent */;
+      error = [NSException exceptionWithHTTPStatus:500 /* server error */
+                          reason: @"could not create folder for draft!"];
+    }
+
+  return error;
+}
+
+- (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
+{
+  id value;
+
+  value = [infoDict objectForKey: @"headers"];
+  if (value)
+    [self setHeaders: value];
+
+  value = [infoDict objectForKey: @"text"];
+  if ([value length] > 0)
+    [self setText: value];
+
+  value = [infoDict objectForKey: @"IMAP4ID"];
+  if (value)
+    [self setIMAP4ID: [value intValue]];
+
+  value = [infoDict objectForKey: @"sourceURL"];
+  if (value)
+    [self setSourceURL: value];
+  value = [infoDict objectForKey: @"sourceFlag"];
+  if (value)
+    [self setSourceFlag: value];
+
+  value = [infoDict objectForKey: @"inReplyTo"];
+  if (value)
+    [self setInReplyTo: value];
+}
+
+- (NSString *) relativeImap4Name
+{
+  return [NSString stringWithFormat: @"%d", IMAP4ID];
 }
-- (NSDictionary *)fetchInfo {
+
+- (void) fetchInfo
+{
   NSString *p;
+  NSDictionary *infos;
+  NSFileManager *fm;
 
-  if (self->info != nil)
-    return self->info;
-  
   p = [self infoPath];
-  if (![[self spoolFileManager] fileExistsAtPath:p]) {
-    [self debugWithFormat:@"Note: info object does not yet exist: %@", p];
-    return nil;
-  }
+
+  fm = [NSFileManager defaultManager];
+  if ([fm fileExistsAtPath: p])
+    {
+      infos = [NSDictionary dictionaryWithContentsOfFile: p];
+      if (infos)
+       [self _loadInfosFromDictionary: infos];
+//       else
+//     [self errorWithFormat: @"draft info dictionary broken at path: %@", p];
+    }
+  else
+    [self debugWithFormat: @"Note: info object does not yet exist: %@", p];
+}
+
+- (void) setIMAP4ID: (int) newIMAP4ID
+{
+  IMAP4ID = newIMAP4ID;
+}
+
+- (int) IMAP4ID
+{
+  return IMAP4ID;
+}
+
+- (int) _IMAP4IDFromAppendResult: (NSDictionary *) result
+{
+  NSDictionary *results;
+  NSString *flag, *newIdString;
+
+  results = [[result objectForKey: @"RawResponse"]
+             objectForKey: @"ResponseResult"];
+  flag = [results objectForKey: @"flag"];
+  newIdString = [[flag componentsSeparatedByString: @" "] objectAtIndex: 2];
+
+  return [newIdString intValue];
+}
+
+- (NSException *) save
+{
+  NGImap4Client *client;
+  NSException *error;
+  NSData *message;
+  NSString *folder;
+  id result;
+
+  error = nil;
+  message = [self mimeMessageAsData];
+
+  client = [[self imap4Connection] client];
+  folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
+  result
+    = [client append: message toFolder: folder
+             withFlags: [NSArray arrayWithObjects: @"seen", @"draft", nil]];
+  if ([[result objectForKey: @"result"] boolValue])
+    {
+      if (IMAP4ID > -1)
+       error = [imap4 markURLDeleted: [self imap4URL]];
+      IMAP4ID = [self _IMAP4IDFromAppendResult: result];
+      [self storeInfo];
+    }
+  else
+    error = [NSException exceptionWithHTTPStatus:500 /* Server Error */
+                        reason: @"Failed to store message"];
+
+  return error;
+}
+
+- (void) _addEMailsOfAddresses: (NSArray *) _addrs
+                      toArray: (NSMutableArray *) _ma
+{
+  unsigned i, count;
+
+  for (i = 0, count = [_addrs count]; i < count; i++)
+    [_ma addObject:
+          [(NGImap4EnvelopeAddress *) [_addrs objectAtIndex: i] email]];
+}
+
+- (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
+                   replyToAll: (BOOL) _replyToAll
+                     envelope: (NGImap4Envelope *) _envelope
+{
+  /*
+    The rules as implemented by Thunderbird:
+    - if there is a 'reply-to' header, only include that (as TO)
+    - if we reply to all, all non-from addresses are added as CC
+    - the from is always the lone TO (except for reply-to)
+    
+    Note: we cannot check reply-to, because Cyrus even sets a reply-to in the
+          envelope if none is contained in the message itself! (bug or
+          feature?)
+    
+    TODO: what about sender (RFC 822 3.6.2)
+  */
+  NSMutableArray *to;
+  NSArray *addrs;
+  
+  to = [NSMutableArray arrayWithCapacity:2];
+
+  /* first check for "reply-to" */
   
-  self->info = [[NSDictionary alloc] initWithContentsOfFile:p];
-  if (self->info == nil)
-    [self errorWithFormat:@"draft info dictionary broken at path: %@", p];
+  addrs = [_envelope replyTo];
+  if ([addrs count] == 0)
+    /* no "reply-to", try "from" */
+    addrs = [_envelope from];
+
+  [self _addEMailsOfAddresses: addrs toArray: to];
+  [_info setObject: to forKey: @"to"];
+
+  /* CC processing if we reply-to-all: add all 'to' and 'cc'  */
   
-  return self->info;
+  if (_replyToAll)
+    {
+      to = [NSMutableArray arrayWithCapacity:8];
+
+      [self _addEMailsOfAddresses: [_envelope to] toArray: to];
+      [self _addEMailsOfAddresses: [_envelope cc] toArray: to];
+    
+      [_info setObject: to forKey: @"cc"];
+    }
+}
+
+- (NSArray *) _attachmentBodiesFromPaths: (NSArray *) paths
+                      fromResponseFetch: (NSDictionary *) fetch;
+{
+  NSEnumerator *attachmentKeys;
+  NSMutableArray *bodies;
+  NSString *currentKey;
+  NSDictionary *body;
+
+  bodies = [NSMutableArray array];
+
+  attachmentKeys = [paths objectEnumerator];
+  while ((currentKey = [attachmentKeys nextObject]))
+    {
+      body = [fetch objectForKey: [currentKey lowercaseString]];
+      [bodies addObject: [body objectForKey: @"data"]];
+    }
+
+  return bodies;
+}
+
+- (void) _fetchAttachments: (NSArray *) parts
+                  fromMail: (SOGoMailObject *) sourceMail
+{
+  unsigned int count, max;
+  NSArray *paths, *bodies;
+  NSData *body;
+  NSDictionary *currentInfo;
+  NGHashMap *response;
+
+  max = [parts count];
+  if (max > 0)
+    {
+      paths = [parts keysWithFormat: @"BODY[%{path}]"];
+      response = [[sourceMail fetchParts: paths] objectForKey: @"RawResponse"];
+      bodies = [self _attachmentBodiesFromPaths: paths
+                    fromResponseFetch: [response objectForKey: @"fetch"]];
+      for (count = 0; count < max; count++)
+       {
+         currentInfo = [parts objectAtIndex: count];
+         body = [[bodies objectAtIndex: count]
+                  bodyDataFromEncoding: [currentInfo
+                                          objectForKey: @"encoding"]];
+         [self saveAttachment: body withMetadata: currentInfo];
+       }
+    }
+}
+
+- (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
+{
+  NSString *subject;
+  NSMutableDictionary *info;
+  NSMutableArray *addresses;
+  NGImap4Envelope *sourceEnvelope;
+
+  [sourceMail fetchCoreInfos];
+
+  [self _fetchAttachments: [sourceMail fetchFileAttachmentKeys]
+       fromMail: sourceMail];
+  info = [NSMutableDictionary dictionaryWithCapacity: 16];
+  subject = [sourceMail subject];
+  if ([subject length] > 0)
+    [info setObject: subject forKey: @"subject"];
+
+  sourceEnvelope = [sourceMail envelope];
+  addresses = [NSMutableArray array];
+  [self _addEMailsOfAddresses: [sourceEnvelope to] toArray: addresses];
+  [info setObject: addresses forKey: @"to"];
+  addresses = [NSMutableArray array];
+  [self _addEMailsOfAddresses: [sourceEnvelope cc] toArray: addresses];
+  if ([addresses count] > 0)
+    [info setObject: addresses forKey: @"cc"];
+  addresses = [NSMutableArray array];
+  [self _addEMailsOfAddresses: [sourceEnvelope bcc] toArray: addresses];
+  if ([addresses count] > 0)
+    [info setObject: addresses forKey: @"bcc"];
+  addresses = [NSMutableArray array];
+  [self _addEMailsOfAddresses: [sourceEnvelope replyTo] toArray: addresses];
+  if ([addresses count] > 0)
+    [info setObject: addresses forKey: @"replyTo"];
+  [self setHeaders: info];
+
+  [self setText: [sourceMail contentForEditing]];
+  [self setSourceURL: [sourceMail imap4URLString]];
+  IMAP4ID = [[sourceMail nameInContainer] intValue];
+
+  [self storeInfo];
+}
+
+- (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
+                       toAll: (BOOL) toAll
+{
+  NSString *contentForReply, *msgID;
+  NSMutableDictionary *info;
+  NGImap4Envelope *sourceEnvelope;
+
+  [sourceMail fetchCoreInfos];
+
+  info = [NSMutableDictionary dictionaryWithCapacity: 16];
+  [info setObject: [sourceMail subjectForReply] forKey: @"subject"];
+
+  sourceEnvelope = [sourceMail envelope];
+  [self _fillInReplyAddresses: info replyToAll: toAll
+       envelope: sourceEnvelope];
+  msgID = [sourceEnvelope messageID];
+  if ([msgID length] > 0)
+    [self setInReplyTo: msgID];
+  contentForReply = [sourceMail contentForReply];
+  [self setText: contentForReply];
+  [self setHeaders: info];
+  [self setSourceURL: [sourceMail imap4URLString]];
+  [self setSourceFlag: @"Answered"];
+  [self storeInfo];
+}
+
+- (void) fetchMailForForwarding: (SOGoMailObject *) sourceMail
+{
+  NSDictionary *info, *attachment;
+  SOGoUser *currentUser;
+
+  [sourceMail fetchCoreInfos];
+
+  info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward]
+                      forKey: @"subject"];
+  [self setHeaders: info];
+  [self setSourceURL: [sourceMail imap4URLString]];
+  [self setSourceFlag: @"$Forwarded"];
+
+  /* attach message */
+  currentUser = [context activeUser];
+  if ([[currentUser messageForwarding] isEqualToString: @"inline"])
+    [self setText: [sourceMail contentForInlineForward]];
+  else
+    {
+  // TODO: use subject for filename?
+//   error = [newDraft saveAttachment:content withName:@"forward.mail"];
+      attachment = [NSDictionary dictionaryWithObjectsAndKeys:
+                                  [sourceMail filenameForForward], @"filename",
+                                @"message/rfc822", @"mimetype",
+                                nil];
+      [self saveAttachment: [sourceMail content]
+           withMetadata: attachment];
+    }
+  [self storeInfo];
 }
 
 /* accessors */
 
-- (NSString *)sender {
+- (NSString *) sender
+{
   id tmp;
   
-  if ((tmp = [[self fetchInfo] objectForKey:@"from"]) == nil)
+  if ((tmp = [headers objectForKey: @"from"]) == nil)
     return nil;
   if ([tmp isKindOfClass:[NSArray class]])
-    return [tmp count] > 0 ? [tmp objectAtIndex:0] : nil;
+    return [tmp count] > 0 ? [tmp objectAtIndex: 0] : nil;
+
   return tmp;
 }
 
 /* attachments */
 
-- (NSArray *)fetchAttachmentNames {
+- (NSArray *) fetchAttachmentNames
+{
   NSMutableArray *ma;
-  NSFileManager  *fm;
-  NSArray        *files;
-  unsigned i, count;
-  
-  fm = [self spoolFileManager];
-  if ((files = [fm directoryContentsAtPath:[self draftFolderPath]]) == nil)
-    return nil;
-  
-  count = [files count];
-  ma    = [NSMutableArray arrayWithCapacity:count];
-  for (i = 0; i < count; i++) {
-    NSString *filename;
-    
-    filename = [files objectAtIndex:i];
-    if ([filename hasPrefix:@"."])
-      continue;
-    
-    [ma addObject:filename];
-  }
+  NSFileManager *fm;
+  NSArray *files;
+  unsigned count, max;
+  NSString *filename;
+
+  fm = [NSFileManager defaultManager];
+  files = [fm directoryContentsAtPath: [self draftFolderPath]];
+
+  max = [files count];
+  ma = [NSMutableArray arrayWithCapacity: max];
+  for (count = 0; count < max; count++)
+    {
+      filename = [files objectAtIndex: count];
+      if (![filename hasPrefix: @"."])
+       [ma addObject: filename];
+    }
+
   return ma;
 }
 
-- (BOOL)isValidAttachmentName:(NSString *)_name {
-  static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", @" ", nil };
+- (BOOL) isValidAttachmentName: (NSString *) _name
+{
+  static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", nil };
   unsigned i;
   NSRange  r;
 
   if (![_name isNotNull])     return NO;
   if ([_name length] == 0)    return NO;
-  if ([_name hasPrefix:@"."]) return NO;
+  if ([_name hasPrefix: @"."]) return NO;
   
   for (i = 0; sescape[i] != nil; i++) {
     r = [_name rangeOfString:sescape[i]];
@@ -216,194 +614,223 @@ static NSString    *fromInternetSuffixPattern = nil;
   return YES;
 }
 
-- (NSString *)pathToAttachmentWithName:(NSString *)_name {
+- (NSString *) pathToAttachmentWithName: (NSString *) _name
+{
   if ([_name length] == 0)
     return nil;
   
   return [[self draftFolderPath] stringByAppendingPathComponent:_name];
 }
 
-- (NSException *)invalidAttachmentNameError:(NSString *)_name {
+- (NSException *) invalidAttachmentNameError: (NSString *) _name
+{
   return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
-                     reason:@"Invalid attachment name!"];
+                     reason: @"Invalid attachment name!"];
 }
 
-- (NSException *)saveAttachment:(NSData *)_attach withName:(NSString *)_name {
-  NSString *p;
-  
+- (NSException *) saveAttachment: (NSData *) _attach
+                   withMetadata: (NSDictionary *) metadata
+{
+  NSString *p, *name, *mimeType;
+  NSRange r;
+
   if (![_attach isNotNull]) {
     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
-                       reason:@"Missing attachment content!"];
+                       reason: @"Missing attachment content!"];
   }
   
   if (![self _ensureDraftFolderPath]) {
     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
-                       reason:@"Could not create folder for draft!"];
+                       reason: @"Could not create folder for draft!"];
   }
-  if (![self isValidAttachmentName:_name])
-    return [self invalidAttachmentNameError:_name];
+
+  name = [metadata objectForKey: @"filename"];
+  r = [name rangeOfString: @"\\"
+           options: NSBackwardsSearch];
+  if (r.length > 0)
+    name = [name substringFromIndex: r.location + 1];
+
+  if (![self isValidAttachmentName: name])
+    return [self invalidAttachmentNameError: name];
   
-  p = [self pathToAttachmentWithName:_name];
-  if (![_attach writeToFile:p atomically:YES]) {
-    return [NSException exceptionWithHTTPStatus:500 /* Server Error */
-                       reason:@"Could not write attachment to draft!"];
-  }
+  p = [self pathToAttachmentWithName: name];
+  if (![_attach writeToFile: p atomically: YES])
+    {
+      return [NSException exceptionWithHTTPStatus:500 /* Server Error */
+                         reason: @"Could not write attachment to draft!"];
+    }
+
+  mimeType = [metadata objectForKey: @"mimetype"];
+  if ([mimeType length] > 0)
+    {
+      p = [self pathToAttachmentWithName:
+                 [NSString stringWithFormat: @".%@.mime", name]];
+      if (![[mimeType dataUsingEncoding: NSUTF8StringEncoding]
+            writeToFile: p atomically: YES])
+       {
+         return [NSException exceptionWithHTTPStatus:500 /* Server Error */
+                             reason: @"Could not write attachment to draft!"];
+       }
+    }
   
   return nil; /* everything OK */
 }
 
-- (NSException *)deleteAttachmentWithName:(NSString *)_name {
+- (NSException *) deleteAttachmentWithName: (NSString *) _name
+{
   NSFileManager *fm;
   NSString *p;
-  
-  if (![self isValidAttachmentName:_name])
-    return [self invalidAttachmentNameError:_name];
-  
-  fm = [self spoolFileManager];
-  p  = [self pathToAttachmentWithName:_name];
-  if (![fm fileExistsAtPath:p])
-    return nil; /* well, doesn't exist, so its deleted ;-) */
-  
-  if (![fm removeFileAtPath:p handler:nil]) {
-    [self logWithFormat:@"ERROR: failed to delete file: %@", p];
-    return [NSException exceptionWithHTTPStatus:500 /* Server Error */
-                       reason:@"Could not delete attachment from draft!"];
-  }
-  return nil; /* everything OK */
+  NSException *error;
+
+  error = nil;
+
+  if ([self isValidAttachmentName:_name]) 
+    {
+      fm = [NSFileManager defaultManager];
+      p = [self pathToAttachmentWithName:_name];
+      if ([fm fileExistsAtPath: p])
+       if (![fm removeFileAtPath: p handler: nil])
+         error
+           = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
+                          reason: @"Could not delete attachment from draft!"];
+    }
+  else
+    error = [self invalidAttachmentNameError:_name];
+
+  return error;  
 }
 
 /* NGMime representations */
 
-- (NGMimeBodyPart *)bodyPartForText {
+- (NGMimeBodyPart *) bodyPartForText
+{
   /*
     This add the text typed by the user (the primary plain/text part).
   */
   NGMutableHashMap *map;
   NGMimeBodyPart   *bodyPart;
-  NSDictionary     *lInfo;
-  id body;
-  
-  if ((lInfo = [self fetchInfo]) == nil)
-    return nil;
   
   /* prepare header of body part */
 
-  map = [[[NGMutableHashMap alloc] initWithCapacity:2] autorelease];
+  map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
 
   // TODO: set charset in header!
-  [map setObject:@"text/plain" forKey:@"content-type"];
-  if ((body = [lInfo objectForKey:@"text"]) != nil) {
-    if ([body isKindOfClass:[NSString class]]) {
-      [map setObject:@"text/plain; charset=utf-8" forKey:@"content-type"];
-      body = [body dataUsingEncoding:NSUTF8StringEncoding];
-    }
-  }
+  [map setObject: @"text/plain" forKey: @"content-type"];
+  if (text)
+    [map setObject: contentTypeValue forKey: @"content-type"];
+
+//   if ((body = text) != nil) {
+//     if ([body isKindOfClass: [NSString class]]) {
+//       [map setObject: contentTypeValue
+//        forKey: @"content-type"];
+// //       body = [body dataUsingEncoding:NSUTF8StringEncoding];
+//     }
+//   }
   
   /* prepare body content */
   
   bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
-  [bodyPart setBody:body];
+  [bodyPart setBody: text];
+
   return bodyPart;
 }
 
-- (NGMimeMessage *)mimeMessageForContentWithHeaderMap:(NGMutableHashMap *)map {
-  NSDictionary  *lInfo;
+- (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
+{
   NGMimeMessage *message;  
-  WOContext     *ctx;
-  NSString *fromInternetSuffix;
-  BOOL     addSuffix;
+//   BOOL     addSuffix;
   id       body;
 
-  if ((lInfo = [self fetchInfo]) == nil)
-    return nil;
-  
-  ctx       = [[WOApplication application] context];
-  addSuffix = [ctx isAccessFromIntranet] ? NO : YES;
-  if (addSuffix) {
-    fromInternetSuffix = 
-      [fromInternetSuffixPattern stringByReplacingVariablesWithBindings:
-                                  [ctx request]
-                                stringForUnknownBindings:@""];
-    
-    addSuffix = [fromInternetSuffix length] > 0 ? YES : NO;
-  }
-  
-  [map setObject:@"text/plain" forKey:@"content-type"];
-  if ((body = [lInfo objectForKey:@"text"]) != nil) {
-    if ([body isKindOfClass:[NSString class]]) {
-      if (addSuffix)
-       body = [body stringByAppendingString:fromInternetSuffix];
-      
-      /* Note: just 'utf8' is displayed wrong in Mail.app */
-      [map setObject:@"text/plain; charset=utf-8" forKey:@"content-type"];
-      body = [body dataUsingEncoding:NSUTF8StringEncoding];
+  [map setObject: @"text/plain" forKey: @"content-type"];
+  body = text;
+  if (body)
+    {
+//       if ([body isKindOfClass:[NSString class]])
+       /* Note: just 'utf8' is displayed wrong in Mail.app */
+       [map setObject: contentTypeValue
+            forKey: @"content-type"];
+//       body = [body dataUsingEncoding:NSUTF8StringEncoding];
+//       else if ([body isKindOfClass:[NSData class]] && addSuffix) {
+//     body = [[body mutableCopy] autorelease];
+//       }
+//       else if (addSuffix) {
+//     [self warnWithFormat: @"Note: cannot add Internet marker to body: %@",
+//           NSStringFromClass([body class])];
+//       }
+
+       message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
+       [message setBody: body];
     }
-    else if ([body isKindOfClass:[NSData class]] && addSuffix) {
-      body = [[body mutableCopy] autorelease];
-      [(NSMutableData *)body appendData:
-                         [fromInternetSuffix dataUsingEncoding:
-                                               NSUTF8StringEncoding]];
-    }
-    else if (addSuffix) {
-      [self warnWithFormat:@"Note: cannot add Internet marker to body: %@",
-             NSStringFromClass([body class])];
-    }
-  }
-  else if (addSuffix)
-    body = [fromInternetSuffix dataUsingEncoding:NSUTF8StringEncoding];
-  
-  message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
-  [message setBody:body];
+  else
+    message = nil;
+
+
   return message;
 }
 
-- (NSString *)mimeTypeForExtension:(NSString *)_ext {
+- (NSString *) mimeTypeForExtension: (NSString *) _ext
+{
   // TODO: make configurable
   // TODO: use /etc/mime-types
-  if ([_ext isEqualToString:@"txt"])  return @"text/plain";
-  if ([_ext isEqualToString:@"html"]) return @"text/html";
-  if ([_ext isEqualToString:@"htm"])  return @"text/html";
-  if ([_ext isEqualToString:@"gif"])  return @"image/gif";
-  if ([_ext isEqualToString:@"jpg"])  return @"image/jpeg";
-  if ([_ext isEqualToString:@"jpeg"]) return @"image/jpeg";
-  if ([_ext isEqualToString:@"mail"]) return @"message/rfc822";
+  if ([_ext isEqualToString: @"txt"])  return @"text/plain";
+  if ([_ext isEqualToString: @"html"]) return @"text/html";
+  if ([_ext isEqualToString: @"htm"])  return @"text/html";
+  if ([_ext isEqualToString: @"gif"])  return @"image/gif";
+  if ([_ext isEqualToString: @"jpg"])  return @"image/jpeg";
+  if ([_ext isEqualToString: @"jpeg"]) return @"image/jpeg";
+  if ([_ext isEqualToString: @"mail"]) return @"message/rfc822";
   return @"application/octet-stream";
 }
 
-- (NSString *)contentTypeForAttachmentWithName:(NSString *)_name {
-  NSString *s;
-  
-  s = [self mimeTypeForExtension:[_name pathExtension]];
-  if ([_name length] > 0) {
-    s = [s stringByAppendingString:@"; name=\""];
-    s = [s stringByAppendingString:_name];
-    s = [s stringByAppendingString:@"\""];
-  }
+- (NSString *) contentTypeForAttachmentWithName: (NSString *) _name
+{
+  NSString *s, *p;
+  NSData *mimeData;
+  
+  p = [self pathToAttachmentWithName:
+             [NSString stringWithFormat: @".%@.mime", _name]];
+  mimeData = [NSData dataWithContentsOfFile: p];
+  if (mimeData)
+    {
+      s = [[NSString alloc] initWithData: mimeData
+                           encoding: NSUTF8StringEncoding];
+      [s autorelease];
+    }
+  else
+    {
+      s = [self mimeTypeForExtension:[_name pathExtension]];
+      if ([_name length] > 0)
+       s = [s stringByAppendingFormat: @"; name=\"%@\"", _name];
+    }
+
   return s;
 }
-- (NSString *)contentDispositionForAttachmentWithName:(NSString *)_name {
+
+- (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
+{
   NSString *type;
   NSString *cdtype;
   NSString *cd;
   
   type = [self contentTypeForAttachmentWithName:_name];
   
-  if ([type hasPrefix:@"text/"])
+  if ([type hasPrefix: @"text/"])
     cdtype = showTextAttachmentsInline ? @"inline" : @"attachment";
-  else if ([type hasPrefix:@"image/"] || [type hasPrefix:@"message"])
+  else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"])
     cdtype = @"inline";
   else
     cdtype = @"attachment";
   
-  cd = [cdtype stringByAppendingString:@"; filename=\""];
-  cd = [cd stringByAppendingString:_name];
-  cd = [cd stringByAppendingString:@"\""];
-  
+  cd = [cdtype stringByAppendingString: @"; filename=\""];
+  cd = [cd stringByAppendingString: _name];
+  cd = [cd stringByAppendingString: @"\""];
+
   // TODO: add size parameter (useful addition, RFC 2183)
   return cd;
 }
 
-- (NGMimeBodyPart *)bodyPartForAttachmentWithName:(NSString *)_name {
+- (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name
+{
   NSFileManager    *fm;
   NGMutableHashMap *map;
   NGMimeBodyPart   *bodyPart;
@@ -417,10 +844,10 @@ static NSString    *fromInternetSuffixPattern = nil;
 
   /* check attachment */
   
-  fm = [self spoolFileManager];
+  fm = [NSFileManager defaultManager];
   p  = [self pathToAttachmentWithName:_name];
   if (![fm isReadableFileAtPath:p]) {
-    [self errorWithFormat:@"did not find attachment: '%@'", _name];
+    [self errorWithFormat: @"did not find attachment: '%@'", _name];
     return nil;
   }
   attachAsString = NO;
@@ -431,14 +858,14 @@ static NSString    *fromInternetSuffixPattern = nil;
   map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
 
   if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
-    [map setObject:s forKey:@"content-type"];
-    if ([s hasPrefix:@"text/"])
+    [map setObject:s forKey: @"content-type"];
+    if ([s hasPrefix: @"text/"])
       attachAsString = YES;
-    else if ([s hasPrefix:@"message/rfc822"])
+    else if ([s hasPrefix: @"message/rfc822"])
       is7bit = YES;
   }
   if ((s = [self contentDispositionForAttachmentWithName:_name]))
-    [map setObject:s forKey:@"content-disposition"];
+    [map setObject:s forKey: @"content-disposition"];
   
   /* prepare body content */
   
@@ -466,9 +893,9 @@ static NSString    *fromInternetSuffixPattern = nil;
              generator!
     */
     body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO];
-    [map setObject:@"7bit" forKey:@"content-transfer-encoding"];
+    [map setObject: @"7bit" forKey: @"content-transfer-encoding"];
     [map setObject:[NSNumber numberWithInt:[body length]] 
-        forKey:@"content-length"];
+        forKey: @"content-length"];
   }
   else {
     /* 
@@ -481,9 +908,9 @@ static NSString    *fromInternetSuffixPattern = nil;
     encoded = [content dataByEncodingBase64];
     [content release]; content = nil;
     
-    [map setObject:@"base64" forKey:@"content-transfer-encoding"];
+    [map setObject: @"base64" forKey: @"content-transfer-encoding"];
     [map setObject:[NSNumber numberWithInt:[encoded length]] 
-        forKey:@"content-length"];
+        forKey: @"content-length"];
     
     /* Note: the -init method will create a temporary file! */
     body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
@@ -497,55 +924,61 @@ static NSString    *fromInternetSuffixPattern = nil;
   return bodyPart;
 }
 
-- (NSArray *)bodyPartsForAllAttachments {
+- (NSArray *) bodyPartsForAllAttachments
+{
   /* returns nil on error */
-  NSMutableArray *bodyParts;
   NSArray  *names;
   unsigned i, count;
-  
+  NGMimeBodyPart *bodyPart;
+  NSMutableArray *bodyParts;
+
   names = [self fetchAttachmentNames];
-  if ((count = [names count]) == 0)
-    return [NSArray array];
-  
-  bodyParts = [NSMutableArray arrayWithCapacity:count];
-  for (i = 0; i < count; i++) {
-    NGMimeBodyPart *bodyPart;
-    
-    bodyPart = [self bodyPartForAttachmentWithName:[names objectAtIndex:i]];
-    if (bodyPart == nil)
-      return nil;
-    
-    [bodyParts addObject:bodyPart];
-  }
+  count = [names count];
+  bodyParts = [NSMutableArray arrayWithCapacity: count];
+
+  for (i = 0; i < count; i++)
+    {
+      bodyPart = [self bodyPartForAttachmentWithName: [names objectAtIndex: i]];
+      [bodyParts addObject: bodyPart];
+    }
+
   return bodyParts;
 }
 
-- (NGMimeMessage *)mimeMultiPartMessageWithHeaderMap:(NGMutableHashMap *)map
-  andBodyParts:(NSArray *)_bodyParts
+- (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
+                                        andBodyParts: (NSArray *) _bodyParts
 {
   NGMimeMessage       *message;  
   NGMimeMultipartBody *mBody;
   NGMimeBodyPart      *part;
   NSEnumerator        *e;
   
-  [map addObject:MultiMixedType forKey:@"content-type"];
-    
-  message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
-  mBody   = [[NGMimeMultipartBody alloc] initWithPart:message];
-  
+  [map addObject: MultiMixedType forKey: @"content-type"];
+
+  message = [[NGMimeMessage alloc] initWithHeader: map];
+  [message autorelease];
+  mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
+
   part = [self bodyPartForText];
-  [mBody addBodyPart:part];
-  
+  [mBody addBodyPart: part];
+
   e = [_bodyParts objectEnumerator];
-  while ((part = [e nextObject]) != nil)
-    [mBody addBodyPart:part];
-  
-  [message setBody:mBody];
-  [mBody release]; mBody = nil;
+  part = [e nextObject];
+  while (part)
+    {
+      [mBody addBodyPart: part];
+      part = [e nextObject];
+    }
+
+  [message setBody: mBody];
+  [mBody release];
+
   return message;
 }
 
-- (void)_addHeaders:(NSDictionary *)_h toHeaderMap:(NGMutableHashMap *)_map {
+- (void) _addHeaders: (NSDictionary *) _h
+         toHeaderMap: (NGMutableHashMap *) _map
+{
   NSEnumerator *names;
   NSString *name;
 
@@ -561,424 +994,239 @@ static NSString    *fromInternetSuffixPattern = nil;
   }
 }
 
-- (BOOL)isEmptyValue:(id)_value {
+- (BOOL) isEmptyValue: (id) _value
+{
   if (![_value isNotNull])
     return YES;
   
-  if ([_value isKindOfClass:[NSArray class]])
+  if ([_value isKindOfClass: [NSArray class]])
     return [_value count] == 0 ? YES : NO;
   
-  if ([_value isKindOfClass:[NSString class]])
+  if ([_value isKindOfClass: [NSString class]])
     return [_value length] == 0 ? YES : NO;
-  
+
   return NO;
 }
 
-- (NGMutableHashMap *)mimeHeaderMapWithHeaders:(NSDictionary *)_headers {
+- (NGMutableHashMap *) mimeHeaderMapWithHeaders: (NSDictionary *) _headers
+{
   NGMutableHashMap *map;
-  NSDictionary *lInfo; // TODO: this should be some kind of object?
   NSArray      *emails;
-  NSString     *s;
+  NSString     *s, *dateString;
   id           from, replyTo;
   
-  if ((lInfo = [self fetchInfo]) == nil)
-    return nil;
-  
   map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
   
   /* add recipients */
   
-  if ((emails = [lInfo objectForKey:@"to"]) != nil) {
-    if ([emails count] == 0) {
-      [self errorWithFormat:@"missing 'to' recipient in email!"];
-      return nil;
-    }
-    [map setObjects:emails forKey:@"to"];
-  }
-  if ((emails = [lInfo objectForKey:@"cc"]) != nil)
-    [map setObjects:emails forKey:@"cc"];
-  if ((emails = [lInfo objectForKey:@"bcc"]) != nil)
-    [map setObjects:emails forKey:@"bcc"];
-  
+  if ((emails = [headers objectForKey: @"to"]) != nil)
+    [map setObjects: emails forKey: @"to"];
+  if ((emails = [headers objectForKey: @"cc"]) != nil)
+    [map setObjects:emails forKey: @"cc"];
+  if ((emails = [headers objectForKey: @"bcc"]) != nil)
+    [map setObjects:emails forKey: @"bcc"];
+
   /* add senders */
   
-  from    = [lInfo objectForKey:@"from"];
-  replyTo = [lInfo objectForKey:@"replyTo"];
+  from = [headers objectForKey: @"from"];
+  replyTo = [headers objectForKey: @"replyTo"];
   
   if (![self isEmptyValue:from]) {
     if ([from isKindOfClass:[NSArray class]])
-      [map setObjects:from forKey:@"from"];
+      [map setObjects: from forKey: @"from"];
     else
-      [map setObject:from forKey:@"from"];
+      [map setObject: from forKey: @"from"];
   }
   
-  if (![self isEmptyValue:replyTo]) {
+  if (![self isEmptyValue: replyTo]) {
     if ([from isKindOfClass:[NSArray class]])
-      [map setObjects:from forKey:@"reply-to"];
+      [map setObjects:from forKey: @"reply-to"];
     else
-      [map setObject:from forKey:@"reply-to"];
+      [map setObject:from forKey: @"reply-to"];
   }
   else if (![self isEmptyValue:from])
-    [map setObjects:[map objectsForKey:@"from"] forKey:@"reply-to"];
+    [map setObjects:[map objectsForKey: @"from"] forKey: @"reply-to"];
   
   /* add subject */
-  
-  if ([(s = [lInfo objectForKey:@"subject"]) length] > 0)
-    [map setObject:s forKey:@"subject"];
+  if (inReplyTo)
+    [map setObject: inReplyTo forKey: @"in-reply-to"];
+
+  if ([(s = [headers objectForKey: @"subject"]) length] > 0)
+    [map setObject: [s asQPSubjectString: @"utf-8"]
+        forKey: @"subject"];
+//     [map setObject: [s asQPSubjectString: @"utf-8"] forKey: @"subject"];
   
   /* add standard headers */
-  
-  [map addObject:[NSCalendarDate date] forKey:@"date"];
-  [map addObject:@"1.0"                forKey:@"MIME-Version"];
-  [map addObject:userAgent             forKey:@"X-Mailer"];
+
+  dateString = [[NSCalendarDate date] rfc822DateString];
+  [map addObject: dateString forKey: @"date"];
+  [map addObject: @"1.0" forKey: @"MIME-Version"];
+  [map addObject: userAgent forKey: @"X-Mailer"];
 
   /* add custom headers */
   
-  [self _addHeaders:[lInfo objectForKey:@"headers"] toHeaderMap:map];
-  [self _addHeaders:_headers                        toHeaderMap:map];
+//   [self _addHeaders: [lInfo objectForKey: @"headers"] toHeaderMap:map];
+  [self _addHeaders: _headers toHeaderMap: map];
   
   return map;
 }
 
-- (NGMimeMessage *)mimeMessageWithHeaders:(NSDictionary *)_headers {
-  NSAutoreleasePool *pool;
+- (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
+{
   NGMutableHashMap  *map;
   NSArray           *bodyParts;
   NGMimeMessage     *message;
-  
-  pool = [[NSAutoreleasePool alloc] init];
-  
-  if ([self fetchInfo] == nil) {
-    [self errorWithFormat:@"could not locate draft fetch info!"];
-    return nil;
-  }
-  
-  if ((map = [self mimeHeaderMapWithHeaders:_headers]) == nil)
-    return nil;
-  [self debugWithFormat:@"MIME Envelope: %@", map];
-  
-  if ((bodyParts = [self bodyPartsForAllAttachments]) == nil) {
-    [self errorWithFormat:
-            @"could not create body parts for attachments!"];
-    return nil; // TODO: improve error handling, return exception
-  }
-  [self debugWithFormat:@"attachments: %@", bodyParts];
-  
-  if ([bodyParts count] == 0) {
-    /* no attachments */
-    message = [self mimeMessageForContentWithHeaderMap:map];
-  }
-  else {
-    /* attachments, create multipart/mixed */
-    message = [self mimeMultiPartMessageWithHeaderMap:map 
-                   andBodyParts:bodyParts];
-  }
-  [self debugWithFormat:@"message: %@", message];
-
-  message = [message retain];
-  [pool release];
-  return [message autorelease];
-}
-- (NGMimeMessage *)mimeMessage {
-  return [self mimeMessageWithHeaders:nil];
-}
 
-- (NSString *)saveMimeMessageToTemporaryFileWithHeaders:(NSDictionary *)_h {
-  NGMimeMessageGenerator *gen;
-  NSAutoreleasePool *pool;
-  NGMimeMessage *message;
-  NSString      *tmpPath;
+  message = nil;
+
+  map = [self mimeHeaderMapWithHeaders: _headers];
+  if (map)
+    {
+      [self debugWithFormat: @"MIME Envelope: %@", map];
+  
+      bodyParts = [self bodyPartsForAllAttachments];
+      if (bodyParts)
+       {
+         [self debugWithFormat: @"attachments: %@", bodyParts];
+  
+         if ([bodyParts count] == 0)
+           /* no attachments */
+           message = [self mimeMessageForContentWithHeaderMap: map];
+         else
+           /* attachments, create multipart/mixed */
+           message = [self mimeMultiPartMessageWithHeaderMap: map 
+                           andBodyParts: bodyParts];
+         [self debugWithFormat: @"message: %@", message];
+       }
+      else
+       [self errorWithFormat:
+               @"could not create body parts for attachments!"];
+    }
 
-  pool = [[NSAutoreleasePool alloc] init];
-  
-  message = [self mimeMessageWithHeaders:_h];
-  if (![message isNotNull])
-    return nil;
-  if ([message isKindOfClass:[NSException class]]) {
-    [self errorWithFormat:@"error: %@", message];
-    return nil;
-  }
-  
-  gen     = [[NGMimeMessageGenerator alloc] init];
-  tmpPath = [[gen generateMimeFromPartToFile:message] copy];
-  [gen release]; gen = nil;
-  
-  [pool release];
-  return [tmpPath autorelease];
+  return message;
 }
-- (NSString *)saveMimeMessageToTemporaryFile {
-  return [self saveMimeMessageToTemporaryFileWithHeaders:nil];
+
+- (NGMimeMessage *) mimeMessage
+{
+  return [self mimeMessageWithHeaders: nil];
 }
 
-- (void)deleteTemporaryMessageFile:(NSString *)_path {
-  NSFileManager *fm;
-  
-  if (![_path isNotNull])
-    return;
+- (NSData *) mimeMessageAsData
+{
+  NGMimeMessageGenerator *generator;
+  NSData *message;
 
-  fm = [NSFileManager defaultManager];
-  if (![fm fileExistsAtPath:_path])
-    return;
-  
-  [fm removeFileAtPath:_path handler:nil];
-}
+  generator = [NGMimeMessageGenerator new];
+  message = [generator generateMimeFromPart: [self mimeMessage]];
+  [generator release];
 
-- (NSArray *)allRecipients {
-  NSDictionary   *lInfo;
-  NSMutableArray *ma;
-  NSArray        *tmp;
-  
-  if ((lInfo = [self fetchInfo]) == nil)
-    return nil;
-  
-  ma = [NSMutableArray arrayWithCapacity:16];
-  if ((tmp = [lInfo objectForKey:@"to"]) != nil)
-    [ma addObjectsFromArray:tmp];
-  if ((tmp = [lInfo objectForKey:@"cc"]) != nil)
-    [ma addObjectsFromArray:tmp];
-  if ((tmp = [lInfo objectForKey:@"bcc"]) != nil)
-    [ma addObjectsFromArray:tmp];
-  return ma;
+  return message;
 }
 
-- (NSException *)sendMimeMessageAtPath:(NSString *)_path {
-  static NGSendMail *mailer = nil;
-  NSArray  *recipients;
-  NSString *from;
-  
-  /* validate */
-  
-  recipients = [self allRecipients];
-  from       = [self sender];
-  if ([recipients count] == 0) {
-    return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"draft has no recipients set!"];
-  }
-  if ([from length] == 0) {
-    return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"draft has no sender (from) set!"];
-  }
-  
-  /* setup mailer object */
-  
-  if (mailer == nil)
-    mailer = [[NGSendMail sharedSendMail] retain];
-  if (![mailer isSendMailAvailable]) {
-    [self errorWithFormat:@"missing sendmail binary!"];
-    return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"did not find sendmail binary!"];
-  }
-  
-  /* send mail */
-  
-  return [mailer sendMailAtPath:_path toRecipients:recipients sender:from];
+- (NSArray *) allRecipients
+{
+  NSMutableArray *allRecipients;
+  NSArray *recipients;
+  NSString *fieldNames[] = {@"to", @"cc", @"bcc"};
+  unsigned int count;
+
+  allRecipients = [NSMutableArray arrayWithCapacity: 16];
+
+  for (count = 0; count < 3; count++)
+    {
+      recipients = [headers objectForKey: fieldNames[count]];
+      if ([recipients count] > 0)
+       [allRecipients addObjectsFromArray: recipients];
+    }
+
+  return allRecipients;
 }
 
-- (NSException *)sendMail {
+- (NSException *) sendMail
+{
   NSException *error;
-  NSString    *tmpPath;
-  
-  /* save MIME mail to file */
-  
-  tmpPath = [self saveMimeMessageToTemporaryFile];
-  if (![tmpPath isNotNull]) {
-    return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"could not save MIME message for draft!"];
-  }
+  SOGoMailFolder *sentFolder;
+  NSData *message;
+  NSURL *sourceIMAP4URL;
   
   /* send mail */
-  error = [self sendMimeMessageAtPath:tmpPath];
-  
-  /* delete temporary file */
-  [self deleteTemporaryMessageFile:tmpPath];
+  sentFolder = [[self mailAccountFolder] sentFolderInContext: context];
+  if ([sentFolder isKindOfClass: [NSException class]])
+    error = (NSException *) sentFolder;
+  else
+    {
+      message = [self mimeMessageAsData];
+      error = [[SOGoMailer sharedMailer] sendMailData: message
+                                        toRecipients: [self allRecipients]
+                                        sender: [self sender]];
+      if (!error)
+       {
+         error = [sentFolder postData: message flags: @"seen"];
+         if (!error)
+           {
+             [self imap4Connection];
+             if (IMAP4ID > -1)
+               [imap4 markURLDeleted: [self imap4URL]];
+             if (sourceURL && sourceFlag)
+               {
+                 sourceIMAP4URL = [NSURL URLWithString: sourceURL];
+                 [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
+               }
+             if (!draftDeleteDisabled)
+               error = [self delete];
+           }
+       }
+    }
 
   return error;
 }
 
-/* operations */
+- (NSException *) delete
+{
+  NSException *error;
 
-- (NSException *)delete {
-  NSFileManager *fm;
-  NSString      *p, *sp;
-  NSEnumerator  *e;
-  
-  if ((fm = [self spoolFileManager]) == nil) {
-    [self errorWithFormat:@"missing spool file manager!"];
-    return [NSException exceptionWithHTTPStatus:500 /* server error */
-                       reason:@"missing spool file manager!"];
-  }
-  
-  p = [self draftFolderPath];
-  if (![fm fileExistsAtPath:p]) {
-    return [NSException exceptionWithHTTPStatus:404 /* not found */
-                       reason:@"did not find draft!"];
-  }
-  
-  e = [[fm directoryContentsAtPath:p] objectEnumerator];
-  while ((sp = [e nextObject])) {
-    sp = [p stringByAppendingPathComponent:sp];
-    if (draftDeleteDisabled) {
-      [self logWithFormat:@"should delete draft file %@ ...", sp];
-      continue;
-    }
-    
-    if (![fm removeFileAtPath:sp handler:nil]) {
-      return [NSException exceptionWithHTTPStatus:500 /* server error */
-                         reason:@"failed to delete draft!"];
-    }
-  }
+  if ([[NSFileManager defaultManager]
+       removeFileAtPath: [self draftFolderPath]
+       handler: nil])
+    error = nil;
+  else
+    error = [NSException exceptionWithHTTPStatus: 500 /* server error */
+                        reason: @"could not delete draft"];
 
-  if (draftDeleteDisabled) {
-    [self logWithFormat:@"should delete draft directory: %@", p];
-  }
-  else {
-    if (![fm removeFileAtPath:p handler:nil]) {
-      return [NSException exceptionWithHTTPStatus:500 /* server error */
-                         reason:@"failed to delete draft directory!"];
-    }
-  }
-  return nil;
+  return error;
 }
 
-- (NSData *)content {
-  /* Note: does not cache, expensive operation */
-  NSData   *data;
-  NSString *p;
-  
-  if ((p = [self saveMimeMessageToTemporaryFile]) == nil)
-    return nil;
-  
-  data = [NSData dataWithContentsOfMappedFile:p];
-  
-  /* delete temporary file */
-  [self deleteTemporaryMessageFile:p];
+/* operations */
 
-  return data;
-}
-- (NSString *)contentAsString {
+- (NSString *) contentAsString
+{
   NSString *str;
-  NSData   *data;
-  
-  if ((data = [self content]) == nil)
-    return nil;
-  
-  str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
-  if (str == nil) {
-    [self errorWithFormat:@"could not load draft as ASCII (data size=%d)",
-         [data length]];
-    return nil;
-  }
-
-  return [str autorelease];
-}
-
-/* actions */
-
-- (id)DELETEAction:(id)_ctx {
-  NSException *error;
-
-  if ((error = [self delete]) != nil)
-    return error;
-  
-  return [NSNumber numberWithBool:YES]; /* delete worked out ... */
-}
-
-- (id)GETAction:(id)_ctx {
-  /* 
-     Override, because SOGoObject's GETAction uses the less efficient
-     -contentAsString method.
-  */
-  WORequest *rq;
-
-  rq = [_ctx request];
-  if ([rq isSoWebDAVRequest]) {
-    WOResponse *r;
-    NSData     *content;
-    
-    if ((content = [self content]) == nil) {
-      return [NSException exceptionWithHTTPStatus:500
-                         reason:@"Could not generate MIME content!"];
+  NSData *message;
+
+  message = [self mimeMessageAsData];
+  if (message)
+    {
+      str = [[NSString alloc] initWithData: message
+                             encoding: NSUTF8StringEncoding];
+      if (!str)
+       [self errorWithFormat: @"could not load draft as UTF-8 (data size=%d)",
+             [message length]];
+      else
+       [str autorelease];
+    }
+  else
+    {
+      [self errorWithFormat: @"message data is empty"];
+      str = nil;
     }
-    r = [_ctx response];
-    [r setHeader:@"message/rfc822" forKey:@"content-type"];
-    [r setContent:content];
-    return r;
-  }
-  
-  return [super GETAction:_ctx];
-}
-
-/* fake being a SOGoMailObject */
-
-- (id)fetchParts:(NSArray *)_parts {
-  return [NSDictionary dictionaryWithObject:self forKey:@"fetch"];
-}
-
-- (NSString *)uid {
-  return [self nameInContainer];
-}
-- (NSArray *)flags {
-  static NSArray *seenFlags = nil;
-  seenFlags = [[NSArray alloc] initWithObjects:@"seen", nil];
-  return seenFlags;
-}
-- (unsigned)size {
-  // TODO: size, hard to support, we would need to generate MIME?
-  return 0;
-}
-
-- (NSArray *)imap4EnvelopeAddressesForStrings:(NSArray *)_emails {
-  NSMutableArray *ma;
-  unsigned i, count;
-  
-  if (_emails == nil)
-    return nil;
-  if ((count = [_emails count]) == 0)
-    return [NSArray array];
-
-  ma = [NSMutableArray arrayWithCapacity:count];
-  for (i = 0; i < count; i++) {
-    NGImap4EnvelopeAddress *envaddr;
-
-    envaddr = [[NGImap4EnvelopeAddress alloc] 
-               initWithString:[_emails objectAtIndex:i]];
-    if ([envaddr isNotNull])
-      [ma addObject:envaddr];
-    [envaddr release];
-  }
-  return ma;
-}
-
-- (NGImap4Envelope *)envelope {
-  NSDictionary *lInfo;
-  id from, replyTo;
-  
-  if (self->envelope != nil)
-    return self->envelope;
-  if ((lInfo = [self fetchInfo]) == nil)
-    return nil;
-  
-  if ((from = [self sender]) != nil)
-    from = [NSArray arrayWithObjects:&from count:1];
 
-  if ((replyTo = [lInfo objectForKey:@"replyTo"]) != nil) {
-    if (![replyTo isKindOfClass:[NSArray class]])
-      replyTo = [NSArray arrayWithObjects:&replyTo count:1];
-  }
-  
-  self->envelope = 
-    [[NGImap4Envelope alloc] initWithMessageID:[self nameInContainer]
-                            subject:[lInfo objectForKey:@"subject"]
-                            from:from replyTo:replyTo
-                            to:[lInfo objectForKey:@"to"]
-                            cc:[lInfo objectForKey:@"cc"]
-                            bcc:[lInfo objectForKey:@"bcc"]];
-  return self->envelope;
+  return str;
 }
 
 /* debugging */
 
-- (BOOL)isDebuggingEnabled {
+- (BOOL) isDebuggingEnabled
+{
   return debugOn;
 }