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 <NGMail/NGMimeMessage.h>
29 #import <NGMail/NGMimeMessageGenerator.h>
30 #import <NGObjWeb/SoSubContext.h>
31 #import <NGObjWeb/NSException+HTTP.h>
32 #import <NGExtensions/NSNull+misc.h>
33 #import <NGExtensions/NSObject+Logs.h>
34 #import <NGExtensions/NSString+misc.h>
35 #import <NGExtensions/NSException+misc.h>
37 #import <SoObjects/Mailer/SOGoDraftObject.h>
38 #import <SoObjects/Mailer/SOGoMailFolder.h>
39 #import <SoObjects/Mailer/SOGoMailAccount.h>
40 #import <SoObjects/Mailer/SOGoMailAccounts.h>
41 #import <SoObjects/Mailer/SOGoMailIdentity.h>
42 #import <SoObjects/SOGo/SOGoUser.h>
43 #import <SOGoUI/UIxComponent.h>
48 An mail editor component which works on SOGoDraftObject's.
51 @class NSArray, NSString;
52 @class SOGoMailFolder;
54 @interface UIxMailEditor : UIxComponent
61 NSMutableArray *fromEMails;
63 SOGoMailFolder *sentFolder;
65 /* these are for the inline attachment list */
66 NSString *attachmentName;
67 NSArray *attachmentNames;
72 @implementation UIxMailEditor
74 static BOOL keepMailTmpFile = NO;
75 static BOOL showInternetMarker = NO;
76 static BOOL useLocationBasedSentFolder = NO;
77 static NSDictionary *internetMailHeaders = nil;
78 static NSArray *infoKeys = nil;
81 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
83 infoKeys = [[NSArray alloc] initWithObjects:
84 @"subject", @"text", @"to", @"cc", @"bcc",
88 keepMailTmpFile = [ud boolForKey:@"SOGoMailEditorKeepTmpFile"];
90 NSLog(@"WARNING: keeping mail files.");
92 useLocationBasedSentFolder =
93 [ud boolForKey:@"SOGoUseLocationBasedSentFolder"];
95 /* Internet mail settings */
97 showInternetMarker = [ud boolForKey:@"SOGoShowInternetMarker"];
98 if (!showInternetMarker) {
99 NSLog(@"Note: visual Internet marker on mail editor disabled "
100 @"(SOGoShowInternetMarker)");
103 internetMailHeaders =
104 [[ud dictionaryForKey:@"SOGoInternetMailHeaders"] copy];
105 NSLog(@"Note: specified %d headers for mails send via the Internet.",
106 [internetMailHeaders count]);
110 [sentFolder release];
111 [fromEMails release];
119 [attachmentName release];
120 [attachmentNames release];
126 - (void)setFrom:(NSString *)_value {
127 ASSIGNCOPY(from, _value);
130 if (![from isNotEmpty])
131 return [[[self context] activeUser] primaryEmail];
135 - (void)setReplyTo:(NSString *)_ignore {
137 - (NSString *)replyTo {
138 /* we are here for future extensibility */
142 - (void)setSubject:(NSString *)_value {
143 ASSIGNCOPY(subject, _value);
145 - (NSString *)subject {
146 return subject ? subject : @"";
149 - (void)setText:(NSString *)_value {
150 ASSIGNCOPY(text, _value);
153 return [text isNotNull] ? text : @"";
156 - (void)setTo:(NSArray *)_value {
157 ASSIGNCOPY(to, _value);
160 return [to isNotNull] ? to : [NSArray array];
163 - (void)setCc:(NSArray *)_value {
164 ASSIGNCOPY(cc, _value);
167 return [cc isNotNull] ? cc : [NSArray array];
170 - (void)setBcc:(NSArray *)_value {
171 ASSIGNCOPY(bcc, _value);
174 return [bcc isNotNull] ? bcc : [NSArray array];
177 - (BOOL)hasOneOrMoreRecipients {
178 if ([[self to] count] > 0) return YES;
179 if ([[self cc] count] > 0) return YES;
180 if ([[self bcc] count] > 0) return YES;
184 - (void)setAttachmentName:(NSString *)_attachmentName {
185 ASSIGN(attachmentName, _attachmentName);
187 - (NSString *)attachmentName {
188 return attachmentName;
193 - (NSArray *) fromEMails
195 NSEnumerator *emails;
196 SOGoUser *activeUser;
197 NSString *cn, *fullMail, *email;
201 fromEMails = [NSMutableArray new];
202 activeUser = [context activeUser];
203 cn = [activeUser cn];
204 if ([cn length] == 0)
206 emails = [[activeUser allEmails] objectEnumerator];
207 email = [emails nextObject];
211 fullMail = [NSString stringWithFormat: @"%@ <%@>", cn, email];
214 [fromEMails addObject: fullMail];
215 email = [emails nextObject];
224 - (NSString *)panelTitle {
225 return [self labelForKey:@"Compose Mail"];
230 - (void)loadInfo:(NSDictionary *)_info {
231 if (![_info isNotNull]) return;
232 [self debugWithFormat:@"loading info ..."];
233 [self takeValuesFromDictionary:_info];
235 - (NSDictionary *)storeInfo {
236 [self debugWithFormat:@"storing info ..."];
237 return [self valuesForKeys:infoKeys];
242 - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{
248 - (NSException *)patchFlagsInStore {
251 if the draft is a reply => [message markAnswered]
252 if the draft is a forward => [message addFlag:@"forwarded"]
254 This is hard, we would need to find the original message in Cyrus.
259 - (id)lookupSentFolderUsingAccount {
260 SOGoMailAccount *account;
261 SOGoMailFolder *folder;
263 if (sentFolder != nil)
264 return [sentFolder isNotNull] ? sentFolder : nil;;
266 account = [[self clientObject] mailAccountFolder];
267 if ([account isKindOfClass:[NSException class]]) return account;
269 folder = [account sentFolderInContext:[self context]];
270 if ([folder isKindOfClass:[NSException class]]) return folder;
271 return ((sentFolder = [folder retain]));
274 - (void)_presetFromBasedOnAccountsQueryParameter {
275 /* preset the from field to the primary identity of the given account */
276 /* Note: The compose action sets the 'accounts' query parameter */
278 SOGoMailAccounts *accounts;
279 SOGoMailAccount *account;
280 SOGoMailIdentity *identity;
282 if (useLocationBasedSentFolder) /* from will be based on location */
285 if ([from isNotEmpty]) /* a from is already set */
288 accountID = [[[self context] request] formValueForKey:@"account"];
289 if (![accountID isNotEmpty])
292 accounts = [[self clientObject] mailAccountsFolder];
293 if ([accounts isExceptionOrNull])
294 return; /* we don't treat this as an error but are tolerant */
296 account = [accounts lookupName:accountID inContext:[self context]
298 if ([account isExceptionOrNull])
299 return; /* we don't treat this as an error but are tolerant */
301 identity = [account valueForKey:@"preferredIdentity"];
302 if (![identity isNotNull]) {
303 [self warnWithFormat:@"Account has no preferred identity: %@", account];
307 [self setFrom: [identity email]];
310 - (SOGoMailIdentity *)selectedMailIdentity {
311 SOGoMailAccounts *accounts;
313 SOGoMailIdentity *identity;
315 accounts = [[self clientObject] mailAccountsFolder];
316 if ([accounts isExceptionOrNull]) return (id)accounts;
318 // TODO: This is still a hack because we detect the identity based on the
319 // from. In Agenor all of the identities have unique emails, but this
320 // is not required for SOGo.
322 if ([[self from] length] == 0)
325 e = [[accounts fetchIdentitiesWithEmitterPermissions] objectEnumerator];
326 while ((identity = [e nextObject]) != nil) {
327 if ([[identity email] isEqualToString:[self from]])
333 - (id)lookupSentFolderUsingFrom {
334 // TODO: if we have the identity we could also support BCC
335 SOGoMailAccounts *accounts;
336 SOGoMailIdentity *identity;
338 NSString *sentFolderName;
339 NSArray *sentFolderPath;
340 NSException *error = nil;
342 if (sentFolder != nil)
343 return [sentFolder isNotNull] ? sentFolder : nil;;
345 identity = [self selectedMailIdentity];
346 if ([identity isKindOfClass:[NSException class]]) return identity;
348 if (![(sentFolderName = [identity sentFolderName]) isNotEmpty]) {
349 [self warnWithFormat:@"Identity has no sent folder name: %@", identity];
353 // TODO: fixme, we treat the foldername as a hardcoded path from SOGoAccounts
354 // TODO: escaping of foldernames with slashes
355 // TODO: maybe the SOGoMailIdentity should have an 'account-identifier'
356 // which is used to lookup the account and _then_ perform an account
357 // local folder lookup? => would not be possible to have identities
358 // saving to different accounts.
359 sentFolderPath = [sentFolderName componentsSeparatedByString:@"/"];
361 accounts = [[self clientObject] mailAccountsFolder];
362 if ([accounts isKindOfClass:[NSException class]]) return (id)accounts;
364 ctx = [[SoSubContext alloc] initWithParentContext:[self context]];
366 sentFolder = [[accounts traversePathArray:sentFolderPath
367 inContext:ctx error:&error
369 [ctx release]; ctx = nil;
371 [self errorWithFormat:@"Sent-Folder lookup for identity %@ failed: %@",
372 identity, sentFolderPath];
377 [self logWithFormat:@"Sent-Folder: %@", sentFolderName];
378 [self logWithFormat:@" object: %@", sentFolder];
383 - (NSException *)storeMailInSentFolder:(NSString *)_path {
384 SOGoMailFolder *folder;
388 folder = useLocationBasedSentFolder
389 ? [self lookupSentFolderUsingAccount]
390 : [self lookupSentFolderUsingFrom];
391 if ([folder isKindOfClass:[NSException class]]) return (id)folder;
392 if (folder == nil) return nil;
394 if ((data = [[NSData alloc] initWithContentsOfMappedFile:_path]) == nil) {
395 return [NSException exceptionWithHTTPStatus:500 /* server error */
396 reason:@"could not find temporary draft file!"];
399 result = [folder postData:data flags:@"seen"];
400 [data release]; data = nil;
406 - (BOOL)_saveFormInfo {
409 if ((info = [self storeInfo]) != nil) {
412 if ((error = [[self clientObject] storeInfo:info]) != nil) {
413 [self errorWithFormat:@"failed to store draft: %@", error];
414 // TODO: improve error handling
419 // TODO: wrap content
423 - (id)failedToSaveFormResponse {
424 // TODO: improve error handling
425 return [NSException exceptionWithHTTPStatus:500 /* server error */
426 reason:@"failed to store draft object on server!"];
429 /* attachment helper */
431 - (NSArray *)attachmentNames {
434 if (attachmentNames != nil)
435 return attachmentNames;
437 a = [[self clientObject] fetchAttachmentNames];
438 a = [a sortedArrayUsingSelector:@selector(compare:)];
439 attachmentNames = [a copy];
440 return attachmentNames;
442 - (BOOL)hasAttachments {
443 return [[self attachmentNames] count] > 0 ? YES : NO;
446 - (NSString *)initialLeftsideStyle {
447 if ([self hasAttachments])
448 return @"width: 67%";
449 return @"width: 100%";
452 - (NSString *)initialRightsideStyle {
453 if ([self hasAttachments])
454 return @"display: block";
455 return @"display: none";
458 - (id)defaultAction {
459 return [self redirectToLocation:@"edit"];
464 [self logWithFormat:@"edit action, load content from: %@",
465 [self clientObject]];
468 [self loadInfo:[[self clientObject] fetchInfo]];
469 [self _presetFromBasedOnAccountsQueryParameter];
474 return [self _saveFormInfo] ? self : [self failedToSaveFormResponse];
477 - (NSException *)validateForSend {
478 // TODO: localize errors
480 if (![self hasOneOrMoreRecipients]) {
481 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
482 reason:@"Please select a recipient!"];
484 if ([[self subject] length] == 0) {
485 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
486 reason:@"Please set a subject!"];
492 - (id <WOActionResults>) sendAction
496 id <WOActionResults> result;
498 // TODO: need to validate whether we have a To etc
500 /* first, save form data */
502 if (![self _saveFormInfo])
503 return [self failedToSaveFormResponse];
505 /* validate for send */
507 if ((error = [self validateForSend]) != nil) {
510 url = [[error reason] stringByEscapingURL];
511 url = [@"edit?error=" stringByAppendingString:url];
512 return [self redirectToLocation:url];
515 /* setup some extra headers if required */
517 /* save mail to file (so that we can upload the mail to Cyrus) */
518 // TODO: all this could be handled by the SOGoDraftObject?
520 mailPath = [[self clientObject] saveMimeMessageToTemporaryFileWithHeaders: internetMailHeaders];
522 /* then, send mail */
524 if ((error = [[self clientObject] sendMimeMessageAtPath:mailPath]) != nil) {
525 // TODO: improve error handling
526 [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
530 /* patch flags in store for replies etc */
532 if ((error = [self patchFlagsInStore]) != nil)
535 /* finally store in Sent */
537 if ((error = [self storeMailInSentFolder:mailPath]) != nil)
540 /* delete temporary mail file */
543 [self warnWithFormat:@"keeping mail file: '%@'", mailPath];
545 [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
550 if ((error = [[self clientObject] delete]) != nil)
553 if ([[[[self context] request] formValueForKey: @"nojs"] intValue])
554 result = [self redirectToLocation: [self applicationPath]];
556 result = [self jsCloseWithRefreshMethod: nil];
565 if ((error = [[self clientObject] delete]) != nil) {
566 /* Note: we ignore 404: those are drafts which were not yet saved */
567 if (![error httpStatus] == 404)
572 page = [self pageWithName:@"UIxMailWindowCloser"];
573 [page takeValue:@"YES" forKey:@"refreshOpener"];
576 // TODO: if we just return nil, we produce a 500
577 return [NSException exceptionWithHTTPStatus:204 /* No Content */
578 reason:@"object was deleted."];
582 @end /* UIxMailEditor */