]> err.no Git - scalable-opengroupware.org/commitdiff
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1149 d1b88da0-ebda-0310...
authorwolfgang <wolfgang@d1b88da0-ebda-0310-925b-ed51d893ca5b>
Sat, 18 Aug 2007 20:41:56 +0000 (20:41 +0000)
committerwolfgang <wolfgang@d1b88da0-ebda-0310-925b-ed51d893ca5b>
Sat, 18 Aug 2007 20:41:56 +0000 (20:41 +0000)
ChangeLog
SoObjects/Mailer/SOGoDraftObject.h
SoObjects/Mailer/SOGoDraftObject.m
SoObjects/Mailer/SOGoMailObject+Draft.h
SoObjects/Mailer/SOGoMailObject+Draft.m
SoObjects/Mailer/SOGoMailObject.m
UI/MailerUI/UIxMailActions.m
UI/MailerUI/product.plist
UI/Templates/MailerUI/UIxMailEditor.wox
UI/WebServerResources/MailerUI.js
UI/WebServerResources/UIxMailEditor.js

index cf536a10df715d1a56142277e7eef6da9526ed8a..17ad6ecd96984a10f1dda1885b6520e33d3a46cf 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,30 @@
+2007-08-18  Wolfgang Sourdeau  <wsourdeau@inverse.ca>
+
+       * UI/MailerUI/UIxMailActions.m ([UIxMailActions -deleteAction]):
+       new category method to reponds to the "delete" web command on
+       drafts.
+       ([UIxMailActions -deleteAttachmentAction]): new category method to
+       reponds to the "deleteAttachment" web command on drafts, taking
+       the "filename" url parameter into account.
+
+       * SoObjects/Mailer/SOGoMailObject+Draft.m ([SOGoMailObject
+       -contentForEditing]): new method that retrieve the editable mail
+       content.
+       ([SOGoMailObject -fetchFileAttachmentKeys]): new method that
+       returns the body keys for attached files (parts with a "filename"
+       attribute).
+
+       * SoObjects/Mailer/SOGoDraftObject.m ([NSString
+       -asQPSubjectString:encoding]): do not change the string if the
+       encoded string has the same length (which means it is already
+       7bit-safe).
+       ([SOGoDraftObject -fetchMailForEditing:sourceMail]): new method
+       that retrieve a draft along with its attachments for editing.
+       ([SOGoDraftObject -mimeHeaderMapWithHeaders:_headers]): no longer
+       choke if the "to" header field is empty.
+       ([SOGoDraftObject -delete]): new method to delete the draft folder
+       whenever operations are done.
+
 2007-08-16  Wolfgang Sourdeau  <wsourdeau@inverse.ca>
 
        * UI/MailerUI/UIxMailEditor.m ([-patchFlagsInStore]): removed
index 95b7c5fc406258a386ccecf96de7ff732e4f5021..0a2415e81abb2ef49ae9be1ad5d2f2200e20bc88 100644 (file)
@@ -94,6 +94,7 @@
 
 /* operations */
 
+- (NSException *) delete;
 - (NSException *) sendMail;
 - (NSException *) save;
 
index e42ec5e4ba86947642d7926d917671f9515c9585..55b98302fa75b59c1b47b772d0b3702083f484a1 100644 (file)
@@ -51,6 +51,7 @@
 #import <NGMime/NGMimeType.h>
 #import <NGMime/NGMimeHeaderFieldGenerator.h>
 
+#import <SoObjects/SOGo/NSArray+Utilities.h>
 #import <SoObjects/SOGo/NSCalendarDate+SOGo.h>
 #import <SoObjects/SOGo/SOGoMailer.h>
 #import "SOGoMailAccount.h"
@@ -74,7 +75,7 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
 
 - (NSString *) asQPSubjectString: (NSString *) encoding;
 {
-  NSString *qpString;
+  NSString *qpString, *subjectString;
   NSData *subjectData, *destSubjectData;
 
   subjectData = [self dataUsingEncoding: NSUTF8StringEncoding];
@@ -83,8 +84,13 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
   qpString = [[NSString alloc] initWithData: destSubjectData
                               encoding: NSASCIIStringEncoding];
   [qpString autorelease];
+  if ([qpString length] > [self length])
+    subjectString = [NSString stringWithFormat: @"=?%@?Q?%@?=",
+                             encoding, qpString];
+  else
+    subjectString = self;
 
-  return [NSString stringWithFormat: @"=?%@?Q?%@?=", encoding, qpString];
+  return subjectString;
 }
 
 @end
