]> err.no Git - scalable-opengroupware.org/blob - UI/MailerUI/UIxMailEditor.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1131 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 <NGObjWeb/SoSubContext.h>
29 #import <NGObjWeb/NSException+HTTP.h>
30 #import <NGExtensions/NSNull+misc.h>
31 #import <NGExtensions/NSObject+Logs.h>
32 #import <NGExtensions/NSString+misc.h>
33 #import <NGExtensions/NSException+misc.h>
34 #import <NGMail/NGMimeMessage.h>
35 #import <NGMail/NGMimeMessageGenerator.h>
36 #import <NGMime/NGMimeBodyPart.h>
37 #import <NGMime/NGMimeHeaderFields.h>
38 #import <NGMime/NGMimeMultipartBody.h>
39
40 #import <SoObjects/Mailer/SOGoDraftObject.h>
41 #import <SoObjects/Mailer/SOGoMailFolder.h>
42 #import <SoObjects/Mailer/SOGoMailAccount.h>
43 #import <SoObjects/Mailer/SOGoMailAccounts.h>
44 #import <SoObjects/Mailer/SOGoMailIdentity.h>
45 #import <SoObjects/SOGo/SOGoUser.h>
46 #import <SOGoUI/UIxComponent.h>
47
48 /*
49   UIxMailEditor
50   
51   An mail editor component which works on SOGoDraftObject's.
52 */
53
54 @class NSArray, NSString;
55 @class SOGoMailFolder;
56
57 @interface UIxMailEditor : UIxComponent
58 {
59   NSArray  *to;
60   NSArray  *cc;
61   NSArray  *bcc;
62   NSString *subject;
63   NSString *text;
64   NSMutableArray  *fromEMails;
65   NSString *from;
66   SOGoMailFolder *sentFolder;
67
68   /* these are for the inline attachment list */
69   NSString *attachmentName;
70   NSArray  *attachmentNames;
71 }
72
73 @end
74
75 @implementation UIxMailEditor
76
77 static BOOL         keepMailTmpFile      = NO;
78 static BOOL         showInternetMarker   = NO;
79 static BOOL         useLocationBasedSentFolder = NO;
80 static NSDictionary *internetMailHeaders = nil;
81 static NSArray      *infoKeys            = nil;
82
83 + (void) initialize
84 {
85   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
86   
87   infoKeys = [[NSArray alloc] initWithObjects:
88                                 @"subject", @"text", @"to", @"cc", @"bcc", 
89                               @"from", @"replyTo",
90                               nil];
91   
92   keepMailTmpFile = [ud boolForKey:@"SOGoMailEditorKeepTmpFile"];
93   if (keepMailTmpFile)
94     NSLog(@"WARNING: keeping mail files.");
95   
96   useLocationBasedSentFolder =
97     [ud boolForKey:@"SOGoUseLocationBasedSentFolder"];
98   
99   /* Internet mail settings */
100   
101   showInternetMarker = [ud boolForKey:@"SOGoShowInternetMarker"];
102   if (!showInternetMarker)
103     NSLog(@"Note: visual Internet marker on mail editor disabled "
104           @"(SOGoShowInternetMarker)");
105   
106   internetMailHeaders = 
107     [[ud dictionaryForKey:@"SOGoInternetMailHeaders"] copy];
108   NSLog (@"Note: specified %d headers for mails send via the Internet.", 
109         [internetMailHeaders count]);
110 }
111
112 - (void) dealloc
113 {
114   [sentFolder release];
115   [fromEMails release];
116   [from    release];
117   [text    release];
118   [subject release];
119   [to      release];
120   [cc      release];
121   [bcc     release];
122   
123   [attachmentName  release];
124   [attachmentNames release];
125   [super dealloc];
126 }
127
128 /* accessors */
129
130 - (void) setFrom: (NSString *) _value
131 {
132   ASSIGNCOPY(from, _value);
133 }
134
135 - (NSString *) from
136 {
137   if (![from isNotEmpty])
138     return [[[self context] activeUser] primaryEmail];
139
140   return from;
141 }
142
143 - (void) setReplyTo: (NSString *) _ignore
144 {
145 }
146
147 - (NSString *) replyTo
148 {
149   /* we are here for future extensibility */
150   return @"";
151 }
152
153 - (void) setSubject: (NSString *) _value
154 {
155   ASSIGNCOPY(subject, _value);
156 }
157
158 - (NSString *) subject
159 {
160   return subject ? subject : @"";
161 }
162
163 - (void) setText: (NSString *) _value
164 {
165   ASSIGNCOPY(text, _value);
166 }
167
168 - (NSString *) text
169 {
170   return [text isNotNull] ? text : @"";
171 }
172
173 - (void) setTo: (NSArray *)_value
174 {
175   ASSIGNCOPY(to, _value);
176 }
177
178 - (NSArray *) to
179 {
180   return [to isNotNull] ? to : [NSArray array];
181 }
182
183 - (void) setCc: (NSArray *) _value
184 {
185   ASSIGNCOPY(cc, _value);
186 }
187
188 - (NSArray *) cc
189 {
190   return [cc isNotNull] ? cc : [NSArray array];
191 }
192
193 - (void) setBcc: (NSArray *) _value
194 {
195   ASSIGNCOPY(bcc, _value);
196 }
197
198 - (NSArray *) bcc
199 {
200   return [bcc isNotNull] ? bcc : [NSArray array];
201 }
202
203 - (BOOL) hasOneOrMoreRecipients
204 {
205   if ([[self to]  count] > 0) return YES;
206   if ([[self cc]  count] > 0) return YES;
207   if ([[self bcc] count] > 0) return YES;
208   return NO;
209 }
210
211 - (void) setAttachmentName: (NSString *) _attachmentName
212 {
213   ASSIGN(attachmentName, _attachmentName);
214 }
215
216 - (NSString *) attachmentName
217 {
218   return attachmentName;
219 }
220
221 /* from addresses */
222
223 - (NSArray *) fromEMails
224 {
225   NSEnumerator *emails;
226   SOGoUser *activeUser;
227   NSString *cn, *fullMail, *email;
228   
229   if (!fromEMails)
230     { 
231       fromEMails = [NSMutableArray new];
232       activeUser = [context activeUser];
233       cn = [activeUser cn];
234       if ([cn length] == 0)
235         cn = nil;
236       emails = [[activeUser allEmails] objectEnumerator];
237       email = [emails nextObject];
238       while (email)
239         {
240           if (cn)
241             fullMail = [NSString stringWithFormat: @"%@ <%@>", cn, email];
242           else
243             fullMail = email;
244           [fromEMails addObject: fullMail];
245           email = [emails nextObject];
246         }
247     }
248
249   return fromEMails;
250 }
251
252 /* title */
253
254 - (NSString *)panelTitle {
255   return [self labelForKey:@"Compose Mail"];
256 }
257
258 /* info loading */
259
260 - (void)loadInfo:(NSDictionary *)_info {
261   if (![_info isNotNull]) return;
262   [self debugWithFormat:@"loading info ..."];
263   [self takeValuesFromDictionary:_info];
264 }
265 - (NSDictionary *)storeInfo {
266   [self debugWithFormat:@"storing info ..."];
267   return [self valuesForKeys:infoKeys];
268 }
269
270 /* requests */
271
272 - (BOOL) shouldTakeValuesFromRequest: (WORequest *) _rq
273                            inContext: (WOContext*) _c
274 {
275   return YES;
276 }
277
278 /* IMAP4 store */
279
280 - (NSException *) patchFlagsInStore
281 {
282   /*
283     Flags we should set:
284       if the draft is a reply   => [message markAnswered]
285       if the draft is a forward => [message addFlag:@"forwarded"]
286       
287     This is hard, we would need to find the original message in Cyrus.
288   */
289   return nil;
290 }
291
292 - (id) lookupSentFolderUsingAccount
293 {
294   SOGoMailAccount *account;
295   SOGoMailFolder  *folder;
296   
297   if (sentFolder != nil)
298     return [sentFolder isNotNull] ? sentFolder : nil;;
299   
300   account = [[self clientObject] mailAccountFolder];
301   if ([account isKindOfClass:[NSException class]]) return account;
302   
303   folder = [account sentFolderInContext:[self context]];
304   if ([folder isKindOfClass:[NSException class]]) return folder;
305   return ((sentFolder = [folder retain]));
306 }
307
308 - (void) _presetFromBasedOnAccountsQueryParameter
309 {
310   /* preset the from field to the primary identity of the given account */
311   /* Note: The compose action sets the 'accounts' query parameter */
312   NSString         *accountID;
313   SOGoMailAccounts *accounts;
314   SOGoMailAccount  *account;
315   SOGoMailIdentity *identity;
316
317   if (useLocationBasedSentFolder) /* from will be based on location */
318     return;
319
320   if ([from isNotEmpty]) /* a from is already set */
321     return;
322
323   accountID = [[[self context] request] formValueForKey:@"account"];
324   if (![accountID isNotEmpty])
325     return;
326
327   accounts = [[self clientObject] mailAccountsFolder];
328   if ([accounts isExceptionOrNull])
329     return; /* we don't treat this as an error but are tolerant */
330
331   account = [accounts lookupName:accountID inContext:[self context]
332                       acquire:NO];
333   if ([account isExceptionOrNull])
334     return; /* we don't treat this as an error but are tolerant */
335   
336   identity = [account valueForKey:@"preferredIdentity"];
337   if (![identity isNotNull]) {
338     [self warnWithFormat:@"Account has no preferred identity: %@", account];
339     return;
340   }
341   
342   [self setFrom: [identity email]];
343 }
344
345 - (SOGoMailIdentity *) selectedMailIdentity
346 {
347   SOGoMailAccounts *accounts;
348   NSEnumerator     *e;
349   SOGoMailIdentity *identity;
350   
351   accounts = [[self clientObject] mailAccountsFolder];
352   if ([accounts isExceptionOrNull]) return (id)accounts;
353   
354   // TODO: This is still a hack because we detect the identity based on the
355   //       from. In Agenor all of the identities have unique emails, but this
356   //       is not required for SOGo.
357   
358   if ([[self from] length] == 0)
359     return nil;
360   
361   e = [[accounts fetchIdentitiesWithEmitterPermissions] objectEnumerator];
362   while ((identity = [e nextObject]) != nil) {
363     if ([[identity email] isEqualToString:[self from]])
364       return identity;
365   }
366   return nil;
367 }
368
369 - (id) lookupSentFolderUsingFrom
370 {
371   // TODO: if we have the identity we could also support BCC
372   SOGoMailAccounts *accounts;
373   SOGoMailIdentity *identity;
374   SoSubContext *ctx;
375   NSString     *sentFolderName;
376   NSArray      *sentFolderPath;
377   NSException  *error = nil;
378   
379   if (sentFolder != nil)
380     return [sentFolder isNotNull] ? sentFolder : nil;;
381   
382   identity = [self selectedMailIdentity];
383   if ([identity isKindOfClass:[NSException class]]) return identity;
384   
385   if (![(sentFolderName = [identity sentFolderName]) isNotEmpty]) {
386     [self warnWithFormat:@"Identity has no sent folder name: %@", identity];
387     return nil;
388   }
389   
390   // TODO: fixme, we treat the foldername as a hardcoded path from SOGoAccounts
391   // TODO: escaping of foldernames with slashes
392   // TODO: maybe the SOGoMailIdentity should have an 'account-identifier'
393   //       which is used to lookup the account and _then_ perform an account
394   //       local folder lookup? => would not be possible to have identities
395   //       saving to different accounts.
396   sentFolderPath = [sentFolderName componentsSeparatedByString:@"/"];
397   
398   accounts = [[self clientObject] mailAccountsFolder];
399   if ([accounts isKindOfClass:[NSException class]]) return (id)accounts;
400   
401   ctx = [[SoSubContext alloc] initWithParentContext:[self context]];
402   
403   sentFolder = [[accounts traversePathArray:sentFolderPath
404                                 inContext:ctx error:&error
405                                 acquire:NO] retain];
406   [ctx release]; ctx = nil;
407   if (error != nil) {
408     [self errorWithFormat:@"Sent-Folder lookup for identity %@ failed: %@",
409             identity, sentFolderPath];
410     return error;
411   }
412   
413 #if 0
414   [self logWithFormat:@"Sent-Folder: %@", sentFolderName];
415   [self logWithFormat:@"  object:    %@", sentFolder];
416 #endif
417   return sentFolder;
418 }
419
420 - (NSException *) storeMailInSentFolder: (NSString *) _path
421 {
422   SOGoMailFolder *folder;
423   NSData *data;
424   id result;
425   
426   folder = useLocationBasedSentFolder 
427     ? [self lookupSentFolderUsingAccount]
428     : [self lookupSentFolderUsingFrom];
429   if ([folder isKindOfClass:[NSException class]]) return (id)folder;
430   if (folder == nil) return nil;
431   
432   if ((data = [[NSData alloc] initWithContentsOfMappedFile:_path]) == nil) {
433     return [NSException exceptionWithHTTPStatus:500 /* server error */
434                         reason:@"could not find temporary draft file!"];
435   }
436   
437   result = [folder postData:data flags:@"seen"];
438   [data release]; data = nil;
439   return result;
440 }
441
442 /* actions */
443
444 - (NSDictionary *) _scanAttachmentFilenamesInRequest: (id) httpBody
445 {
446   NSMutableDictionary *filenames;
447   NSDictionary *attachment;
448   NSArray *parts;
449   unsigned int count, max;
450   NGMimeBodyPart *part;
451   NGMimeContentDispositionHeaderField *header;
452   NSString *mimeType;
453
454   parts = [httpBody parts];
455   max = [parts count];
456   filenames = [NSMutableDictionary dictionaryWithCapacity: max];
457
458   for (count = 0; count < max; count++)
459     {
460       part = [parts objectAtIndex: count];
461       header = [part headerForKey: @"content-disposition"];
462       mimeType = [[part headerForKey: @"content-type"] stringValue];
463       attachment = [NSDictionary dictionaryWithObjectsAndKeys:
464                                    [header filename], @"filename",
465                                  mimeType, @"mime-type", nil];
466       [filenames setObject: attachment
467                  forKey: [header name]];
468     }
469
470   return filenames;
471 }
472
473 - (BOOL) _saveAttachments
474 {
475   WORequest *request;
476   NSEnumerator *allKeys;
477   NSString *key;
478   BOOL success;
479   NSDictionary *filenames;
480   id httpBody;
481   SOGoDraftObject *co;
482
483   success = YES;
484   request = [context request];
485
486   httpBody = [[request httpRequest] body];
487   filenames = [self _scanAttachmentFilenamesInRequest: httpBody];
488
489   co = [self clientObject];
490   allKeys = [[request formValueKeys] objectEnumerator];
491   key = [allKeys nextObject];
492   while (key && success)
493     {
494       if ([key hasPrefix: @"attachment"])
495         success
496           = (![co saveAttachment: (NSData *) [request formValueForKey: key]
497                   withMetadata: [filenames objectForKey: key]]);
498       key = [allKeys nextObject];
499     }
500
501   return success;
502 }
503
504 - (BOOL) _saveFormInfo
505 {
506   NSDictionary *info;
507   NSException *error;
508   BOOL success;
509
510   success = YES;
511
512   if ([self _saveAttachments])
513     {
514       info = [self storeInfo];
515       if (info)
516         {
517           error = [[self clientObject] storeInfo:info];
518           if (error)
519             {
520               [self errorWithFormat:@"failed to store draft: %@", error];
521               // TODO: improve error handling
522               success = NO;
523             }
524         }
525     }
526   else
527     success = NO;
528   
529   // TODO: wrap content
530   
531   return success;
532 }
533
534 - (id) failedToSaveFormResponse
535 {
536   // TODO: improve error handling
537   return [NSException exceptionWithHTTPStatus:500 /* server error */
538                       reason:@"failed to store draft object on server!"];
539 }
540
541 /* attachment helper */
542
543 - (NSArray *) attachmentNames
544 {
545   NSArray *a;
546   
547   if (attachmentNames != nil)
548     return attachmentNames;
549   
550   a = [[self clientObject] fetchAttachmentNames];
551   a = [a sortedArrayUsingSelector:@selector(compare:)];
552   attachmentNames = [a copy];
553   return attachmentNames;
554 }
555
556 - (BOOL) hasAttachments
557 {
558   return [[self attachmentNames] count] > 0 ? YES : NO;
559 }
560
561 - (id) defaultAction
562 {
563 #if 0
564   [self logWithFormat:@"edit action, load content from: %@",
565           [self clientObject]];
566 #endif
567   
568   [self loadInfo:[[self clientObject] fetchInfo]];
569   [self _presetFromBasedOnAccountsQueryParameter];
570   return self;
571 }
572
573 - (id) saveAction
574 {
575   return [self _saveFormInfo] ? self : [self failedToSaveFormResponse];
576 }
577
578 - (NSException *) validateForSend
579 {
580   // TODO: localize errors
581   
582   if (![self hasOneOrMoreRecipients]) {
583     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
584                         reason:@"Please select a recipient!"];
585   }
586   if ([[self subject] length] == 0) {
587     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
588                         reason:@"Please set a subject!"];
589   }
590   
591   return nil;
592 }
593
594 - (id <WOActionResults>) sendAction
595 {
596   NSException  *error;
597   NSString     *mailPath;
598   id <WOActionResults> result;
599
600   // TODO: need to validate whether we have a To etc
601   
602   /* first, save form data */
603   
604   if (![self _saveFormInfo])
605     return [self failedToSaveFormResponse];
606   
607   /* validate for send */
608   
609   if ((error = [self validateForSend]) != nil) {
610     id url;
611     
612     url = [[error reason] stringByEscapingURL];
613     url = [@"edit?error=" stringByAppendingString:url];
614     return [self redirectToLocation:url];
615   }
616   
617   /* setup some extra headers if required */
618   
619   /* save mail to file (so that we can upload the mail to Cyrus) */
620   // TODO: all this could be handled by the SOGoDraftObject?
621   
622   mailPath = [[self clientObject] saveMimeMessageToTemporaryFileWithHeaders: internetMailHeaders];
623
624   /* then, send mail */
625   
626   if ((error = [[self clientObject] sendMimeMessageAtPath:mailPath]) != nil) {
627     // TODO: improve error handling
628     [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
629     return error;
630   }
631   
632   /* patch flags in store for replies etc */
633   
634   if ((error = [self patchFlagsInStore]) != nil)
635      return error;
636   
637   /* finally store in Sent */
638   
639   if ((error = [self storeMailInSentFolder:mailPath]) != nil)
640     return error;
641   
642   /* delete temporary mail file */
643   
644   if (keepMailTmpFile)
645     [self warnWithFormat:@"keeping mail file: '%@'", mailPath];
646   else
647     [[NSFileManager defaultManager] removeFileAtPath:mailPath handler:nil];
648   mailPath = nil;
649   
650   /* delete draft */
651   
652   if ((error = [[self clientObject] delete]) != nil)
653     return error;
654
655   if ([[[[self context] request] formValueForKey: @"nojs"] intValue])
656     result = [self redirectToLocation: [self applicationPath]];
657   else
658     result = [self jsCloseWithRefreshMethod: nil];
659
660   return result;
661 }
662
663 - (id) deleteAction
664 {
665   NSException *error;
666   id page;
667   
668   if ((error = [[self clientObject] delete]) != nil) {
669     /* Note: we ignore 404: those are drafts which were not yet saved */
670     if (![error httpStatus] == 404)
671       return error;
672   }
673   
674 #if 1
675   page = [self pageWithName:@"UIxMailWindowCloser"];
676   [page takeValue:@"YES" forKey:@"refreshOpener"];
677   return page;
678 #else
679   // TODO: if we just return nil, we produce a 500
680   return [NSException exceptionWithHTTPStatus:204 /* No Content */
681                       reason:@"object was deleted."];
682 #endif
683 }
684
685 @end /* UIxMailEditor */