/* MAPIStoreRecurrenceUtils.m - this file is part of SOGo * * Copyright (C) 2011-2012 Inverse inc * * Author: Wolfgang Sourdeau * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3, or (at your option) * any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import "NSDate+MAPIStore.h" #import "MAPIStoreRecurrenceUtils.h" #import "MAPIStoreTypes.h" #include #include #include #include @implementation iCalCalendar (MAPIStoreRecurrence) - (void) setupRecurrenceWithMasterEntity: (iCalRepeatableEntityObject *) entity fromRecurrencePattern: (struct RecurrencePattern *) rp withExceptions: (struct ExceptionInfo *) exInfos andExceptionCount: (uint16_t) exInfoCount inTimeZone: (NSTimeZone *) tz { NSCalendarDate *startDate, *olEndDate, *untilDate, *exDate; NSString *monthDay, *month; NSMutableSet *exceptionDates; NSArray *realExDates; iCalRecurrenceRule *rule; iCalByDayMask *byDayMask; iCalWeekOccurrence weekOccurrence; iCalWeekOccurrences dayMaskDays; NSUInteger count, max; NSInteger bySetPos; unsigned char maskValue; [entity removeAllRecurrenceRules]; [entity removeAllExceptionRules]; [entity removeAllExceptionDates]; rule = [iCalRecurrenceRule elementWithTag: @"rrule"]; [entity addToRecurrenceRules: rule]; startDate = [entity startDate]; // DEBUG(5, ("From client:\n")); // NDR_PRINT_DEBUG(AppointmentRecurrencePattern, pattern); memset (&dayMaskDays, 0, sizeof (iCalWeekOccurrences)); if (rp->PatternType == PatternType_Day) { [rule setFrequency: iCalRecurrenceFrequenceDaily]; [rule setRepeatInterval: rp->Period / SOGoMinutesPerDay]; } else if (rp->PatternType == PatternType_Week) { [rule setFrequency: iCalRecurrenceFrequenceWeekly]; [rule setRepeatInterval: rp->Period]; /* MAPI values for days are the same as in NGCards */ for (count = 0; count < 7; count++) { maskValue = 1 << count; if ((rp->PatternTypeSpecific.WeekRecurrencePattern & maskValue)) dayMaskDays[count] = iCalWeekOccurrenceAll; } byDayMask = [iCalByDayMask byDayMaskWithDays: dayMaskDays]; [rule setByDayMask: byDayMask]; } else { if (rp->RecurFrequency == RecurFrequency_Monthly) { [rule setFrequency: iCalRecurrenceFrequenceMonthly]; [rule setRepeatInterval: rp->Period]; } else if (rp->RecurFrequency == RecurFrequency_Yearly) { [rule setFrequency: iCalRecurrenceFrequenceYearly]; [rule setRepeatInterval: rp->Period / 12]; month = [NSString stringWithFormat: @"%d", [startDate monthOfYear]]; [rule setSingleValue: month forKey: @"bymonth"]; } else [self errorWithFormat: @"unhandled frequency case for Month pattern type: %d", rp->RecurFrequency]; if ((rp->PatternType & 3) == 3) { /* HjMonthNth and MonthNth */ if (rp->PatternTypeSpecific.MonthRecurrencePattern.WeekRecurrencePattern == 0x7f) { /* firsts or last day of month */ if (rp->PatternTypeSpecific.MonthRecurrencePattern.N == RecurrenceN_Last) monthDay = @"-1"; else monthDay = [NSString stringWithFormat: @"%d", rp->PatternTypeSpecific.MonthRecurrencePattern.N]; [rule setSingleValue: monthDay forKey: @"bymonthday"]; } else if ((rp->PatternTypeSpecific.MonthRecurrencePattern.WeekRecurrencePattern == 0x3e) /* Nth week day */ || (rp->PatternTypeSpecific.MonthRecurrencePattern.WeekRecurrencePattern == 0x41)) /* Nth week-end day */ { for (count = 0; count < 7; count++) { maskValue = 1 << count; if ((rp->PatternTypeSpecific.MonthRecurrencePattern.WeekRecurrencePattern & maskValue)) dayMaskDays[count] = iCalWeekOccurrenceAll; } byDayMask = [iCalByDayMask byDayMaskWithDays: dayMaskDays]; [rule setByDayMask: byDayMask]; if (rp->PatternTypeSpecific.MonthRecurrencePattern.N == RecurrenceN_Last) bySetPos = -1; else bySetPos = rp->PatternTypeSpecific.MonthRecurrencePattern.N; [rule setSingleValue: [NSString stringWithFormat: @"%d", bySetPos] forKey: @"bysetpos"]; } else { if (rp->PatternTypeSpecific.MonthRecurrencePattern.N < RecurrenceN_Last) weekOccurrence = (1 << (rp->PatternTypeSpecific.MonthRecurrencePattern.N - 1)); else weekOccurrence = iCalWeekOccurrenceLast; for (count = 0; count < 7; count++) { maskValue = 1 << count; if ((rp->PatternTypeSpecific.MonthRecurrencePattern.WeekRecurrencePattern & maskValue)) dayMaskDays[count] = weekOccurrence; } byDayMask = [iCalByDayMask byDayMaskWithDays: dayMaskDays]; [rule setByDayMask: byDayMask]; } } else if ((rp->PatternType & 2) == 2 || (rp->PatternType & 4) == 4) { /* MonthEnd, HjMonth and HjMonthEnd */ [rule setSingleValue: [NSString stringWithFormat: @"%d", rp->PatternTypeSpecific.Day] forKey: @"bymonthday"]; } else [self errorWithFormat: @"invalid value for PatternType: %.4x", rp->PatternType]; } switch (rp->EndType) { case END_NEVER_END: case NEVER_END: break; case END_AFTER_N_OCCURRENCES: [rule setRepeatCount: rp->OccurrenceCount]; break; case END_AFTER_DATE: olEndDate = [NSCalendarDate dateFromMinutesSince1601: rp->EndDate]; untilDate = [NSCalendarDate dateWithYear: [olEndDate yearOfCommonEra] month: [olEndDate monthOfYear] day: [olEndDate dayOfMonth] hour: [startDate hourOfDay] minute: [startDate minuteOfHour] second: [startDate secondOfMinute] timeZone: [startDate timeZone]]; [rule setUntilDate: untilDate]; break; default: [self errorWithFormat: @"invalid value for EndType: %.4x", rp->EndType]; } /* exception dates: - take all deleted instances - remove all modified instances available in ExceptionInfo from the above set - add remaining instances, in chronological order */ /* Heuristic to avoid these loops */ if (rp->DeletedInstanceCount != rp->ModifiedInstanceCount) { exceptionDates = [NSMutableSet set]; for (count = 0; count < rp->DeletedInstanceCount; count++) { exDate = [NSDate dateFromMinutesSince1601: rp->DeletedInstanceDates[count]]; exDate = [exDate hour: [startDate hourOfDay] minute: [startDate minuteOfHour] second: [startDate secondOfMinute]]; [exceptionDates addObject: exDate]; } /* Read the exceptions to remove the instances that are modified and not deleted */ if (exInfos && exInfoCount > 0) { for (count = 0; count < exInfoCount; count++) { /* The OriginalStartDate is in local time */ exDate = [NSDate dateFromMinutesSince1601: exInfos[count].OriginalStartDate]; exDate = [exDate dateByAddingYears: 0 months: 0 days: 0 hours: 0 minutes: 0 seconds: - [tz secondsFromGMT]]; [exceptionDates removeObject: exDate]; } } realExDates = [[exceptionDates allObjects] sortedArrayUsingSelector: @selector (compare:)]; max = [realExDates count]; for (count = 0; count < max; count++) [entity addToExceptionDates: [realExDates objectAtIndex: count]]; } } @end @implementation iCalRecurrenceRule (MAPIStoreRecurrence) - (void) fillRecurrencePattern: (struct RecurrencePattern *) rp withEvent: (iCalEvent *) event inTimeZone: (NSTimeZone *) timeZone inMemCtx: (TALLOC_CTX *) memCtx { iCalRecurrenceFrequency freq; iCalByDayMask *byDayMask; NSString *byMonthDay, *bySetPos; NSCalendarDate *startDate, *endDate, *untilDate, *beginOfWeek, *minimumDate, *moduloDate, *midnight; iCalWeekOccurrences *days; NSInteger dayOfWeek, repeatInterval, repeatCount, count, firstOccurrence, max; uint32_t nbrMonths, mask; NSArray *events; NSMutableArray *deletedDates, *modifiedDates; startDate = [event firstRecurrenceStartDate]; [startDate setTimeZone: timeZone]; endDate = [event lastPossibleRecurrenceStartDate]; [endDate setTimeZone: timeZone]; rp->ReaderVersion = 0x3004; rp->WriterVersion = 0x3004; rp->StartDate = [[startDate beginOfDay] asMinutesSince1601]; untilDate = [self untilDate]; if (untilDate) { rp->EndDate = [untilDate asMinutesSince1601]; rp->EndType = END_AFTER_DATE; } else { repeatCount = [self repeatCount]; if (repeatCount > 0) { rp->EndDate = [endDate asMinutesSince1601]; rp->OccurrenceCount = repeatCount; rp->EndType = END_AFTER_N_OCCURRENCES; } else { rp->EndDate = 0x5ae980df; rp->OccurrenceCount = 0xa; rp->EndType = END_NEVER_END; } } freq = [self frequency]; repeatInterval = [self repeatInterval]; if (freq == iCalRecurrenceFrequenceDaily) { rp->RecurFrequency = RecurFrequency_Daily; rp->PatternType = PatternType_Day; rp->Period = repeatInterval * SOGoMinutesPerDay; rp->FirstDateTime = rp->StartDate % rp->Period; } else if (freq == iCalRecurrenceFrequenceWeekly) { rp->RecurFrequency = RecurFrequency_Weekly; rp->PatternType = PatternType_Week; rp->Period = repeatInterval; dayOfWeek = [startDate dayOfWeek]; mask = 0; byDayMask = [self byDayMask]; if (byDayMask) { for (count = 0; count < 7; count++) if ([byDayMask occursOnDay: count]) mask |= 1 << count; } else { /* Set the recurrence pattern using start date */ mask |= 1 << dayOfWeek; } rp->PatternTypeSpecific.WeekRecurrencePattern = mask; /* FirstDateTime */ if (dayOfWeek) beginOfWeek = [startDate dateByAddingYears: 0 months: 0 days: -dayOfWeek hours: 0 minutes: 0 seconds: 0]; else beginOfWeek = startDate; rp->FirstDateTime = ([[beginOfWeek beginOfDay] asMinutesSince1601] % (repeatInterval * 10080)); } else { if (freq == iCalRecurrenceFrequenceMonthly) { rp->RecurFrequency = RecurFrequency_Monthly; rp->Period = repeatInterval; } else if (freq == iCalRecurrenceFrequenceYearly) { rp->RecurFrequency = RecurFrequency_Yearly; rp->Period = 12; if (repeatInterval != 1) [self errorWithFormat: @"yearly interval '%d' cannot be converted", repeatInterval]; } else [self errorWithFormat: @"frequency '%d' cannot be converted", freq]; /* FirstDateTime */ midnight = [[startDate firstDayOfMonth] beginOfDay]; minimumDate = [NSCalendarDate dateFromMinutesSince1601: 0]; nbrMonths = (([midnight yearOfCommonEra] - [minimumDate yearOfCommonEra]) * 12 + [midnight monthOfYear] - 1); moduloDate = [minimumDate dateByAddingYears: 0 months: (nbrMonths % rp->Period) days: 0 hours: 0 minutes: 0 seconds: 0]; rp->FirstDateTime = [moduloDate asMinutesSince1601]; byMonthDay = [[self byMonthDay] objectAtIndex: 0]; if (!byMonthDay && (freq == iCalRecurrenceFrequenceYearly)) { byMonthDay = [NSString stringWithFormat: @"%d", [startDate dayOfMonth]]; [self warnWithFormat: @"no month day specified in yearly" @" recurrence: we deduce it from the start date: %@", byMonthDay]; } if (byMonthDay) { if ([byMonthDay intValue] < 0) { /* This means we cannot handle values of BYMONTHDAY that are < -7. */ rp->PatternType = PatternType_MonthNth; rp->PatternTypeSpecific.MonthRecurrencePattern.WeekRecurrencePattern = 0x7f; rp->PatternTypeSpecific.MonthRecurrencePattern.N = RecurrenceN_Last; } else { rp->PatternType = PatternType_Month; rp->PatternTypeSpecific.Day = [byMonthDay intValue]; } } else { rp->PatternType = PatternType_MonthNth; byDayMask = [self byDayMask]; mask = 0; days = [byDayMask weekDayOccurrences]; if (days) { for (count = 0; count < 7; count++) if (days[0][count]) mask |= 1 << count; } if (mask) { rp->PatternTypeSpecific.MonthRecurrencePattern.WeekRecurrencePattern = mask; bySetPos = [self flattenedValuesForKey: @"bysetpos"]; if ([bySetPos length]) rp->PatternTypeSpecific.MonthRecurrencePattern.N = ([bySetPos hasPrefix: @"-"] ? RecurrenceN_Last : [bySetPos intValue]); else { firstOccurrence = [byDayMask firstOccurrence]; if (firstOccurrence) rp->PatternTypeSpecific.MonthRecurrencePattern.N = ((firstOccurrence > -1) ? firstOccurrence : RecurrenceN_Last); } } else [self errorWithFormat: @"rule for an event that never occurs"]; } } events = [[event parent] events]; max = [events count]; modifiedDates = [NSMutableArray arrayWithCapacity: max]; for (count = 1; count < max; count++) { startDate = [[events objectAtIndex: count] recurrenceId]; if (startDate) [modifiedDates addObject: startDate]; else [self errorWithFormat: @"missing recurrence-id for event %d", count]; } max = [modifiedDates count]; rp->ModifiedInstanceCount = max; rp->ModifiedInstanceDates = talloc_array (memCtx, uint32_t, max); for (count = 0; count < max; count++) { startDate = [[modifiedDates objectAtIndex: count] hour: 0 minute: 0 second: 0]; *(rp->ModifiedInstanceDates + count) = [startDate asMinutesSince1601]; } deletedDates = [modifiedDates mutableCopy]; [deletedDates autorelease]; [deletedDates addObjectsFromArray: [event exceptionDatesWithTimeZone: utcTZ]]; [deletedDates sortUsingFunction: NSDateCompare context: NULL]; max = [deletedDates count]; rp->DeletedInstanceCount = max; rp->DeletedInstanceDates = talloc_array (memCtx, uint32_t, max); for (count = 0; count < max; count++) { startDate = [[deletedDates objectAtIndex: count] hour: 0 minute: 0 second: 0]; *(rp->DeletedInstanceDates + count) = [startDate asMinutesSince1601]; } } @end