@@ -365,11 +371,6 @@ static BOOL        showTextAttachmentsInline  = NO;
   return error;
 }
 
-- (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
-{
-#warning unimplemented
-}
-
 - (void) _addEMailsOfAddresses: (NSArray *) _addrs
                       toArray: (NSMutableArray *) _ma
 {
@@ -424,6 +425,75 @@ static BOOL        showTextAttachmentsInline  = NO;
     }
 }
 
+- (void) _fetchAttachments: (NSArray *) parts
+                  fromMail: (SOGoMailObject *) sourceMail
+{
+  unsigned int count, max;
+  NSDictionary *currentPart, *attachment, *body;
+  NSArray *paths, *result;
+
+  max = [parts count];
+  if (max > 0)
+    {
+      paths = [parts keysWithFormat: @"BODY[%{path}]"];
+      result = [[sourceMail fetchParts: paths] objectForKey: @"fetch"];
+      for (count = 0; count < max; count++)
+       {
+         currentPart = [parts objectAtIndex: count];
+         body = [[result objectAtIndex: count] objectForKey: @"body"];
+         attachment = [NSDictionary dictionaryWithObjectsAndKeys:
+                                      [currentPart objectForKey: @"filename"],
+                                    @"filename",
+                                    [currentPart objectForKey: @"mimetype"],
+                                    @"mime-type",
+                                    nil];
+         [self saveAttachment: [body objectForKey: @"data"]
+               withMetadata: attachment];
+       }
+    }
+}
+
+- (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
 {
@@ -726,9 +796,9 @@ static BOOL        showTextAttachmentsInline  = NO;
     cdtype = @"attachment";
   
   cd = [cdtype stringByAppendingString: @"; filename=\""];
-  cd = [cd stringByAppendingString:_name];
+  cd = [cd stringByAppendingString: _name];
   cd = [cd stringByAppendingString: @"\""];
-  
+
   // TODO: add size parameter (useful addition, RFC 2183)
   return cd;
 }
@@ -923,21 +993,16 @@ static BOOL        showTextAttachmentsInline  = NO;
   
   /* add recipients */
   
-  if ((emails = [headers objectForKey: @"to"]) != nil) {
-    if ([emails count] == 0) {
-      [self errorWithFormat: @"missing 'to' recipient in email!"];
-      return nil;
-    }
+  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    = [headers objectForKey: @"from"];
+  from = [headers objectForKey: @"from"];
   replyTo = [headers objectForKey: @"replyTo"];
   
   if (![self isEmptyValue:from]) {
@@ -1079,6 +1144,8 @@ static BOOL        showTextAttachmentsInline  = NO;
                  sourceIMAP4URL = [NSURL URLWithString: sourceURL];
                  [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
                }
+             if (!draftDeleteDisabled)
+               error = [self delete];
            }
        }
     }
@@ -1086,6 +1153,21 @@ static BOOL        showTextAttachmentsInline  = NO;
   return error;
 }
 
+- (NSException *) delete
+{
+  NSException *error;
+
+  if ([[NSFileManager defaultManager]
+       removeFileAtPath: [self draftFolderPath]
+       handler: nil])
+    error = nil;
+  else
+    error = [NSException exceptionWithHTTPStatus: 500 /* server error */
+                        reason: @"could not delete draft"];
+
+  return error;
+}
+
 /* operations */
 
 - (NSString *) contentAsString
index 5da1cdaee2a7dd58ea1356b31a1e6926dc63fc6d..ddc534adc457c9dbfdbe12ff2f654e5140ba4dba 100644 (file)
@@ -27,6 +27,9 @@
 
 @interface SOGoMailObject (SOGoDraftObjectExtensions)
 
+- (NSString *) contentForEditing;
+- (NSArray *) fetchFileAttachmentKeys;
+
 - (NSString *) subjectForReply;
 - (NSString *) contentForReply;
 
index a5d8da06286dd3a5f80dd29930b4fc217163a136..12e9341b6f8335b99f823c8daf22f417001d8721 100644 (file)
@@ -62,8 +62,8 @@
   return newSubject;
 }
 
