]> err.no Git - scalable-opengroupware.org/blob - UI/MailerUI/UIxMailEditor.m
initial sync
[scalable-opengroupware.org] / UI / MailerUI / UIxMailEditor.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #import <SOGoUI/UIxComponent.h>
23
24 /*
25   UIxMailEditor
26   
27   An mail editor component which works on SOGoDraftObject's.
28 */
29
30 @class NSArray, NSString;
31 @class SOGoMailFolder;
32
33 @interface UIxMailEditor : UIxComponent
34 {
35   NSArray  *to;
36   NSArray  *cc;
37   NSArray  *bcc;
38   NSString *subject;
39   NSString *text;
40   NSArray  *fromEMails;
41   NSString *from;
42   SOGoMailFolder *sentFolder;
43
44   /* these are for the inline attachment list */
45   NSString *attachmentName;
46   NSArray  *attachmentNames;
47 }
48
49 @end
50
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>
60 #import "common.h"
61
62 @implementation UIxMailEditor
63
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;
69
70 + (void)initialize {
71   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
72   
73   infoKeys = [[NSArray alloc] initWithObjects:
74                                 @"subject", @"text", @"to", @"cc", @"bcc", 
75                                 @"from", @"replyTo",
76                               nil];
77   
78   keepMailTmpFile = [ud boolForKey:@"SOGoMailEditorKeepTmpFile"];
79   if (keepMailTmpFile)
80     NSLog(@"WARNING: keeping mail files.");
81   
82   useLocationBasedSentFolder =
83     [ud boolForKey:@"SOGoUseLocationBasedSentFolder"];
84   
85   /* Internet mail settings */
86   
87   showInternetMarker = [ud boolForKey:@"SOGoShowInternetMarker"];
88   if (!showInternetMarker) {
89     NSLog(@"Note: visual Internet marker on mail editor disabled "
90           @"(SOGoShowInternetMarker)");
91   }
92   
93   internetMailHeaders = 
94     [[ud dictionaryForKey:@"SOGoInternetMailHeaders"] copy];
95   NSLog(@"Note: specified %d headers for mails send via the Internet.", 
96         [internetMailHeaders count]);
97 }
98
99 - (void)dealloc {
100   [self->sentFolder release];
101   [self->fromEMails release];
102   [self->from    release];
103   [self->text    release];
104   [self->subject release];
105   [self->to      release];
106   [self->cc      release];
107   [self->bcc     release];
108   
109   [self->attachmentName  release];
110   [self->attachmentNames release];
111   [super dealloc];
112 }
113
114 /* accessors */
115
116 - (void)setFrom:(NSString *)_value {
117   ASSIGNCOPY(self->from, _value);
118 }
119 - (NSString *)from {
120   if (![self->from isNotEmpty])
121     return [[[self context] activeUser] email];
122   return self->from;
123 }
124
125 - (void)setReplyTo:(NSString *)_ignore {
126 }
127 - (NSString *)replyTo {
128   /* we are here for future extensibility */
129   return @"";
130 }
131
132 - (void)setSubject:(NSString *)_value {
133   ASSIGNCOPY(self->subject, _value);
134 }
135 - (NSString *)subject {
136   return self->subject ? self->subject : @"";
137 }
138
139 - (void)setText:(NSString *)_value {
140   ASSIGNCOPY(self->text, _value);
141 }
142 - (NSString *)text {
143   return [self->text isNotNull] ? self->text : @"";
144 }
145
146 - (void)setTo:(NSArray *)_value {
147   ASSIGNCOPY(self->to, _value);
148 }
149 - (NSArray *)to {
150   return [self->to isNotNull] ? self->to : [NSArray array];
151 }
152
153 - (void)setCc:(NSArray *)_value {
154   ASSIGNCOPY(self->cc, _value);
155 }
156 - (NSArray *)cc {
157   return [self->cc isNotNull] ? self->cc : [NSArray array];
158 }
159
160 - (void)setBcc:(NSArray *)_value {
161   ASSIGNCOPY(self->bcc, _value);
162 }
163 - (NSArray *)bcc {
164   return [self->bcc isNotNull] ? self->bcc : [NSArray array];
165 }
166
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;
171   return NO;
172 }
173
174 - (void)setAttachmentName:(NSString *)_attachmentName {
175   ASSIGN(self->attachmentName, _attachmentName);
176 }
177 - (NSString *)attachmentName {
178   return self->attachmentName;
179 }
180
181 /* from addresses */
182
183 - (NSArray *)fromEMails {
184   NSString *primary, *uid;
185   NSArray  *shares;
186   
187   if (self->fromEMails != nil) 
188     return self->fromEMails;
189   
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;
195   }
196   
197   shares = 
198     [[[self context] activeUser] valueForKey:@"additionalEMailAddresses"];
199   if ([shares count] == 0)
200     self->fromEMails = [[NSArray alloc] initWithObjects:&primary count:1];
201   else {
202     id tmp;
203
204     tmp = [[NSArray alloc] initWithObjects:&primary count:1];
205     self->fromEMails = [[tmp arrayByAddingObjectsFromArray:shares] copy];
206     [tmp release]; tmp = nil;
207   }
208   return self->fromEMails;
209 }
210
211 /* title */
212
213 - (NSString *)panelTitle {
214   return [self labelForKey:@"Compose Mail"];
215 }
216
217 /* detect webmail being accessed from the outside */
218
219 - (BOOL)isInternetRequest {
220   // DEPRECATED
221   return [[self context] isAccessFromIntranet] ? NO : YES;
222 }
223
224 - (BOOL)showInternetMarker {
225   if (!showInternetMarker)
226     return NO;
227   return [[self context] isAccessFromIntranet] ? NO : YES;
228 }
229
230 /* info loading */
231
232 - (void)loadInfo:(NSDictionary *)_info {
233   if (![_info isNotNull]) return;
234   [self debugWithFormat:@"loading info ..."];
235   [self takeValuesFromDictionary:_info];
236 }
237 - (NSDictionary *)storeInfo {
238   [self debugWithFormat:@"storing info ..."];
239   return [self valuesForKeys:infoKeys];
240 }
241
242 /* requests */
243
244 - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{
245   return YES;
246 }
247
248 /* IMAP4 store */
249
250 - (NSException *)patchFlagsInStore {
251   /*
252     Flags we should set:
253       if the draft is a reply   => [message markAnswered]
254       if the draft is a forward => [message addFlag:@"forwarded"]
255       
256     This is hard, we would need to find the original message in Cyrus.
257   */
258   return nil;
259 }
260
261 - (id)lookupSentFolderUsingAccount {
262   SOGoMailAccount *account;
263   SOGoMailFolder  *folder;
264   
265   if (self->sentFolder != nil)
266     return [self->sentFolder isNotNull] ? self->sentFolder : nil;;
267   
268   account = [[self clientObject] mailAccountFolder];
269   if ([account isKindOfClass:[NSException class]]) return account;
270   
271   folder = [account sentFolderInContext:[self context]];
272   if ([folder isKindOfClass:[NSException class]]) return folder;
273   return ((self->sentFolder = [folder retain]));
274 }
275
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 */
279   NSString         *accountID;
280   SOGoMailAccounts *accounts;
281   SOGoMailAccount  *account;
282   SOGoMailIdentity *identity;
283
284   if (useLocationBasedSentFolder) /* from will be based on location */
285     return;
286
287   if ([self->from isNotEmpty]) /* a from is already set */
288     return;
289
290   accountID = [[[self context] request] formValueForKey:@"account"];
291   if (![accountID isNotEmpty])
292     return;
293
294   accounts = [[self clientObject] mailAccountsFolder];
295   if ([accounts isExceptionOrNull])
296     return; /* we don't treat this as an error but are tolerant */
297
298   account = [accounts lookupName:accountID inContext:[self context]
299                       acquire:NO];
300   if ([account isExceptionOrNull])
301     return; /* we don't treat this as an error but are tolerant */
302   
303   identity = [account valueForKey:@"preferredIdentity"];
304   if (![identity isNotNull]) {
305     [self warnWithFormat:@"Account has no preferred identity: %@", account];
306     return;
307   }
308   
309   [self setFrom: [identity email]];
310 }
311
312 - (SOGoMailIdentity *)selectedMailIdentity {
313   SOGoMailAccounts *accounts;
314   NSEnumerator     *e;
315   SOGoMailIdentity *identity;
316   
317   accounts = [[self clientObject] mailAccountsFolder];
318   if ([accounts isExceptionOrNull]) return (id)accounts;
319   
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.
323   
324   if ([[self from] length] == 0)
325     return nil;
326   
327   e = [[accounts fetchIdentitiesWithEmitterPermissions] objectEnumerator];
328   while ((identity = [e nextObject]) != nil) {
329     if ([[identity email] isEqualToString:[self from]])
330       return identity;
331   }
332   return nil;
333 }
334
335 - (id)lookupSentFolderUsingFrom {
336   // TODO: if we have the identity we could also support BCC
337   SOGoMailAccounts *accounts;
338   SOGoMailIdentity *identity;
339   SoSubContext *ctx;
340   NSString     *sentFolderName;
341   NSArray      *sentFolderPath;
342   NSException  *error = nil;
343   
344   if (self->sentFolder != nil)
345     return [self->sentFolder isNotNull] ? self->sentFolder : nil;;
346   
347   identity = [self selectedMailIdentity];
348   if ([identity isKindOfClass:[NSException class]]) return identity;
349   
350   if (![(sentFolderName = [identity sentFolderName]) isNotEmpty]) {
351     [self warnWithFormat:@"Identity has no sent folder name: %@", identity];
352     return nil;
353   }
354   
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:@"/"];
362   
363   accounts = [[self clientObject] mailAccountsFolder];
364   if ([accounts isKindOfClass:[NSException class]]) return (id)accounts;
365   
366   ctx = [[SoSubContext alloc] initWithParentContext:[self context]];
367   
368   self->sentFolder = [[accounts traversePathArray:sentFolderPath
369                                 inContext:ctx error:&error
370                                 acquire:NO] retain];
371   [ctx release]; ctx = nil;
372   if (error != nil) {
373     [self errorWithFormat:@"Sent-Folder lookup for identity %@ failed: %@",
374             identity, sentFolderPath];
375     return error;
376   }
377   
378 #if 0
379   [self logWithFormat:@"Sent-Folder: %@", sentFolderName];
380   [self logWithFormat:@"  object:    %@", self->sentFolder];
381 #endif
382   return self->sentFolder;
383 }
384
385 - (NSException *)storeMailInSentFolder:(NSString *)_path {
386   SOGoMailFolder *folder;
387   NSData *data;
388   id result;
389   
390   folder = useLocationBasedSentFolder 
391     ? [self lookupSentFolderUsingAccount]
392     : [self lookupSentFolderUsingFrom];
393   if ([folder isKindOfClass:[NSException class]]) return (id)folder;
394   if (folder == nil) return nil;
395   
396   if ((data = [[NSData alloc] initWithContentsOfMappedFile:_path]) == nil) {
397     return [NSException exceptionWithHTTPStatus:500 /* server error */
398                         reason:@"could not find temporary draft file!"];
399   }
400   
401   result = [folder postData:data flags:@"seen"];
402   [data release]; data = nil;
403   return result;
404 }
405
406 /* actions */
407
408 - (BOOL)_saveFormInfo {
409   NSDictionary *info;
410   
411   if ((info = [self storeInfo]) != nil) {
412     NSException *error;
413     
414     if ((error = [[self clientObject] storeInfo:info]) != nil) {
415       [self errorWithFormat:@"failed to store draft: %@", error];
416       // TODO: improve error handling
417       return NO;
418     }
419   }
420   
421   // TODO: wrap content
422   
423   return YES;
424 }
425 - (id)failedToSaveFormResponse {
426   // TODO: improve error handling
427   return [NSException exceptionWithHTTPStatus:500 /* server error */
428                       reason:@"failed to store draft object on server!"];
429 }
430
431 /* attachment helper */
432
433 - (NSArray *)attachmentNames {
434   NSArray *a;
435   
436   if (self->attachmentNames != nil)
437     return self->attachmentNames;
438   
439   a = [[self clientObject] fetchAttachmentNames];
440   a = [a sortedArrayUsingSelector:@selector(compare:)];
441   self->attachmentNames = [a copy];
442   return self->attachmentNames;
443 }
444 - (BOOL)hasAttachments {
445   return [[self attachmentNames] count] > 0 ? YES : NO;
446 }
447
448 - (NSString *)initialLeftsideStyle {
449   if ([self hasAttachments])
450     return @"width: 67%";
451   return @"width: 100%";
452 }
453
454 - (NSString *)initialRightsideStyle {
455   if ([self hasAttachments])
456     return @"display: block";
457   return @"display: none";
458 }
459
460 - (id)defaultAction {
461   return [self redirectToLocation:@"edit"];
462 }
463
464 - (id)editAction {
465 #if 0
466   [self logWithFormat:@"edit action, load content from: %@",
467           [self clientObject]];
468 #endif
469   
470   [self loadInfo:[[self clientObject] fetchInfo]];
471   [self _presetFromBasedOnAccountsQueryParameter];
472   return self;
473 }
474
475 - (id)saveAction {
476   return [self _saveFormInfo] ? self : [self failedToSaveFormResponse];
477 }
478
479 - (NSException *)validateForSend {
480   // TODO: localize errors
481   
482   if (![self hasOneOrMoreRecipients]) {
483     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
484                         reason:@"Please select a recipient!"];
485   }
486   if ([[self subject] length] == 0) {
487     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
488                         reason:@"Please set a subject!"];
489   }
490   
491   return nil;
492 }
493
494 - (id <WOActionResults>) sendAction
495 {
496   NSException  *error;
497   NSString     *mailPath;
498   NSDictionary *h;
499   id <WOActionResults> result;
500
501   // TODO: need to validate whether we have a To etc
502   
503   /* first, save form data */
504   
505   if (![self _saveFormInfo])
506     return [self failedToSaveFormResponse];
507   
508   /* validate for send */
509   
510   if ((error = [self validateForSend]) != nil) {
511     id url;
512     
513     url = [[error reason] stringByEscapingURL];
514     url = [@"edit?error=" stringByAppendingString:url];
515     return [self redirectToLocation:url];
516   }
517   
518   /* setup some extra headers if required */
519   
520   h = [[self context] isAccessFromIntranet] ? nil : internetMailHeaders;
521   
522   /* save mail to file (so that we can upload the mail to Cyrus) */
523   // TODO: all this could be handled by the SOGoDraftObject?
524   
525   mailPath = [[self clientObject] saveMimeMessageToTemporaryFileWithHeaders:h];
526
527   /* then, send mail */
528   
529   if ((error = [[self clientObject] sendMimeMessageAtPath:mailPath]) != nil) {
530     // TODO: improve error handling
531     [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
532     return error;
533   }
534   
535   /* patch flags in store for replies etc */
536   
537   if ((error = [self patchFlagsInStore]) != nil)
538      return error;
539   
540   /* finally store in Sent */
541   
542   if ((error = [self storeMailInSentFolder:mailPath]) != nil)
543     return error;
544   
545   /* delete temporary mail file */
546   
547   if (keepMailTmpFile)
548     [self warnWithFormat:@"keeping mail file: '%@'", mailPath];
549   else
550     [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
551   mailPath = nil;
552   
553   /* delete draft */
554   
555   if ((error = [[self clientObject] delete]) != nil)
556     return error;
557
558   if ([[[[self context] request] formValueForKey: @"nojs"] intValue])
559     result = [self redirectToLocation: [self applicationPath]];
560   else
561     result = [self jsCloseWithRefreshMethod: nil];
562
563   return result;
564 }
565
566 - (id)deleteAction {
567   NSException *error;
568   id page;
569   
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)
573       return error;
574   }
575   
576 #if 1
577   page = [self pageWithName:@"UIxMailWindowCloser"];
578   [page takeValue:@"YES" forKey:@"refreshOpener"];
579   return page;
580 #else
581   // TODO: if we just return nil, we produce a 500
582   return [NSException exceptionWithHTTPStatus:204 /* No Content */
583                       reason:@"object was deleted."];
584 #endif
585 }
586
587 @end /* UIxMailEditor */