Skip to content

Commit d33bbe8

Browse files
committed
new Exceptions file
1 parent 57e5b92 commit d33bbe8

1 file changed

Lines changed: 391 additions & 0 deletions

File tree

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
function Read-UInt16LE {
5+
param(
6+
[byte[]]$Bytes,
7+
[ref]$Offset
8+
)
9+
10+
if (($Offset.Value + 2) -gt $Bytes.Length) {
11+
throw "Unexpected end of AppointmentRecurrenceBlob while reading UInt16 at offset [$($Offset.Value)]."
12+
}
13+
14+
$value = [BitConverter]::ToUInt16($Bytes, $Offset.Value)
15+
$Offset.Value += 2
16+
return $value
17+
}
18+
19+
function Read-UInt32LE {
20+
param(
21+
[byte[]]$Bytes,
22+
[ref]$Offset
23+
)
24+
25+
if (($Offset.Value + 4) -gt $Bytes.Length) {
26+
throw "Unexpected end of AppointmentRecurrenceBlob while reading UInt32 at offset [$($Offset.Value)]."
27+
}
28+
29+
$value = [BitConverter]::ToUInt32($Bytes, $Offset.Value)
30+
$Offset.Value += 4
31+
return $value
32+
}
33+
34+
function Convert-RecurMinutesToDateTime {
35+
param(
36+
[UInt32]$Minutes
37+
)
38+
39+
return ([datetime]'1601-01-01').AddMinutes([double]$Minutes)
40+
}
41+
42+
function Get-RecurrencePatternSpecificSize {
43+
param(
44+
[UInt16]$PatternType
45+
)
46+
47+
switch ($PatternType) {
48+
0 { return 0 }
49+
1 { return 4 }
50+
2 { return 4 }
51+
3 { return 8 }
52+
4 { return 4 }
53+
10 { return 4 }
54+
11 { return 8 }
55+
12 { return 4 }
56+
default {
57+
throw "Unsupported recurrence PatternType [$PatternType] in AppointmentRecurrenceBlob."
58+
}
59+
}
60+
}
61+
62+
function Convert-AppointmentRecurrenceBlobToBytes {
63+
param(
64+
$AppointmentRecurrenceBlob
65+
)
66+
67+
if ($AppointmentRecurrenceBlob -is [byte[]]) {
68+
return ,$AppointmentRecurrenceBlob
69+
}
70+
71+
$blobText = [string]$AppointmentRecurrenceBlob
72+
if ([string]::IsNullOrWhiteSpace($blobText)) {
73+
throw "AppointmentRecurrenceBlob is empty."
74+
}
75+
76+
$blobText = ($blobText -replace '\s+', '').Trim()
77+
if (($blobText.Length % 2) -ne 0) {
78+
throw "AppointmentRecurrenceBlob length is invalid. Expected an even-length hex string."
79+
}
80+
81+
$bytes = [byte[]]::new($blobText.Length / 2)
82+
for ($index = 0; $index -lt $blobText.Length; $index += 2) {
83+
$bytes[$index / 2] = [Convert]::ToByte($blobText.Substring($index, 2), 16)
84+
}
85+
86+
return ,$bytes
87+
}
88+
89+
function Get-RecentExceptionCutoff {
90+
if ($FastExceptions.IsPresent -and -not $AllExceptions.IsPresent) {
91+
return (Get-Date).Date.AddMonths(-6)
92+
}
93+
94+
return $null
95+
}
96+
97+
function Get-CalendarDiagnosticObjectDeduplicationKey {
98+
param(
99+
$CalLog
100+
)
101+
102+
$itemId = ""
103+
if ($null -ne $CalLog.ItemId) {
104+
if ($CalLog.ItemId.PSObject.Properties.Name -contains 'ObjectId') {
105+
$itemId = [string]$CalLog.ItemId.ObjectId
106+
} else {
107+
$itemId = [string]$CalLog.ItemId
108+
}
109+
}
110+
111+
$calendarLogRequestId = if ($null -ne $CalLog.CalendarLogRequestId) { [string]$CalLog.CalendarLogRequestId } else { "" }
112+
$logTimestamp = if ($null -ne $CalLog.LogTimestamp) { [string]$CalLog.LogTimestamp } else { "" }
113+
$originalStartDate = if ($null -ne $CalLog.OriginalStartDate) { [string]$CalLog.OriginalStartDate } else { "" }
114+
$triggerAction = if ($null -ne $CalLog.CalendarLogTriggerAction) { [string]$CalLog.CalendarLogTriggerAction } else { "" }
115+
$itemClass = if ($null -ne $CalLog.ItemClass) { [string]$CalLog.ItemClass } else { "" }
116+
$itemVersion = if ($null -ne $CalLog.ItemVersion) { [string]$CalLog.ItemVersion } else { "" }
117+
$logRowType = if ($null -ne $CalLog.LogRowType) { [string]$CalLog.LogRowType } else { "" }
118+
$responsibleUserName = if ($null -ne $CalLog.ResponsibleUserName) { [string]$CalLog.ResponsibleUserName } else { "" }
119+
$clientInfo = if ($null -ne $CalLog.LogClientInfoString) { [string]$CalLog.LogClientInfoString } else { "" }
120+
121+
return "$itemId|$calendarLogRequestId|$logTimestamp|$originalStartDate|$triggerAction|$itemClass|$itemVersion|$logRowType|$responsibleUserName|$clientInfo"
122+
}
123+
124+
function Remove-DuplicateCalendarDiagnosticObjects {
125+
param(
126+
[array]$CalLogs,
127+
[switch]$Quiet
128+
)
129+
130+
if ($null -eq $CalLogs -or $CalLogs.Count -le 1) {
131+
return @($CalLogs)
132+
}
133+
134+
$uniqueLogs = @($CalLogs | Group-Object -Property { Get-CalendarDiagnosticObjectDeduplicationKey -CalLog $_ } | ForEach-Object {
135+
$_.Group | Select-Object -First 1
136+
} | Sort-Object { ConvertDateTime($_.LogTimestamp.ToString()) })
137+
138+
$duplicateCount = $CalLogs.Count - $uniqueLogs.Count
139+
if (($duplicateCount -gt 0) -and (-not $Quiet.IsPresent)) {
140+
Write-Host -ForegroundColor Yellow "Removed [$duplicateCount] duplicate Calendar Log entries before processing."
141+
}
142+
143+
return $uniqueLogs
144+
}
145+
146+
function Merge-CalendarDiagnosticObjects {
147+
param(
148+
[array]$BaseLogs,
149+
[array]$AdditionalLogs
150+
)
151+
152+
$combinedLogs = @($BaseLogs) + @($AdditionalLogs)
153+
return Remove-DuplicateCalendarDiagnosticObjects -CalLogs $combinedLogs -Quiet
154+
}
155+
156+
function Filter-ExceptionLogsByRecency {
157+
param(
158+
[array]$ExceptionLogs
159+
)
160+
161+
$cutoffDate = Get-RecentExceptionCutoff
162+
if ($null -eq $cutoffDate) {
163+
return @($ExceptionLogs)
164+
}
165+
166+
return @($ExceptionLogs | Where-Object {
167+
if ($null -eq $_.OriginalStartDate -or [string]::IsNullOrEmpty([string]$_.OriginalStartDate) -or $_.OriginalStartDate -eq "NotFound") {
168+
return $false
169+
}
170+
171+
$originalStartDate = ConvertDateTime($_.OriginalStartDate.ToString())
172+
return ($originalStartDate -is [datetime]) -and $originalStartDate -ne [datetime]::MinValue -and $originalStartDate -ge $cutoffDate
173+
})
174+
}
175+
176+
function Get-AppointmentExceptionDatesFromBlob {
177+
param(
178+
$AppointmentRecurrenceBlob
179+
)
180+
181+
[byte[]]$bytes = Convert-AppointmentRecurrenceBlobToBytes -AppointmentRecurrenceBlob $AppointmentRecurrenceBlob
182+
$offset = [ref]0
183+
184+
$readerVersion = Read-UInt16LE -Bytes $bytes -Offset $offset
185+
$writerVersion = Read-UInt16LE -Bytes $bytes -Offset $offset
186+
$null = Read-UInt16LE -Bytes $bytes -Offset $offset
187+
$patternType = Read-UInt16LE -Bytes $bytes -Offset $offset
188+
$null = Read-UInt16LE -Bytes $bytes -Offset $offset
189+
$null = Read-UInt32LE -Bytes $bytes -Offset $offset
190+
$null = Read-UInt32LE -Bytes $bytes -Offset $offset
191+
$null = Read-UInt32LE -Bytes $bytes -Offset $offset
192+
193+
$patternSpecificSize = Get-RecurrencePatternSpecificSize -PatternType $patternType
194+
if (($offset.Value + $patternSpecificSize) -gt $bytes.Length) {
195+
throw "AppointmentRecurrenceBlob ended before PatternTypeSpecific data was fully read."
196+
}
197+
$offset.Value += $patternSpecificSize
198+
199+
$null = Read-UInt32LE -Bytes $bytes -Offset $offset
200+
$null = Read-UInt32LE -Bytes $bytes -Offset $offset
201+
$null = Read-UInt32LE -Bytes $bytes -Offset $offset
202+
203+
$deletedInstanceCount = Read-UInt32LE -Bytes $bytes -Offset $offset
204+
$deletedInstanceDates = [System.Collections.Generic.List[datetime]]::new()
205+
for ($index = 0; $index -lt $deletedInstanceCount; $index++) {
206+
$deletedInstanceDates.Add((Convert-RecurMinutesToDateTime -Minutes (Read-UInt32LE -Bytes $bytes -Offset $offset)).Date)
207+
}
208+
209+
$modifiedInstanceCount = Read-UInt32LE -Bytes $bytes -Offset $offset
210+
$modifiedInstanceDates = [System.Collections.Generic.List[datetime]]::new()
211+
for ($index = 0; $index -lt $modifiedInstanceCount; $index++) {
212+
$modifiedInstanceDates.Add((Convert-RecurMinutesToDateTime -Minutes (Read-UInt32LE -Bytes $bytes -Offset $offset)).Date)
213+
}
214+
215+
$seriesStartDate = Convert-RecurMinutesToDateTime -Minutes (Read-UInt32LE -Bytes $bytes -Offset $offset)
216+
$seriesEndDate = Convert-RecurMinutesToDateTime -Minutes (Read-UInt32LE -Bytes $bytes -Offset $offset)
217+
$readerVersion2 = Read-UInt32LE -Bytes $bytes -Offset $offset
218+
$writerVersion2 = Read-UInt32LE -Bytes $bytes -Offset $offset
219+
$null = Read-UInt32LE -Bytes $bytes -Offset $offset
220+
$null = Read-UInt32LE -Bytes $bytes -Offset $offset
221+
$exceptionCount = Read-UInt16LE -Bytes $bytes -Offset $offset
222+
223+
if ($readerVersion -ne 0x3004 -or $writerVersion -ne 0x3004) {
224+
throw "Unexpected recurrence blob header versions [$readerVersion/$writerVersion]."
225+
}
226+
if ($readerVersion2 -lt 0x3006 -or $writerVersion2 -lt 0x3006) {
227+
throw "Unexpected recurrence blob secondary versions [$readerVersion2/$writerVersion2]."
228+
}
229+
if ($modifiedInstanceCount -lt $exceptionCount) {
230+
throw "ModifiedInstanceCount [$modifiedInstanceCount] is less than ExceptionCount [$exceptionCount]."
231+
}
232+
233+
return [PSCustomObject]@{
234+
DeletedInstanceCount = $deletedInstanceCount
235+
ModifiedInstanceCount = $modifiedInstanceCount
236+
ExceptionCount = $exceptionCount
237+
DeletedInstanceDates = @($deletedInstanceDates)
238+
ModifiedInstanceDates = @($modifiedInstanceDates)
239+
Dates = @($deletedInstanceDates + $modifiedInstanceDates | Sort-Object -Unique)
240+
SeriesStartDate = $seriesStartDate
241+
SeriesEndDate = $seriesEndDate
242+
PatternType = $patternType
243+
}
244+
}
245+
246+
function Get-ExceptionDatesFromMostRecentAppointmentBlob {
247+
param(
248+
[array]$CalLogs
249+
)
250+
251+
$recurringAppointments = @($CalLogs | Where-Object {
252+
$_.ItemClass -eq 'IPM.Appointment' -and
253+
$_.AppointmentRecurring -and
254+
$null -ne $_.AppointmentRecurrenceBlob -and
255+
-not [string]::IsNullOrWhiteSpace([string]$_.AppointmentRecurrenceBlob)
256+
})
257+
258+
if ($recurringAppointments.Count -eq 0) {
259+
throw "No recurring IPM.Appointment with an AppointmentRecurrenceBlob was found in the Calendar Logs."
260+
}
261+
262+
$blobSource = $recurringAppointments | Where-Object { $_.CalendarItemType.ToString() -eq 'RecurringMaster' } | Sort-Object @{ Expression = { $itemVersion = 0; [void][int]::TryParse([string]$_.ItemVersion, [ref]$itemVersion); $itemVersion } }, @{ Expression = { ConvertDateTime($_.LogTimestamp.ToString()) } } -Descending | Select-Object -First 1
263+
if ($null -eq $blobSource) {
264+
$blobSource = $recurringAppointments | Sort-Object @{ Expression = { $itemVersion = 0; [void][int]::TryParse([string]$_.ItemVersion, [ref]$itemVersion); $itemVersion } }, @{ Expression = { ConvertDateTime($_.LogTimestamp.ToString()) } } -Descending | Select-Object -First 1
265+
}
266+
267+
$parsedBlob = Get-AppointmentExceptionDatesFromBlob -AppointmentRecurrenceBlob $blobSource.AppointmentRecurrenceBlob
268+
$exceptionDates = @($parsedBlob.Dates)
269+
270+
$cutoffDate = Get-RecentExceptionCutoff
271+
if ($null -ne $cutoffDate) {
272+
$exceptionDates = @($exceptionDates | Where-Object { $_ -ge $cutoffDate })
273+
}
274+
275+
return [PSCustomObject]@{
276+
SourceLog = $blobSource
277+
ParsedBlob = $parsedBlob
278+
ExceptionDates = $exceptionDates
279+
}
280+
}
281+
282+
function Collect-ExceptionLogsLegacy {
283+
param(
284+
[string]$Identity,
285+
[string]$MeetingID,
286+
[switch]$UsedAsFallback
287+
)
288+
289+
$ExceptionLogs = @()
290+
$LogToExamine = @()
291+
$LogToExamine = $script:GCDO | Where-Object { $_.ItemClass -like 'IPM.Appointment*' } | Sort-Object ItemVersion
292+
293+
Write-Host -ForegroundColor Cyan "Found $($LogToExamine.count) CalLogs to examine for Exception Logs."
294+
if ($LogToExamine.count -gt 100) {
295+
Write-Host -ForegroundColor Cyan "`t This is a large number of logs to examine, this may take a while."
296+
}
297+
$logLeftCount = $LogToExamine.count
298+
299+
$ExceptionLogs = $LogToExamine | ForEach-Object {
300+
$logLeftCount -= 1
301+
Write-Verbose "Getting Exception Logs for [$($_.ItemId.ObjectId)]"
302+
Get-CalendarDiagnosticObjects -Identity $Identity -ItemIds $_.ItemId.ObjectId -ShouldFetchRecurrenceExceptions $true -CustomPropertyNames $CustomPropertyNameList -ShouldBindToItem $true 3>$null
303+
if (($logLeftCount % 10 -eq 0) -and ($logLeftCount -gt 0)) {
304+
Write-Host -ForegroundColor Cyan "`t [$($logLeftCount)] logs left to examine..."
305+
}
306+
}
307+
308+
$ExceptionLogs = $ExceptionLogs | Where-Object { $_.ItemClass -notlike "IPM.Appointment*" }
309+
$cutoffDate = Get-RecentExceptionCutoff
310+
if ($null -ne $cutoffDate) {
311+
Write-Host -ForegroundColor Yellow "Filtering legacy Exception logs to only keep items with OriginalStartDate in the last 6 months."
312+
$ExceptionLogs = Filter-ExceptionLogsByRecency -ExceptionLogs $ExceptionLogs
313+
}
314+
315+
Write-Host -ForegroundColor Cyan "Found $($ExceptionLogs.count) Exception Logs, adding them into the CalLogs."
316+
$script:GCDO = Merge-CalendarDiagnosticObjects -BaseLogs $script:GCDO -AdditionalLogs $ExceptionLogs
317+
if ($UsedAsFallback.IsPresent) {
318+
$script:ExceptionCollectionStatus = "CollectedLegacyFallback"
319+
} else {
320+
$script:ExceptionCollectionStatus = "Collected"
321+
}
322+
}
323+
324+
function Collect-ExceptionLogsFast {
325+
param(
326+
[string]$Identity,
327+
[string]$MeetingID
328+
)
329+
330+
$blobResult = Get-ExceptionDatesFromMostRecentAppointmentBlob -CalLogs $script:GCDO
331+
$exceptionDates = @($blobResult.ExceptionDates | Sort-Object -Unique)
332+
333+
Write-Host -ForegroundColor Cyan "Fast exception collection selected blob from ItemVersion [$($blobResult.SourceLog.ItemVersion)] with [$($blobResult.ParsedBlob.ExceptionCount)] exception entries."
334+
Write-Host -ForegroundColor Cyan "Found [$($blobResult.ParsedBlob.DeletedInstanceCount)] deleted exception dates and [$($blobResult.ParsedBlob.ModifiedInstanceCount)] modified exception dates in the recurrence blob."
335+
336+
$cutoffDate = Get-RecentExceptionCutoff
337+
if ($null -ne $cutoffDate) {
338+
Write-Host -ForegroundColor Cyan "Keeping only Exception dates from the last 6 months: [$($cutoffDate.ToString('yyyy-MM-dd'))] and newer."
339+
Write-Host -ForegroundColor Cyan "[$($exceptionDates.Count)] Exception date(s) match the 6 month time frame."
340+
}
341+
342+
if ($exceptionDates.Count -eq 0) {
343+
$script:ExceptionCollectionStatus = "NoExceptionDates"
344+
Write-Host -ForegroundColor Cyan "No matching Exception dates were found in the AppointmentRecurrenceBlob."
345+
return
346+
}
347+
348+
$collectedExceptionLogs = @()
349+
foreach ($exceptionOriginalStartDate in $exceptionDates) {
350+
$collectedExceptionLogs += GetCalendarDiagnosticObjects -Identity $Identity -MeetingID $MeetingID -ExceptionDateOverride $exceptionOriginalStartDate
351+
}
352+
353+
$collectedExceptionLogs = @($collectedExceptionLogs | Where-Object {
354+
$_.ItemClass -notlike "IPM.Appointment*" -or
355+
($null -ne $_.OriginalStartDate -and $_.OriginalStartDate -ne "NotFound" -and -not [string]::IsNullOrEmpty([string]$_.OriginalStartDate))
356+
})
357+
358+
Write-Host -ForegroundColor Cyan "Collected $($collectedExceptionLogs.count) Exception-related logs from [$($exceptionDates.count)] ExceptionDate queries."
359+
$script:GCDO = Merge-CalendarDiagnosticObjects -BaseLogs $script:GCDO -AdditionalLogs $collectedExceptionLogs
360+
$script:ExceptionCollectionStatus = "CollectedFast"
361+
}
362+
363+
function Collect-ExceptionLogs {
364+
param(
365+
[string]$Identity,
366+
[string]$MeetingID
367+
)
368+
369+
Write-Verbose "Looking for Exception Logs..."
370+
$IsRecurring = SetIsRecurring -CalLogs $script:GCDO
371+
Write-Verbose "Meeting IsRecurring: $IsRecurring"
372+
373+
if ($IsRecurring) {
374+
if ($FastExceptions.IsPresent -and [string]::IsNullOrEmpty($ExceptionDate)) {
375+
try {
376+
Collect-ExceptionLogsFast -Identity $Identity -MeetingID $MeetingID
377+
} catch {
378+
$script:ExceptionCollectionStatus = "CollectedLegacyFallback"
379+
Write-DashLineBoxColor "FAST EXCEPTION COLLECTION FAILED",
380+
"Error: $($_.Exception.Message)",
381+
"Falling back to the legacy per-appointment Exception collector." -Color Red -DashChar "="
382+
Collect-ExceptionLogsLegacy -Identity $Identity -MeetingID $MeetingID -UsedAsFallback
383+
}
384+
} else {
385+
Collect-ExceptionLogsLegacy -Identity $Identity -MeetingID $MeetingID
386+
}
387+
} else {
388+
$script:ExceptionCollectionStatus = "NotRecurring"
389+
Write-Host -ForegroundColor Cyan "No Recurring Meetings found, no Exception Logs to collect."
390+
}
391+
}

0 commit comments

Comments
 (0)