-- (NSString *) contentForReplyOnParts: (NSDictionary *) _prts
-                                 keys: (NSArray *) _k
+- (NSString *) contentForEditingOnParts: (NSDictionary *) _prts
+                                  keys: (NSArray *) _k
 {
   static NSString *textPartSeparator = @"\n---\n";
   NSMutableString *ms;
@@ -92,7 +92,7 @@
        {
          if (count > 0)
            [ms appendString: textPartSeparator];
-         [ms appendString: [v stringByApplyingMailQuoting]];
+         [ms appendString: v];
        }
       else
        [self logWithFormat:@"Note: cannot show part %@", k];
 
 #warning this method should be fixed to return the first available text/plain \
          part, and otherwise the first text/html part converted to text
-- (NSString *) contentForReply
+- (NSString *) contentForEditing
 {
   NSArray *keys;
   NSDictionary *parts;
   NSMutableArray *topLevelKeys = nil;
   unsigned int count, max;
   NSRange r;
-  NSString *contentForReply;
+  NSString *contentForEditing;
 
 //   SOGoMailObject *co;
 
        }
 
       parts = [self fetchPlainTextStrings: keys];
-      contentForReply = [self contentForReplyOnParts: parts
-                             keys: keys];
+      contentForEditing = [self contentForEditingOnParts: parts
+                               keys: keys];
     }
   else
-    contentForReply = nil;
+    contentForEditing = nil;
 
-  return contentForReply;
+  return contentForEditing;
+}
+
+- (NSString *) contentForReply
+{
+  return [[self contentForEditing] stringByApplyingMailQuoting];
 }
 
 - (NSString *) filenameForForward
   return newSubject;
 }
 
+- (void) _fetchFileAttachmentKey: (NSDictionary *) part
+                      intoArray: (NSMutableArray *) keys
+                       withPath: (NSString *) path
+{
+  NSDictionary *parameters, *currentFile;
+  NSString *filename, *mimeType;
+
+  parameters = [[part objectForKey: @"disposition"]
+                objectForKey: @"parameterList"];
+  if (parameters)
+    {
+      filename = [parameters objectForKey: @"filename"];
+      mimeType = [NSString stringWithFormat: @"%@/%@",
+                          [part objectForKey: @"type"],
+                          [part objectForKey: @"subtype"]];
+      currentFile = [NSDictionary dictionaryWithObjectsAndKeys:
+                                   filename, @"filename",
+                                 [mimeType lowercaseString], @"mimetype",
+                                 path, @"path", nil];
+      [keys addObject: currentFile];
+    }
+}
+
+- (void) _fetchFileAttachmentKeysInPart: (NSDictionary *) part
+                              intoArray: (NSMutableArray *) keys
+                              withPath: (NSString *) path
+{
+  NSEnumerator *subparts;
+  NSString *type;
+  unsigned int count;
+  NSDictionary *currentPart;
+  NSString *newPath;
+
+  type = [[part objectForKey: @"type"] lowercaseString];
+  if ([type isEqualToString: @"multipart"])
+    {
+      subparts = [[part objectForKey: @"parts"] objectEnumerator];
+      currentPart = [subparts nextObject];
+      count = 1;
+      while (currentPart)
+       {
+         if (path)
+           newPath = [NSString stringWithFormat: @"%@.%d", path, count];
+         else
+           newPath = [NSString stringWithFormat: @"%d", count];
+         [self _fetchFileAttachmentKeysInPart: currentPart
+               intoArray: keys
+               withPath: newPath];
+         currentPart = [subparts nextObject];
+         count++;
+       }
+    }
+  else
+    [self _fetchFileAttachmentKey: part intoArray: keys withPath: path];
+}
+
+#warning we might need to handle parts with a "name" attribute
+- (NSArray *) fetchFileAttachmentKeys
+{
+  NSMutableArray *keys;
+
+  keys = [NSMutableArray array];
+  [self _fetchFileAttachmentKeysInPart: [self bodyStructure]
+       intoArray: keys withPath: nil];
+
+  return keys;
+}
+
 @end
index 9465de049c5b3bee04a7204671bd13564f622729..5921f289c6485badf65d055bc03a395e88f3e150 100644 (file)
@@ -221,21 +221,17 @@ static BOOL debugSoParts       = NO;
 - (id) fetchCoreInfos
 {
   id msgs;
-  
-  if (coreInfos != nil)
-    return [coreInfos isNotNull] ? 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;
-  
-  coreInfos = [[msgs objectAtIndex:0] retain];
+
+  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;
 }
