2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #import <Foundation/NSFileManager.h>
23 #import <Foundation/NSKeyValueCoding.h>
24 #import <Foundation/NSString.h>
25 #import <Foundation/NSUserDefaults.h>
27 #import <NGObjWeb/WORequest.h>
28 #import <NGObjWeb/SoSubContext.h>
29 #import <NGObjWeb/NSException+HTTP.h>
30 #import <NGExtensions/NSNull+misc.h>
31 #import <NGExtensions/NSObject+Logs.h>
32 #import <NGExtensions/NSString+misc.h>
33 #import <NGExtensions/NSException+misc.h>
34 #import <NGMail/NGMimeMessage.h>
35 #import <NGMail/NGMimeMessageGenerator.h>
36 #import <NGMime/NGMimeBodyPart.h>
37 #import <NGMime/NGMimeHeaderFields.h>
38 #import <NGMime/NGMimeMultipartBody.h>
40 #import <SoObjects/Mailer/SOGoDraftObject.h>
41 #import <SoObjects/Mailer/SOGoMailFolder.h>
42 #import <SoObjects/Mailer/SOGoMailAccount.h>
43 #import <SoObjects/Mailer/SOGoMailAccounts.h>
44 #import <SoObjects/Mailer/SOGoMailIdentity.h>
45 #import <SoObjects/SOGo/SOGoUser.h>
46 #import <SOGoUI/UIxComponent.h>
51 An mail editor component which works on SOGoDraftObject's.
54 @class NSArray, NSString;
55 @class SOGoMailFolder;
57 @interface UIxMailEditor : UIxComponent
64 NSMutableArray *fromEMails;
66 SOGoMailFolder *sentFolder;
68 /* these are for the inline attachment list */
69 NSString *attachmentName;
70 NSArray *attachmentNames;
75 @implementation UIxMailEditor
77 static BOOL keepMailTmpFile = NO;
78 static BOOL showInternetMarker = NO;
79 static BOOL useLocationBasedSentFolder = NO;
80 static NSDictionary *internetMailHeaders = nil;
81 static NSArray *infoKeys = nil;
85 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
87 infoKeys = [[NSArray alloc] initWithObjects:
88 @"subject", @"text", @"to", @"cc", @"bcc",
92 keepMailTmpFile = [ud boolForKey:@"SOGoMailEditorKeepTmpFile"];
94 NSLog(@"WARNING: keeping mail files.");
96 useLocationBasedSentFolder =
97 [ud boolForKey:@"SOGoUseLocationBasedSentFolder"];
99 /* Internet mail settings */
101 showInternetMarker = [ud boolForKey:@"SOGoShowInternetMarker"];
102 if (!showInternetMarker)
103 NSLog(@"Note: visual Internet marker on mail editor disabled "
104 @"(SOGoShowInternetMarker)");
106 internetMailHeaders =
107 [[ud dictionaryForKey:@"SOGoInternetMailHeaders"] copy];
108 NSLog (@"Note: specified %d headers for mails send via the Internet.",
109 [internetMailHeaders count]);
114 [sentFolder release];
115 [fromEMails release];
123 [attachmentName release];
124 [attachmentNames release];
130 - (void) setFrom: (NSString *) _value
132 ASSIGNCOPY(from, _value);
137 if (![from isNotEmpty])
138 return [[[self context] activeUser] primaryEmail];
143 - (void) setReplyTo: (NSString *) _ignore
147 - (NSString *) replyTo
149 /* we are here for future extensibility */
153 - (void) setSubject: (NSString *) _value
155 ASSIGNCOPY(subject, _value);
158 - (NSString *) subject
160 return subject ? subject : @"";
163 - (void) setText: (NSString *) _value
165 ASSIGNCOPY(text, _value);
170 return [text isNotNull] ? text : @"";
173 - (void) setTo: (NSArray *)_value
175 ASSIGNCOPY(to, _value);
180 return [to isNotNull] ? to : [NSArray array];
183 - (void) setCc: (NSArray *) _value
185 ASSIGNCOPY(cc, _value);
190 return [cc isNotNull] ? cc : [NSArray array];
193 - (void) setBcc: (NSArray *) _value
195 ASSIGNCOPY(bcc, _value);
200 return [bcc isNotNull] ? bcc : [NSArray array];
203 - (BOOL) hasOneOrMoreRecipients
205 if ([[self to] count] > 0) return YES;
206 if ([[self cc] count] > 0) return YES;
207 if ([[self bcc] count] > 0) return YES;
211 - (void) setAttachmentName: (NSString *) _attachmentName
213 ASSIGN(attachmentName, _attachmentName);
216 - (NSString *) attachmentName
218 return attachmentName;
223 - (NSArray *) fromEMails
225 NSEnumerator *emails;
226 SOGoUser *activeUser;
227 NSString *cn, *fullMail, *email;
231 fromEMails = [NSMutableArray new];
232 activeUser = [context activeUser];
233 cn = [activeUser cn];
234 if ([cn length] == 0)
236 emails = [[activeUser allEmails] objectEnumerator];
237 email = [emails nextObject];
241 fullMail = [NSString stringWithFormat: @"%@ <%@>", cn, email];
244 [fromEMails addObject: fullMail];
245 email = [emails nextObject];
254 - (NSString *)panelTitle {
255 return [self labelForKey:@"Compose Mail"];
260 - (void)loadInfo:(NSDictionary *)_info {
261 if (![_info isNotNull]) return;
262 [self debugWithFormat:@"loading info ..."];
263 [self takeValuesFromDictionary:_info];
265 - (NSDictionary *)storeInfo {
266 [self debugWithFormat:@"storing info ..."];
267 return [self valuesForKeys:infoKeys];
272 - (BOOL) shouldTakeValuesFromRequest: (WORequest *) _rq
273 inContext: (WOContext*) _c
280 - (NSException *) patchFlagsInStore
284 if the draft is a reply => [message markAnswered]
285 if the draft is a forward => [message addFlag:@"forwarded"]
287 This is hard, we would need to find the original message in Cyrus.
292 - (id) lookupSentFolderUsingAccount
294 SOGoMailAccount *account;
295 SOGoMailFolder *folder;
297 if (sentFolder != nil)
298 return [sentFolder isNotNull] ? sentFolder : nil;;
300 account = [[self clientObject] mailAccountFolder];
301 if ([account isKindOfClass:[NSException class]]) return account;
303 folder = [account sentFolderInContext:[self context]];
304 if ([folder isKindOfClass:[NSException class]]) return folder;
305 return ((sentFolder = [folder retain]));
308 - (void) _presetFromBasedOnAccountsQueryParameter
310 /* preset the from field to the primary identity of the given account */
311 /* Note: The compose action sets the 'accounts' query parameter */
313 SOGoMailAccounts *accounts;
314 SOGoMailAccount *account;
315 SOGoMailIdentity *identity;
317 if (useLocationBasedSentFolder) /* from will be based on location */
320 if ([from isNotEmpty]) /* a from is already set */
323 accountID = [[[self context] request] formValueForKey:@"account"];
324 if (![accountID isNotEmpty])
327 accounts = [[self clientObject] mailAccountsFolder];
328 if ([accounts isExceptionOrNull])
329 return; /* we don't treat this as an error but are tolerant */
331 account = [accounts lookupName:accountID inContext:[self context]
333 if ([account isExceptionOrNull])
334 return; /* we don't treat this as an error but are tolerant */
336 identity = [account valueForKey:@"preferredIdentity"];
337 if (![identity isNotNull]) {
338 [self warnWithFormat:@"Account has no preferred identity: %@", account];
342 [self setFrom: [identity email]];
345 - (SOGoMailIdentity *) selectedMailIdentity
347 SOGoMailAccounts *accounts;
349 SOGoMailIdentity *identity;
351 accounts = [[self clientObject] mailAccountsFolder];
352 if ([accounts isExceptionOrNull]) return (id)accounts;
354 // TODO: This is still a hack because we detect the identity based on the
355 // from. In Agenor all of the identities have unique emails, but this
356 // is not required for SOGo.
358 if ([[self from] length] == 0)
361 e = [[accounts fetchIdentitiesWithEmitterPermissions] objectEnumerator];
362 while ((identity = [e nextObject]) != nil) {
363 if ([[identity email] isEqualToString:[self from]])
369 - (id) lookupSentFolderUsingFrom
371 // TODO: if we have the identity we could also support BCC
372 SOGoMailAccounts *accounts;
373 SOGoMailIdentity *identity;
375 NSString *sentFolderName;
376 NSArray *sentFolderPath;
377 NSException *error = nil;
379 if (sentFolder != nil)
380 return [sentFolder isNotNull] ? sentFolder : nil;;
382 identity = [self selectedMailIdentity];
383 if ([identity isKindOfClass:[NSException class]]) return identity;
385 if (![(sentFolderName = [identity sentFolderName]) isNotEmpty]) {
386 [self warnWithFormat:@"Identity has no sent folder name: %@", identity];
390 // TODO: fixme, we treat the foldername as a hardcoded path from SOGoAccounts
391 // TODO: escaping of foldernames with slashes
392 // TODO: maybe the SOGoMailIdentity should have an 'account-identifier'
393 // which is used to lookup the account and _then_ perform an account
394 // local folder lookup? => would not be possible to have identities
395 // saving to different accounts.
396 sentFolderPath = [sentFolderName componentsSeparatedByString:@"/"];
398 accounts = [[self clientObject] mailAccountsFolder];
399 if ([accounts isKindOfClass:[NSException class]]) return (id)accounts;
401 ctx = [[SoSubContext alloc] initWithParentContext:[self context]];
403 sentFolder = [[accounts traversePathArray:sentFolderPath
404 inContext:ctx error:&error
406 [ctx release]; ctx = nil;
408 [self errorWithFormat:@"Sent-Folder lookup for identity %@ failed: %@",
409 identity, sentFolderPath];
414 [self logWithFormat:@"Sent-Folder: %@", sentFolderName];
415 [self logWithFormat:@" object: %@", sentFolder];
420 - (NSException *) storeMailInSentFolder: (NSString *) _path
422 SOGoMailFolder *folder;
426 folder = useLocationBasedSentFolder
427 ? [self lookupSentFolderUsingAccount]
428 : [self lookupSentFolderUsingFrom];
429 if ([folder isKindOfClass:[NSException class]]) return (id)folder;
430 if (folder == nil) return nil;
432 if ((data = [[NSData alloc] initWithContentsOfMappedFile:_path]) == nil) {
433 return [NSException exceptionWithHTTPStatus:500 /* server error */
434 reason:@"could not find temporary draft file!"];
437 result = [folder postData:data flags:@"seen"];
438 [data release]; data = nil;
444 - (NSDictionary *) _scanAttachmentFilenamesInRequest: (id) httpBody
446 NSMutableDictionary *filenames;
447 NSDictionary *attachment;
449 unsigned int count, max;
450 NGMimeBodyPart *part;
451 NGMimeContentDispositionHeaderField *header;
454 parts = [httpBody parts];
456 filenames = [NSMutableDictionary dictionaryWithCapacity: max];
458 for (count = 0; count < max; count++)
460 part = [parts objectAtIndex: count];
461 header = [part headerForKey: @"content-disposition"];
462 mimeType = [[part headerForKey: @"content-type"] stringValue];
463 attachment = [NSDictionary dictionaryWithObjectsAndKeys:
464 [header filename], @"filename",
465 mimeType, @"mime-type", nil];
466 [filenames setObject: attachment
467 forKey: [header name]];
473 - (BOOL) _saveAttachments
476 NSEnumerator *allKeys;
479 NSDictionary *filenames;
484 request = [context request];
486 httpBody = [[request httpRequest] body];
487 filenames = [self _scanAttachmentFilenamesInRequest: httpBody];
489 co = [self clientObject];
490 allKeys = [[request formValueKeys] objectEnumerator];
491 key = [allKeys nextObject];
492 while (key && success)
494 if ([key hasPrefix: @"attachment"])
496 = (![co saveAttachment: (NSData *) [request formValueForKey: key]
497 withMetadata: [filenames objectForKey: key]]);
498 key = [allKeys nextObject];
504 - (BOOL) _saveFormInfo
512 if ([self _saveAttachments])
514 info = [self storeInfo];
517 error = [[self clientObject] storeInfo:info];
520 [self errorWithFormat:@"failed to store draft: %@", error];
521 // TODO: improve error handling
529 // TODO: wrap content
534 - (id) failedToSaveFormResponse
536 // TODO: improve error handling
537 return [NSException exceptionWithHTTPStatus:500 /* server error */
538 reason:@"failed to store draft object on server!"];
541 /* attachment helper */
543 - (NSArray *) attachmentNames
547 if (attachmentNames != nil)
548 return attachmentNames;
550 a = [[self clientObject] fetchAttachmentNames];
551 a = [a sortedArrayUsingSelector:@selector(compare:)];
552 attachmentNames = [a copy];
553 return attachmentNames;
556 - (BOOL) hasAttachments
558 return [[self attachmentNames] count] > 0 ? YES : NO;
564 [self logWithFormat:@"edit action, load content from: %@",
565 [self clientObject]];
568 [self loadInfo:[[self clientObject] fetchInfo]];
569 [self _presetFromBasedOnAccountsQueryParameter];
575 return [self _saveFormInfo] ? self : [self failedToSaveFormResponse];
578 - (NSException *) validateForSend
580 // TODO: localize errors
582 if (![self hasOneOrMoreRecipients]) {
583 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
584 reason:@"Please select a recipient!"];
586 if ([[self subject] length] == 0) {
587 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
588 reason:@"Please set a subject!"];
594 - (id <WOActionResults>) sendAction
598 id <WOActionResults> result;
600 // TODO: need to validate whether we have a To etc
602 /* first, save form data */
604 if (![self _saveFormInfo])
605 return [self failedToSaveFormResponse];
607 /* validate for send */
609 if ((error = [self validateForSend]) != nil) {
612 url = [[error reason] stringByEscapingURL];
613 url = [@"edit?error=" stringByAppendingString:url];
614 return [self redirectToLocation:url];
617 /* setup some extra headers if required */
619 /* save mail to file (so that we can upload the mail to Cyrus) */
620 // TODO: all this could be handled by the SOGoDraftObject?
622 mailPath = [[self clientObject] saveMimeMessageToTemporaryFileWithHeaders: internetMailHeaders];
624 /* then, send mail */
626 if ((error = [[self clientObject] sendMimeMessageAtPath:mailPath]) != nil) {
627 // TODO: improve error handling
628 [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
632 /* patch flags in store for replies etc */
634 if ((error = [self patchFlagsInStore]) != nil)
637 /* finally store in Sent */
639 if ((error = [self storeMailInSentFolder:mailPath]) != nil)
642 /* delete temporary mail file */
645 [self warnWithFormat:@"keeping mail file: '%@'", mailPath];
647 [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
652 if ((error = [[self clientObject] delete]) != nil)
655 if ([[[[self context] request] formValueForKey: @"nojs"] intValue])
656 result = [self redirectToLocation: [self applicationPath]];
658 result = [self jsCloseWithRefreshMethod: nil];
668 if ((error = [[self clientObject] delete]) != nil) {
669 /* Note: we ignore 404: those are drafts which were not yet saved */
670 if (![error httpStatus] == 404)
675 page = [self pageWithName:@"UIxMailWindowCloser"];
676 [page takeValue:@"YES" forKey:@"refreshOpener"];
679 // TODO: if we just return nil, we produce a 500
680 return [NSException exceptionWithHTTPStatus:204 /* No Content */
681 reason:@"object was deleted."];
685 @end /* UIxMailEditor */