/* Copyright (c) 2014, Inverse inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Inverse inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "iCalEvent+ActiveSync.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #include "iCalAlarm+ActiveSync.h" #include "iCalRecurrenceRule+ActiveSync.h" #include "iCalTimeZone+ActiveSync.h" #include "NSDate+ActiveSync.h" #include "NSString+ActiveSync.h" @implementation iCalEvent (ActiveSync) - (int) _attendeeStatus: (iCalPerson *) attendee { int attendee_status; attendee_status = 5; if ([[attendee partStat] caseInsensitiveCompare: @"ACCEPTED"] == NSOrderedSame) attendee_status = 3; else if ([[attendee partStat] caseInsensitiveCompare: @"DECLINED"] == NSOrderedSame) attendee_status = 4; else if ([[attendee partStat] caseInsensitiveCompare: @"TENTATIVE"] == NSOrderedSame) attendee_status = 2; return attendee_status; } - (NSString *) activeSyncRepresentationInContext: (WOContext *) context { NSMutableString *s; NSArray *attendees, *categories; iCalPerson *organizer, *attendee; iCalTimeZone *tz; id o; int v, i, meetingStatus; NSTimeZone *userTimeZone; userTimeZone = [[[context activeUser] userDefaults] timeZone]; s = [NSMutableString string]; [s appendFormat: @"%d", ([self isAllDay] ? 1 : 0)]; // DTStamp -- http://msdn.microsoft.com/en-us/library/ee219470(v=exchg.80).aspx if ([self timeStampAsDate]) [s appendFormat: @"%@", [[self timeStampAsDate] activeSyncRepresentationWithoutSeparatorsInContext: context]]; else if ([self created]) [s appendFormat: @"%@", [[self created] activeSyncRepresentationWithoutSeparatorsInContext: context]]; // Timezone tz = [(iCalDateTime *)[self firstChildWithTag: @"dtstart"] timeZone]; // StartTime -- http://msdn.microsoft.com/en-us/library/ee157132(v=exchg.80).aspx if ([self startDate]) { if ([self isAllDay] && !tz) [s appendFormat: @"%@", [[[self startDate] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: ([userTimeZone secondsFromGMTForDate: [self startDate]])*-1] activeSyncRepresentationWithoutSeparatorsInContext: context]]; else [s appendFormat: @"%@", [[self startDate] activeSyncRepresentationWithoutSeparatorsInContext: context]]; } // EndTime -- http://msdn.microsoft.com/en-us/library/ee157945(v=exchg.80).aspx if ([self endDate]) { if ([self isAllDay] && !tz) [s appendFormat: @"%@", [[[self endDate] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: ([userTimeZone secondsFromGMTForDate: [self endDate]])*-1] activeSyncRepresentationWithoutSeparatorsInContext: context]]; else [s appendFormat: @"%@", [[self endDate] activeSyncRepresentationWithoutSeparatorsInContext: context]]; } if (!tz) tz = [iCalTimeZone timeZoneForName: [userTimeZone name]]; if (![self recurrenceId]) [s appendFormat: @"%@", [tz activeSyncRepresentationInContext: context]]; // Organizer and other invitations related properties if ((organizer = [self organizer])) { meetingStatus = 1; // meeting and the user is the meeting organizer. o = [organizer rfc822Email]; if (![self recurrenceId] && [o length]) { [s appendFormat: @"%@", o]; o = [organizer cn]; if ([o length]) [s appendFormat: @"%@", o]; } } // Attendees attendees = [self attendees]; if ([attendees count]) { int i, attendee_status, attendee_type; [s appendString: @""]; for (i = 0; i < [attendees count]; i++) { [s appendString: @""]; attendee = [attendees objectAtIndex: i]; [s appendFormat: @"%@", [[attendee rfc822Email] activeSyncRepresentationInContext: context]]; [s appendFormat: @"%@", [[attendee cn] activeSyncRepresentationInContext: context]]; attendee_status = [self _attendeeStatus: attendee]; [s appendFormat: @"%d", attendee_status]; // FIXME: handle resource if ([[attendee role] caseInsensitiveCompare: @"REQ-PARTICIPANT"] == NSOrderedSame) attendee_type = 1; else attendee_type = 2; [s appendFormat: @"%d", attendee_type]; [s appendString: @""]; } [s appendString: @""]; } else { meetingStatus = 0; // appointment } // This depends on the 'NEEDS-ACTION' parameter. // This will trigger the SendMail command if ([self userIsAttendee: [context activeUser]]) { iCalPerson *attendee; int attendee_status; meetingStatus = 3; // event is a meeting, and the user is not the meeting organizer attendee = [self userAsAttendee: [context activeUser]]; attendee_status = [self _attendeeStatus: attendee]; [s appendFormat: @"%d", 1]; [s appendFormat: @"%d", attendee_status]; [s appendFormat: @"%d", 1]; // BusyStatus -- http://msdn.microsoft.com/en-us/library/ee202290(v=exchg.80).aspx [s appendFormat: @"%d", 2]; } [s appendFormat: @"%d", meetingStatus]; // Subject -- http://msdn.microsoft.com/en-us/library/ee157192(v=exchg.80).aspx if ([[self summary] length]) [s appendFormat: @"%@", [[self summary] activeSyncRepresentationInContext: context]]; // Location if ([[self location] length]) [s appendFormat: @"%@", [[self location] activeSyncRepresentationInContext: context]]; // Importance - NOT SUPPORTED - DO NOT ENABLE //o = [self priority]; //if ([o isEqualToString: @"9"]) // v = 0; //else if ([o isEqualToString: @"1"]) // v = 2; //else // v = 1; //[s appendFormat: @"%d", v]; // UID -- http://msdn.microsoft.com/en-us/library/ee159919(v=exchg.80).aspx if (![self recurrenceId] && [[self uid] length]) [s appendFormat: @"%@", [[self uid] activeSyncRepresentationInContext: context]]; // Recurrence rules if ([self isRecurrent]) [s appendString: [[[self recurrenceRules] lastObject] activeSyncRepresentationInContext: context]]; // Comment o = [self comment]; //if (![self recurrenceId] && [o length]) if ([o length]) { // It is very important here to NOT set 0 in the response, // otherwise it'll prevent WP8 phones from sync'ing. See #3028 for details. o = [o activeSyncRepresentationInContext: context]; if ([[[context request] headerForKey: @"MS-ASProtocolVersion"] isEqualToString: @"2.5"]) { [s appendFormat: @"%@", o]; [s appendString: @"0"]; } else { [s appendString: @""]; [s appendFormat: @"%d", 1]; [s appendFormat: @"%d", (int)[o length]]; [s appendFormat: @"%@", o]; [s appendString: @""]; } } // Sensitivity if ([[self accessClass] isEqualToString: @"PRIVATE"]) v = 2; else if ([[self accessClass] isEqualToString: @"CONFIDENTIAL"]) v = 3; else v = 0; [s appendFormat: @"%d", v]; categories = [self categories]; if ([categories count]) { [s appendFormat: @""]; for (i = 0; i < [categories count]; i++) { [s appendFormat: @"%@", [[categories objectAtIndex: i] activeSyncRepresentationInContext: context]]; } [s appendFormat: @""]; } // Reminder -- http://msdn.microsoft.com/en-us/library/ee219691(v=exchg.80).aspx // TODO: improve this to handle more alarm types if ([self hasAlarms]) { iCalAlarm *alarm; alarm = [self firstDisplayOrAudioAlarm]; [s appendString: [alarm activeSyncRepresentationInContext: context]]; } // Exceptions if ([self isRecurrent]) { NSMutableArray *components, *exdates; iCalEvent *current_component; NSString *recurrence_id; unsigned int count, max, i; components = [NSMutableArray arrayWithArray: [[self parent] events]]; max = [components count]; if (max > 1 || [self hasExceptionDates]) { exdates = [NSMutableArray arrayWithArray: [self exceptionDates]]; [s appendString: @""]; for (count = 1; count < max; count++) { current_component = [components objectAtIndex: count] ; if ([self isAllDay]) { recurrence_id = [NSString stringWithFormat: @"%@", [[[current_component recurrenceId] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: -[userTimeZone secondsFromGMTForDate: [current_component recurrenceId]]] activeSyncRepresentationWithoutSeparatorsInContext: context]]; } else { recurrence_id = [NSString stringWithFormat: @"%@", [[current_component recurrenceId] activeSyncRepresentationWithoutSeparatorsInContext: context]]; } [s appendString: @""]; [s appendFormat: @"%@", recurrence_id]; [s appendFormat: @"%@", [current_component activeSyncRepresentationInContext: context]]; [s appendString: @""]; } for (i = 0; i < [exdates count]; i++) { [s appendString: @""]; [s appendString: @"1"]; if ([self isAllDay]) { recurrence_id = [NSString stringWithFormat: @"%@", [[[[exdates objectAtIndex: i] asCalendarDate] dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: -[userTimeZone secondsFromGMTForDate: [[exdates objectAtIndex: i] asCalendarDate]]] activeSyncRepresentationWithoutSeparatorsInContext: context]]; } else { recurrence_id = [NSString stringWithFormat: @"%@", [[[exdates objectAtIndex: i] asCalendarDate] activeSyncRepresentationWithoutSeparatorsInContext: context]]; } [s appendFormat: @"%@", recurrence_id]; [s appendString: @""]; } [s appendString: @""]; } } if (![self recurrenceId]) [s appendFormat: @"%d", 1]; return s; } // // To understand meeting requests/responses, see: // // http://blogs.msdn.com/b/exchangedev/archive/2011/07/22/working-with-meeting-requests-in-exchange-activesync.aspx // http://blogs.msdn.com/b/exchangedev/archive/2011/07/29/working-with-meeting-responses-in-exchange-activesync.aspx // // // Here is an example of a Sync call when sogo10 accepts an invitation from sogo3: // // // 2978-52EA9D00-1-A253E70.ics // // LAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w== // 0 // 20140207T130000Z // 20140207T140000Z // 20140130T185245Z // test 8 // 0 // // 1 // // // sogo3@example.com // 2978-52EA9D00-1-A253E70 // // // sogo10 // sogo10@example.com // 1 // // // 2 // 3 // Wolfgang Fritz // // // - (void) takeActiveSyncValues: (NSDictionary *) theValues inContext: (WOContext *) context { iCalDateTime *start, *end; //, *oldstart; NSCalendarDate *oldstart; NSTimeZone *userTimeZone; iCalTimeZone *tz; id o; int deltasecs; BOOL isAllDay; NSMutableArray *occurences; occurences = [NSMutableArray arrayWithArray: [[self parent] events]]; if ((o = [theValues objectForKey: @"UID"])) [self setUid: o]; // FIXME: merge with iCalToDo if ((o = [theValues objectForKey: @"Subject"])) [self setSummary: o]; isAllDay = NO; if ([[theValues objectForKey: @"AllDayEvent"] intValue]) { isAllDay = YES; } else if ([occurences count] && !([theValues objectForKey: @"AllDayEvent"])) { // If the occurence has no AllDay tag use it from master. isAllDay = [[occurences objectAtIndex: 0] isAllDay]; } // // 0- free, 1- tentative, 2- busy and 3- out of office // if ((o = [theValues objectForKey: @"BusyStatus"])) { [o intValue]; } // // 0- normal, 1- personal, 2- private and 3-confidential // if ((o = [theValues objectForKey: @"Sensitivity"])) { switch ([o intValue]) { case 2: [self setAccessClass: @"PRIVATE"]; break; case 3: [self setAccessClass: @"CONFIDENTIAL"]; break; case 0: case 1: default: [self setAccessClass: @"PUBLIC"]; } } // Categories if ((o = [theValues objectForKey: @"Categories"]) && [o length]) [self setCategories: o]; // We ignore TimeZone sent by mobile devices for now. // Some Windows devices don't send during event updates. //if ((o = [theValues objectForKey: @"TimeZone"])) // { // } //else { // We haven't received a timezone, let's use the user's timezone // specified in SOGo for now. userTimeZone = [[[context activeUser] userDefaults] timeZone]; tz = [iCalTimeZone timeZoneForName: [userTimeZone name]]; [(iCalCalendar *) parent addTimeZone: tz]; } // FIXME: merge with iCalToDo if ([[[context request] headerForKey: @"MS-ASProtocolVersion"] isEqualToString: @"2.5"]) { if ((o = [theValues objectForKey: @"Body"])) [self setComment: o]; } else { if ((o = [[theValues objectForKey: @"Body"] objectForKey: @"Data"])) [self setComment: o]; } if ((o = [theValues objectForKey: @"Location"])) [self setLocation: o]; if ((o = [theValues objectForKey: @"StartTime"])) { o = [o calendarDate]; start = (iCalDateTime *) [self uniqueChildWithTag: @"dtstart"]; oldstart = [start dateTime]; [start setTimeZone: tz]; if (isAllDay) { [start setDate: o]; [start setTimeZone: nil]; } else { [start setDateTime: o]; } // Calculate delta if start date has been changed. if (oldstart) deltasecs = [[start dateTime ] timeIntervalSinceDate: oldstart] * -1; else deltasecs = 0; } if ((o = [theValues objectForKey: @"EndTime"])) { o = [o calendarDate]; end = (iCalDateTime *) [self uniqueChildWithTag: @"dtend"]; [end setTimeZone: tz]; if (isAllDay) { [end setDate: o]; [end setTimeZone: nil]; } else { [end setDateTime: o]; } } // // If an alarm is deinfed with an action != DISPLAY, we ignore the alarm - don't want to overwrite. // if ([self hasAlarms] && [[[[self alarms] objectAtIndex: 0] action] caseInsensitiveCompare: @"DISPLAY"] != NSOrderedSame) { // Ignore the alarm for now } else if ((o = [theValues objectForKey: @"Reminder"])) { // NOTE: Outlook sends a 15 min reminder (18 hour for allday) if no reminder is specified // although no default reminder is defined (File -> Options -> Clendar -> Calendar Options - > Default Reminders) // // http://answers.microsoft.com/en-us/office/forum/office_2013_release-outlook/desktop-outlook-calendar-creates-entries-with/9aef72d8-81bb-4a32-a6ab-bf7d216fb811?page=5&tm=1395690285088 // iCalAlarm *alarm; alarm = [[iCalAlarm alloc] init]; [alarm takeActiveSyncValues: theValues inContext: context]; [self removeAllAlarms]; [self addToAlarms: alarm]; RELEASE(alarm); } else { // We remove existing alarm since no reminder in the ActiveSync payload [self removeAllAlarms]; } // Recurrence if ((o = [theValues objectForKey: @"Recurrence"])) { iCalRecurrenceRule *rule; rule = [[iCalRecurrenceRule alloc] init]; [self setRecurrenceRules: [NSArray arrayWithObject: rule]]; RELEASE(rule); [rule takeActiveSyncValues: o inContext: context]; // Exceptions if ((o = [theValues objectForKey: @"Exceptions"]) && [o isKindOfClass: [NSArray class]]) { NSCalendarDate *recurrenceId, *adjustedRecurrenceId, *currentId; NSMutableArray *exdates, *exceptionstouched; iCalEvent *currentOccurence; NSDictionary *exception; unsigned int i, count, max; [self removeAllExceptionDates]; exdates = [NSMutableArray array]; exceptionstouched = [NSMutableArray array]; for (i = 0; i < [o count]; i++) { exception = [o objectAtIndex: i]; if (![[exception objectForKey: @"Exception_Deleted"] intValue]) { recurrenceId = [[exception objectForKey: @"Exception_StartTime"] asCalendarDate]; if (isAllDay) recurrenceId = [recurrenceId dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: [userTimeZone secondsFromGMTForDate:recurrenceId]]; // When moving the calendar entry (i.e. changing the startDate) iOS and Android sometimes send a value for // Exception_StartTime which is not what we expect. // With this we make sure the recurrenceId is always correct. // // iOS problem - If the master is a no-allday-event but the exception is, an invalid Exception_StartTime is in the request. // e.g. it sends 20150409T040000Z instead of 20150409T060000Z; // This might be a special case since iPhone doesn't allow an allday-exception on a non-allday-event. if (!([[start dateTime] compare: [[start dateTime] hour:[recurrenceId hourOfDay] minute:[recurrenceId minuteOfHour]]] == NSOrderedSame)) recurrenceId = [recurrenceId hour:[[start dateTime] hourOfDay] minute:[[start dateTime] minuteOfHour]]; // We need to store the recurrenceIds and exception dates adjusted by deltasecs. // This ensures that the adjustment in SOGoCalendarComponent->updateComponent->_updateRecurrenceIDsWithEvent gives the correct dates. adjustedRecurrenceId = [recurrenceId dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: deltasecs]; // search for an existing occurence and update it max = [occurences count]; currentOccurence = nil; count = 1; while (count < max) { currentOccurence = [occurences objectAtIndex: count] ; currentId = [currentOccurence recurrenceId]; if ([currentId compare: adjustedRecurrenceId] == NSOrderedSame) { [exceptionstouched addObject: adjustedRecurrenceId]; [currentOccurence takeActiveSyncValues: exception inContext: context]; break; } count++; currentOccurence = nil; } // Create a new occurence if we found none to update. if (!currentOccurence) { iCalDateTime *recid; currentOccurence = [self mutableCopy]; [currentOccurence removeAllRecurrenceRules]; [currentOccurence removeAllExceptionRules]; [currentOccurence removeAllExceptionDates]; [[self parent] addToEvents: currentOccurence]; [currentOccurence takeActiveSyncValues: exception inContext: context]; recid = (iCalDateTime *)[currentOccurence uniqueChildWithTag: @"recurrence-id"]; if (isAllDay) [recid setDate: adjustedRecurrenceId]; else [recid setDateTime: adjustedRecurrenceId]; [exceptionstouched addObject: [recid dateTime]]; } } else if ([[exception objectForKey: @"Exception_Deleted"] intValue]) { recurrenceId = [[exception objectForKey: @"Exception_StartTime"] asCalendarDate]; if (isAllDay) recurrenceId = [recurrenceId dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: [userTimeZone secondsFromGMTForDate:recurrenceId]]; // We add only valid exception dates. if ([self doesOccurOnDate: recurrenceId] && ([[start dateTime] compare: [[start dateTime] hour:[recurrenceId hourOfDay] minute:[recurrenceId minuteOfHour]]] == NSOrderedSame)) { // We need to store the recurrenceIds and exception dates adjusted by deltasecs. // This ensures that the adjustment in SOGoCalendarComponent->updateComponent->_updateRecurrenceIDsWithEvent gives the correct dates. [exdates addObject: [recurrenceId dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: deltasecs]]; } } } // Update exception dates in master event. max = [exdates count]; if (max > 0) { for (count = 0; count < max; count++) { if (([exceptionstouched indexOfObject: [exdates objectAtIndex: count]] == NSNotFound)) [self addToExceptionDates: [exdates objectAtIndex: count]]; } } // Remove all exceptions included in the request. max = [occurences count]; count = 1; // skip the master event while (count < max) { currentOccurence = [occurences objectAtIndex: count] ; currentId = [currentOccurence recurrenceId]; // Delete all occurences which are not touched (modified/added). if (([exceptionstouched indexOfObject: currentId] == NSNotFound)) [[[self parent] children] removeObject: currentOccurence]; count++; } } else if ([self isRecurrent]) { // We have no exceptions in request but there is a recurrence rule. // Remove all excpetions and exception dates. iCalEvent *currentOccurence; unsigned int count, max; [self removeAllExceptionDates]; max = [occurences count]; count = 1; // skip the master event while (count < max) { currentOccurence = [occurences objectAtIndex: count]; [[[self parent] children] removeObject: currentOccurence]; count++; } } } else if ([self isRecurrent]) { // We have no recurrence rule in request. // Remove all exceptions, exception dates and recurrence rules. iCalEvent *currentOccurence; unsigned int count, max; [self removeAllRecurrenceRules]; [self removeAllExceptionRules]; [self removeAllExceptionDates]; max = [occurences count]; count = 1; // skip the master event while (count < max) { currentOccurence = [occurences objectAtIndex: count]; [[[self parent] children] removeObject: currentOccurence]; count++; } } // Organizer - we don't touch the value unless we're the organizer if ((o = [theValues objectForKey: @"Organizer_Email"]) && ([self userIsOrganizer: [context activeUser]] || [[context activeUser] hasEmail: o])) { iCalPerson *person; person = [iCalPerson elementWithTag: @"organizer"]; [person setEmail: o]; [person setCn: [theValues objectForKey: @"Organizer_Name"]]; [person setPartStat: @"ACCEPTED"]; [self setOrganizer: person]; } if ((o = [theValues objectForKey: @"MeetingStatus"])) { // // iOS is plain stupid here. It seends event invitations with no Organizer. // We check this corner-case and if MeetingStatus == 1 (see http://msdn.microsoft.com/en-us/library/ee219342(v=exchg.80).aspx or details) // and there's no organizer, we fake one. // if ([o intValue] == 1 && ![theValues objectForKey: @"Organizer_Email"] && ![[[self organizer] rfc822Email] length]) { iCalPerson *person; person = [iCalPerson elementWithTag: @"organizer"]; [person setEmail: [[[context activeUser] primaryIdentity] objectForKey: @"email"]]; [person setCn: [[context activeUser] cn]]; [person setPartStat: @"ACCEPTED"]; [self setOrganizer: person]; } // // When MeetingResponse fails Outlook still sends a new calendar entry with MeetingStatus=3. // Use the organizer from the request if the event has no organizer. // if ([o intValue] == 3 && [theValues objectForKey: @"Organizer_Email"] && ![[[self organizer] rfc822Email] length]) { iCalPerson *person; person = [iCalPerson elementWithTag: @"organizer"]; [person setEmail: [theValues objectForKey: @"Organizer_Email"]]; [person setCn: [theValues objectForKey: @"Organizer_Name"]]; [person setPartStat: @"ACCEPTED"]; [self setOrganizer: person]; } } // Attendees - we don't touch the values if we're an attendee. This is gonna // be done automatically by the ActiveSync client when invoking MeetingResponse. if (![self userIsAttendee: [context activeUser]]) { // Windows phones sens sometimes an empty Attendees tag. // We check it's an array before processing it. if ((o = [theValues objectForKey: @"Attendees"]) && [o isKindOfClass: [NSArray class]]) { NSMutableArray *attendees; NSDictionary *attendee; iCalPerson *person; int status, i; attendees = [NSMutableArray array]; for (i = 0; i < [o count]; i++) { // Each attendee has is a dictionary similar to this: // { "Attendee_Email" = "sogo3@example.com"; "Attendee_Name" = "Wolfgang Fritz"; "Attendee_Status" = 5; "Attendee_Type" = 1; } attendee = [o objectAtIndex: i]; person = [iCalPerson elementWithTag: @"attendee"]; [person setCn: [attendee objectForKey: @"Attendee_Name"]]; [person setEmail: [attendee objectForKey: @"Attendee_Email"]]; status = [[attendee objectForKey: @"Attendee_Status"] intValue]; switch (status) { case 2: [person setPartStat: @"TENTATIVE"]; break; case 3: [person setPartStat: @"ACCEPTED"]; break; case 4: [person setPartStat: @"DECLINED"]; break; case 0: case 5: default: [person setPartStat: @"NEEDS-ACTION"]; break; } // FIXME: handle Attendee_Type [attendees addObject: person]; } [self setAttendees: attendees]; } } } - (void) changeParticipationStatus: (NSDictionary *) theValues inContext: (WOContext *) context component: (id) component { NSString *status; id o; // See: https://msdn.microsoft.com/en-us/library/ee202290(v=exchg.80).aspx // // 0 == Free // 1 == Tentative // 2 == Busy // 3 == Out of Office // 4 == Working elsehwere // if ((o = [theValues objectForKey: @"BusyStatus"])) { // We translate >= 2 into ACCEPTED if ([o intValue] == 0) status = @"NEEDS-ACTION"; else if ([o intValue] >= 2) status = @"ACCEPTED"; else status = @"TENTATIVE"; // There's no delegate in EAS [(SOGoAppointmentObject *) component changeParticipationStatus: status withDelegate: nil alarm: nil]; } } @end