@@ -273,24 +269,33 @@ static BOOL debugSoParts       = NO;
   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 {
+- (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;
   
@@ -493,7 +498,6 @@ static BOOL debugSoParts       = NO;
   NSString *sp;
   id childInfo;
     
-
   /* Note: if the part itself doesn't qualify, we still check subparts */
   fetchPart = [self shouldFetchPartOfType: [_info valueForKey: @"type"]
                    subtype: [_info valueForKey: @"subtype"]];
@@ -556,12 +560,14 @@ static BOOL debugSoParts       = NO;
   NSMutableArray *ma;
   
   ma = [NSMutableArray arrayWithCapacity:4];
-  [self addRequiredKeysOfStructure: [[self clientObject] bodyStructure]
+  [self addRequiredKeysOfStructure: [self bodyStructure]
        path: @"" toArray: ma recurse: YES];
+
   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;
index 7e177cda385e7806247a5ab0f3bc9ffc0f5223b1..b8ca9cf9fb9ed86eddf62183669a0e639cb11f2f 100644 (file)
@@ -22,6 +22,9 @@
 
 #import <Foundation/NSString.h>
 
+#import <NGObjWeb/WOContext.h>
+#import <NGObjWeb/WORequest.h>
+#import <NGObjWeb/WOResponse.h>
 #import <SoObjects/Mailer/SOGoDraftObject.h>
 #import <SoObjects/Mailer/SOGoDraftsFolder.h>
 #import <SoObjects/Mailer/SOGoMailAccount.h>
   return [self redirectToLocation: newLocation];
 }
 
+/* SOGoDraftObject */
+- (id) deleteAction
+{
+  SOGoDraftObject *draft;
+  NSException *error;
+  id response;
+
+  draft = [self clientObject];
+  error = [draft delete];
+  if (error)
+    response = error;
+  else
+    {
+      response = [context response];
+      [response setStatus: 204];
+    }
+
+  return response;
+}
+
+- (WOResponse *) deleteAttachmentAction
+{
+  WOResponse *response;
+  NSString *filename;
+
+  response = [context response];
+
+  filename = [[context request] formValueForKey: @"filename"];
+  if ([filename length] > 0)
+    {
+      [[self clientObject] deleteAttachmentWithName: filename];
+      [response setStatus: 204];
+    }
+  else
+    {
+      [response setStatus: 500];
+      [response appendContentString: @"How did you end up here?"];
+    }
+
+  return response;
+}
+
 @end
index f974b07284cfa58df06eda0d753c29fbd4179ce9..843c51c085d445cf1ee0c22c8aabff5ad60069e3 100644 (file)
@@ -342,6 +342,16 @@ categories = {
            pageName    = "UIxMailEditor";
            actionName  = "send";
         };
+        delete = {
+           protectedBy = "View";
+           actionClass = "UIxMailActions";
+           actionName = "delete";
+        };
+        deleteAttachment = {
+           protectedBy = "View";
+           actionClass = "UIxMailActions";
+           actionName = "deleteAttachment";
+        };
       };
    };
 
index 229a502b1c39adbe12964d73d72efdee9bdd78a2..2106c7ba9a6e9bd5f26abe1e850a9343e2bb4df1 100644 (file)
     </ul>
   </div>
 
-  <form name="pageform" enctype="multipart/form-data">
+  <form name="pageform" enctype="multipart/form-data" accept-charset="UTF-8">
     <div id="headerArea">
       <div id="attachmentsArea">
        <var:string label:value="Attachments:" />
        <ul id="attachments">
          <var:foreach list="attachmentNames" item="attachmentName"
-           ><img rsrc:img="attachment.gif"
-             /><var:string value="attachmentName"
-             /></var:foreach>
+           ><li><img rsrc:src="attachment.gif"
+               /><var:string value="attachmentName"
+               /></li></var:foreach>
        </ul>
       </div>
       <span class="headerField"><var:string label:value="From" />:</span>
