+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
/* operations */
+- (NSException *) delete;
- (NSException *) sendMail;
- (NSException *) save;
#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"
- (NSString *) asQPSubjectString: (NSString *) encoding;
{
- NSString *qpString;
+ NSString *qpString, *subjectString;
NSData *subjectData, *destSubjectData;
subjectData = [self dataUsingEncoding: NSUTF8StringEncoding];
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
return error;
}
-- (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
-{
-#warning unimplemented
-}
-
- (void) _addEMailsOfAddresses: (NSArray *) _addrs
toArray: (NSMutableArray *) _ma
{
}
}
+- (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
{
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;
}
/* 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]) {
sourceIMAP4URL = [NSURL URLWithString: sourceURL];
[imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
}
+ if (!draftDeleteDisabled)
+ error = [self delete];
}
}
}
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
@interface SOGoMailObject (SOGoDraftObjectExtensions)
+- (NSString *) contentForEditing;
+- (NSArray *) fetchFileAttachmentKeys;
+
- (NSString *) subjectForReply;
- (NSString *) contentForReply;
return newSubject;
}
-- (NSString *) contentForReplyOnParts: (NSDictionary *) _prts
- keys: (NSArray *) _k
+- (NSString *) contentForEditingOnParts: (NSDictionary *) _prts
+ keys: (NSArray *) _k
{
static NSString *textPartSeparator = @"\n---\n";
NSMutableString *ms;
{
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
- (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;
}
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;
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"]];
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;
#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
pageName = "UIxMailEditor";
actionName = "send";
};
+ delete = {
+ protectedBy = "View";
+ actionClass = "UIxMailActions";
+ actionName = "delete";
+ };
+ deleteAttachment = {
+ protectedBy = "View";
+ actionClass = "UIxMailActions";
+ actionName = "deleteAttachment";
+ };
};
};
</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>
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++;
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"];
document.messageListAjaxRequest
= triggerAjaxRequest(url, messageListCallback,
- currentMessages[mailbox]);
+ currentMessage);
var quotasUrl = ApplicationBaseURL + mailbox + "/quotas";
document.quotasAjaxRequest
if (!validateEditorInput(sender))
return false;
+ window.shouldPreserve = true;
document.pageform.action = "send";
document.pageform.submit();
function clickedEditorAttach(sender) {
var area = $("attachmentsArea");
-
area.setStyle({ display: "block" });
var inputs = area.getElementsByTagName("input");
}
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");
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() {
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);
}
}
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);