sogo/OpenChange/MAPIStoreRecurrenceUtils.m
Enrique J. Hernández Blasco 6f44ec42c1 oc-calendar: Fix import in SOGo exceptions from recurring series
According to [MS-OXICAL] Section 2.1.3.1.1.20.13, the EXDATE property
must be written only if there are ocurrences from the series that have
been deleted and before this commit ModifiedInstanceDates were also
included.

We check against every ExceptionInfo from exception ocurrences of the series
to know if the ocurrence was deleted or only modified.
2015-03-18 15:08:24 +01:00

476 lines
17 KiB
Objective-C

/* MAPIStoreRecurrenceUtils.m - this file is part of SOGo
*
* Copyright (C) 2011-2012 Inverse inc
*
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
*
* 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 <Foundation/NSArray.h>
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSSet.h>
#import <Foundation/NSString.h>
#import <Foundation/NSTimeZone.h>
#import <NGExtensions/NSCalendarDate+misc.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGCards/iCalByDayMask.h>
#import <NGCards/iCalDateTime.h>
#import <NGCards/iCalEvent.h>
#import <NGCards/iCalRepeatableEntityObject.h>
#import <NGCards/iCalRecurrenceRule.h>
#import <NGCards/iCalTimeZone.h>
#import "NSDate+MAPIStore.h"
#import "MAPIStoreRecurrenceUtils.h"
#import "MAPIStoreTypes.h"
#include <stdbool.h>
#include <talloc.h>
#include <util/time.h>
#include <gen_ndr/property.h>
@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;
mask = 0;
byDayMask = [self byDayMask];
for (count = 0; count < 7; count++)
if ([byDayMask occursOnDay: count])
mask |= 1 << count;
rp->PatternTypeSpecific.WeekRecurrencePattern = mask;
/* FirstDateTime */
dayOfWeek = [startDate dayOfWeek];
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