1/*
2  Copyright (C) 2007-2015 Inverse inc.
3  Copyright (C) 2004-2005 SKYRIX Software AG
4
5  This file is part of SOGo
6
7  SOGo is free software; you can redistribute it and/or modify it under
8  the terms of the GNU Lesser General Public License as published by the
9  Free Software Foundation; either version 2, or (at your option) any
10  later version.
11
12  SOGo is distributed in the hope that it will be useful, but WITHOUT ANY
13  WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
15  License for more details.
16
17  You should have received a copy of the GNU Lesser General Public
18  License along with OGo; see the file COPYING.  If not, write to the
19  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
20  02111-1307, USA.
21*/
22
23#import <Foundation/NSCalendarDate.h>
24#import <Foundation/NSDictionary.h>
25#import <Foundation/NSEnumerator.h>
26#import <Foundation/NSTimeZone.h>
27#import <Foundation/NSValue.h>
28#import <Foundation/NSPredicate.h>
29
30#import <NGObjWeb/NSException+HTTP.h>
31#import <NGObjWeb/WOContext+SoObjects.h>
32#import <NGObjWeb/WOResponse.h>
33#import <NGExtensions/NGCalendarDateRange.h>
34#import <NGExtensions/NSNull+misc.h>
35#import <NGExtensions/NSObject+Logs.h>
36#import <NGCards/iCalCalendar.h>
37#import <NGCards/iCalDateTime.h>
38#import <NGCards/iCalEvent.h>
39#import <NGCards/iCalEventChanges.h>
40#import <NGCards/iCalPerson.h>
41#import <NGCards/iCalRecurrenceCalculator.h>
42#import <NGCards/NSCalendarDate+NGCards.h>
43#import <SaxObjC/XMLNamespaces.h>
44
45#import <NGCards/iCalDateTime.h>
46#import <NGCards/iCalTimeZone.h>
47#import <NGCards/iCalTimeZonePeriod.h>
48#import <NGCards/iCalToDo.h>
49#import <NGCards/NSString+NGCards.h>
50
51#import <SOGo/SOGoConstants.h>
52#import <SOGo/SOGoUserManager.h>
53#import <SOGo/NSArray+Utilities.h>
54#import <SOGo/NSDictionary+Utilities.h>
55#import <SOGo/NSObject+DAV.h>
56#import <SOGo/NSString+Utilities.h>
57#import <SOGo/SOGoObject.h>
58#import <SOGo/SOGoPermissions.h>
59#import <SOGo/SOGoGroup.h>
60#import <SOGo/SOGoUser.h>
61#import <SOGo/SOGoUserSettings.h>
62#import <SOGo/SOGoDomainDefaults.h>
63#import <SOGo/SOGoWebDAVValue.h>
64#import <SOGo/WORequest+SOGo.h>
65
66#import "iCalCalendar+SOGo.h"
67#import "iCalEventChanges+SOGo.h"
68#import "iCalEntityObject+SOGo.h"
69#import "iCalPerson+SOGo.h"
70#import "NSArray+Appointments.h"
71#import "SOGoAppointmentFolder.h"
72#import "SOGoAppointmentOccurence.h"
73#import "SOGoCalendarComponent.h"
74
75#import "SOGoAppointmentObject.h"
76
77@implementation SOGoAppointmentObject
78
79- (NSString *) componentTag
80{
81  return @"vevent";
82}
83
84- (SOGoComponentOccurence *) occurence: (iCalRepeatableEntityObject *) occ
85{
86  NSArray *allEvents;
87
88  allEvents = [[occ parent] events];
89
90  return [SOGoAppointmentOccurence
91	   occurenceWithComponent: occ
92	   withMasterComponent: [allEvents objectAtIndex: 0]
93	   inContainer: self];
94}
95
96/**
97 * Return a new exception in the recurrent event.
98 * @param theRecurrenceID the ID of the occurence.
99 * @return a new occurence.
100 */
101- (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) theRecurrenceID
102{
103  iCalEvent *newOccurence, *master;
104  NSCalendarDate *date, *firstDate;
105  unsigned int interval, nbrDays;
106
107  newOccurence = (iCalEvent *) [super newOccurenceWithID: theRecurrenceID];
108  date = [newOccurence recurrenceId];
109
110  master = [self component: NO secure: NO];
111  firstDate = [master startDate];
112
113  interval = [[master endDate]
114               timeIntervalSinceDate: firstDate];
115  if ([newOccurence isAllDay])
116    {
117      nbrDays = ((float) abs (interval) / 86400);
118      [newOccurence setAllDayWithStartDate: date
119                                  duration: nbrDays];
120    }
121  else
122    {
123      [newOccurence setStartDate: date];
124      [newOccurence setEndDate: [date addYear: 0
125                                        month: 0
126                                          day: 0
127                                         hour: 0
128                                       minute: 0
129                                       second: interval]];
130    }
131
132  return newOccurence;
133}
134
135- (iCalRepeatableEntityObject *) lookupOccurrence: (NSString *) recID
136{
137  return [[self calendar: NO secure: NO] eventWithRecurrenceID: recID];
138}
139
140- (SOGoAppointmentObject *) _lookupEvent: (NSString *) eventUID
141				  forUID: (NSString *) uid
142{
143  SOGoAppointmentFolder *folder;
144  SOGoAppointmentObject *object;
145  NSArray *folders;
146  NSEnumerator *e;
147  NSString *possibleName;
148
149  object = nil;
150  folders = [container lookupCalendarFoldersForUID: uid];
151  e = [folders objectEnumerator];
152  while ( object == nil && (folder = [e nextObject]) )
153    {
154      object = [folder lookupName: nameInContainer
155                        inContext: context
156                          acquire: NO];
157      if ([object isKindOfClass: [NSException class]] || [object isNew])
158        {
159          possibleName = [folder resourceNameForEventUID: eventUID];
160          if (possibleName)
161            {
162              object = [folder lookupName: possibleName
163                                inContext: context acquire: NO];
164              if ([object isKindOfClass: [NSException class]] || [object isNew])
165                object = nil;
166            }
167          else
168            object = nil;
169        }
170    }
171
172  if (!object)
173    {
174      // Create the event in the user's personal calendar.
175      folder = [[SOGoUser userWithLogin: uid]
176                 personalCalendarFolderInContext: context];
177      object = [SOGoAppointmentObject objectWithName: nameInContainer
178				                                 inContainer: folder];
179      [object setIsNew: YES];
180    }
181
182  return object;
183}
184
185//
186//
187//
188- (void) _addOrUpdateEvent: (iCalEvent *) theEvent
189                    forUID: (NSString *) theUID
190                     owner: (NSString *) theOwner
191{
192  if (![theUID isEqualToString: theOwner])
193    {
194      SOGoAppointmentObject *attendeeObject;
195      iCalCalendar *iCalendarToSave;
196
197      iCalendarToSave = nil;
198      attendeeObject = [self _lookupEvent: [theEvent uid] forUID: theUID];
199
200      // We must add an occurence to a non-existing event. We have
201      // to handle this with care, as in the postCalDAVEventRequestTo:from:
202      if ([attendeeObject isNew] && [theEvent recurrenceId])
203        {
204          iCalEvent *ownerEvent;
205          iCalPerson *person;
206          SOGoUser *user;
207
208          // We check if the attendee that was added to a single occurence is
209          // present in the master component. If not, we add it with a participation
210          // status set to "DECLINED".
211          ownerEvent = [[[theEvent parent] events] objectAtIndex: 0];
212          user = [SOGoUser userWithLogin: theUID];
213          if (![ownerEvent userAsAttendee: user])
214            {
215              // Update the master event in the owner's calendar with the
216              // status of the new attendee set as "DECLINED".
217              person = [iCalPerson elementWithTag: @"attendee"];
218              [person setCn: [user cn]];
219              [person setEmail: [[user allEmails] objectAtIndex: 0]];
220              [person setParticipationStatus: iCalPersonPartStatDeclined];
221              [person setRsvp: @"TRUE"];
222              [person setRole: @"REQ-PARTICIPANT"];
223              [ownerEvent addToAttendees: person];
224
225              iCalendarToSave = [ownerEvent parent];
226            }
227        }
228      else
229        {
230          // TODO : if [theEvent recurrenceId], only update this occurrence
231          // in attendee's calendar
232
233          // TODO : when updating the master event, handle exception dates
234          // in attendee's calendar (add exception dates and remove matching
235          // occurrences) -- see _updateRecurrenceIDsWithEvent:
236
237          iCalendarToSave = [theEvent parent];
238        }
239
240      // Save the event in the attendee's calendar
241      if (iCalendarToSave)
242        [attendeeObject saveCalendar: iCalendarToSave];
243    }
244}
245
246//
247//
248//
249- (void) _removeEventFromUID: (NSString *) theUID
250                       owner: (NSString *) theOwner
251            withRecurrenceId: (NSCalendarDate *) recurrenceId
252{
253  if (![theUID isEqualToString: theOwner])
254    {
255      SOGoAppointmentFolder *folder;
256      SOGoAppointmentObject *object;
257      iCalEntityObject *currentOccurence;
258      iCalRepeatableEntityObject *event;
259      iCalCalendar *calendar;
260      NSCalendarDate *currentId;
261      NSArray *occurences;
262      int max, count;
263
264      // Invitations are always written to the personal folder; it's not necessay
265      // to look into all folders of the user
266      folder = [[SOGoUser userWithLogin: theUID]
267                personalCalendarFolderInContext: context];
268      object = [folder lookupName: nameInContainer
269                        inContext: context acquire: NO];
270      if (![object isKindOfClass: [NSException class]])
271        {
272          if (recurrenceId == nil)
273          [object delete];
274          else
275            {
276              calendar = [object calendar: NO secure: NO];
277
278              // If recurrenceId is defined, remove the occurence from
279              // the repeating event.
280              occurences = [calendar events];
281              max = [occurences count];
282              count = 1;
283              while (count < max)
284                {
285                  currentOccurence = [occurences objectAtIndex: count];
286                  currentId = [currentOccurence recurrenceId];
287                  if ([currentId compare: recurrenceId] == NSOrderedSame)
288                    {
289                      [[calendar children] removeObject: currentOccurence];
290                      break;
291                    }
292                  count++;
293                }
294
295              // Add an date exception.
296              event = (iCalRepeatableEntityObject*)[calendar firstChildWithTag: [object componentTag]];
297              [event addToExceptionDates: recurrenceId];
298
299              [event increaseSequence];
300
301              // We save the updated iCalendar in the database.
302              [object saveCalendar: calendar];
303            }
304        }
305    }
306}
307
308//
309//
310//
311- (void) _handleRemovedUsers: (NSArray *) attendees
312            withRecurrenceId: (NSCalendarDate *) recurrenceId
313{
314  NSEnumerator *enumerator;
315  iCalPerson *currentAttendee;
316  NSString *currentUID;
317
318  enumerator = [attendees objectEnumerator];
319  while ((currentAttendee = [enumerator nextObject]))
320    {
321      currentUID = [currentAttendee uidInContext: context];
322      if (currentUID)
323        [self _removeEventFromUID: currentUID
324                            owner: owner
325	      withRecurrenceId: recurrenceId];
326    }
327}
328
329//
330//
331//
332- (void) _removeDelegationChain: (iCalPerson *) delegate
333                        inEvent: (iCalEvent *) event
334{
335  NSString *delegatedTo, *mailTo;
336
337  delegatedTo = [delegate delegatedTo];
338  if ([delegatedTo length] > 0)
339    {
340      mailTo = [delegatedTo rfc822Email];
341      delegate = [event findAttendeeWithEmail: mailTo];
342      if (delegate)
343        {
344          [self _removeDelegationChain: delegate
345                               inEvent: event];
346          [event removeFromAttendees: delegate];
347        }
348      else
349        [self errorWithFormat:@"broken chain: delegate with email '%@' was not found", mailTo];
350    }
351}
352
353//
354// This method returns YES when any attendee has been removed
355// and NO otherwise.
356//
357- (BOOL) _requireResponseFromAttendees: (iCalEvent *) event
358{
359  NSArray *attendees;
360  iCalPerson *currentAttendee;
361  BOOL listHasChanged = NO;
362  int count, max;
363
364  attendees = [event attendees];
365  max = [attendees count];
366
367  for (count = 0; count < max; count++)
368    {
369      currentAttendee = [attendees objectAtIndex: count];
370      if ([[currentAttendee delegatedTo] length] > 0)
371        {
372          [self _removeDelegationChain: currentAttendee
373                               inEvent: event];
374          [currentAttendee setDelegatedTo: nil];
375          listHasChanged = YES;
376        }
377      [currentAttendee setRsvp: @"TRUE"];
378      [currentAttendee setParticipationStatus: iCalPersonPartStatNeedsAction];
379    }
380
381  return listHasChanged;
382}
383
384//
385//
386//
387- (void) _handleSequenceUpdateInEvent: (iCalEvent *) newEvent
388		    ignoringAttendees: (NSArray *) attendees
389		         fromOldEvent: (iCalEvent *) oldEvent
390{
391  NSMutableArray *updateAttendees;
392  NSEnumerator *enumerator;
393  iCalPerson *currentAttendee;
394  NSString *currentUID;
395
396  updateAttendees = [NSMutableArray arrayWithArray: [newEvent attendees]];
397  [updateAttendees removeObjectsInArray: attendees];
398
399  enumerator = [updateAttendees objectEnumerator];
400  while ((currentAttendee = [enumerator nextObject]))
401    {
402      currentUID = [currentAttendee uidInContext: context];
403      if (currentUID)
404        [self _addOrUpdateEvent: newEvent
405                         forUID: currentUID
406                          owner: owner];
407    }
408
409  [self sendEMailUsingTemplateNamed: @"Update"
410                          forObject: [newEvent itipEntryWithMethod: @"request"]
411                     previousObject: oldEvent
412                        toAttendees: updateAttendees
413                           withType: @"calendar:invitation-update"];
414}
415
416// This method scans the list of attendees.
417- (NSException *) _handleAttendeeAvailability: (NSArray *) theAttendees
418                                     forEvent: (iCalEvent *) theEvent
419{
420  iCalPerson *currentAttendee;
421  SOGoUser *user;
422  SOGoUserSettings *us;
423  NSMutableArray *unavailableAttendees;
424  NSEnumerator *enumerator;
425  NSString *currentUID, *ownerUID;
426  NSMutableString *reason;
427  NSDictionary *values;
428  NSMutableDictionary *value, *moduleSettings;
429  id whiteList;
430
431  int i, count;
432
433  i = count = 0;
434
435  // Build list of the attendees uids
436  unavailableAttendees = [[NSMutableArray alloc] init];
437  enumerator = [theAttendees objectEnumerator];
438  ownerUID = [[[self context] activeUser] login];
439
440  while ((currentAttendee = [enumerator nextObject]))
441    {
442      currentUID = [currentAttendee uidInContext: context];
443
444      if (currentUID)
445        {
446          user = [SOGoUser userWithLogin: currentUID];
447          us = [user userSettings];
448          moduleSettings = [us objectForKey:@"Calendar"];
449
450          // Check if the user prevented their account from beeing invited to events
451          if ([[moduleSettings objectForKey:@"PreventInvitations"] boolValue])
452            {
453              // Check if the user have a whiteList
454              whiteList = [moduleSettings objectForKey:@"PreventInvitationsWhitelist"];
455
456              // For backward <= 2.2.17 compatibility
457              if ([whiteList isKindOfClass: [NSString class]])
458                whiteList = [whiteList objectFromJSONString];
459
460              // If the filter have a hit, do not add the currentUID to the unavailableAttendees array
461              if (![whiteList objectForKey:ownerUID])
462                {
463                  values = [NSDictionary dictionaryWithObject:[user cn] forKey:@"Cn"];
464                  [unavailableAttendees addObject:values];
465                }
466            }
467        }
468    }
469
470  count = [unavailableAttendees count];
471
472  if (count > 0)
473    {
474      reason = [NSMutableString stringWithString:[self labelForKey: @"Inviting the following persons is prohibited:"]];
475
476      // Add all the unavailable users in the warning message
477      for (i = 0; i < count; i++)
478        {
479          value = [unavailableAttendees objectAtIndex:i];
480          [reason appendString:[value keysWithFormat: @"\n %{Cn}"]];
481          if (i < count-2)
482            [reason appendString:@", "];
483        }
484
485      [unavailableAttendees release];
486
487      return [NSException exceptionWithHTTPStatus:409 reason: reason];
488    }
489
490  [unavailableAttendees release];
491
492  return nil;
493}
494
495//
496// This methods scans the list of attendees. If they are
497// considered as resource, it checks for conflicting
498// dates for the event.
499//
500// We check for between startDate + 1 second and
501// endDate - 1 second
502//
503//
504// It also CHANGES the participation status of resources
505// depending on constraints defined on them.
506//
507// Note that it doesn't matter if it changes the participation
508// status since in case of an error, nothing will get saved.
509//
510- (NSException *) _handleResourcesConflicts: (NSArray *) theAttendees
511                                   forEvent: (iCalEvent *) theEvent
512{
513  iCalPerson *currentAttendee;
514  NSMutableArray *attendees;
515  NSEnumerator *enumerator;
516  NSString *currentUID;
517  SOGoUser *user, *currentUser;
518
519  // Build a list of the attendees uids
520  attendees = [NSMutableArray arrayWithCapacity: [theAttendees count]];
521  enumerator = [theAttendees objectEnumerator];
522  while ((currentAttendee = [enumerator nextObject]))
523    {
524      currentUID = [currentAttendee uidInContext: context];
525      if (currentUID)
526        {
527          [attendees addObject: currentUID];
528        }
529    }
530
531  // If the active user is not the owner of the calendar, check possible conflict when
532  // the owner is a resource
533  currentUser = [context activeUser];
534  if (!activeUserIsOwner && ![currentUser isSuperUser])
535    {
536      [attendees addObject: owner];
537    }
538
539  enumerator = [attendees objectEnumerator];
540  while ((currentUID = [enumerator nextObject]))
541    {
542      user = [SOGoUser userWithLogin: currentUID];
543
544      if ([user isResource])
545        {
546          NSCalendarDate *start, *end, *rangeStartDate, *rangeEndDate;
547          SOGoAppointmentFolder *folder;
548          NGCalendarDateRange *range;
549          NSMutableArray *fbInfo;
550          NSArray *allOccurences;
551
552          BOOL must_delete;
553          int i, j, delta;
554
555          // We get the start/end date for our conflict range. If the event to be added is recurring, we
556          // check for at least a year to start with.
557          start = [[theEvent startDate] dateByAddingYears: 0  months: 0  days: 0  hours: 0  minutes: 0  seconds: 1];
558          end = [[theEvent endDate] dateByAddingYears: ([theEvent isRecurrent] ? 1 : 0)  months: 0  days: 0  hours: 0  minutes: 0  seconds: -1];
559
560          folder = [user personalCalendarFolderInContext: context];
561
562          // Deny access to the resource if the ACLs don't allow the user
563          if (![folder aclSQLListingFilter])
564            {
565              NSDictionary *values;
566              NSString *reason;
567
568              values = [NSDictionary dictionaryWithObjectsAndKeys:
569                        [user cn], @"Cn",
570                        [user systemEmail], @"SystemEmail"], nil;
571              reason = [values keysWithFormat: [self labelForKey: @"Cannot access resource: \"%{Cn} %{SystemEmail}\""]];
572              return [NSException exceptionWithHTTPStatus:409 reason: reason];
573            }
574
575          fbInfo = [NSMutableArray arrayWithArray: [folder fetchFreeBusyInfosFrom: start
576                                                                               to: end]];
577
578          // We first remove any occurences in the freebusy that corresponds to the
579          // current event. We do this to avoid raising a conflict if we move a 1 hour
580          // meeting from 12:00-13:00 to 12:15-13:15. We would overlap on ourself otherwise.
581          //
582          // We must also check here for repetitive events that don't overlap our event.
583          // We remove all events that don't overlap. The events here are already
584          // decomposed.
585          //
586          if ([theEvent isRecurrent])
587            allOccurences = [theEvent recurrenceRangesWithinCalendarDateRange: [NGCalendarDateRange calendarDateRangeWithStartDate: start
588                                                                                                                           endDate: end]
589                                               firstInstanceCalendarDateRange: [NGCalendarDateRange calendarDateRangeWithStartDate: [theEvent startDate]
590                                                                                                                           endDate: [theEvent endDate]]];
591          else
592            allOccurences = nil;
593
594          for (i = [fbInfo count]-1; i >= 0; i--)
595            {
596              // We MUST use the -uniqueChildWithTag method here because the event has been flattened, so its timezone has been
597              // modified in SOGoAppointmentFolder: -fixupCycleRecord: ....
598              rangeStartDate = [[fbInfo objectAtIndex: i] objectForKey: @"startDate"];
599              delta = [[rangeStartDate timeZoneDetail] timeZoneSecondsFromGMT] - [[[(iCalDateTime *)[theEvent uniqueChildWithTag: @"dtstart"] timeZone] periodForDate: [theEvent startDate]] secondsOffsetFromGMT];
600              rangeStartDate = [rangeStartDate dateByAddingYears: 0  months: 0  days: 0  hours: 0  minutes: 0  seconds: delta];
601
602              rangeEndDate = [[fbInfo objectAtIndex: i] objectForKey: @"endDate"];
603              delta = [[rangeEndDate timeZoneDetail] timeZoneSecondsFromGMT] - [[[(iCalDateTime *)[theEvent uniqueChildWithTag: @"dtend"] timeZone] periodForDate: [theEvent endDate]] secondsOffsetFromGMT];
604              rangeEndDate = [rangeEndDate dateByAddingYears: 0  months: 0  days: 0  hours: 0  minutes: 0  seconds: delta];
605
606              range = [NGCalendarDateRange calendarDateRangeWithStartDate: rangeStartDate
607                                                                  endDate: rangeEndDate];
608
609              if ([[[fbInfo objectAtIndex: i] objectForKey: @"c_uid"] compare: [theEvent uid]] == NSOrderedSame)
610                {
611                  [fbInfo removeObjectAtIndex: i];
612                  continue;
613                }
614
615              // No need to check if the event isn't recurrent here as it's handled correctly
616              // when we compute the "end" date.
617              if ([allOccurences count])
618                {
619                  must_delete = YES;
620
621                  for (j = 0; j < [allOccurences count]; j++)
622                    {
623                      if ([range doesIntersectWithDateRange: [allOccurences objectAtIndex: j]])
624                        {
625                          must_delete = NO;
626                          break;
627                        }
628                    }
629
630                  if (must_delete)
631                    [fbInfo removeObjectAtIndex: i];
632                }
633            }
634
635          // Find the attendee associated to the current UID
636          for (i = 0; i < [theAttendees count]; i++)
637            {
638              currentAttendee = [theAttendees objectAtIndex: i];
639              if ([[currentAttendee uidInContext: context] isEqualToString: currentUID])
640                break;
641              else
642                currentAttendee = nil;
643            }
644
645          if ([fbInfo count])
646            {
647              // If we always force the auto-accept if numberOfSimultaneousBookings <= 0 (ie., no limit
648              // is imposed) or if numberOfSimultaneousBookings is greater than the number of
649              // overlapping events
650              if ([user numberOfSimultaneousBookings] <= 0 ||
651                  [user numberOfSimultaneousBookings] > [fbInfo count])
652                {
653                  if (currentAttendee)
654                    {
655                      [[currentAttendee attributes] removeObjectForKey: @"RSVP"];
656                      [currentAttendee setParticipationStatus: iCalPersonPartStatAccepted];
657                    }
658                }
659              else
660                {
661                  iCalCalendar *calendar;
662                  NSDictionary *values;
663                  NSString *reason;
664                  iCalEvent *event;
665
666                  calendar =  [iCalCalendar parseSingleFromSource: [[fbInfo objectAtIndex: 0] objectForKey: @"c_content"]];
667                  event = [[calendar events] lastObject];
668
669                  values = [NSDictionary dictionaryWithObjectsAndKeys:
670                            [NSString stringWithFormat: @"%d", [user numberOfSimultaneousBookings]], @"NumberOfSimultaneousBookings",
671                            [user cn], @"Cn",
672                            [user systemEmail], @"SystemEmail",
673                            ([event summary] ? [event summary] : @""), @"EventTitle",
674                            [[fbInfo objectAtIndex: 0] objectForKey: @"startDate"], @"StartDate",
675                            nil];
676
677                  reason = [values keysWithFormat: [self labelForKey: @"Maximum number of simultaneous bookings (%{NumberOfSimultaneousBookings}) reached for resource \"%{Cn} %{SystemEmail}\". The conflicting event is \"%{EventTitle}\", and starts on %{StartDate}."]];
678
679                  return [NSException exceptionWithHTTPStatus: 409
680                                                       reason: reason];
681                }
682            }
683          else if (currentAttendee)
684            {
685              // No conflict, we auto-accept. We do this for resources automatically if no
686              // double-booking is observed. If it's not the desired behavior, just don't
687              // set the resource as one!
688              [[currentAttendee attributes] removeObjectForKey: @"RSVP"];
689              [currentAttendee setParticipationStatus: iCalPersonPartStatAccepted];
690            }
691        }
692    }
693
694  return nil;
695}
696
697//
698//
699//
700- (NSException *) _handleAddedUsers: (NSArray *) attendees
701                          fromEvent: (iCalEvent *) newEvent
702{
703  iCalPerson *currentAttendee;
704  NSEnumerator *enumerator;
705  NSString *currentUID;
706  NSException *e;
707
708  // We check for conflicts
709  if ((e = [self _handleResourcesConflicts: attendees  forEvent: newEvent]))
710    return e;
711  if ((e = [self _handleAttendeeAvailability: attendees  forEvent: newEvent]))
712    return e;
713
714  enumerator = [attendees objectEnumerator];
715  while ((currentAttendee = [enumerator nextObject]))
716    {
717      currentUID = [currentAttendee uidInContext: context];
718      if (currentUID)
719      [self _addOrUpdateEvent: newEvent
720                       forUID: currentUID
721                        owner: owner];
722    }
723
724  return nil;
725}
726
727
728//
729//
730//
731- (void) _addOrDeleteAttendees: (NSArray *) theAttendees
732inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent
733                           add: (BOOL) shouldAdd
734{
735
736  NSArray *events;
737  iCalEvent *e;
738  int i,j;
739
740  // We don't add/delete attendees to all recurrence exceptions if
741  // the modification was actually NOT made on the master event
742  if ([theEvent recurrenceId])
743    return;
744
745  events = [[theEvent parent] events];
746
747  for (i = 0; i < [events count]; i++)
748    {
749      e = [events objectAtIndex: i];
750      if ([e recurrenceId])
751        for (j = 0; j < [theAttendees count]; j++) {
752          if (shouldAdd) {
753            [e addToAttendees: [theAttendees objectAtIndex: j]];
754          }
755          else {
756            [e removeFromAttendees: [theAttendees objectAtIndex: j]];
757          }
758        }
759    }
760}
761
762//
763//
764//
765- (NSException *) _handleUpdatedEvent: (iCalEvent *) newEvent
766		         fromOldEvent: (iCalEvent *) oldEvent
767{
768  NSArray *addedAttendees, *deletedAttendees, *updatedAttendees;
769  iCalEventChanges *changes;
770  NSException *ex;
771
772  addedAttendees = nil;
773  deletedAttendees = nil;
774  updatedAttendees = nil;
775
776  changes = [newEvent getChangesRelativeToEvent: oldEvent];
777  if ([changes sequenceShouldBeIncreased])
778    {
779      // Set new attendees status to "needs action" and recompute changes when
780      // the list of attendees has changed. The list might have changed since
781      // by changing a major property of the event, we remove all the delegation
782      // chains to "other" attendees
783      if ([self _requireResponseFromAttendees: newEvent])
784        changes = [newEvent getChangesRelativeToEvent: oldEvent];
785    }
786
787  deletedAttendees = [changes deletedAttendees];
788
789  // We delete the attendees in all exception occurences, if
790  // the attendees were removed from the master event.
791  [self _addOrDeleteAttendees: deletedAttendees
792	inRecurrenceExceptionsForEvent: newEvent
793			  add: NO];
794
795  if ([deletedAttendees count])
796    {
797      [self _handleRemovedUsers: deletedAttendees
798               withRecurrenceId: [newEvent recurrenceId]];
799      [self sendEMailUsingTemplateNamed: @"Deletion"
800                              forObject: [newEvent itipEntryWithMethod: @"cancel"]
801                         previousObject: oldEvent
802                            toAttendees: deletedAttendees
803                               withType: @"calendar:cancellation"];
804    }
805
806  if ((ex = [self _handleResourcesConflicts: [newEvent attendees] forEvent: newEvent]))
807    return ex;
808  if ((ex = [self _handleAttendeeAvailability: [newEvent attendees] forEvent: newEvent]))
809    return ex;
810
811  addedAttendees = [changes insertedAttendees];
812
813  // We insert the attendees in all exception occurences, if
814  // the attendees were added to the master event.
815  [self _addOrDeleteAttendees: addedAttendees
816        inRecurrenceExceptionsForEvent: newEvent
817                          add: YES];
818
819  if ([changes sequenceShouldBeIncreased])
820    {
821      [newEvent increaseSequence];
822
823      // Update attendees calendars and send them an update
824      // notification by email. We ignore the newly added
825      // attendees as we don't want to send them invitation
826      // update emails
827      [self _handleSequenceUpdateInEvent: newEvent
828                       ignoringAttendees: addedAttendees
829                            fromOldEvent: oldEvent];
830    }
831  else
832    {
833      // If other attributes have changed, update the event
834      // in each attendee's calendar
835      if ([[changes updatedProperties] count])
836        {
837          NSEnumerator *enumerator;
838          iCalPerson *currentAttendee;
839          NSString *currentUID;
840
841          updatedAttendees = [newEvent attendees];
842          enumerator = [updatedAttendees objectEnumerator];
843          while ((currentAttendee = [enumerator nextObject]))
844            {
845              currentUID = [currentAttendee uidInContext: context];
846              if (currentUID)
847                [self _addOrUpdateEvent: newEvent
848                                 forUID: currentUID
849                                  owner: owner];
850            }
851        }
852    }
853
854  if ([addedAttendees count])
855    {
856      // Send an invitation to new attendees
857      if ((ex = [self _handleAddedUsers: addedAttendees fromEvent: newEvent]))
858        return ex;
859
860      [self sendEMailUsingTemplateNamed: @"Invitation"
861                              forObject: [newEvent itipEntryWithMethod: @"request"]
862                         previousObject: oldEvent
863                            toAttendees: addedAttendees
864                               withType: @"calendar:invitation"];
865    }
866
867      [self sendReceiptEmailForObject: newEvent
868                       addedAttendees: addedAttendees
869                     deletedAttendees: deletedAttendees
870                     updatedAttendees: updatedAttendees
871                            operation: EventUpdated];
872
873  return nil;
874}
875
876//
877// Workflow :                             +----------------------+
878//                                        |                      |
879// [saveComponent:]---> _handleAddedUsers:fromEvent: <-+         |
880//       |                                             |         v
881//       +------------> _handleUpdatedEvent:fromOldEvent: ---> _addOrUpdateEvent:forUID:owner:  <-----------+
882//                               |           |                   ^                                          |
883//                               v           v                   |                                          |
884//  _handleRemovedUsers:withRecurrenceId:  _handleSequenceUpdateInEvent:ignoringAttendees:fromOldEvent:     |
885//                     |                                                                                    |
886//                     |             [DELETEAction:]                                                        |
887//                     |                    |              {_handleAdded/Updated...}<--+                    |
888//                     |                    v                                          |                    |
889//                     |         [prepareDeleteOccurence:]                    [PUTAction:]                  |
890//                     |               |              |                            |                        |
891//                     v               v              v                            v                        |
892// _removeEventFromUID:owner:withRecurrenceId:  [changeParticipationStatus:withDelegate:forRecurrenceId:]   |
893//                     |                                          |                                         |
894//                     |                                          v                                         |
895//                     +------------------------> _handleAttendee:withDelegate:ownerUser:statusChange:inEvent: ---> [sendResponseToOrganizer:from:]
896//                                                  |
897//                                                  v
898//  _updateAttendee:withDelegate:ownerUser:forEventUID:withRecurrenceId:withSequence:forUID:shouldAddSentBy:
899//
900//
901- (NSException *) saveComponent: (iCalEvent *) newEvent
902{
903  iCalEvent *oldEvent, *oldMasterEvent;
904  NSCalendarDate *recurrenceId;
905  NSString *recurrenceTime;
906  SOGoUser *ownerUser;
907  NSArray *attendees;
908  NSException *ex;
909
910  [[newEvent parent] setMethod: @""];
911  ownerUser = [SOGoUser userWithLogin: owner];
912
913  [self expandGroupsInEvent: newEvent];
914
915  // We first update the event. It is important to do this initially
916  // as the event's UID might get modified.
917  [super updateComponent: newEvent];
918
919  if ([self isNew])
920    {
921      // New event -- send invitation to all attendees
922      attendees = [newEvent attendeesWithoutUser: ownerUser];
923
924      // We catch conflicts and abort the save process immediately
925      // in case of one with resources
926      if ((ex = [self _handleAddedUsers: attendees fromEvent: newEvent]))
927        return ex;
928
929      if ([attendees count])
930        {
931          [self sendEMailUsingTemplateNamed: @"Invitation"
932                                  forObject: [newEvent itipEntryWithMethod: @"request"]
933                             previousObject: nil
934                                toAttendees: attendees
935                                   withType: @"calendar:invitation"];
936        }
937
938      [self sendReceiptEmailForObject: newEvent
939                       addedAttendees: attendees
940                     deletedAttendees: nil
941                     updatedAttendees: nil
942                            operation: EventCreated];
943    }
944  else
945    {
946      BOOL hasOrganizer;
947
948      // Event is modified -- sent update status to all attendees
949      // and modify their calendars.
950      recurrenceId = [newEvent recurrenceId];
951      if (recurrenceId == nil)
952        oldEvent = [self component: NO secure: NO];
953      else
954        {
955          // If recurrenceId is defined, find the specified occurence
956          // within the repeating vEvent.
957          recurrenceTime = [NSString stringWithFormat: @"%f", [recurrenceId timeIntervalSince1970]];
958          oldEvent = (iCalEvent*)[self lookupOccurrence: recurrenceTime];
959          if (oldEvent == nil) // If no occurence found, create one
960            oldEvent = (iCalEvent *)[self newOccurenceWithID: recurrenceTime];
961        }
962
963      oldMasterEvent = (iCalEvent *)[[oldEvent parent] firstChildWithTag: [self componentTag]];
964      hasOrganizer = [[[oldMasterEvent organizer] email] length];
965
966      if (!hasOrganizer || [oldMasterEvent userIsOrganizer: ownerUser])
967      // The owner is the organizer of the event; handle the modifications. We aslo
968      // catch conflicts just like when the events are created
969        if ((ex = [self _handleUpdatedEvent: newEvent fromOldEvent: oldEvent]))
970          return ex;
971    }
972
973  [super saveComponent: newEvent];
974  [self flush];
975
976  return nil;
977}
978
979//
980// This method is used to update the status of an attendee.
981//
982// - theOwnerUser is owner of the calendar where the attendee
983//   participation state has changed.
984// - uid is the actual UID of the user for whom we must
985//   update the calendar event (with the participation change)
986// - delegate is the delegate attendee if any
987//
988// This method is called multiple times, in order to update the
989// status of the attendee in calendars for the particular event UID.
990//
991- (NSException *) _updateAttendee: (iCalPerson *) attendee
992                     withDelegate: (iCalPerson *) delegate
993                        ownerUser: (SOGoUser *) theOwnerUser
994                      forEventUID: (NSString *) eventUID
995                 withRecurrenceId: (NSCalendarDate *) recurrenceId
996                     withSequence: (NSNumber *) sequence
997                           forUID: (NSString *) uid
998                  shouldAddSentBy: (BOOL) b
999{
1000  SOGoAppointmentObject *eventObject;
1001  iCalCalendar *calendar;
1002  iCalEntityObject *event;
1003  iCalPerson *otherAttendee, *otherDelegate;
1004  NSString *recurrenceTime, *delegateEmail;
1005  NSException *error;
1006  BOOL addDelegate, removeDelegate;
1007
1008  error = nil;
1009
1010  eventObject = [self _lookupEvent: eventUID forUID: uid];
1011  if (![eventObject isNew])
1012    {
1013      if (recurrenceId == nil)
1014        {
1015          // We must update main event and all its occurences (if any).
1016          calendar = [eventObject calendar: NO secure: NO];
1017          event = (iCalEntityObject*)[calendar firstChildWithTag: [self componentTag]];
1018        }
1019      else
1020        {
1021          // If recurrenceId is defined, find the specified occurence
1022          // within the repeating vEvent.
1023          recurrenceTime = [NSString stringWithFormat: @"%f", [recurrenceId timeIntervalSince1970]];
1024          event = [eventObject lookupOccurrence: recurrenceTime];
1025
1026          if (event == nil)
1027          // If no occurence found, create one
1028          event = [eventObject newOccurenceWithID: recurrenceTime];
1029        }
1030
1031      if ([[event sequence] intValue] <= [sequence intValue])
1032        {
1033          SOGoUser *currentUser;
1034
1035          currentUser = [context activeUser];
1036          otherAttendee = [event userAsAttendee: theOwnerUser];
1037
1038          delegateEmail = [otherAttendee delegatedTo];
1039          if ([delegateEmail length])
1040            delegateEmail = [delegateEmail rfc822Email];
1041          if ([delegateEmail length])
1042            otherDelegate = [event findAttendeeWithEmail: delegateEmail];
1043          else
1044            otherDelegate = nil;
1045
1046          /* we handle the addition/deletion of delegate users */
1047          addDelegate = NO;
1048          removeDelegate = NO;
1049          if (delegate)
1050            {
1051              if (otherDelegate)
1052                {
1053                  if (![delegate hasSameEmailAddress: otherDelegate])
1054                    {
1055                      removeDelegate = YES;
1056                      addDelegate = YES;
1057                    }
1058                }
1059              else
1060                addDelegate = YES;
1061            }
1062          else
1063            {
1064              if (otherDelegate)
1065                removeDelegate = YES;
1066            }
1067
1068          if (removeDelegate)
1069            {
1070              while (otherDelegate)
1071                {
1072                  [event removeFromAttendees: otherDelegate];
1073
1074                  // Verify if the delegate was already delegate
1075                  delegateEmail = [otherDelegate delegatedTo];
1076                  if ([delegateEmail length])
1077                    delegateEmail = [delegateEmail rfc822Email];
1078
1079                  if ([delegateEmail length])
1080                    otherDelegate = [event findAttendeeWithEmail: delegateEmail];
1081                  else
1082                    otherDelegate = nil;
1083                }
1084            }
1085          if (addDelegate)
1086            [event addToAttendees: delegate];
1087
1088          [otherAttendee setPartStat: [attendee partStat]];
1089          [otherAttendee setDelegatedTo: [attendee delegatedTo]];
1090          [otherAttendee setDelegatedFrom: [attendee delegatedFrom]];
1091
1092          // Remove the RSVP attribute, as an action from the attendee
1093          // was actually performed, and this confuses iCal (bug #1850)
1094          [[otherAttendee attributes] removeObjectForKey: @"RSVP"];
1095
1096          // If one has accepted / declined an invitation on behalf of
1097          // the attendee, we add the user to the SENT-BY attribute.
1098          if (b && ![[currentUser login] isEqualToString: [theOwnerUser login]])
1099            {
1100              NSString *currentEmail, *quotedEmail;
1101              currentEmail = [[currentUser allEmails] objectAtIndex: 0];
1102              quotedEmail = [NSString stringWithFormat: @"\"MAILTO:%@\"", currentEmail];
1103              [otherAttendee setValue: 0 ofAttribute: @"SENT-BY"
1104                                   to: quotedEmail];
1105            }
1106          else
1107            {
1108              // We must REMOVE any SENT-BY here. This is important since if A accepted
1109              // the event for B and then, B changes by theirself their participation status,
1110              // we don't want to keep the previous SENT-BY attribute there.
1111              [(NSMutableDictionary *)[otherAttendee attributes] removeObjectForKey: @"SENT-BY"];
1112            }
1113        }
1114
1115      // We save the updated iCalendar in the database.
1116      error = [eventObject saveCalendar: [event parent]];
1117    }
1118
1119  return error;
1120}
1121
1122
1123//
1124// This method is invoked from the SOGo Web interface or from the DAV interface.
1125//
1126// - theOwnerUser is owner of the calendar where the attendee
1127//   participation state has changed.
1128//
1129- (NSException *) _handleAttendee: (iCalPerson *) attendee
1130                     withDelegate: (iCalPerson *) delegate
1131                        ownerUser: (SOGoUser *) theOwnerUser
1132                     statusChange: (NSString *) newStatus
1133                          inEvent: (iCalEvent *) event
1134{
1135  NSString *currentStatus, *organizerUID;
1136  SOGoUser *ownerUser, *currentUser;
1137  NSException *ex;
1138
1139  ex = nil;
1140
1141  currentStatus = [attendee partStat];
1142
1143  iCalPerson *otherAttendee, *otherDelegate;
1144  NSString *delegateEmail;
1145  BOOL addDelegate, removeDelegate;
1146
1147  otherAttendee = attendee;
1148
1149  delegateEmail = [otherAttendee delegatedTo];
1150  if ([delegateEmail length])
1151    delegateEmail = [delegateEmail rfc822Email];
1152
1153  if ([delegateEmail length])
1154    otherDelegate = [event findAttendeeWithEmail: delegateEmail];
1155  else
1156    otherDelegate = nil;
1157
1158  /* We handle the addition/deletion of delegate users */
1159  addDelegate = NO;
1160  removeDelegate = NO;
1161  if (delegate)
1162    {
1163      if (otherDelegate)
1164        {
1165          // There was already a delegate
1166          if (![delegate hasSameEmailAddress: otherDelegate])
1167            {
1168              // The delegate has changed
1169              removeDelegate = YES;
1170              addDelegate = YES;
1171            }
1172        }
1173      else
1174        // There was no previous delegate
1175        addDelegate = YES;
1176    }
1177  else
1178    {
1179      if (otherDelegate)
1180        // The user has removed the delegate
1181        removeDelegate = YES;
1182    }
1183
1184  if (addDelegate || removeDelegate
1185                  || [currentStatus caseInsensitiveCompare: newStatus] != NSOrderedSame)
1186    {
1187      NSMutableArray *delegates;
1188      NSString *delegatedUID;
1189
1190      delegatedUID = nil;
1191      [attendee setPartStat: newStatus];
1192
1193      // If one has accepted / declined an invitation on behalf of
1194      // the attendee, we add the user to the SENT-BY attribute.
1195      currentUser = [context activeUser];
1196      if (![[currentUser login] isEqualToString: [theOwnerUser login]])
1197        {
1198          NSString *currentEmail, *quotedEmail;
1199          currentEmail = [[currentUser allEmails] objectAtIndex: 0];
1200          quotedEmail = [NSString stringWithFormat: @"\"MAILTO:%@\"", currentEmail];
1201          [attendee setValue: 0 ofAttribute: @"SENT-BY"
1202                          to: quotedEmail];
1203        }
1204      else
1205        {
1206          // We must REMOVE any SENT-BY here. This is important since if A accepted
1207          // the event for B and then, B changes by theirself their participation status,
1208          // we don't want to keep the previous SENT-BY attribute there.
1209          [(NSMutableDictionary *)[attendee attributes] removeObjectForKey: @"SENT-BY"];
1210        }
1211
1212      [attendee setDelegatedTo: [delegate email]];
1213
1214      if (removeDelegate)
1215        {
1216          delegates = [NSMutableArray array];
1217
1218          while (otherDelegate)
1219            {
1220              [delegates addObject: otherDelegate];
1221
1222              delegatedUID = [otherDelegate uidInContext: context];
1223              if (delegatedUID)
1224                // Delegate attendee is a local user; remove event from their calendar
1225                [self _removeEventFromUID: delegatedUID
1226                                    owner: [theOwnerUser login]
1227                         withRecurrenceId: [event recurrenceId]];
1228
1229              [event removeFromAttendees: otherDelegate];
1230
1231              // Verify if the delegate was already delegated
1232              delegateEmail = [otherDelegate delegatedTo];
1233              if ([delegateEmail length])
1234                delegateEmail = [delegateEmail rfc822Email];
1235
1236              if ([delegateEmail length])
1237                otherDelegate = [event findAttendeeWithEmail: delegateEmail];
1238              else
1239                otherDelegate = nil;
1240            }
1241
1242          [self sendEMailUsingTemplateNamed: @"Deletion"
1243                                  forObject: [event itipEntryWithMethod: @"cancel"]
1244                             previousObject: nil
1245                                toAttendees: delegates
1246                                   withType: @"calendar:cancellation"];
1247        } // if (removeDelegate)
1248
1249      if (addDelegate)
1250        {
1251          delegatedUID = [delegate uidInContext: context];
1252          delegates = [NSArray arrayWithObject: delegate];
1253          [event addToAttendees: delegate];
1254
1255          if (delegatedUID)
1256            // Delegate attendee is a local user; add event to their calendar
1257            [self _addOrUpdateEvent: event
1258                             forUID: delegatedUID
1259                              owner: [theOwnerUser login]];
1260
1261          [self sendEMailUsingTemplateNamed: @"Invitation"
1262                                  forObject: [event itipEntryWithMethod: @"request"]
1263                             previousObject: nil
1264                                toAttendees: delegates
1265                                   withType: @"calendar:invitation"];
1266        } // if (addDelegate)
1267
1268      // If the current user isn't the organizer of the event
1269      // that has just been updated, we update the event and
1270      // send a notification
1271      ownerUser = [SOGoUser userWithLogin: owner];
1272      if (!(ex || [event userIsOrganizer: ownerUser]))
1273        {
1274          if ([event isStillRelevant])
1275            [self sendResponseToOrganizer: event
1276                                     from: ownerUser];
1277
1278          organizerUID = [[event organizer] uidInContext: context];
1279
1280          // Event is an exception to a recurring event; retrieve organizer from master event
1281          if (!organizerUID)
1282            organizerUID = [[(iCalEntityObject*)[[event parent] firstChildWithTag: [self componentTag]] organizer] uidInContext: context];
1283
1284          if (organizerUID)
1285            // Update the attendee in organizer's calendar.
1286            ex = [self _updateAttendee: attendee
1287                          withDelegate: delegate
1288                             ownerUser: theOwnerUser
1289                           forEventUID: [event uid]
1290                      withRecurrenceId: [event recurrenceId]
1291                          withSequence: [event sequence]
1292                                forUID: organizerUID
1293                       shouldAddSentBy: YES];
1294        }
1295
1296      // We update the calendar of all attendees that are
1297      // local to the system. This is useful in case user A accepts
1298      // invitation from organizer B and users C, D, E who are also
1299      // attendees need to verify if A has accepted.
1300      NSArray *attendees;
1301      iCalPerson *att;
1302      NSString *uid;
1303      int i;
1304
1305      attendees = [event attendees];
1306      for (i = 0; i < [attendees count]; i++)
1307        {
1308          att = [attendees objectAtIndex: i];
1309          uid = [att uidInContext: context];
1310          if (uid && att != attendee && ![uid isEqualToString: delegatedUID])
1311            [self _updateAttendee: attendee
1312                     withDelegate: delegate
1313                        ownerUser: theOwnerUser
1314                      forEventUID: [event uid]
1315                 withRecurrenceId: [event recurrenceId]
1316                     withSequence: [event sequence]
1317                           forUID: uid
1318                  shouldAddSentBy: YES];
1319        }
1320    }
1321
1322  return ex;
1323}
1324
1325//
1326//
1327//
1328- (NSDictionary *) _caldavSuccessCodeWithRecipient: (NSString *) recipient
1329{
1330  NSMutableArray *element;
1331  NSDictionary *code;
1332
1333  element = [NSMutableArray array];
1334  [element addObject: davElementWithContent (@"recipient", XMLNS_CALDAV, recipient)];
1335  [element addObject: davElementWithContent (@"request-status", XMLNS_CALDAV, @"2.0;Success")];
1336  code = davElementWithContent (@"response", XMLNS_CALDAV, element);
1337
1338  return code;
1339}
1340
1341//
1342// Old CalDAV scheduling (draft 4 and below) methods. We keep them since we still
1343// advertise for its support but we do everything within the calendar-auto-scheduling code
1344//
1345- (NSArray *) postCalDAVEventRequestTo: (NSArray *) recipients
1346                                  from: (NSString *) originator
1347{
1348  NSEnumerator *recipientsEnum;
1349  NSMutableArray *elements;
1350  NSString *recipient;
1351
1352  elements = [NSMutableArray array];
1353
1354  recipientsEnum = [recipients objectEnumerator];
1355
1356  while ((recipient = [recipientsEnum nextObject]))
1357    if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
1358      {
1359        [elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
1360      }
1361
1362  return elements;
1363}
1364
1365- (NSArray *) postCalDAVEventCancelTo: (NSArray *) recipients
1366                                 from: (NSString *) originator
1367{
1368  NSEnumerator *recipientsEnum;
1369  NSMutableArray *elements;
1370
1371  NSString *recipient;
1372
1373  elements = [NSMutableArray array];
1374
1375  recipientsEnum = [recipients objectEnumerator];
1376
1377  while ((recipient = [recipientsEnum nextObject]))
1378    if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
1379      {
1380        [elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
1381      }
1382
1383  return elements;
1384}
1385
1386- (NSArray *) postCalDAVEventReplyTo: (NSArray *) recipients
1387                                from: (NSString *) originator
1388{
1389  NSEnumerator *recipientsEnum;
1390  NSMutableArray *elements;
1391  NSString *recipient;
1392
1393  elements = [NSMutableArray array];
1394  recipientsEnum = [recipients objectEnumerator];
1395
1396  while ((recipient = [recipientsEnum nextObject]))
1397    if ([[recipient lowercaseString] hasPrefix: @"mailto:"])
1398      {
1399        [elements addObject: [self _caldavSuccessCodeWithRecipient: recipient]];
1400      }
1401
1402  return elements;
1403}
1404
1405//
1406//
1407//
1408- (NSException *) changeParticipationStatus: (NSString *) status
1409                               withDelegate: (iCalPerson *) delegate
1410                                      alarm: (iCalAlarm *) alarm
1411{
1412  return [self changeParticipationStatus: status
1413                            withDelegate: delegate
1414                                   alarm: alarm
1415                         forRecurrenceId: nil];
1416}
1417
1418//
1419//
1420//
1421- (NSException *) changeParticipationStatus: (NSString *) _status
1422                               withDelegate: (iCalPerson *) delegate
1423                                      alarm: (iCalAlarm *) alarm
1424                            forRecurrenceId: (NSCalendarDate *) _recurrenceId
1425{
1426  iCalCalendar *calendar;
1427  iCalEvent *event;
1428  iCalPerson *attendee;
1429  NSException *ex;
1430  SOGoUser *ownerUser, *delegatedUser;
1431  NSString *recurrenceTime, *delegatedUid;
1432
1433  event = nil;
1434  ex = nil;
1435  delegatedUser = nil;
1436
1437  calendar = [[self calendar: NO secure: NO] mutableCopy];
1438  [calendar autorelease];
1439
1440  if (_recurrenceId)
1441    {
1442      // If _recurrenceId is defined, find the specified occurence
1443      // within the repeating vEvent.
1444      recurrenceTime = [NSString stringWithFormat: @"%f", [_recurrenceId timeIntervalSince1970]];
1445      event = (iCalEvent*)[self lookupOccurrence: recurrenceTime];
1446
1447      if (event == nil)
1448        // If no occurence found, create one
1449        event = (iCalEvent*)[self newOccurenceWithID: recurrenceTime];
1450    }
1451  else
1452    // No specific occurence specified; return the first vEvent of
1453    // the vCalendar.
1454    event = (iCalEvent*)[calendar firstChildWithTag: [self componentTag]];
1455
1456  if (event)
1457    {
1458      // ownerUser will actually be the owner of the calendar
1459      // where the participation change on the event occurs. The particpation
1460      // change will be on the attendee corresponding to the ownerUser.
1461      ownerUser = [SOGoUser userWithLogin: owner];
1462
1463      attendee = [event userAsAttendee: ownerUser];
1464      if (attendee)
1465        {
1466          if (delegate && ![[delegate email] isEqualToString: [attendee delegatedTo]])
1467            {
1468              delegatedUid = [delegate uidInContext: context];
1469              if (delegatedUid)
1470                delegatedUser = [SOGoUser userWithLogin: delegatedUid];
1471              if (delegatedUser != nil && [event userIsOrganizer: delegatedUser])
1472                ex = [NSException exceptionWithHTTPStatus: 409
1473                                                   reason: @"delegate is organizer"];
1474              if ([event isAttendee: [[delegate email] rfc822Email]])
1475                ex = [NSException exceptionWithHTTPStatus: 409
1476                                                   reason: @"delegate is a participant"];
1477              else if ([SOGoGroup groupWithEmail: [[delegate email] rfc822Email]
1478                                        inDomain: [ownerUser domain]])
1479                ex = [NSException exceptionWithHTTPStatus: 409
1480                                                   reason: @"delegate is a group"];
1481            }
1482          if (ex == nil)
1483            {
1484              // Remove the RSVP attribute, as an action from the attendee
1485              // was actually performed, and this confuses iCal (bug #1850)
1486              [[attendee attributes] removeObjectForKey: @"RSVP"];
1487              ex = [self _handleAttendee: attendee
1488                            withDelegate: delegate
1489                               ownerUser: ownerUser
1490                            statusChange: _status
1491                                 inEvent: event];
1492            }
1493          if (ex == nil)
1494            {
1495              // We generate the updated iCalendar file and we save it in
1496              // the database. We do this ONLY when using SOGo from the
1497              // Web interface or over ActiveSync.
1498              // Over DAV, it'll be handled directly in PUTAction:
1499              if (![context request] || [[context request] handledByDefaultHandler]
1500                                     || [[[context request] requestHandlerKey] isEqualToString: @"Microsoft-Server-ActiveSync"])
1501                {
1502                  // If an alarm was specified, let's use it. This would happen if an attendee accepts/declines/etc. an
1503                  // event invitation and also sets an alarm along the way. This would happen ONLY from the web interface.
1504                  [event removeAllAlarms];
1505
1506                  if (alarm)
1507                    {
1508                      [event addToAlarms: alarm];
1509                    }
1510
1511                  ex = [self saveCalendar: [event parent]];
1512                }
1513            }
1514        }
1515      else
1516        ex = [NSException exceptionWithHTTPStatus: 404 // Not Found
1517                                           reason: @"user does not participate in this calendar event"];
1518    }
1519      else
1520        ex = [NSException exceptionWithHTTPStatus: 500 // Server Error
1521                                           reason: @"unable to parse event record"];
1522
1523  return ex;
1524}
1525
1526
1527//
1528//
1529//
1530- (BOOL) _shouldScheduleEvent: (iCalPerson *) theOrganizer
1531{
1532  NSArray *userAgents;
1533  NSString *v;
1534  BOOL b;
1535  int i;
1536
1537  b = YES;
1538
1539  if (theOrganizer && (v = [theOrganizer value: 0  ofAttribute: @"SCHEDULE-AGENT"]))
1540    {
1541      if ([v caseInsensitiveCompare: @"NONE"] == NSOrderedSame ||
1542          [v caseInsensitiveCompare: @"CLIENT"] == NSOrderedSame)
1543        b = NO;
1544    }
1545
1546  //
1547  // If we have to deal with Thunderbird/Lightning, we always send invitation
1548  // reponses, as Lightning v2.6 (at least this version) sets SCHEDULE-AGENT
1549  // to NONE/CLIENT when responding to an external invitation received by
1550  // SOGo - so no invitation responses are ever sent by Lightning. See
1551  // https://bugzilla.mozilla.org/show_bug.cgi?id=865726 and
1552  // https://bugzilla.mozilla.org/show_bug.cgi?id=997784
1553  //
1554  userAgents = [[context request] headersForKey: @"User-Agent"];
1555
1556  for (i = 0; i < [userAgents count]; i++)
1557    {
1558      if ([[userAgents objectAtIndex: i] rangeOfString: @"Thunderbird"].location != NSNotFound &&
1559          [[userAgents objectAtIndex: i] rangeOfString: @"Lightning"].location != NSNotFound)
1560        {
1561          b = YES;
1562          break;
1563        }
1564    }
1565
1566  return b;
1567}
1568
1569
1570//
1571//
1572//
1573- (void) prepareDeleteOccurence: (iCalEvent *) occurence
1574{
1575  SOGoUser *ownerUser, *currentUser;
1576  NSCalendarDate *recurrenceId;
1577  NSArray *attendees;
1578  iCalEvent *event;
1579  BOOL send_receipt;
1580
1581
1582  ownerUser = [SOGoUser userWithLogin: owner];
1583  event = [self component: NO secure: NO];
1584  send_receipt = YES;
1585
1586  if (![self _shouldScheduleEvent: [event organizer]])
1587    return;
1588
1589  if (occurence == nil)
1590    {
1591      // No occurence specified; use the master event.
1592      occurence = event;
1593      recurrenceId = nil;
1594    }
1595  else
1596    // Retrieve this occurence ID.
1597    recurrenceId = [occurence recurrenceId];
1598
1599  if ([event userIsOrganizer: ownerUser])
1600    {
1601      // The organizer deletes an occurence.
1602      currentUser = [context activeUser];
1603      attendees = [occurence attendeesWithoutUser: currentUser];
1604
1605#warning Make sure this is correct ..
1606      if (![attendees count] && event != occurence)
1607        attendees = [event attendeesWithoutUser: currentUser];
1608
1609      if ([attendees count])
1610        {
1611          // Remove the event from all attendees calendars
1612          // and send them an email.
1613          [self _handleRemovedUsers: attendees
1614                   withRecurrenceId: recurrenceId];
1615          [self sendEMailUsingTemplateNamed: @"Deletion"
1616                                  forObject: [occurence itipEntryWithMethod: @"cancel"]
1617                             previousObject: nil
1618                                toAttendees: attendees
1619                                   withType: @"calendar:cancellation"];
1620        }
1621    }
1622  else if ([occurence userIsAttendee: ownerUser])
1623    {
1624      // The current user deletes the occurence; let the organizer know that
1625      // the user has declined this occurence.
1626      [self changeParticipationStatus: @"DECLINED"
1627                         withDelegate: nil
1628                                alarm: nil
1629                      forRecurrenceId: recurrenceId];
1630      send_receipt = NO;
1631    }
1632
1633      if (send_receipt)
1634        [self sendReceiptEmailForObject: event
1635                         addedAttendees: nil
1636                       deletedAttendees: nil
1637                       updatedAttendees: nil
1638                              operation: EventDeleted];
1639}
1640
1641- (NSException *) prepareDelete
1642{
1643  [self prepareDeleteOccurence: nil];
1644
1645  return [super prepareDelete];
1646}
1647
1648- (NSDictionary *) _partStatsFromCalendar: (iCalCalendar *) calendar
1649{
1650  NSMutableDictionary *partStats;
1651  NSArray *allEvents;
1652  int count, max;
1653  iCalEvent *currentEvent;
1654  iCalPerson *ownerAttendee;
1655  NSString *key;
1656  SOGoUser *ownerUser;
1657
1658  ownerUser = [SOGoUser userWithLogin: owner];
1659
1660  allEvents = [calendar events];
1661  max = [allEvents count];
1662  partStats = [NSMutableDictionary dictionaryWithCapacity: max];
1663
1664  for (count = 0; count < max; count++)
1665    {
1666      currentEvent = [allEvents objectAtIndex: count];
1667      ownerAttendee = [currentEvent userAsAttendee: ownerUser];
1668      if (ownerAttendee)
1669        {
1670          if (count == 0)
1671            key = @"master";
1672          else
1673            key = [[currentEvent recurrenceId] iCalFormattedDateTimeString];
1674          [partStats setObject: ownerAttendee forKey: key];
1675        }
1676    }
1677
1678  return partStats;
1679}
1680
1681- (iCalCalendar *) _setupResponseInRequestCalendar: (iCalCalendar *) rqCalendar
1682{
1683  iCalCalendar *calendar;
1684  NSArray *keys;
1685  NSDictionary *partStats, *newPartStats;
1686  NSString *partStat, *key;
1687  int count, max;
1688
1689  calendar = [self calendar: NO secure: NO];
1690  partStats = [self _partStatsFromCalendar: calendar];
1691  keys = [partStats allKeys];
1692  max = [keys count];
1693  if (max > 0)
1694    {
1695      newPartStats = [self _partStatsFromCalendar: rqCalendar];
1696      if ([keys isEqualToArray: [newPartStats allKeys]])
1697        {
1698          for (count = 0; count < max; count++)
1699            {
1700              key = [keys objectAtIndex: count];
1701              partStat = [[newPartStats objectForKey: key] partStat];
1702              [[partStats objectForKey: key] setPartStat: partStat];
1703            }
1704        }
1705    }
1706
1707  return calendar;
1708}
1709
1710- (void) _adjustTransparencyInRequestCalendar: (iCalCalendar *) rqCalendar
1711{
1712  NSArray *allEvents;
1713  iCalEvent *event;
1714  int i;
1715
1716  allEvents = [rqCalendar events];
1717  for (i = 0; i < [allEvents count]; i++)
1718    {
1719      event = [allEvents objectAtIndex: i];
1720      if ([event isAllDay] && [event isOpaque])
1721          [event setTransparency: @"TRANSPARENT"];
1722    }
1723}
1724
1725//
1726// iOS devices (and potentially others) send event invitations with no PARTSTAT defined.
1727// This confuses DAV clients like Thunderbird, or event SOGo web. The RFC says:
1728//
1729//    Description: This parameter can be specified on properties with a
1730//    CAL-ADDRESS value type. The parameter identifies the participation
1731//    status for the calendar user specified by the property value. The
1732//    parameter values differ depending on whether they are associated with
1733//    a group scheduled "VEVENT", "VTODO" or "VJOURNAL". The values MUST
1734//    match one of the values allowed for the given calendar component. If
1735//    not specified on a property that allows this parameter, the default
1736//    value is NEEDS-ACTION.
1737//
1738- (void) _adjustPartStatInRequestCalendar: (iCalCalendar *) rqCalendar
1739{
1740  NSArray *allObjects, *allAttendees;
1741  iCalPerson *attendee;
1742  id entity;
1743
1744  int i, j;
1745
1746  allObjects = [rqCalendar allObjects];
1747
1748  for (i = 0; i < [allObjects count]; i++)
1749    {
1750      entity = [allObjects objectAtIndex: i];
1751
1752      if ([entity isKindOfClass: [iCalEvent class]])
1753        {
1754          allAttendees = [entity attendees];
1755
1756          for (j = 0; j < [allAttendees count]; j++)
1757            {
1758              attendee = [allAttendees objectAtIndex: j];
1759
1760              if (![[attendee partStat] length])
1761                [attendee setPartStat: @"NEEDS-ACTION"];
1762            }
1763        }
1764    }
1765}
1766
1767/**
1768 * Verify vCalendar for any inconsistency or missing attributes.
1769 * Currently only check if the events have an end date or a duration.
1770 * We also check for the default transparency parameters.
1771 * We also check for broken ORGANIZER such as "ORGANIZER;:mailto:sogo3@example.com"
1772 * @param rq the HTTP PUT request
1773 */
1774- (void) _adjustEventsInRequestCalendar: (iCalCalendar *) rqCalendar
1775{
1776  NSArray *allEvents;
1777  iCalEvent *event;
1778  NSUInteger i;
1779  int j;
1780
1781  allEvents = [rqCalendar events];
1782
1783  for (i = 0; i < [allEvents count]; i++)
1784    {
1785      event = [allEvents objectAtIndex: i];
1786
1787      if (![event hasEndDate] && ![event hasDuration])
1788        {
1789          // No end date, no duration
1790          if ([event isAllDay])
1791            [event setDuration: @"P1D"];
1792          else
1793            [event setDuration: @"PT1H"];
1794          [self warnWithFormat: @"Invalid event: no end date; setting duration to %@", [event duration]];
1795        }
1796
1797      if ([event organizer])
1798        {
1799          NSString *uid;
1800
1801          if (![[[event organizer] cn] length])
1802            {
1803              [[event organizer] setCn: [[event organizer] rfc822Email]];
1804            }
1805
1806          // We now make sure that the organizer, if managed by SOGo, is using
1807          // its default email when creating events and inviting attendees.
1808          uid = [[event organizer] uidInContext: context];
1809          if (uid)
1810            {
1811	      iCalPerson *attendee, *organizer;
1812              NSDictionary *defaultIdentity;
1813	      SOGoUser *organizerUser;
1814	      NSArray *allAttendees;
1815
1816	      organizerUser = [SOGoUser userWithLogin: uid];
1817              defaultIdentity = [organizerUser defaultIdentity];
1818	      organizer = [[event organizer] copy];
1819              [organizer setCn: [defaultIdentity objectForKey: @"fullName"]];
1820              [organizer setEmail: [defaultIdentity objectForKey: @"email"]];
1821
1822	      // We now check if one of the attendee is also the organizer. If so,
1823	      // we remove it. See bug #3905 (https://sogo.nu/bugs/view.php?id=3905)
1824	      // for more details. This is a Calendar app bug on Apple Yosemite.
1825	      allAttendees = [event attendees];
1826
1827	      for (j = [allAttendees count]-1; j >= 0; j--)
1828		{
1829		  attendee = [allAttendees objectAtIndex: j];
1830		  if ([organizerUser hasEmail: [attendee rfc822Email]])
1831		    [event removeFromAttendees: attendee];
1832		}
1833
1834	      // We reset the organizer
1835	      [event setOrganizer: organizer];
1836	      RELEASE(organizer);
1837            }
1838        }
1839    }
1840}
1841
1842//
1843// This is similar to what we do in SOGoAppointmentFolder: -importCalendar:
1844//
1845- (void) _adjustFloatingTimeInRequestCalendar: (iCalCalendar *) rqCalendar
1846{
1847  iCalDateTime *startDate, *endDate;
1848  NSString *startDateAsString;
1849  SOGoUserDefaults *ud;
1850  NSArray *allEvents;
1851  iCalTimeZone *tz;
1852  iCalEvent *event;
1853  int i, delta;
1854
1855  allEvents = [rqCalendar events];
1856  for (i = 0; i < [allEvents count]; i++)
1857    {
1858      event = [allEvents objectAtIndex: i];
1859
1860      if ([event isAllDay])
1861	continue;
1862
1863      startDate =  (iCalDateTime *)[event uniqueChildWithTag: @"dtstart"];
1864      startDateAsString = [[startDate valuesAtIndex: 0 forKey: @""] objectAtIndex: 0];
1865
1866      if (![startDate timeZone] &&
1867	  ![startDateAsString hasSuffix: @"Z"] &&
1868	  ![startDateAsString hasSuffix: @"z"])
1869	{
1870	  ud = [[context activeUser] userDefaults];
1871          tz = [iCalTimeZone timeZoneForName: [ud timeZoneName]];
1872          if ([rqCalendar addTimeZone: tz])
1873            {
1874	      delta = [[tz periodForDate: [startDate dateTime]] secondsOffsetFromGMT];
1875
1876	      [event setStartDate: [[event startDate] dateByAddingYears: 0  months: 0  days: 0  hours: 0  minutes: 0  seconds: -delta]];
1877	      [startDate setTimeZone: tz];
1878
1879	      endDate = (iCalDateTime *) [event uniqueChildWithTag: @"dtend"];
1880	      if (endDate)
1881		{
1882		  [event setEndDate: [[event endDate] dateByAddingYears: 0  months: 0  days: 0  hours: 0  minutes: 0  seconds: -delta]];
1883		  [endDate setTimeZone: tz];
1884		}
1885            }
1886	}
1887    }
1888}
1889
1890- (void) _decomposeGroupsInRequestCalendar: (iCalCalendar *) rqCalendar
1891{
1892  NSArray *allEvents;
1893  iCalEvent *event;
1894  int i;
1895
1896  // The algorithm is pretty straightforward:
1897  //
1898  // We get all events
1899  //   We get all attendees
1900  //     If some are groups, we decompose them
1901  // We regenerate the iCalendar string
1902  //
1903  allEvents = [rqCalendar events];
1904  for (i = 0; i < [allEvents count]; i++)
1905    {
1906      event = [allEvents objectAtIndex: i];
1907      [self expandGroupsInEvent: event];
1908    }
1909}
1910
1911
1912//
1913// If theRecurrenceId is nil, it returns immediately the
1914// first event that has a RECURRENCE-ID.
1915//
1916// Otherwise, it return values that matches.
1917//
1918- (iCalEvent *) _eventFromRecurrenceId: (NSCalendarDate *) theRecurrenceId
1919                                events: (NSArray *) allEvents
1920{
1921  iCalEvent *event;
1922  int i;
1923
1924  for (i = 0; i < [allEvents count]; i++)
1925    {
1926      event = [allEvents objectAtIndex: i];
1927
1928      if ([event recurrenceId] && !theRecurrenceId)
1929        return event;
1930
1931      if ([event recurrenceId] && [[event recurrenceId] compare: theRecurrenceId] == NSOrderedSame)
1932        return event;
1933    }
1934
1935  return nil;
1936}
1937
1938//
1939//
1940//
1941- (NSCalendarDate *) _addedExDate: (iCalEvent *) oldEvent
1942                         newEvent: (iCalEvent *) newEvent
1943{
1944  NSArray *oldExDates, *newExDates;
1945  NSMutableArray *dates;
1946  int i;
1947
1948  dates = [NSMutableArray array];
1949
1950  newExDates = [newEvent childrenWithTag: @"exdate"];
1951  for (i = 0; i < [newExDates count]; i++)
1952    [dates addObject: [[newExDates objectAtIndex: i] dateTime]];
1953
1954  oldExDates = [oldEvent childrenWithTag: @"exdate"];
1955  for (i = 0; i < [oldExDates count]; i++)
1956    [dates removeObject: [[oldExDates objectAtIndex: i] dateTime]];
1957
1958  return [dates lastObject];
1959}
1960
1961
1962//
1963//
1964//
1965- (id) DELETEAction: (WOContext *) _ctx
1966{
1967  [self prepareDelete];
1968  return [super DELETEAction: _ctx];
1969}
1970
1971//
1972// This method is meant to be the common point of any save operation from web
1973// and DAV requests, as well as from code making use of SOGo as a library
1974// (OpenChange)
1975//
1976- (NSException *) updateContentWithCalendar: (iCalCalendar *) calendar
1977                                fromRequest: (WORequest *) rq
1978{
1979  NSException *ex;
1980  NSArray *roles;
1981  SOGoUser *ownerUser;
1982
1983  if (calendar == fullCalendar || calendar == safeCalendar
1984                               || calendar == originalCalendar)
1985    [NSException raise: NSInvalidArgumentException format: @"the 'calendar' argument must be a distinct instance" @" from the original object"];
1986
1987  ownerUser = [SOGoUser userWithLogin: owner];
1988
1989  roles = [[context activeUser] rolesForObject: self
1990                                     inContext: context];
1991  //
1992  // We check if we gave only the "Respond To" right and someone is actually
1993  // responding to one of our invitation. In this case, _setupResponseCalendarInRequest
1994  // will only take the new attendee status and actually discard any other modifications.
1995  //
1996  if ([roles containsObject: @"ComponentResponder"] && ![roles containsObject: @"ComponentModifier"])
1997    calendar = [self _setupResponseInRequestCalendar: calendar];
1998  else
1999    {
2000      if (![[rq headersForKey: @"X-SOGo"] containsObject: @"NoGroupsDecomposition"])
2001        [self _decomposeGroupsInRequestCalendar: calendar];
2002
2003      if ([[ownerUser domainDefaults] iPhoneForceAllDayTransparency] && [rq isIPhone])
2004	[self _adjustTransparencyInRequestCalendar: calendar];
2005
2006      [self _adjustEventsInRequestCalendar: calendar];
2007      [self adjustClassificationInRequestCalendar: calendar];
2008      [self _adjustPartStatInRequestCalendar: calendar];
2009      [self _adjustFloatingTimeInRequestCalendar: calendar];
2010    }
2011
2012  //
2013  // We first check if it's a new event
2014  //
2015  if ([self isNew])
2016    {
2017      iCalEvent *event;
2018      NSArray *attendees;
2019      NSString *eventUID;
2020      BOOL scheduling;
2021
2022      attendees = nil;
2023
2024      event = [[calendar events] objectAtIndex: 0];
2025      eventUID = [event uid];
2026      scheduling = [self _shouldScheduleEvent: [event organizer]];
2027
2028      // make sure eventUID doesn't conflict with an existing event -  see bug #1853
2029      // TODO: send out a no-uid-conflict (DAV:href) xml element (rfc4791 section 5.3.2.1)
2030      if ([container resourceNameForEventUID: eventUID])
2031        {
2032          return [NSException exceptionWithHTTPStatus: 409
2033                                               reason: [NSString stringWithFormat: @"Event UID already in use. (%@)", eventUID]];
2034        }
2035
2036      //
2037      // New event and we're the organizer -- send invitation to all attendees
2038      //
2039      if (scheduling && [event userIsOrganizer: ownerUser])
2040      {
2041        attendees = [event attendeesWithoutUser: ownerUser];
2042        if ([attendees count])
2043          {
2044            if ((ex = [self _handleAddedUsers: attendees fromEvent: event]))
2045              return ex;
2046            else
2047              {
2048                // We might have auto-accepted resources here. If that's the
2049                // case, let's regenerate the versitstring and replace the
2050                // one from the request.
2051                [rq setContent: [[[event parent] versitString] dataUsingEncoding: [rq contentEncoding]]];
2052              }
2053
2054            [self sendEMailUsingTemplateNamed: @"Invitation"
2055                                    forObject: [event itipEntryWithMethod: @"request"]
2056                               previousObject: nil
2057                                  toAttendees: attendees
2058                                     withType: @"calendar:invitation"];
2059          }
2060      }
2061      //
2062      // We aren't the organizer but we're an attendee. That can happen when
2063      // we receive an external invitation (IMIP/ITIP) and we accept it
2064      // from a CUA - it gets added to a specific CalDAV calendar using a PUT
2065      //
2066      else if (scheduling && [event userIsAttendee: ownerUser])
2067        {
2068          [self sendIMIPReplyForEvent: event
2069                                 from: ownerUser
2070                                   to: [event organizer]];
2071        }
2072
2073      [self sendReceiptEmailForObject: event
2074		       addedAttendees: attendees
2075		     deletedAttendees: nil
2076		     updatedAttendees: nil
2077			    operation: EventCreated];
2078    }  // if ([self isNew])
2079  else
2080    {
2081      iCalCalendar *oldCalendar;
2082      iCalEvent *oldEvent, *newEvent;
2083      iCalEventChanges *changes;
2084      NSMutableArray *oldEvents, *newEvents;
2085      NSCalendarDate *recurrenceId;
2086      int i;
2087
2088      //
2089      // We check what has changed in the event and react accordingly.
2090      //
2091      newEvents = [NSMutableArray arrayWithArray: [calendar events]];
2092
2093      oldCalendar = [self calendar: NO secure: NO];
2094      oldEvents = [NSMutableArray arrayWithArray: [oldCalendar events]];
2095      recurrenceId = nil;
2096
2097      for (i = [newEvents count]-1; i >= 0; i--)
2098        {
2099          newEvent = [newEvents objectAtIndex: i];
2100
2101          if ([newEvent recurrenceId])
2102            {
2103              // Find the corresponding RECURRENCE-ID in the old calendar
2104              // If not present, we assume it was created before the PUT
2105              oldEvent = [self _eventFromRecurrenceId: [newEvent recurrenceId]
2106                                               events: oldEvents];
2107
2108              if (oldEvent == nil)
2109                {
2110                  NSString *recurrenceTime;
2111                  recurrenceTime = [NSString stringWithFormat: @"%f", [[newEvent recurrenceId] timeIntervalSince1970]];
2112                  oldEvent = (iCalEvent *)[self newOccurenceWithID: recurrenceTime];
2113                }
2114
2115              // If present, we look for changes
2116              changes = [iCalEventChanges changesFromEvent: oldEvent  toEvent: newEvent];
2117
2118              if ([changes sequenceShouldBeIncreased] | [changes hasAttendeeChanges])
2119                {
2120                  // We found a RECURRENCE-ID with changes, we consider it
2121                  recurrenceId = [newEvent recurrenceId];
2122                  break;
2123                }
2124              else
2125                {
2126                  [newEvents removeObject: newEvent];
2127                  [oldEvents removeObject: oldEvent];
2128                }
2129            }
2130
2131          oldEvent = nil;
2132          newEvent = nil;
2133        }
2134
2135      // If no changes were observed, let's see if we have any left overs
2136      // in the oldEvents or in the newEvents array
2137      if (!oldEvent && !newEvent)
2138        {
2139          // We check if we only have to deal with the MASTER event
2140          if ([newEvents count] == [oldEvents count])
2141            {
2142              oldEvent = [oldEvents objectAtIndex: 0];
2143              newEvent = [newEvents objectAtIndex: 0];
2144            }
2145          // A RECURRENCE-ID was added
2146          else if ([newEvents count] > [oldEvents count])
2147            {
2148              oldEvent = nil;
2149              newEvent = [self _eventFromRecurrenceId: nil  events: newEvents];
2150              recurrenceId = [newEvent recurrenceId];
2151            }
2152          // A RECURRENCE-ID was removed
2153          else
2154            {
2155              oldEvent = [self _eventFromRecurrenceId: nil  events: oldEvents];
2156              newEvent = nil;
2157              recurrenceId = [oldEvent recurrenceId];
2158            }
2159        }
2160
2161      // We check if the PUT call is actually an PART-STATE change
2162      // from one of the attendees - here's the logic :
2163      //
2164      // if owner == organizer
2165      //
2166      //    if [context activeUser] == organizer
2167      //      [send the invitation update]
2168      //    else
2169      //      [react on SENT-BY as someone else is acting for the organizer]
2170      //
2171      //
2172      if ([[newEvent attendees] count] || [[oldEvent attendees] count])
2173        {
2174          BOOL userIsOrganizer;
2175
2176          // newEvent might be nil here, if we're deleting a RECURRENCE-ID with attendees
2177          // If that's the case, we use the oldEvent to obtain the organizer
2178          if (newEvent)
2179            userIsOrganizer = [newEvent userIsOrganizer: ownerUser];
2180          else
2181            userIsOrganizer = [oldEvent userIsOrganizer: ownerUser];
2182
2183          // With Thunderbird 10, if you create a recurring event with an exception
2184          // occurence, and invite someone, the PUT will have the organizer in the
2185          // recurrence-id and not in the master event. We must fix this, otherwise
2186          // SOGo will break.
2187          if (!recurrenceId && ![[[[[newEvent parent] events] objectAtIndex: 0] organizer] uidInContext: context])
2188            [[[[newEvent parent] events] objectAtIndex: 0] setOrganizer: [newEvent organizer]];
2189
2190          if (userIsOrganizer)
2191            {
2192	      // We check ACLs of the 'organizer' - in case someone forges the SENT-BY
2193	      NSString *uid;
2194
2195	      uid = [[oldEvent organizer] uidInContext: context];
2196
2197	      if (uid && [[[context activeUser] login] caseInsensitiveCompare: uid] != NSOrderedSame)
2198		{
2199		  SOGoAppointmentObject *organizerObject;
2200
2201		  organizerObject = [self _lookupEvent: [oldEvent uid] forUID: uid];
2202		  roles = [[context activeUser] rolesForObject: organizerObject
2203						     inContext: context];
2204
2205		  if (![roles containsObject: @"ComponentModifier"])
2206		    {
2207		      return [NSException exceptionWithHTTPStatus: 409
2208							   reason: @"Not allowed to perform this action. Wrong SENT-BY being used regarding access rights on organizer's calendar."];
2209		    }
2210		}
2211
2212              // A RECCURENCE-ID was removed
2213              if (!newEvent && oldEvent)
2214                [self prepareDeleteOccurence: oldEvent];
2215              // The master event was changed, A RECCURENCE-ID was added or modified
2216              else if ((ex = [self _handleUpdatedEvent: newEvent  fromOldEvent: oldEvent]))
2217                return ex;
2218            }
2219          //
2220          // else => attendee is responding
2221          //
2222          //   if [context activeUser] == attendee
2223          //       [we change the PART-STATE]
2224          //   else
2225          //      [react on SENT-BY as someone else is acting for the attendee]
2226          else
2227            {
2228              iCalPerson *attendee, *delegate;
2229              NSString *delegateEmail;
2230
2231              attendee = [oldEvent userAsAttendee: [SOGoUser userWithLogin: owner]];
2232
2233              if (!attendee)
2234                attendee = [newEvent userAsAttendee: [SOGoUser userWithLogin: owner]];
2235              else
2236                {
2237                  // We must do an extra check here since Bob could have invited Alice
2238                  // using alice@example.com but she would have accepted with ATTENDEE set
2239                  // to sexy@example.com. That would duplicate the ATTENDEE and set the
2240                  // participation status to ACCEPTED for sexy@example.com but leave it
2241                  // to NEEDS-ACTION to alice@example. This can happen in Mozilla Thunderbird/Lightning
2242                  // when a user with multiple identities accepts an event invitation to one
2243                  // of its identity (which is different than the email address associated with
2244                  // the mail account) prior doing a calendar refresh.
2245                  NSMutableArray *attendees;
2246                  iCalPerson *participant;
2247
2248                  attendees = [NSMutableArray arrayWithArray: [newEvent attendeesWithoutUser: [SOGoUser userWithLogin: owner]]];
2249
2250                  participant = [newEvent participantForUser: [SOGoUser userWithLogin: owner]
2251                                                    attendee: attendee];
2252                  [attendee setPartStat: [participant partStat]];
2253                  [attendee setDelegatedFrom: [participant delegatedFrom]];
2254                  [attendee setDelegatedTo: [participant delegatedTo]];
2255                  [attendees addObject: attendee];
2256                  [newEvent setAttendees: attendees];
2257                }
2258
2259              // We first check of the sequences are alright. We don't accept attendees
2260              // accepting "old" invitations. If that's the case, we return a 409
2261              if ([[newEvent sequence] intValue] < [[oldEvent sequence] intValue])
2262                return [NSException exceptionWithHTTPStatus: 409
2263                                                     reason: @"sequences don't match"];
2264
2265              // Remove the RSVP attribute, as an action from the attendee
2266              // was actually performed, and this confuses iCal (bug #1850)
2267              [[attendee attributes] removeObjectForKey: @"RSVP"];
2268
2269              delegate = nil;
2270              delegateEmail = [attendee delegatedTo];
2271
2272              if ([delegateEmail length])
2273                {
2274                  delegateEmail = [delegateEmail substringFromIndex: 7];
2275                  if ([delegateEmail length])
2276                    delegate = [newEvent findAttendeeWithEmail: delegateEmail];
2277                }
2278
2279              changes = [iCalEventChanges changesFromEvent: oldEvent  toEvent: newEvent];
2280
2281              // The current user deletes the occurence; let the organizer know that
2282              // the user has declined this occurence.
2283              if ([[changes updatedProperties] containsObject: @"exdate"])
2284                {
2285                  [self changeParticipationStatus: @"DECLINED"
2286                                     withDelegate: nil // FIXME (specify delegate?)
2287                                            alarm: nil
2288                                  forRecurrenceId: [self _addedExDate: oldEvent  newEvent: newEvent]];
2289                }
2290              else if (attendee)
2291                {
2292                  [self changeParticipationStatus: [attendee partStat]
2293                                     withDelegate: delegate
2294                                            alarm: nil
2295                                  forRecurrenceId: recurrenceId];
2296                }
2297              // All attendees and the organizer field were removed. Apple iCal does
2298              // that when we remove the last attendee of an event.
2299              //
2300              // We must update previous's attendees' calendars to actually
2301              // remove the event in each of them.
2302              else
2303                {
2304                  [self _handleRemovedUsers: [changes deletedAttendees]
2305                           withRecurrenceId: recurrenceId];
2306                }
2307            }
2308        } // if ([[newEvent attendees] count] || [[oldEvent attendees] count])
2309      else
2310        {
2311          [self sendReceiptEmailForObject: newEvent
2312                           addedAttendees: nil
2313                         deletedAttendees: nil
2314                         updatedAttendees: nil
2315                                operation: EventUpdated];
2316        }
2317    }  // else of if (isNew) ...
2318
2319  unsigned int baseVersion;
2320  // We must NOT invoke [super PUTAction:] here as it'll resave
2321  // the content string and we could have etag mismatches.
2322  baseVersion = (isNew ? 0 : version);
2323
2324  ex = [self saveComponent: calendar
2325               baseVersion: baseVersion];
2326
2327  return ex;
2328}
2329
2330//
2331// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we
2332// simply invoke super's PUTAction.
2333//
2334// We also check if we must force transparency on all day events
2335// from iPhone clients.
2336//
2337- (id) PUTAction: (WOContext *) _ctx
2338{
2339  NSException *ex;
2340  NSString *etag;
2341  WORequest *rq;
2342  WOResponse *response;
2343  iCalCalendar *rqCalendar;
2344
2345  rq = [_ctx request];
2346  rqCalendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]];
2347
2348  if (![self isNew])
2349    {
2350      //
2351      // We must check for etag changes prior doing anything since an attendee could
2352      // have changed its participation status and the organizer didn't get the
2353      // copy and is trying to do a modification to the event.
2354      //
2355      ex = [self matchesRequestConditionInContext: context];
2356      if (ex)
2357        return ex;
2358    }
2359
2360  ex = [self updateContentWithCalendar: rqCalendar fromRequest: rq];
2361  if (ex)
2362    response = (WOResponse *) ex;
2363  else
2364    {
2365      response = [_ctx response];
2366      if (isNew)
2367        [response setStatus: 201 /* Created */];
2368      else
2369        [response setStatus: 204 /* No Content */];
2370      etag = [self davEntityTag];
2371      if (etag)
2372        [response setHeader: etag forKey: @"etag"];
2373    }
2374
2375  return response;
2376}
2377
2378@end /* SOGoAppointmentObject */
2379