]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoTaskObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1025 d1b88da0-ebda-0310...
[scalable-opengroupware.org] / SoObjects / Appointments / SOGoTaskObject.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 "SOGoTaskObject.h"
23
24 #import <NGCards/iCalCalendar.h>
25 #import <NGCards/iCalToDo.h>
26 #import <NGCards/iCalEventChanges.h>
27 #import <NGCards/iCalPerson.h>
28 #import <SOGo/AgenorUserManager.h>
29 #import <NGMime/NGMime.h>
30 #import <NGMail/NGMail.h>
31 #import <NGMail/NGSendMail.h>
32 #import "SOGoAptMailNotification.h"
33 #import "common.h"
34
35 #import "NSArray+Appointments.h"
36
37 @interface SOGoTaskObject (PrivateAPI)
38
39 - (NSString *) homePageURLForPerson: (iCalPerson *) _person;
40   
41 @end
42
43 @implementation SOGoTaskObject
44
45 static NSString                  *mailTemplateDefaultLanguage = nil;
46
47 + (void)initialize {
48   NSUserDefaults      *ud;
49   static BOOL         didInit = NO;
50   
51   if (didInit) return;
52   didInit = YES;
53   
54   ud = [NSUserDefaults standardUserDefaults];
55   mailTemplateDefaultLanguage = [[ud stringForKey:@"SOGoDefaultLanguage"]
56                                      retain];
57   if (!mailTemplateDefaultLanguage)
58     mailTemplateDefaultLanguage = @"French";
59 }
60
61 - (NSString *) componentTag
62 {
63   return @"vtodo";
64 }
65
66 /* iCal handling */
67
68 - (NSArray *)attendeeUIDsFromTask:(iCalToDo *)_task {
69   AgenorUserManager *um;
70   NSMutableArray    *uids;
71   NSArray  *attendees;
72   unsigned i, count;
73   NSString *email, *uid;
74   
75   if (![_task isNotNull])
76     return nil;
77   
78   if ((attendees = [_task attendees]) == nil)
79     return nil;
80   count = [attendees count];
81   uids = [NSMutableArray arrayWithCapacity:count + 1];
82   
83   um = [AgenorUserManager sharedUserManager];
84   
85   /* add organizer */
86   
87   email = [[_task organizer] rfc822Email];
88   if ([email isNotNull]) {
89     uid = [um getUIDForEmail:email];
90     if ([uid isNotNull]) {
91       [uids addObject:uid];
92     }
93     else
94       [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
95   }
96
97   /* add attendees */
98   
99   for (i = 0; i < count; i++) {
100     iCalPerson *person;
101     
102     person = [attendees objectAtIndex:i];
103     email  = [person rfc822Email];
104     if (![email isNotNull]) continue;
105     
106     uid = [um getUIDForEmail:email];
107     if (![uid isNotNull]) {
108       [self logWithFormat:@"Note: got no uid for email: '%@'", email];
109       continue;
110     }
111     if (![uids containsObject:uid])
112       [uids addObject:uid];
113   }
114   
115   return uids;
116 }
117
118 /* folder management */
119
120 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
121   // TODO: what does this do? lookup the home of the organizer?
122   return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx];
123 }
124 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
125   return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx];
126 }
127
128 /* store in all the other folders */
129
130 - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids {
131   NSEnumerator *e;
132   id           folder;
133   NSException  *allErrors = nil;
134   id ctx;
135
136   ctx = [[WOApplication application] context];
137   
138   e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
139              objectEnumerator];
140   while ((folder = [e nextObject]) != nil) {
141     NSException           *error;
142     SOGoTaskObject *task;
143     
144     if (![folder isNotNull]) /* no folder was found for given UID */
145       continue;
146     
147     task = [folder lookupName:[self nameInContainer] inContext:ctx
148                   acquire:NO];
149     if ([task isKindOfClass: [NSException class]])
150       {
151         [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@",
152               [self nameInContainer], folder];
153         [self logWithFormat:@"the exception reason was: %@",
154               [(NSException *) task reason]];
155         continue;
156       }
157
158     if (![task isNotNull]) {
159       [self logWithFormat:@"Note: did not find '%@' in folder: %@",
160               [self nameInContainer], folder];
161       continue;
162     }
163     
164     if ((error = [task primarySaveContentString:_iCal]) != nil) {
165       [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
166       // TODO: make compound
167       allErrors = error;
168     }
169   }
170   return allErrors;
171 }
172 - (NSException *)deleteInUIDs:(NSArray *)_uids {
173   NSEnumerator *e;
174   id           folder;
175   NSException  *allErrors = nil;
176   id           ctx;
177   
178   ctx = [[WOApplication application] context];
179   
180   e = [[self lookupCalendarFoldersForUIDs:_uids inContext:ctx]
181              objectEnumerator];
182   while ((folder = [e nextObject])) {
183     NSException           *error;
184     SOGoTaskObject *task;
185     
186     task = [folder lookupName:[self nameInContainer] inContext:ctx
187                    acquire:NO];
188     if (![task isNotNull]) {
189       [self logWithFormat:@"Note: did not find '%@' in folder: %@",
190               [self nameInContainer], folder];
191       continue;
192     }
193     if ([task isKindOfClass: [NSException class]]) {
194       [self logWithFormat:@"Exception: %@", [(NSException *) task reason]];
195       continue;
196     }
197     
198     if ((error = [task primaryDelete]) != nil) {
199       [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
200       // TODO: make compound
201       allErrors = error;
202     }
203   }
204   return allErrors;
205 }
206
207 /* "iCal multifolder saves" */
208
209 - (NSException *) saveContentString: (NSString *) _iCal
210                        baseSequence: (int) _v
211 {
212   /* 
213      Note: we need to delete in all participants folders and send iMIP messages
214            for all external accounts.
215      
216      Steps:
217      - fetch stored content
218      - parse old content
219      - check if sequence matches (or if 0=ignore)
220      - extract old attendee list + organizer (make unique)
221      - parse new content (ensure that sequence is increased!)
222      - extract new attendee list + organizer (make unique)
223      - make a diff => new, same, removed
224      - write to new, same
225      - delete in removed folders
226      - send iMIP mail for all folders not found
227   */
228 //   AgenorUserManager *um;
229 //   iCalCalendar *calendar;
230 //   iCalToDo *oldApt, *newApt;
231 // //   iCalToDoChanges  *changes;
232 //   iCalPerson        *organizer;
233 //   NSString          *oldContent, *uid;
234 //   NSArray           *uids, *props;
235 //   NSMutableArray    *attendees, *storeUIDs, *removedUIDs;
236   NSException       *storeError, *delError;
237 //   BOOL              updateForcesReconsider;
238   
239 //   updateForcesReconsider = NO;
240
241 //   if ([_iCal length] == 0) {
242 //     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
243 //                      reason:@"got no iCalendar content to store!"];
244 //   }
245
246 //   um = [AgenorUserManager sharedUserManager];
247
248 //   /* handle old content */
249   
250 //   oldContent = [self contentAsString]; /* if nil, this is a new task */
251 //   if ([oldContent length] == 0)
252 //     {
253 //     /* new task */
254 //       [self debugWithFormat:@"saving new task: %@", _iCal];
255 //       oldApt = nil;
256 //     }
257 //   else
258 //     {
259 //       calendar = [iCalCalendar parseSingleFromSource: oldContent];
260 //       oldApt = [self firstTaskFromCalendar: calendar];
261 //     }
262   
263 //   /* compare sequence if requested */
264
265 //   if (_v != 0) {
266 //     // TODO
267 //   }
268   
269   
270 //   /* handle new content */
271   
272 //   calendar = [iCalCalendar parseSingleFromSource: _iCal];
273 //   newApt = [self firstTaskFromCalendar: calendar];
274 //   if (newApt == nil) {
275 //     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
276 //                      reason:@"could not parse iCalendar content!"];
277 //   }
278   
279 //   /* diff */
280   
281 //   changes = [iCalToDoChanges changesFromEvent: oldApt
282 //                               toEvent: newApt];
283
284 //   uids        = [um getUIDsForICalPersons:[changes deletedAttendees]
285 //                     applyStrictMapping:NO];
286 //   removedUIDs = [NSMutableArray arrayWithArray:uids];
287
288 //   uids        = [um getUIDsForICalPersons:[newApt attendees]
289 //                     applyStrictMapping:NO];
290 //   storeUIDs   = [NSMutableArray arrayWithArray:uids];
291 //   props       = [changes updatedProperties];
292
293 //   /* detect whether sequence has to be increased */
294 //   if ([changes hasChanges])
295 //     [newApt increaseSequence];
296
297 //   /* preserve organizer */
298
299 //   organizer = [newApt organizer];
300 //   uid       = [um getUIDForICalPerson:organizer];
301 //   if (uid) {
302 //     if (![storeUIDs containsObject:uid])
303 //       [storeUIDs addObject:uid];
304 //     [removedUIDs removeObject:uid];
305 //   }
306
307 //   /* organizer might have changed completely */
308
309 //   if (oldApt && ([props containsObject: @"organizer"])) {
310 //     uid = [um getUIDForICalPerson:[oldApt organizer]];
311 //     if (uid) {
312 //       if (![storeUIDs containsObject:uid]) {
313 //         if (![removedUIDs containsObject:uid]) {
314 //           [removedUIDs addObject:uid];
315 //         }
316 //       }
317 //     }
318 //   }
319
320 //   [self debugWithFormat:@"UID ops:\n  store: %@\n  remove: %@",
321 //                         storeUIDs, removedUIDs];
322
323 //   /* if time did change, all participants have to re-decide ...
324 //    * ... exception from that rule: the organizer
325 //    */
326
327 //   if (oldApt != nil &&
328 //       ([props containsObject:@"startDate"] ||
329 //        [props containsObject:@"endDate"]   ||
330 //        [props containsObject:@"duration"]))
331 //   {
332 //     NSArray  *ps;
333 //     unsigned i, count;
334     
335 //     ps    = [newApt attendees];
336 //     count = [ps count];
337 //     for (i = 0; i < count; i++) {
338 //       iCalPerson *p;
339       
340 //       p = [ps objectAtIndex:i];
341 //       if (![p hasSameEmailAddress:organizer])
342 //         [p setParticipationStatus:iCalPersonPartStatNeedsAction];
343 //     }
344 //     _iCal = [[newApt parent] versitString];
345 //     updateForcesReconsider = YES;
346 //   }
347
348 //   /* perform storing */
349
350   storeError = [self primarySaveContentString: _iCal];
351
352 //   storeError = [self saveContentString:_iCal inUIDs:storeUIDs];
353 //   delError   = [self deleteInUIDs:removedUIDs];
354
355   // TODO: make compound
356   if (storeError != nil) return storeError;
357 //   if (delError   != nil) return delError;
358
359   /* email notifications */
360 //   if ([self sendEMailNotifications])
361 //     {
362 //   attendees = [NSMutableArray arrayWithArray:[changes insertedAttendees]];
363 //   [attendees removePerson:organizer];
364 //   [self sendInvitationEMailForTask:newApt
365 //         toAttendees:attendees];
366
367 //   if (updateForcesReconsider) {
368 //     attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
369 //     [attendees removeObjectsInArray:[changes insertedAttendees]];
370 //     [attendees removePerson:organizer];
371 //       [self sendEMailUsingTemplateNamed: @"Update"
372 //             forOldObject: oldApt
373 //             andNewObject: newApt
374 //             toAttendees: attendees];
375 //   }
376
377 //   attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]];
378 //   [attendees removePerson: organizer];
379 //   if ([attendees count]) {
380 //     iCalToDo *canceledApt;
381     
382 //     canceledApt = [newApt copy];
383 //     [(iCalCalendar *) [canceledApt parent] setMethod: @"cancel"];
384 //           [self sendEMailUsingTemplateNamed: @"Removal"
385 //                 forOldObject: nil
386 //                 andNewObject: canceledApt
387 //                 toAttendees: attendees];
388 //     [canceledApt release];
389 //   }
390 // }
391
392   return nil;
393 }
394
395 - (NSException *)deleteWithBaseSequence:(int)_v {
396   /* 
397      Note: We need to delete in all participants folders and send iMIP messages
398            for all external accounts.
399            Delete is basically identical to save with all attendees and the
400            organizer being deleted.
401
402      Steps:
403      - fetch stored content
404      - parse old content
405      - check if sequence matches (or if 0=ignore)
406      - extract old attendee list + organizer (make unique)
407      - delete in removed folders
408      - send iMIP mail for all folders not found
409   */
410   iCalToDo *task;
411   NSArray         *removedUIDs;
412   NSMutableArray  *attendees;
413
414   /* load existing content */
415   
416   task = (iCalToDo *) [self component];
417   
418   /* compare sequence if requested */
419
420   if (_v != 0) {
421     // TODO
422   }
423   
424   removedUIDs = [self attendeeUIDsFromTask:task];
425
426   if ([self sendEMailNotifications])
427     {
428       /* send notification email to attendees excluding organizer */
429       attendees = [NSMutableArray arrayWithArray:[task attendees]];
430       [attendees removePerson:[task organizer]];
431   
432       /* flag task as being canceled */
433       [(iCalCalendar *) [task parent] setMethod: @"cancel"];
434       [task increaseSequence];
435
436       /* remove all attendees to signal complete removal */
437       [task removeAllAttendees];
438
439       /* send notification email */
440       [self sendEMailUsingTemplateNamed: @"Deletion"
441             forOldObject: nil
442             andNewObject: task
443             toAttendees: attendees];
444     }
445
446   /* perform */
447   
448   return [self deleteInUIDs:removedUIDs];
449 }
450
451 - (NSException *)saveContentString:(NSString *)_iCalString {
452   return [self saveContentString:_iCalString baseSequence:0];
453 }
454
455 - (NSException *)changeParticipationStatus:(NSString *)_status
456   inContext:(id)_ctx
457 {
458   iCalToDo *task;
459   iCalPerson      *p;
460   NSString        *newContent;
461   NSException     *ex;
462   NSString        *myEMail;
463   
464   // TODO: do we need to use SOGoTask? (prefer iCalToDo?)
465   task = (iCalToDo *) [self component];
466
467   if (task == nil) {
468     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
469                         reason:@"unable to parse task record"];
470   }
471   
472   myEMail = [[_ctx activeUser] email];
473   if ((p = [task findParticipantWithEmail:myEMail]) == nil) {
474     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
475                         reason:@"user does not participate in this "
476                                @"task"];
477   }
478   
479   [p setPartStat:_status];
480   newContent = [[task parent] versitString];
481   
482   // TODO: send iMIP reply mails?
483   
484 //   [task release]; task = nil;
485   
486   if (newContent == nil) {
487     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
488                         reason:@"Could not generate iCalendar data ..."];
489   }
490   
491   if ((ex = [self saveContentString:newContent]) != nil) {
492     // TODO: why is the exception wrapped?
493     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
494                         reason:[ex reason]];
495   }
496   
497   return nil /* means: no error */;
498 }
499
500
501 /* message type */
502
503 - (NSString *)outlookMessageClass {
504   return @"IPM.Task";
505 }
506
507 /* EMail Notifications */
508
509 - (NSString *)homePageURLForPerson:(iCalPerson *)_person {
510   static AgenorUserManager *um      = nil;
511   static NSString          *baseURL = nil;
512   NSString *uid;
513
514   if (!um) {
515     WOContext *ctx;
516     NSArray   *traversalObjects;
517
518     um = [[AgenorUserManager sharedUserManager] retain];
519
520     /* generate URL from traversal stack */
521     ctx = [[WOApplication application] context];
522     traversalObjects = [ctx objectTraversalStack];
523     if ([traversalObjects count] >= 1) {
524       baseURL = [[[traversalObjects objectAtIndex:0] baseURLInContext:ctx]
525                                                      retain];
526     }
527     else {
528       [self warnWithFormat:@"Unable to create baseURL from context!"];
529       baseURL = @"http://localhost/";
530     }
531   }
532   uid = [um getUIDForEmail:[_person rfc822Email]];
533   if (!uid) return nil;
534   return [NSString stringWithFormat:@"%@%@", baseURL, uid];
535 }
536
537 @end /* SOGoTaskObject */