index 15e4347131824d0b2c8624e453c2e7ddba47dde8..d0aa924f97579fa26c5d621c2ff042002a93be0c 100644 (file)
@@ -252,7 +252,7 @@ function uixDeleteSelectedMessages(sender) {
     var messageId = currentMailbox + "/" + rowId;
     url = ApplicationBaseURL + messageId + "/trash?jsonly=1";
     http = createHTTPClient();
-    http.open("GET", url, false /* not async */);
+    http.open("POST", url, false /* not async */);
     http.send("");
     if (http.status != 200) { /* request failed */
       failCount++;
@@ -404,10 +404,14 @@ function openMailbox(mailbox, reload, idx) {
     var url = ApplicationBaseURL + mailbox + "/view?noframe=1";
     var messageContent = $("messageContent");
     messageContent.innerHTML = '';
-      
-    if (currentMessages[mailbox]) {
-      loadMessage(currentMessages[mailbox]);
-      url += '&pageforuid=' + currentMessages[mailbox];
+
+    var currentMessage;
+    if (!idx) {
+      currentMessage = currentMessages[mailbox];
+      if (currentMessage) {
+       loadMessage(currentMessage);
+       url += '&pageforuid=' + currentMessage;
+      }
     }
 
     var searchValue = search["value"];
@@ -438,7 +442,7 @@ function openMailbox(mailbox, reload, idx) {
 
     document.messageListAjaxRequest
       = triggerAjaxRequest(url, messageListCallback,
-                          currentMessages[mailbox]);
+                          currentMessage);
 
     var quotasUrl = ApplicationBaseURL + mailbox + "/quotas";
     document.quotasAjaxRequest
index a38d0088e6a07a26650365a1a13043ef87fbcf9e..6f8018e4f887bf20b452c89fd66ccb25b2479fdd 100644 (file)
@@ -144,6 +144,7 @@ function clickedEditorSend(sender) {
    if (!validateEditorInput(sender))
       return false;
 
+   window.shouldPreserve = true;
    document.pageform.action = "send";
    document.pageform.submit();
 
@@ -152,7 +153,6 @@ function clickedEditorSend(sender) {
 
 function clickedEditorAttach(sender) {
   var area = $("attachmentsArea");
-
   area.setStyle({ display: "block" });
   
   var inputs = area.getElementsByTagName("input");
@@ -210,20 +210,13 @@ function createAttachment(node, list) {
 }
 
 function clickedEditorSave(sender) {
+  window.shouldPreserve = true;
   document.pageform.action = "save";
   document.pageform.submit();
 
   return false;
 }
 
-function clickedEditorDelete(sender) {
-  document.pageform.action = "delete";
-  document.pageform.submit();
-  window.close();
-
-  return false;
-}
-
 function initMailEditor() {
   var list = $("attachments");
   $(list).attachMenu("attachmentsMenu");
@@ -232,8 +225,14 @@ function initMailEditor() {
     Event.observe(elements[i], "click",
                  onRowClick.bindAsEventListener(elements[i]));
   }
+
+  var listContent = $("attachments").childNodesWithTag("li");
+  if (listContent.length > 0)
+    $("attachmentsArea").setStyle({ display: "block" });
+
   onWindowResize(null);
   Event.observe(window, "resize", onWindowResize);
+  Event.observe(window, "beforeunload", onMailEditorClose);
 }
 
 function getMenus() {
@@ -252,8 +251,31 @@ function onRemoveAttachments() {
       input.parentNode.removeChild(input);
       list.removeChild(nodes[i]);
     }
+    else {
+      var filename = "";
+      var childNodes = nodes[i].childNodes;
+      for (var j = 0; j < childNodes.length; j++) {
+       if (childNodes[j].nodeType == 3)
+         filename += childNodes[j].nodeValue;
+      }
+      var url = "" + window.location;
+      var parts = url.split("/");
+      parts[parts.length-1] = "deleteAttachment?filename=" + encodeURIComponent(filename);
+      url = parts.join("/");
+      triggerAjaxRequest(url, attachmentDeleteCallback,
+                        nodes[i]);
+    }
+  }
+}
+
+function attachmentDeleteCallback(http) {
+  if (http.readyState == 4) {
+    if (http.status == 204) {
+      var node = http.callbackData;
+      node.parentNode.removeChild(node);
+    }
     else
-      window.alert("Server attachments not handled");
+      log("attachmentDeleteCallback: an error occured: " + http.responseText);
   }
 }
 
@@ -274,4 +296,19 @@ function onWindowResize(event) {
   log ("onWindowResize new number of rows = " + textarea.rows);
 }
 
+function onMailEditorClose(event) {
+  if (window.shouldPreserve)
+    window.shouldPreserve = false;
+  else {
+    var url = "" + window.location;
+    var parts = url.split("/");
+    parts[parts.length-1] = "delete";
+    url = parts.join("/");
+
+    http = createHTTPClient();
+    http.open("POST", url, false /* not async */);
+    http.send("");
+  }
+}
+
 addEvent(window, 'load', initMailEditor);