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