]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1251 d1b88da0-ebda-0310...
[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 <Foundation/NSCalendarDate.h>
23
24 #import <NGObjWeb/NSException+HTTP.h>
25 #import <NGObjWeb/WOContext.h>
26 #import <NGExtensions/NSNull+misc.h>
27 #import <NGExtensions/NSObject+Logs.h>
28 #import <NGCards/iCalCalendar.h>
29 #import <NGCards/iCalEvent.h>
30 #import <NGCards/iCalEventChanges.h>
31 #import <NGCards/iCalPerson.h>
32
33 #import <SoObjects/SOGo/LDAPUserManager.h>
34 #import <SoObjects/SOGo/SOGoObject.h>
35 #import <SoObjects/SOGo/SOGoPermissions.h>
36 #import <SoObjects/SOGo/WORequest+SOGo.h>
37
38 #import "NSArray+Appointments.h"
39 #import "SOGoAppointmentFolder.h"
40
41 #import "SOGoAppointmentObject.h"
42
43 @implementation SOGoAppointmentObject
44
45 - (NSString *) componentTag
46 {
47   return @"vevent";
48 }
49
50 /* iCal handling */
51 - (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt
52 {
53   LDAPUserManager *um;
54   NSMutableArray *uids;
55   NSArray *attendees;
56   unsigned i, count;
57   NSString *email, *uid;
58   
59   if (![_apt isNotNull])
60     return nil;
61   
62   if ((attendees = [_apt attendees]) == nil)
63     return nil;
64   count = [attendees count];
65   uids = [NSMutableArray arrayWithCapacity:count + 1];
66   
67   um = [LDAPUserManager sharedUserManager];
68   
69   /* add organizer */
70   
71   email = [[_apt organizer] rfc822Email];
72   if ([email isNotNull]) {
73     uid = [um getUIDForEmail: email];
74     if ([uid isNotNull]) {
75       [uids addObject:uid];
76     }
77     else
78       [self logWithFormat:@"Note: got no uid for organizer: '%@'", email];
79   }
80
81   /* add attendees */
82   
83   for (i = 0; i < count; i++)
84     {
85       iCalPerson *person;
86     
87       person = [attendees objectAtIndex:i];
88       email  = [person rfc822Email];
89       if (![email isNotNull]) continue;
90     
91       uid = [um getUIDForEmail:email];
92       if (![uid isNotNull]) {
93         [self logWithFormat:@"Note: got no uid for email: '%@'", email];
94         continue;
95       }
96       if (![uids containsObject:uid])
97         [uids addObject:uid];
98     }
99
100   return uids;
101 }
102
103 /* store in all the other folders */
104
105 - (NSException *) saveContentString: (NSString *) _iCal
106                              inUIDs: (NSArray *) _uids
107 {
108   NSEnumerator *e;
109   id folder;
110   NSException *allErrors = nil;
111   NSException           *error;
112   SOGoAppointmentObject *apt;
113
114   e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context]
115         objectEnumerator];
116   while ((folder = [e nextObject]))
117     {
118       apt = [SOGoAppointmentObject objectWithName: nameInContainer
119                                    inContainer: folder];
120       error = [apt primarySaveContentString:_iCal];
121       if (error)
122         {
123           [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder];
124           // TODO: make compound
125           allErrors = error;
126         }
127     }
128
129   return allErrors;
130 }
131
132 - (NSException *) deleteInUIDs: (NSArray *) _uids
133 {
134   NSEnumerator *e;
135   id folder;
136   NSException *allErrors = nil;
137   NSException           *error;
138   SOGoAppointmentObject *apt;
139   
140   e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context]
141         objectEnumerator];
142   while ((folder = [e nextObject]))
143     {
144       apt = [folder lookupName: [self nameInContainer]
145                     inContext: context
146                     acquire:NO];
147       if ([apt isKindOfClass: [NSException class]]) {
148         [self logWithFormat: @"%@", [(NSException *) apt reason]];
149         continue;
150       }
151     
152       if ((error = [apt primaryDelete]) != nil) {
153         [self logWithFormat:@"Note: failed to delete in folder: %@", folder];
154         // TODO: make compound
155         allErrors = error;
156       }
157     }
158
159   return allErrors;
160 }
161
162 /* "iCal multifolder saves" */
163 - (BOOL) _aptIsStillRelevant: (iCalEvent *) appointment
164 {
165   NSCalendarDate *now;
166
167   now = [NSCalendarDate calendarDate];
168
169   return ([[appointment endDate] earlierDate: now] == now);
170 }
171
172 - (NSException *) saveContentString: (NSString *) _iCal
173                        baseSequence: (int) _v
174 {
175   /* 
176      Note: we need to delete in all participants folders and send iMIP messages
177            for all external accounts.
178      
179      Steps:
180      - fetch stored content
181      - parse old content
182      - check if sequence matches (or if 0=ignore)
183      - extract old attendee list + organizer (make unique)
184      - parse new content (ensure that sequence is increased!)
185      - extract new attendee list + organizer (make unique)
186      - make a diff => new, same, removed
187      - write to new, same
188      - delete in removed folders
189      - send iMIP mail for all folders not found
190   */
191   LDAPUserManager *um;
192   iCalEvent *oldApt, *newApt;
193   iCalEventChanges *changes;
194   iCalPerson *organizer;
195   NSString *oldContent, *uid;
196   NSArray *uids, *props;
197   NSMutableArray *attendees, *storeUIDs, *removedUIDs;
198   NSException *storeError, *delError;
199   BOOL updateForcesReconsider;
200   
201   if ([[context request] handledByDefaultHandler])
202     {
203       updateForcesReconsider = NO;
204
205       if ([_iCal length] == 0)
206         return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
207                             reason: @"got no iCalendar content to store!"];
208
209       um = [LDAPUserManager sharedUserManager];
210
211       /* handle old content */
212   
213       oldContent = [self contentAsString]; /* if nil, this is a new appointment */
214       if ([oldContent length] == 0)
215         {
216           /* new appointment */
217           [self debugWithFormat:@"saving new appointment: %@", _iCal];
218           oldApt = nil;
219         }
220       else
221         oldApt = (iCalEvent *) [self component: NO];
222   
223       /* compare sequence if requested */
224       if (_v != 0) {
225         // TODO
226       }
227   
228       /* handle new content */
229   
230       newApt = (iCalEvent *) [self component: NO];
231       if (!newApt)
232         return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
233                             reason: @"could not parse iCalendar content!"];
234
235       /* diff */
236   
237       changes = [iCalEventChanges changesFromEvent: oldApt toEvent: newApt];
238       uids = [self getUIDsForICalPersons: [changes deletedAttendees]];
239       removedUIDs = [NSMutableArray arrayWithArray: uids];
240
241       uids = [self getUIDsForICalPersons: [newApt attendees]];
242       storeUIDs = [NSMutableArray arrayWithArray: uids];
243       props = [changes updatedProperties];
244
245       /* detect whether sequence has to be increased */
246       if ([changes hasChanges])
247         [newApt increaseSequence];
248
249       /* preserve organizer */
250
251       organizer = [newApt organizer];
252       uid = [self getUIDForICalPerson: organizer];
253       if (!uid)
254         uid = [self ownerInContext: nil];
255       if (uid) {
256         if (![storeUIDs containsObject:uid])
257           [storeUIDs addObject:uid];
258         [removedUIDs removeObject:uid];
259       }
260
261       /* organizer might have changed completely */
262
263       if (oldApt && ([props containsObject: @"organizer"])) {
264         uid = [self getUIDForICalPerson:[oldApt organizer]];
265         if (uid) {
266           if (![storeUIDs containsObject:uid]) {
267             if (![removedUIDs containsObject:uid]) {
268               [removedUIDs addObject:uid];
269             }
270           }
271         }
272       }
273
274       [self debugWithFormat:@"UID ops:\n  store: %@\n  remove: %@",
275             storeUIDs, removedUIDs];
276
277       /* if time did change, all participants have to re-decide ...
278        * ... exception from that rule: the organizer
279        */
280
281       if (oldApt != nil &&
282           ([props containsObject: @"startDate"] ||
283            [props containsObject: @"endDate"]   ||
284            [props containsObject: @"duration"]))
285         {
286           NSArray  *ps;
287           unsigned i, count;
288     
289           ps    = [newApt attendees];
290           count = [ps count];
291           for (i = 0; i < count; i++) {
292             iCalPerson *p;
293       
294             p = [ps objectAtIndex:i];
295             if (![p hasSameEmailAddress:organizer])
296               [p setParticipationStatus:iCalPersonPartStatNeedsAction];
297           }
298           _iCal = [[newApt parent] versitString];
299           updateForcesReconsider = YES;
300         }
301
302       /* perform storing */
303
304       storeError = [self saveContentString: _iCal inUIDs: storeUIDs];
305       delError = [self deleteInUIDs: removedUIDs];
306
307       // TODO: make compound
308       if (storeError != nil) return storeError;
309       if (delError   != nil) return delError;
310
311       /* email notifications */
312       if ([self sendEMailNotifications]
313           && [self _aptIsStillRelevant: newApt])
314         {
315           attendees
316             = [NSMutableArray arrayWithArray: [changes insertedAttendees]];
317           [attendees removePerson: organizer];
318           [self sendEMailUsingTemplateNamed: @"Invitation"
319                 forOldObject: nil
320                 andNewObject: newApt
321                 toAttendees: attendees];
322
323           if (updateForcesReconsider) {
324             attendees = [NSMutableArray arrayWithArray:[newApt attendees]];
325             [attendees removeObjectsInArray:[changes insertedAttendees]];
326             [attendees removePerson:organizer];
327             [self sendEMailUsingTemplateNamed: @"Update"
328                   forOldObject: oldApt
329                   andNewObject: newApt
330                   toAttendees: attendees];
331           }
332
333           attendees
334             = [NSMutableArray arrayWithArray: [changes deletedAttendees]];
335           [attendees removePerson: organizer];
336           if ([attendees count])
337             {
338               iCalEvent *cancelledApt;
339     
340               cancelledApt = [newApt copy];
341               [(iCalCalendar *) [cancelledApt parent] setMethod: @"cancel"];
342               [self sendEMailUsingTemplateNamed: @"Removal"
343                     forOldObject: nil
344                     andNewObject: cancelledApt
345                     toAttendees: attendees];
346               [cancelledApt release];
347             }
348         }
349     }
350   else
351     [self primarySaveContentString: _iCal];
352
353   return nil;
354 }
355
356 - (NSException *) deleteWithBaseSequence: (int)_v
357 {
358   /* 
359      Note: We need to delete in all participants folders and send iMIP messages
360            for all external accounts.
361            Delete is basically identical to save with all attendees and the
362            organizer being deleted.
363
364      Steps:
365      - fetch stored content
366      - parse old content
367      - check if sequence matches (or if 0=ignore)
368      - extract old attendee list + organizer (make unique)
369      - delete in removed folders
370      - send iMIP mail for all folders not found
371   */
372   iCalEvent *apt;
373   NSMutableArray *attendees, *removedUIDs;
374   NSException *error;
375
376   if ([[context request] handledByDefaultHandler])
377     {
378   /* load existing content */
379       apt = (iCalEvent *) [self component: NO];
380   
381   /* compare sequence if requested */
382
383 //   if (_v != 0) {
384 //     // TODO
385 //   }
386   
387       removedUIDs = [NSMutableArray arrayWithArray:
388                                       [self attendeeUIDsFromAppointment: apt]];
389       if (![removedUIDs containsObject: owner])
390         [removedUIDs addObject: owner];
391
392       if ([self sendEMailNotifications]
393           && [self _aptIsStillRelevant: apt])
394         {
395           /* send notification email to attendees excluding organizer */
396           attendees = [NSMutableArray arrayWithArray:[apt attendees]];
397           [attendees removePerson:[apt organizer]];
398   
399           /* flag appointment as being cancelled */
400           [(iCalCalendar *) [apt parent] setMethod: @"cancel"];
401           [apt increaseSequence];
402
403           /* remove all attendees to signal complete removal */
404           [apt removeAllAttendees];
405
406           /* send notification email */
407           [self sendEMailUsingTemplateNamed: @"Deletion"
408                 forOldObject: nil
409                 andNewObject: apt
410                 toAttendees: attendees];
411         }
412
413       error = [self deleteInUIDs: removedUIDs];
414     }
415   else
416     error = [self primaryDelete];
417
418   return error;
419 }
420
421 - (NSException *) saveContentString: (NSString *) _iCalString
422 {
423   return [self saveContentString: _iCalString baseSequence: 0];
424 }
425
426 /* message type */
427
428 - (NSString *) outlookMessageClass
429 {
430   return @"IPM.Appointment";
431 }
432
433 @end /* SOGoAppointmentObject */