#import <Foundation/NSString.h>
#import <Foundation/NSUserDefaults.h>
-#import <NGObjWeb/WORequest.h>
-#import <NGMail/NGMimeMessage.h>
-#import <NGMail/NGMimeMessageGenerator.h>
-#import <NGObjWeb/SoSubContext.h>
#import <NGObjWeb/NSException+HTTP.h>
+#import <NGObjWeb/SoSubContext.h>
+#import <NGObjWeb/WORequest.h>
+#import <NGObjWeb/WOResponse.h>
#import <NGExtensions/NSNull+misc.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <NGExtensions/NSException+misc.h>
+#import <NGMail/NGMimeMessage.h>
+#import <NGMail/NGMimeMessageGenerator.h>
+#import <NGMime/NGMimeBodyPart.h>
+#import <NGMime/NGMimeHeaderFields.h>
+#import <NGMime/NGMimeMultipartBody.h>
#import <SoObjects/Mailer/SOGoDraftObject.h>
#import <SoObjects/Mailer/SOGoMailFolder.h>
#import <SoObjects/Mailer/SOGoMailAccount.h>
#import <SoObjects/Mailer/SOGoMailAccounts.h>
-#import <SoObjects/Mailer/SOGoMailIdentity.h>
#import <SoObjects/SOGo/SOGoUser.h>
-#import <SoObjects/SOGo/WOContext+Agenor.h>
+#import <SoObjects/SOGo/NSArray+Utilities.h>
+#import <SoObjects/SOGo/NSDictionary+Utilities.h>
#import <SOGoUI/UIxComponent.h>
/*
An mail editor component which works on SOGoDraftObject's.
*/
-@class NSArray, NSString;
-@class SOGoMailFolder;
-
@interface UIxMailEditor : UIxComponent
{
NSArray *to;
NSArray *bcc;
NSString *subject;
NSString *text;
- NSMutableArray *fromEMails;
+ NSArray *fromEMails;
NSString *from;
SOGoMailFolder *sentFolder;
@implementation UIxMailEditor
-static BOOL keepMailTmpFile = NO;
-static BOOL showInternetMarker = NO;
-static BOOL useLocationBasedSentFolder = NO;
+static BOOL showInternetMarker = NO;
+static BOOL useLocationBasedSentFolder = NO;
static NSDictionary *internetMailHeaders = nil;
-static NSArray *infoKeys = nil;
+static NSArray *infoKeys = nil;
-+ (void)initialize {
++ (void) initialize
+{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
infoKeys = [[NSArray alloc] initWithObjects:
- @"subject", @"text", @"to", @"cc", @"bcc",
- @"from", @"replyTo",
+ @"subject", @"to", @"cc", @"bcc",
+ @"from", @"replyTo", @"inReplyTo",
nil];
- keepMailTmpFile = [ud boolForKey:@"SOGoMailEditorKeepTmpFile"];
- if (keepMailTmpFile)
- NSLog(@"WARNING: keeping mail files.");
-
useLocationBasedSentFolder =
[ud boolForKey:@"SOGoUseLocationBasedSentFolder"];
/* Internet mail settings */
showInternetMarker = [ud boolForKey:@"SOGoShowInternetMarker"];
- if (!showInternetMarker) {
+ if (!showInternetMarker)
NSLog(@"Note: visual Internet marker on mail editor disabled "
@"(SOGoShowInternetMarker)");
- }
internetMailHeaders =
[[ud dictionaryForKey:@"SOGoInternetMailHeaders"] copy];
- NSLog(@"Note: specified %d headers for mails send via the Internet.",
+ NSLog (@"Note: specified %d headers for mails send via the Internet.",
[internetMailHeaders count]);
}
-- (void)dealloc {
+- (void) dealloc
+{
[sentFolder release];
[fromEMails release];
- [from release];
- [text release];
+ [from release];
+ [text release];
[subject release];
- [to release];
- [cc release];
- [bcc release];
-
- [attachmentName release];
+ [to release];
+ [cc release];
+ [bcc release];
+ [attachmentName release];
[attachmentNames release];
[super dealloc];
}
/* accessors */
-- (void)setFrom:(NSString *)_value {
- ASSIGNCOPY(from, _value);
+- (void) setFrom: (NSString *) newFrom
+{
+ ASSIGN (from, newFrom);
}
-- (NSString *)from {
- if (![from isNotEmpty])
- return [[[self context] activeUser] primaryEmail];
+
+- (NSString *) from
+{
+ NSDictionary *identity;
+
+ if (!from)
+ {
+ identity = [[context activeUser] primaryIdentity];
+ from = [identity keysWithFormat: @"%{fullName} <%{email}>"];
+ }
+
return from;
}
-- (void)setReplyTo:(NSString *)_ignore {
-}
-- (NSString *)replyTo {
- /* we are here for future extensibility */
- return @"";
-}
+// - (void) setReplyTo: (NSString *) ignore
+// {
+// }
+
+// - (NSString *) replyTo
+// {
+// /* we are here for future extensibility */
+// return @"";
+// }
-- (void)setSubject:(NSString *)_value {
- ASSIGNCOPY(subject, _value);
+- (void) setSubject: (NSString *) newSubject
+{
+ ASSIGN (subject, newSubject);
}
-- (NSString *)subject {
- return subject ? subject : @"";
+
+- (NSString *) subject
+{
+ return subject;
}
-- (void)setText:(NSString *)_value {
- ASSIGNCOPY(text, _value);
+- (void) setText: (NSString *) newText
+{
+ ASSIGN (text, newText);
}
-- (NSString *)text {
- return [text isNotNull] ? text : @"";
+
+- (NSString *) text
+{
+ return text;
}
-- (void)setTo:(NSArray *)_value {
- ASSIGNCOPY(to, _value);
+- (void) setTo: (NSArray *) newTo
+{
+ if ([newTo isKindOfClass: [NSNull class]])
+ newTo = nil;
+
+ ASSIGN (to, newTo);
}
-- (NSArray *)to {
- return [to isNotNull] ? to : [NSArray array];
+
+- (NSArray *) to
+{
+ return to;
}
-- (void)setCc:(NSArray *)_value {
- ASSIGNCOPY(cc, _value);
+- (void) setCc: (NSArray *) newCc
+{
+ if ([newCc isKindOfClass: [NSNull class]])
+ newCc = nil;
+
+ ASSIGN (cc, newCc);
}
-- (NSArray *)cc {
- return [cc isNotNull] ? cc : [NSArray array];
+
+- (NSArray *) cc
+{
+ return cc;
}
-- (void)setBcc:(NSArray *)_value {
- ASSIGNCOPY(bcc, _value);
+- (void) setBcc: (NSArray *) newBcc
+{
+ if ([newBcc isKindOfClass: [NSNull class]])
+ newBcc = nil;
+
+ ASSIGN (bcc, newBcc);
}
-- (NSArray *)bcc {
- return [bcc isNotNull] ? bcc : [NSArray array];
+
+- (NSArray *) bcc
+{
+ return bcc;
}
-- (BOOL)hasOneOrMoreRecipients {
- if ([[self to] count] > 0) return YES;
- if ([[self cc] count] > 0) return YES;
- if ([[self bcc] count] > 0) return YES;
- return NO;
+- (BOOL) hasOneOrMoreRecipients
+{
+ return (([to count] + [cc count] + [bcc count]) > 0);
}
-- (void)setAttachmentName:(NSString *)_attachmentName {
- ASSIGN(attachmentName, _attachmentName);
+- (void) setAttachmentName: (NSString *) newAttachmentName
+{
+ ASSIGN (attachmentName, newAttachmentName);
}
-- (NSString *)attachmentName {
+
+- (NSString *) attachmentName
+{
return attachmentName;
}
- (NSArray *) fromEMails
{
- NSEnumerator *emails;
- SOGoUser *activeUser;
- NSString *cn, *fullMail, *email;
-
+ NSArray *allIdentities;
+
if (!fromEMails)
{
- fromEMails = [NSMutableArray new];
- activeUser = [context activeUser];
- cn = [activeUser cn];
- if ([cn length] == 0)
- cn = nil;
- emails = [[activeUser allEmails] objectEnumerator];
- email = [emails nextObject];
- while (email)
- {
- if (cn)
- fullMail = [NSString stringWithFormat: @"%@ <%@>", cn, email];
- else
- fullMail = email;
- [fromEMails addObject: fullMail];
- email = [emails nextObject];
- }
+ allIdentities = [[context activeUser] allIdentities];
+ fromEMails = [allIdentities keysWithFormat: @"%{fullName} <%{email}>"];
+ [fromEMails retain];
}
return fromEMails;
}
-/* title */
-
-- (NSString *)panelTitle {
- return [self labelForKey:@"Compose Mail"];
-}
-
-/* detect webmail being accessed from the outside */
-
-- (BOOL)isInternetRequest {
- // DEPRECATED
- return [[self context] isAccessFromIntranet] ? NO : YES;
-}
-
-- (BOOL)showInternetMarker {
- if (!showInternetMarker)
- return NO;
- return [[self context] isAccessFromIntranet] ? NO : YES;
-}
-
/* info loading */
-- (void)loadInfo:(NSDictionary *)_info {
+- (void) loadInfo: (NSDictionary *) _info
+{
if (![_info isNotNull]) return;
[self debugWithFormat:@"loading info ..."];
[self takeValuesFromDictionary:_info];
}
-- (NSDictionary *)storeInfo {
+
+- (NSDictionary *) storeInfo
+{
[self debugWithFormat:@"storing info ..."];
return [self valuesForKeys:infoKeys];
}
/* requests */
-- (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{
+- (BOOL) shouldTakeValuesFromRequest: (WORequest *) request
+ inContext: (WOContext*) localContext
+{
return YES;
}
-/* IMAP4 store */
+/* actions */
-- (NSException *)patchFlagsInStore {
- /*
- Flags we should set:
- if the draft is a reply => [message markAnswered]
- if the draft is a forward => [message addFlag:@"forwarded"]
-
- This is hard, we would need to find the original message in Cyrus.
- */
- return nil;
-}
+- (NSDictionary *) _scanAttachmentFilenamesInRequest: (id) httpBody
+{
+ NSMutableDictionary *filenames;
+ NSDictionary *attachment;
+ NSArray *parts;
+ unsigned int count, max;
+ NGMimeBodyPart *part;
+ NGMimeContentDispositionHeaderField *header;
+ NSString *mimeType;
+
+ parts = [httpBody parts];
+ max = [parts count];
+ filenames = [NSMutableDictionary dictionaryWithCapacity: max];
+
+ for (count = 0; count < max; count++)
+ {
+ part = [parts objectAtIndex: count];
+ header = (NGMimeContentDispositionHeaderField *) [part headerForKey: @"content-disposition"];
+ mimeType = [(NGMimeType *) [part headerForKey: @"content-type"] stringValue];
+ attachment = [NSDictionary dictionaryWithObjectsAndKeys:
+ [header filename], @"filename",
+ mimeType, @"mime-type", nil];
+ [filenames setObject: attachment
+ forKey: [header name]];
+ }
-- (id)lookupSentFolderUsingAccount {
- SOGoMailAccount *account;
- SOGoMailFolder *folder;
-
- if (sentFolder != nil)
- return [sentFolder isNotNull] ? sentFolder : nil;;
-
- account = [[self clientObject] mailAccountFolder];
- if ([account isKindOfClass:[NSException class]]) return account;
-
- folder = [account sentFolderInContext:[self context]];
- if ([folder isKindOfClass:[NSException class]]) return folder;
- return ((sentFolder = [folder retain]));
+ return filenames;
}
-- (void)_presetFromBasedOnAccountsQueryParameter {
- /* preset the from field to the primary identity of the given account */
- /* Note: The compose action sets the 'accounts' query parameter */
- NSString *accountID;
- SOGoMailAccounts *accounts;
- SOGoMailAccount *account;
- SOGoMailIdentity *identity;
-
- if (useLocationBasedSentFolder) /* from will be based on location */
- return;
-
- if ([from isNotEmpty]) /* a from is already set */
- return;
-
- accountID = [[[self context] request] formValueForKey:@"account"];
- if (![accountID isNotEmpty])
- return;
-
- accounts = [[self clientObject] mailAccountsFolder];
- if ([accounts isExceptionOrNull])
- return; /* we don't treat this as an error but are tolerant */
+- (BOOL) _saveAttachments
+{
+ WORequest *request;
+ NSEnumerator *allKeys;
+ NSString *key;
+ BOOL success;
+ NSDictionary *filenames;
+ id httpBody;
+ SOGoDraftObject *co;
+
+ success = YES;
+ request = [context request];
+
+ httpBody = [[request httpRequest] body];
+ filenames = [self _scanAttachmentFilenamesInRequest: httpBody];
+
+ co = [self clientObject];
+ allKeys = [[request formValueKeys] objectEnumerator];
+ key = [allKeys nextObject];
+ while (key && success)
+ {
+ if ([key hasPrefix: @"attachment"])
+ success
+ = (![co saveAttachment: (NSData *) [request formValueForKey: key]
+ withMetadata: [filenames objectForKey: key]]);
+ key = [allKeys nextObject];
+ }
- account = [accounts lookupName:accountID inContext:[self context]
- acquire:NO];
- if ([account isExceptionOrNull])
- return; /* we don't treat this as an error but are tolerant */
-
- identity = [account valueForKey:@"preferredIdentity"];
- if (![identity isNotNull]) {
- [self warnWithFormat:@"Account has no preferred identity: %@", account];
- return;
- }
-
- [self setFrom: [identity email]];
+ return success;
}
-- (SOGoMailIdentity *)selectedMailIdentity {
- SOGoMailAccounts *accounts;
- NSEnumerator *e;
- SOGoMailIdentity *identity;
-
- accounts = [[self clientObject] mailAccountsFolder];
- if ([accounts isExceptionOrNull]) return (id)accounts;
-
- // TODO: This is still a hack because we detect the identity based on the
- // from. In Agenor all of the identities have unique emails, but this
- // is not required for SOGo.
-
- if ([[self from] length] == 0)
- return nil;
-
- e = [[accounts fetchIdentitiesWithEmitterPermissions] objectEnumerator];
- while ((identity = [e nextObject]) != nil) {
- if ([[identity email] isEqualToString:[self from]])
- return identity;
- }
- return nil;
-}
-
-- (id)lookupSentFolderUsingFrom {
- // TODO: if we have the identity we could also support BCC
- SOGoMailAccounts *accounts;
- SOGoMailIdentity *identity;
- SoSubContext *ctx;
- NSString *sentFolderName;
- NSArray *sentFolderPath;
- NSException *error = nil;
-
- if (sentFolder != nil)
- return [sentFolder isNotNull] ? sentFolder : nil;;
-
- identity = [self selectedMailIdentity];
- if ([identity isKindOfClass:[NSException class]]) return identity;
-
- if (![(sentFolderName = [identity sentFolderName]) isNotEmpty]) {
- [self warnWithFormat:@"Identity has no sent folder name: %@", identity];
- return nil;
- }
-
- // TODO: fixme, we treat the foldername as a hardcoded path from SOGoAccounts
- // TODO: escaping of foldernames with slashes
- // TODO: maybe the SOGoMailIdentity should have an 'account-identifier'
- // which is used to lookup the account and _then_ perform an account
- // local folder lookup? => would not be possible to have identities
- // saving to different accounts.
- sentFolderPath = [sentFolderName componentsSeparatedByString:@"/"];
-
- accounts = [[self clientObject] mailAccountsFolder];
- if ([accounts isKindOfClass:[NSException class]]) return (id)accounts;
-
- ctx = [[SoSubContext alloc] initWithParentContext:[self context]];
-
- sentFolder = [[accounts traversePathArray:sentFolderPath
- inContext:ctx error:&error
- acquire:NO] retain];
- [ctx release]; ctx = nil;
- if (error != nil) {
- [self errorWithFormat:@"Sent-Folder lookup for identity %@ failed: %@",
- identity, sentFolderPath];
- return error;
- }
-
-#if 0
- [self logWithFormat:@"Sent-Folder: %@", sentFolderName];
- [self logWithFormat:@" object: %@", sentFolder];
-#endif
- return sentFolder;
-}
+- (BOOL) _saveFormInfo
+{
+ NSDictionary *info;
+ NSException *error;
+ BOOL success;
+ SOGoDraftObject *co;
-- (NSException *)storeMailInSentFolder:(NSString *)_path {
- SOGoMailFolder *folder;
- NSData *data;
- id result;
-
- folder = useLocationBasedSentFolder
- ? [self lookupSentFolderUsingAccount]
- : [self lookupSentFolderUsingFrom];
- if ([folder isKindOfClass:[NSException class]]) return (id)folder;
- if (folder == nil) return nil;
-
- if ((data = [[NSData alloc] initWithContentsOfMappedFile:_path]) == nil) {
- return [NSException exceptionWithHTTPStatus:500 /* server error */
- reason:@"could not find temporary draft file!"];
- }
-
- result = [folder postData:data flags:@"seen"];
- [data release]; data = nil;
- return result;
-}
+ co = [self clientObject];
+ [co fetchInfo];
-/* actions */
+ success = YES;
-- (BOOL)_saveFormInfo {
- NSDictionary *info;
-
- if ((info = [self storeInfo]) != nil) {
- NSException *error;
-
- if ((error = [[self clientObject] storeInfo:info]) != nil) {
- [self errorWithFormat:@"failed to store draft: %@", error];
- // TODO: improve error handling
- return NO;
+ if ([self _saveAttachments])
+ {
+ info = [self storeInfo];
+ [co setHeaders: info];
+ [co setText: text];
+ error = [co storeInfo];
+ if (error)
+ {
+ [self errorWithFormat: @"failed to store draft: %@", error];
+ // TODO: improve error handling
+ success = NO;
+ }
}
- }
-
+ else
+ success = NO;
+
// TODO: wrap content
- return YES;
+ return success;
}
-- (id)failedToSaveFormResponse {
+
+- (id) failedToSaveFormResponse
+{
// TODO: improve error handling
return [NSException exceptionWithHTTPStatus:500 /* server error */
reason:@"failed to store draft object on server!"];
/* attachment helper */
-- (NSArray *)attachmentNames {
+- (NSArray *) attachmentNames
+{
NSArray *a;
-
+
if (attachmentNames != nil)
return attachmentNames;
-
+
a = [[self clientObject] fetchAttachmentNames];
- a = [a sortedArrayUsingSelector:@selector(compare:)];
+ a = [a sortedArrayUsingSelector: @selector (compare:)];
attachmentNames = [a copy];
+
return attachmentNames;
}
-- (BOOL)hasAttachments {
- return [[self attachmentNames] count] > 0 ? YES : NO;
-}
-- (NSString *)initialLeftsideStyle {
- if ([self hasAttachments])
- return @"width: 67%";
- return @"width: 100%";
+- (BOOL) hasAttachments
+{
+ return [[self attachmentNames] count] > 0 ? YES : NO;
}
-- (NSString *)initialRightsideStyle {
- if ([self hasAttachments])
- return @"display: block";
- return @"display: none";
-}
+- (id) defaultAction
+{
+ SOGoDraftObject *co;
-- (id)defaultAction {
- return [self redirectToLocation:@"edit"];
-}
+ co = [self clientObject];
+ [co fetchInfo];
+ [self loadInfo: [co headers]];
+ [self setText: [co text]];
-- (id)editAction {
-#if 0
- [self logWithFormat:@"edit action, load content from: %@",
- [self clientObject]];
-#endif
-
- [self loadInfo:[[self clientObject] fetchInfo]];
- [self _presetFromBasedOnAccountsQueryParameter];
return self;
}
-- (id)saveAction {
- return [self _saveFormInfo] ? self : [self failedToSaveFormResponse];
+- (id <WOActionResults>) saveAction
+{
+ id result;
+
+ if ([self _saveFormInfo])
+ {
+ result = [[self clientObject] save];
+ if (!result)
+ result = [self responseWith204];
+ }
+ else
+ result = [self failedToSaveFormResponse];
+
+ return result;
}
-- (NSException *)validateForSend {
- // TODO: localize errors
-
- if (![self hasOneOrMoreRecipients]) {
- return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
- reason:@"Please select a recipient!"];
- }
- if ([[self subject] length] == 0) {
- return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
- reason:@"Please set a subject!"];
- }
+- (NSException *) validateForSend
+{
+ NSException *error;
+
+ if (![self hasOneOrMoreRecipients])
+ error = [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
+ reason: @"Please select a recipient!"];
+ else if ([[self subject] length] == 0)
+ error = [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
+ reason: @"Please set a subject!"];
+ else
+ error = nil;
- return nil;
+ return error;
}
- (id <WOActionResults>) sendAction
{
- NSException *error;
- NSString *mailPath;
- NSDictionary *h;
id <WOActionResults> result;
// TODO: need to validate whether we have a To etc
/* first, save form data */
-
- if (![self _saveFormInfo])
- return [self failedToSaveFormResponse];
-
- /* validate for send */
-
- if ((error = [self validateForSend]) != nil) {
- id url;
-
- url = [[error reason] stringByEscapingURL];
- url = [@"edit?error=" stringByAppendingString:url];
- return [self redirectToLocation:url];
- }
-
- /* setup some extra headers if required */
-
- h = [[self context] isAccessFromIntranet] ? nil : internetMailHeaders;
-
- /* save mail to file (so that we can upload the mail to Cyrus) */
- // TODO: all this could be handled by the SOGoDraftObject?
-
- mailPath = [[self clientObject] saveMimeMessageToTemporaryFileWithHeaders:h];
-
- /* then, send mail */
-
- if ((error = [[self clientObject] sendMimeMessageAtPath:mailPath]) != nil) {
- // TODO: improve error handling
- [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
- return error;
- }
-
- /* patch flags in store for replies etc */
-
- if ((error = [self patchFlagsInStore]) != nil)
- return error;
-
- /* finally store in Sent */
-
- if ((error = [self storeMailInSentFolder:mailPath]) != nil)
- return error;
-
- /* delete temporary mail file */
-
- if (keepMailTmpFile)
- [self warnWithFormat:@"keeping mail file: '%@'", mailPath];
- else
- [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
- mailPath = nil;
-
- /* delete draft */
-
- if ((error = [[self clientObject] delete]) != nil)
- return error;
-
- if ([[[[self context] request] formValueForKey: @"nojs"] intValue])
- result = [self redirectToLocation: [self applicationPath]];
- else
- result = [self jsCloseWithRefreshMethod: nil];
+ result = [self validateForSend];
+ if (!result)
+ {
+ if ([self _saveFormInfo])
+ {
+ result = [[self clientObject] sendMail];
+ if (!result)
+ result = [self jsCloseWithRefreshMethod: @"refreshFolderByType(\"sent\")"];
+ }
+ else
+ result = [self failedToSaveFormResponse];
+ }
return result;
}
-- (id)deleteAction {
- NSException *error;
- id page;
-
- if ((error = [[self clientObject] delete]) != nil) {
- /* Note: we ignore 404: those are drafts which were not yet saved */
- if (![error httpStatus] == 404)
- return error;
- }
-
-#if 1
- page = [self pageWithName:@"UIxMailWindowCloser"];
- [page takeValue:@"YES" forKey:@"refreshOpener"];
- return page;
-#else
- // TODO: if we just return nil, we produce a 500
- return [NSException exceptionWithHTTPStatus:204 /* No Content */
- reason:@"object was deleted."];
-#endif
-}
-
@end /* UIxMailEditor */