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 #include <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 #include <SoObjects/Mailer/SOGoDraftObject.h>
52 #include <SoObjects/Mailer/SOGoMailFolder.h>
53 #include <SoObjects/Mailer/SOGoMailAccount.h>
54 #include <SoObjects/Mailer/SOGoMailAccounts.h>
55 #include <SoObjects/Mailer/SOGoMailIdentity.h>
56 #include <SoObjects/SOGo/WOContext+Agenor.h>
57 #include <SoObjects/SOGo/SOGoUser.h>
58 #include <NGMail/NGMimeMessage.h>
59 #include <NGMail/NGMimeMessageGenerator.h>
60 #include <NGObjWeb/SoSubContext.h>
63 @implementation UIxMailEditor
65 static BOOL keepMailTmpFile = NO;
66 static BOOL showInternetMarker = NO;
67 static BOOL useLocationBasedSentFolder = NO;
68 static NSDictionary *internetMailHeaders = nil;
69 static NSArray *infoKeys = nil;
72 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
74 infoKeys = [[NSArray alloc] initWithObjects:
75 @"subject", @"text", @"to", @"cc", @"bcc",
79 keepMailTmpFile = [ud boolForKey:@"SOGoMailEditorKeepTmpFile"];
81 NSLog(@"WARNING: keeping mail files.");
83 useLocationBasedSentFolder =
84 [ud boolForKey:@"SOGoUseLocationBasedSentFolder"];
86 /* Internet mail settings */
88 showInternetMarker = [ud boolForKey:@"SOGoShowInternetMarker"];
89 if (!showInternetMarker) {
90 NSLog(@"Note: visual Internet marker on mail editor disabled "
91 @"(SOGoShowInternetMarker)");
95 [[ud dictionaryForKey:@"SOGoInternetMailHeaders"] copy];
96 NSLog(@"Note: specified %d headers for mails send via the Internet.",
97 [internetMailHeaders count]);
101 [self->sentFolder release];
102 [self->fromEMails release];
103 [self->from release];
104 [self->text release];
105 [self->subject release];
110 [self->attachmentName release];
111 [self->attachmentNames release];
117 - (void)setFrom:(NSString *)_value {
118 ASSIGNCOPY(self->from, _value);
121 if (![self->from isNotEmpty])
122 return [[[self context] activeUser] email];
126 - (void)setReplyTo:(NSString *)_ignore {
128 - (NSString *)replyTo {
129 /* we are here for future extensibility */
133 - (void)setSubject:(NSString *)_value {
134 ASSIGNCOPY(self->subject, _value);
136 - (NSString *)subject {
137 return self->subject ? self->subject : @"";
140 - (void)setText:(NSString *)_value {
141 ASSIGNCOPY(self->text, _value);
144 return [self->text isNotNull] ? self->text : @"";
147 - (void)setTo:(NSArray *)_value {
148 ASSIGNCOPY(self->to, _value);
151 return [self->to isNotNull] ? self->to : [NSArray array];
154 - (void)setCc:(NSArray *)_value {
155 ASSIGNCOPY(self->cc, _value);
158 return [self->cc isNotNull] ? self->cc : [NSArray array];
161 - (void)setBcc:(NSArray *)_value {
162 ASSIGNCOPY(self->bcc, _value);
165 return [self->bcc isNotNull] ? self->bcc : [NSArray array];
168 - (BOOL)hasOneOrMoreRecipients {
169 if ([[self to] count] > 0) return YES;
170 if ([[self cc] count] > 0) return YES;
171 if ([[self bcc] count] > 0) return YES;
175 - (void)setAttachmentName:(NSString *)_attachmentName {
176 ASSIGN(self->attachmentName, _attachmentName);
178 - (NSString *)attachmentName {
179 return self->attachmentName;
184 - (NSArray *)fromEMails {
185 NSString *primary, *uid;
188 if (self->fromEMails != nil)
189 return self->fromEMails;
191 uid = [[self user] login];
192 primary = [[[self context] activeUser] email];
193 if (![[self context] isAccessFromIntranet]) {
194 self->fromEMails = [[NSArray alloc] initWithObjects:&primary count:1];
195 return self->fromEMails;
199 [[[self context] activeUser] valueForKey:@"additionalEMailAddresses"];
200 if ([shares count] == 0)
201 self->fromEMails = [[NSArray alloc] initWithObjects:&primary count:1];
205 tmp = [[NSArray alloc] initWithObjects:&primary count:1];
206 self->fromEMails = [[tmp arrayByAddingObjectsFromArray:shares] copy];
207 [tmp release]; tmp = nil;
209 return self->fromEMails;
214 - (NSString *)panelTitle {
215 return [self labelForKey:@"Compose Mail"];
218 /* detect webmail being accessed from the outside */
220 - (BOOL)isInternetRequest {
222 return [[self context] isAccessFromIntranet] ? NO : YES;
225 - (BOOL)showInternetMarker {
226 if (!showInternetMarker)
228 return [[self context] isAccessFromIntranet] ? NO : YES;
233 - (void)loadInfo:(NSDictionary *)_info {
234 if (![_info isNotNull]) return;
235 [self debugWithFormat:@"loading info ..."];
236 [self takeValuesFromDictionary:_info];
238 - (NSDictionary *)storeInfo {
239 [self debugWithFormat:@"storing info ..."];
240 return [self valuesForKeys:infoKeys];
245 - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{
251 - (NSException *)patchFlagsInStore {
254 if the draft is a reply => [message markAnswered]
255 if the draft is a forward => [message addFlag:@"forwarded"]
257 This is hard, we would need to find the original message in Cyrus.
262 - (id)lookupSentFolderUsingAccount {
263 SOGoMailAccount *account;
264 SOGoMailFolder *folder;
266 if (self->sentFolder != nil)
267 return [self->sentFolder isNotNull] ? self->sentFolder : nil;;
269 account = [[self clientObject] mailAccountFolder];
270 if ([account isKindOfClass:[NSException class]]) return account;
272 folder = [account sentFolderInContext:[self context]];
273 if ([folder isKindOfClass:[NSException class]]) return folder;
274 return ((self->sentFolder = [folder retain]));
277 - (void)_presetFromBasedOnAccountsQueryParameter {
278 /* preset the from field to the primary identity of the given account */
279 /* Note: The compose action sets the 'accounts' query parameter */
281 SOGoMailAccounts *accounts;
282 SOGoMailAccount *account;
283 SOGoMailIdentity *identity;
285 if (useLocationBasedSentFolder) /* from will be based on location */
288 if ([self->from isNotEmpty]) /* a from is already set */
291 accountID = [[[self context] request] formValueForKey:@"account"];
292 if (![accountID isNotEmpty])
295 accounts = [[self clientObject] mailAccountsFolder];
296 if ([accounts isExceptionOrNull])
297 return; /* we don't treat this as an error but are tolerant */
299 account = [accounts lookupName:accountID inContext:[self context]
301 if ([account isExceptionOrNull])
302 return; /* we don't treat this as an error but are tolerant */
304 identity = [account valueForKey:@"preferredIdentity"];
305 if (![identity isNotNull]) {
306 [self warnWithFormat:@"Account has no preferred identity: %@", account];
310 [self setFrom:[identity email]];
313 - (SOGoMailIdentity *)selectedMailIdentity {
314 SOGoMailAccounts *accounts;
316 SOGoMailIdentity *identity;
318 accounts = [[self clientObject] mailAccountsFolder];
319 if ([accounts isExceptionOrNull]) return (id)accounts;
321 // TODO: This is still a hack because we detect the identity based on the
322 // from. In Agenor all of the identities have unique emails, but this
323 // is not required for SOGo.
325 if ([[self from] length] == 0)
328 e = [[accounts fetchIdentitiesWithEmitterPermissions] objectEnumerator];
329 while ((identity = [e nextObject]) != nil) {
330 if ([[identity email] isEqualToString:[self from]])
336 - (id)lookupSentFolderUsingFrom {
337 // TODO: if we have the identity we could also support BCC
338 SOGoMailAccounts *accounts;
339 SOGoMailIdentity *identity;
341 NSString *sentFolderName;
342 NSArray *sentFolderPath;
343 NSException *error = nil;
345 if (self->sentFolder != nil)
346 return [self->sentFolder isNotNull] ? self->sentFolder : nil;;
348 identity = [self selectedMailIdentity];
349 if ([identity isKindOfClass:[NSException class]]) return identity;
351 if (![(sentFolderName = [identity sentFolderName]) isNotEmpty]) {
352 [self warnWithFormat:@"Identity has no sent folder name: %@", identity];
356 // TODO: fixme, we treat the foldername as a hardcoded path from SOGoAccounts
357 // TODO: escaping of foldernames with slashes
358 // TODO: maybe the SOGoMailIdentity should have an 'account-identifier'
359 // which is used to lookup the account and _then_ perform an account
360 // local folder lookup? => would not be possible to have identities
361 // saving to different accounts.
362 sentFolderPath = [sentFolderName componentsSeparatedByString:@"/"];
364 accounts = [[self clientObject] mailAccountsFolder];
365 if ([accounts isKindOfClass:[NSException class]]) return (id)accounts;
367 ctx = [[SoSubContext alloc] initWithParentContext:[self context]];
369 self->sentFolder = [[accounts traversePathArray:sentFolderPath
370 inContext:ctx error:&error
372 [ctx release]; ctx = nil;
374 [self errorWithFormat:@"Sent-Folder lookup for identity %@ failed: %@",
375 identity, sentFolderPath];
380 [self logWithFormat:@"Sent-Folder: %@", sentFolderName];
381 [self logWithFormat:@" object: %@", self->sentFolder];
383 return self->sentFolder;
386 - (NSException *)storeMailInSentFolder:(NSString *)_path {
387 SOGoMailFolder *folder;
391 folder = useLocationBasedSentFolder
392 ? [self lookupSentFolderUsingAccount]
393 : [self lookupSentFolderUsingFrom];
394 if ([folder isKindOfClass:[NSException class]]) return (id)folder;
395 if (folder == nil) return nil;
397 if ((data = [[NSData alloc] initWithContentsOfMappedFile:_path]) == nil) {
398 return [NSException exceptionWithHTTPStatus:500 /* server error */
399 reason:@"could not find temporary draft file!"];
402 result = [folder postData:data flags:@"seen"];
403 [data release]; data = nil;
409 - (BOOL)_saveFormInfo {
412 if ((info = [self storeInfo]) != nil) {
415 if ((error = [[self clientObject] storeInfo:info]) != nil) {
416 [self errorWithFormat:@"failed to store draft: %@", error];
417 // TODO: improve error handling
422 // TODO: wrap content
426 - (id)failedToSaveFormResponse {
427 // TODO: improve error handling
428 return [NSException exceptionWithHTTPStatus:500 /* server error */
429 reason:@"failed to store draft object on server!"];
432 /* attachment helper */
434 - (NSArray *)attachmentNames {
437 if (self->attachmentNames != nil)
438 return self->attachmentNames;
440 a = [[self clientObject] fetchAttachmentNames];
441 a = [a sortedArrayUsingSelector:@selector(compare:)];
442 self->attachmentNames = [a copy];
443 return self->attachmentNames;
445 - (BOOL)hasAttachments {
446 return [[self attachmentNames] count] > 0 ? YES : NO;
449 - (NSString *)initialLeftsideStyle {
450 if ([self hasAttachments])
451 return @"width: 67%";
452 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!"];
499 // TODO: need to validate whether we have a To etc
501 /* first, save form data */
503 if (![self _saveFormInfo])
504 return [self failedToSaveFormResponse];
506 /* validate for send */
508 if ((error = [self validateForSend]) != nil) {
511 url = [[error reason] stringByEscapingURL];
512 url = [@"edit?error=" stringByAppendingString:url];
513 return [self redirectToLocation:url];
516 /* setup some extra headers if required */
518 h = [[self context] isAccessFromIntranet] ? nil : internetMailHeaders;
520 /* save mail to file (so that we can upload the mail to Cyrus) */
521 // TODO: all this could be handled by the SOGoDraftObject?
523 mailPath = [[self clientObject] saveMimeMessageToTemporaryFileWithHeaders:h];
525 /* then, send mail */
527 if ((error = [[self clientObject] sendMimeMessageAtPath:mailPath]) != nil) {
528 // TODO: improve error handling
529 [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
533 /* patch flags in store for replies etc */
535 if ((error = [self patchFlagsInStore]) != nil)
538 /* finally store in Sent */
540 if ((error = [self storeMailInSentFolder:mailPath]) != nil)
543 /* delete temporary mail file */
546 [self warnWithFormat:@"keeping mail file: '%@'", mailPath];
548 [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
553 if ((error = [[self clientObject] delete]) != nil)
556 // if everything is ok, close the window (send a JS closing the Window)
557 return [self pageWithName:@"UIxMailWindowCloser"];
564 if ((error = [[self clientObject] delete]) != nil) {
565 /* Note: we ignore 404: those are drafts which were not yet saved */
566 if (![error httpStatus] == 404)
571 page = [self pageWithName:@"UIxMailWindowCloser"];
572 [page takeValue:@"YES" forKey:@"refreshOpener"];
575 // TODO: if we just return nil, we produce a 500
576 return [NSException exceptionWithHTTPStatus:204 /* No Content */
577 reason:@"object was deleted."];
581 @end /* UIxMailEditor */