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