From 3ac68938ac9be655dd515c3c659ef3c3273c118e Mon Sep 17 00:00:00 2001 From: wolfgang Date: Sat, 18 Aug 2007 20:41:56 +0000 Subject: [PATCH] git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1149 d1b88da0-ebda-0310-925b-ed51d893ca5b --- ChangeLog | 27 ++++++ SoObjects/Mailer/SOGoDraftObject.h | 1 + SoObjects/Mailer/SOGoDraftObject.m | 116 ++++++++++++++++++++---- SoObjects/Mailer/SOGoMailObject+Draft.h | 3 + SoObjects/Mailer/SOGoMailObject+Draft.m | 91 +++++++++++++++++-- SoObjects/Mailer/SOGoMailObject.m | 54 ++++++----- UI/MailerUI/UIxMailActions.m | 45 +++++++++ UI/MailerUI/product.plist | 10 ++ UI/Templates/MailerUI/UIxMailEditor.wox | 8 +- UI/WebServerResources/MailerUI.js | 16 ++-- UI/WebServerResources/UIxMailEditor.js | 57 ++++++++++-- 11 files changed, 358 insertions(+), 70 deletions(-) diff --git a/ChangeLog b/ChangeLog index cf536a10..17ad6ecd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,30 @@ +2007-08-18 Wolfgang Sourdeau + + * 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 * UI/MailerUI/UIxMailEditor.m ([-patchFlagsInStore]): removed diff --git a/SoObjects/Mailer/SOGoDraftObject.h b/SoObjects/Mailer/SOGoDraftObject.h index 95b7c5fc..0a2415e8 100644 --- a/SoObjects/Mailer/SOGoDraftObject.h +++ b/SoObjects/Mailer/SOGoDraftObject.h @@ -94,6 +94,7 @@ /* operations */ +- (NSException *) delete; - (NSException *) sendMail; - (NSException *) save; diff --git a/SoObjects/Mailer/SOGoDraftObject.m b/SoObjects/Mailer/SOGoDraftObject.m index e42ec5e4..55b98302 100644 --- a/SoObjects/Mailer/SOGoDraftObject.m +++ b/SoObjects/Mailer/SOGoDraftObject.m @@ -51,6 +51,7 @@ #import #import +#import #import #import #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 diff --git a/SoObjects/Mailer/SOGoMailObject+Draft.h b/SoObjects/Mailer/SOGoMailObject+Draft.h index 5da1cdae..ddc534ad 100644 --- a/SoObjects/Mailer/SOGoMailObject+Draft.h +++ b/SoObjects/Mailer/SOGoMailObject+Draft.h @@ -27,6 +27,9 @@ @interface SOGoMailObject (SOGoDraftObjectExtensions) +- (NSString *) contentForEditing; +- (NSArray *) fetchFileAttachmentKeys; + - (NSString *) subjectForReply; - (NSString *) contentForReply; diff --git a/SoObjects/Mailer/SOGoMailObject+Draft.m b/SoObjects/Mailer/SOGoMailObject+Draft.m index a5d8da06..12e9341b 100644 --- a/SoObjects/Mailer/SOGoMailObject+Draft.m +++ b/SoObjects/Mailer/SOGoMailObject+Draft.m @@ -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]; @@ -103,14 +103,14 @@ #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; @@ -147,13 +147,18 @@ } 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 @@ -202,4 +207,72 @@ 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 diff --git a/SoObjects/Mailer/SOGoMailObject.m b/SoObjects/Mailer/SOGoMailObject.m index 9465de04..5921f289 100644 --- a/SoObjects/Mailer/SOGoMailObject.m +++ b/SoObjects/Mailer/SOGoMailObject.m @@ -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; diff --git a/UI/MailerUI/UIxMailActions.m b/UI/MailerUI/UIxMailActions.m index 7e177cda..b8ca9cf9 100644 --- a/UI/MailerUI/UIxMailActions.m +++ b/UI/MailerUI/UIxMailActions.m @@ -22,6 +22,9 @@ #import +#import +#import +#import #import #import #import @@ -104,4 +107,46 @@ 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 diff --git a/UI/MailerUI/product.plist b/UI/MailerUI/product.plist index f974b072..843c51c0 100644 --- a/UI/MailerUI/product.plist +++ b/UI/MailerUI/product.plist @@ -342,6 +342,16 @@ categories = { pageName = "UIxMailEditor"; actionName = "send"; }; + delete = { + protectedBy = "View"; + actionClass = "UIxMailActions"; + actionName = "delete"; + }; + deleteAttachment = { + protectedBy = "View"; + actionClass = "UIxMailActions"; + actionName = "deleteAttachment"; + }; }; }; diff --git a/UI/Templates/MailerUI/UIxMailEditor.wox b/UI/Templates/MailerUI/UIxMailEditor.wox index 229a502b..2106c7ba 100644 --- a/UI/Templates/MailerUI/UIxMailEditor.wox +++ b/UI/Templates/MailerUI/UIxMailEditor.wox @@ -21,15 +21,15 @@ -
+
    + >
: diff --git a/UI/WebServerResources/MailerUI.js b/UI/WebServerResources/MailerUI.js index 15e43471..d0aa924f 100644 --- a/UI/WebServerResources/MailerUI.js +++ b/UI/WebServerResources/MailerUI.js @@ -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 diff --git a/UI/WebServerResources/UIxMailEditor.js b/UI/WebServerResources/UIxMailEditor.js index a38d0088..6f8018e4 100644 --- a/UI/WebServerResources/UIxMailEditor.js +++ b/UI/WebServerResources/UIxMailEditor.js @@ -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); -- 2.39.5