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