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 <SOGoUI/UIxComponent.h>
27 An mail editor component which works on SOGoDraftObject's.
30 @class NSArray, NSString;
31 @class SOGoMailFolder;
33 @interface UIxMailEditor : UIxComponent
42 SOGoMailFolder *sentFolder;
44 /* these are for the inline attachment list */
45 NSString *attachmentName;
46 NSArray *attachmentNames;
51 #import <SoObjects/Mailer/SOGoDraftObject.h>
52 #import <SoObjects/Mailer/SOGoMailFolder.h>
53 #import <SoObjects/Mailer/SOGoMailAccount.h>
54 #import <SoObjects/Mailer/SOGoMailAccounts.h>
55 #import <SoObjects/Mailer/SOGoMailIdentity.h>
56 #import <SoObjects/SOGo/WOContext+Agenor.h>
57 #import <NGMail/NGMimeMessage.h>
58 #import <NGMail/NGMimeMessageGenerator.h>
59 #import <NGObjWeb/SoSubContext.h>
62 @implementation UIxMailEditor
64 static BOOL keepMailTmpFile = NO;
65 static BOOL showInternetMarker = NO;
66 static BOOL useLocationBasedSentFolder = NO;
67 static NSDictionary *internetMailHeaders = nil;
68 static NSArray *infoKeys = nil;
71 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
73 infoKeys = [[NSArray alloc] initWithObjects:
74 @"subject", @"text", @"to", @"cc", @"bcc",
78 keepMailTmpFile = [ud boolForKey:@"SOGoMailEditorKeepTmpFile"];
80 NSLog(@"WARNING: keeping mail files.");
82 useLocationBasedSentFolder =
83 [ud boolForKey:@"SOGoUseLocationBasedSentFolder"];
85 /* Internet mail settings */
87 showInternetMarker = [ud boolForKey:@"SOGoShowInternetMarker"];
88 if (!showInternetMarker) {
89 NSLog(@"Note: visual Internet marker on mail editor disabled "
90 @"(SOGoShowInternetMarker)");
94 [[ud dictionaryForKey:@"SOGoInternetMailHeaders"] copy];
95 NSLog(@"Note: specified %d headers for mails send via the Internet.",
96 [internetMailHeaders count]);
100 [self->sentFolder release];
101 [self->fromEMails release];
102 [self->from release];
103 [self->text release];
104 [self->subject release];
109 [self->attachmentName release];
110 [self->attachmentNames release];
116 - (void)setFrom:(NSString *)_value {
117 ASSIGNCOPY(self->from, _value);
120 if (![self->from isNotEmpty])
121 return [[[self context] activeUser] email];
125 - (void)setReplyTo:(NSString *)_ignore {
127 - (NSString *)replyTo {
128 /* we are here for future extensibility */
132 - (void)setSubject:(NSString *)_value {
133 ASSIGNCOPY(self->subject, _value);
135 - (NSString *)subject {
136 return self->subject ? self->subject : @"";
139 - (void)setText:(NSString *)_value {
140 ASSIGNCOPY(self->text, _value);
143 return [self->text isNotNull] ? self->text : @"";
146 - (void)setTo:(NSArray *)_value {
147 ASSIGNCOPY(self->to, _value);
150 return [self->to isNotNull] ? self->to : [NSArray array];
153 - (void)setCc:(NSArray *)_value {
154 ASSIGNCOPY(self->cc, _value);
157 return [self->cc isNotNull] ? self->cc : [NSArray array];
160 - (void)setBcc:(NSArray *)_value {
161 ASSIGNCOPY(self->bcc, _value);
164 return [self->bcc isNotNull] ? self->bcc : [NSArray array];
167 - (BOOL)hasOneOrMoreRecipients {
168 if ([[self to] count] > 0) return YES;
169 if ([[self cc] count] > 0) return YES;
170 if ([[self bcc] count] > 0) return YES;
174 - (void)setAttachmentName:(NSString *)_attachmentName {
175 ASSIGN(self->attachmentName, _attachmentName);
177 - (NSString *)attachmentName {
178 return self->attachmentName;
183 - (NSArray *)fromEMails {
184 NSString *primary, *uid;
187 if (self->fromEMails != nil)
188 return self->fromEMails;
190 uid = [[self user] login];
191 primary = [[[self context] activeUser] email];
192 if (![[self context] isAccessFromIntranet]) {
193 self->fromEMails = [[NSArray alloc] initWithObjects:&primary count:1];
194 return self->fromEMails;
198 [[[self context] activeUser] valueForKey:@"additionalEMailAddresses"];
199 if ([shares count] == 0)
200 self->fromEMails = [[NSArray alloc] initWithObjects:&primary count:1];
204 tmp = [[NSArray alloc] initWithObjects:&primary count:1];
205 self->fromEMails = [[tmp arrayByAddingObjectsFromArray:shares] copy];
206 [tmp release]; tmp = nil;
208 return self->fromEMails;
213 - (NSString *)panelTitle {
214 return [self labelForKey:@"Compose Mail"];
217 /* detect webmail being accessed from the outside */
219 - (BOOL)isInternetRequest {
221 return [[self context] isAccessFromIntranet] ? NO : YES;
224 - (BOOL)showInternetMarker {
225 if (!showInternetMarker)
227 return [[self context] isAccessFromIntranet] ? NO : YES;
232 - (void)loadInfo:(NSDictionary *)_info {
233 if (![_info isNotNull]) return;
234 [self debugWithFormat:@"loading info ..."];
235 [self takeValuesFromDictionary:_info];
237 - (NSDictionary *)storeInfo {
238 [self debugWithFormat:@"storing info ..."];
239 return [self valuesForKeys:infoKeys];
244 - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{
250 - (NSException *)patchFlagsInStore {
253 if the draft is a reply => [message markAnswered]
254 if the draft is a forward => [message addFlag:@"forwarded"]
256 This is hard, we would need to find the original message in Cyrus.
261 - (id)lookupSentFolderUsingAccount {
262 SOGoMailAccount *account;
263 SOGoMailFolder *folder;
265 if (self->sentFolder != nil)
266 return [self->sentFolder isNotNull] ? self->sentFolder : nil;;
268 account = [[self clientObject] mailAccountFolder];
269 if ([account isKindOfClass:[NSException class]]) return account;
271 folder = [account sentFolderInContext:[self context]];
272 if ([folder isKindOfClass:[NSException class]]) return folder;
273 return ((self->sentFolder = [folder retain]));
276 - (void)_presetFromBasedOnAccountsQueryParameter {
277 /* preset the from field to the primary identity of the given account */
278 /* Note: The compose action sets the 'accounts' query parameter */
280 SOGoMailAccounts *accounts;
281 SOGoMailAccount *account;
282 SOGoMailIdentity *identity;
284 if (useLocationBasedSentFolder) /* from will be based on location */
287 if ([self->from isNotEmpty]) /* a from is already set */
290 accountID = [[[self context] request] formValueForKey:@"account"];
291 if (![accountID isNotEmpty])
294 accounts = [[self clientObject] mailAccountsFolder];
295 if ([accounts isExceptionOrNull])
296 return; /* we don't treat this as an error but are tolerant */
298 account = [accounts lookupName:accountID inContext:[self context]
300 if ([account isExceptionOrNull])
301 return; /* we don't treat this as an error but are tolerant */
303 identity = [account valueForKey:@"preferredIdentity"];
304 if (![identity isNotNull]) {
305 [self warnWithFormat:@"Account has no preferred identity: %@", account];
309 [self setFrom: [identity email]];
312 - (SOGoMailIdentity *)selectedMailIdentity {
313 SOGoMailAccounts *accounts;
315 SOGoMailIdentity *identity;
317 accounts = [[self clientObject] mailAccountsFolder];
318 if ([accounts isExceptionOrNull]) return (id)accounts;
320 // TODO: This is still a hack because we detect the identity based on the
321 // from. In Agenor all of the identities have unique emails, but this
322 // is not required for SOGo.
324 if ([[self from] length] == 0)
327 e = [[accounts fetchIdentitiesWithEmitterPermissions] objectEnumerator];
328 while ((identity = [e nextObject]) != nil) {
329 if ([[identity email] isEqualToString:[self from]])
335 - (id)lookupSentFolderUsingFrom {
336 // TODO: if we have the identity we could also support BCC
337 SOGoMailAccounts *accounts;
338 SOGoMailIdentity *identity;
340 NSString *sentFolderName;
341 NSArray *sentFolderPath;
342 NSException *error = nil;
344 if (self->sentFolder != nil)
345 return [self->sentFolder isNotNull] ? self->sentFolder : nil;;
347 identity = [self selectedMailIdentity];
348 if ([identity isKindOfClass:[NSException class]]) return identity;
350 if (![(sentFolderName = [identity sentFolderName]) isNotEmpty]) {
351 [self warnWithFormat:@"Identity has no sent folder name: %@", identity];
355 // TODO: fixme, we treat the foldername as a hardcoded path from SOGoAccounts
356 // TODO: escaping of foldernames with slashes
357 // TODO: maybe the SOGoMailIdentity should have an 'account-identifier'
358 // which is used to lookup the account and _then_ perform an account
359 // local folder lookup? => would not be possible to have identities
360 // saving to different accounts.
361 sentFolderPath = [sentFolderName componentsSeparatedByString:@"/"];
363 accounts = [[self clientObject] mailAccountsFolder];
364 if ([accounts isKindOfClass:[NSException class]]) return (id)accounts;
366 ctx = [[SoSubContext alloc] initWithParentContext:[self context]];
368 self->sentFolder = [[accounts traversePathArray:sentFolderPath
369 inContext:ctx error:&error
371 [ctx release]; ctx = nil;
373 [self errorWithFormat:@"Sent-Folder lookup for identity %@ failed: %@",
374 identity, sentFolderPath];
379 [self logWithFormat:@"Sent-Folder: %@", sentFolderName];
380 [self logWithFormat:@" object: %@", self->sentFolder];
382 return self->sentFolder;
385 - (NSException *)storeMailInSentFolder:(NSString *)_path {
386 SOGoMailFolder *folder;
390 folder = useLocationBasedSentFolder
391 ? [self lookupSentFolderUsingAccount]
392 : [self lookupSentFolderUsingFrom];
393 if ([folder isKindOfClass:[NSException class]]) return (id)folder;
394 if (folder == nil) return nil;
396 if ((data = [[NSData alloc] initWithContentsOfMappedFile:_path]) == nil) {
397 return [NSException exceptionWithHTTPStatus:500 /* server error */
398 reason:@"could not find temporary draft file!"];
401 result = [folder postData:data flags:@"seen"];
402 [data release]; data = nil;
408 - (BOOL)_saveFormInfo {
411 if ((info = [self storeInfo]) != nil) {
414 if ((error = [[self clientObject] storeInfo:info]) != nil) {
415 [self errorWithFormat:@"failed to store draft: %@", error];
416 // TODO: improve error handling
421 // TODO: wrap content
425 - (id)failedToSaveFormResponse {
426 // TODO: improve error handling
427 return [NSException exceptionWithHTTPStatus:500 /* server error */
428 reason:@"failed to store draft object on server!"];
431 /* attachment helper */
433 - (NSArray *)attachmentNames {
436 if (self->attachmentNames != nil)
437 return self->attachmentNames;
439 a = [[self clientObject] fetchAttachmentNames];
440 a = [a sortedArrayUsingSelector:@selector(compare:)];
441 self->attachmentNames = [a copy];
442 return self->attachmentNames;
444 - (BOOL)hasAttachments {
445 return [[self attachmentNames] count] > 0 ? YES : NO;
448 - (NSString *)initialLeftsideStyle {
449 if ([self hasAttachments])
450 return @"width: 67%";
451 return @"width: 100%";
454 - (NSString *)initialRightsideStyle {
455 if ([self hasAttachments])
456 return @"display: block";
457 return @"display: none";
460 - (id)defaultAction {
461 return [self redirectToLocation:@"edit"];
466 [self logWithFormat:@"edit action, load content from: %@",
467 [self clientObject]];
470 [self loadInfo:[[self clientObject] fetchInfo]];
471 [self _presetFromBasedOnAccountsQueryParameter];
476 return [self _saveFormInfo] ? self : [self failedToSaveFormResponse];
479 - (NSException *)validateForSend {
480 // TODO: localize errors
482 if (![self hasOneOrMoreRecipients]) {
483 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
484 reason:@"Please select a recipient!"];
486 if ([[self subject] length] == 0) {
487 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
488 reason:@"Please set a subject!"];
494 - (id <WOActionResults>) sendAction
499 id <WOActionResults> result;
501 // TODO: need to validate whether we have a To etc
503 /* first, save form data */
505 if (![self _saveFormInfo])
506 return [self failedToSaveFormResponse];
508 /* validate for send */
510 if ((error = [self validateForSend]) != nil) {
513 url = [[error reason] stringByEscapingURL];
514 url = [@"edit?error=" stringByAppendingString:url];
515 return [self redirectToLocation:url];
518 /* setup some extra headers if required */
520 h = [[self context] isAccessFromIntranet] ? nil : internetMailHeaders;
522 /* save mail to file (so that we can upload the mail to Cyrus) */
523 // TODO: all this could be handled by the SOGoDraftObject?
525 mailPath = [[self clientObject] saveMimeMessageToTemporaryFileWithHeaders:h];
527 /* then, send mail */
529 if ((error = [[self clientObject] sendMimeMessageAtPath:mailPath]) != nil) {
530 // TODO: improve error handling
531 [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
535 /* patch flags in store for replies etc */
537 if ((error = [self patchFlagsInStore]) != nil)
540 /* finally store in Sent */
542 if ((error = [self storeMailInSentFolder:mailPath]) != nil)
545 /* delete temporary mail file */
548 [self warnWithFormat:@"keeping mail file: '%@'", mailPath];
550 [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
555 if ((error = [[self clientObject] delete]) != nil)
558 if ([[[[self context] request] formValueForKey: @"nojs"] intValue])
559 result = [self redirectToLocation: [self applicationPath]];
561 result = [self jsCloseWithRefreshMethod: nil];
570 if ((error = [[self clientObject] delete]) != nil) {
571 /* Note: we ignore 404: those are drafts which were not yet saved */
572 if (![error httpStatus] == 404)
577 page = [self pageWithName:@"UIxMailWindowCloser"];
578 [page takeValue:@"YES" forKey:@"refreshOpener"];
581 // TODO: if we just return nil, we produce a 500
582 return [NSException exceptionWithHTTPStatus:204 /* No Content */
583 reason:@"object was deleted."];
587 @end /* UIxMailEditor */