Skip to content

Commit 232ba3a

Browse files
authored
Merge pull request #2528 from microsoft/BigAprilUpdate
Backlog of bugs
2 parents a0930bf + cff5881 commit 232ba3a

11 files changed

Lines changed: 895 additions & 214 deletions

Calendar/CalLogHelpers/CalLogCSVFunctions.ps1

Lines changed: 105 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,27 @@
44
# ===================================================================================================
55
# Constants to support the script
66
# ===================================================================================================
7+
# Non-US date formats for TryParseExact fallback (constant, created once)
8+
[string[]]$script:DateFormats = @(
9+
"d/M/yyyy h:mm:ss tt",
10+
"d/M/yyyy H:mm:ss",
11+
"d/M/yyyy H:mm",
12+
"d/M/yyyy h:mm tt",
13+
"dd/MM/yyyy h:mm:ss tt",
14+
"dd/MM/yyyy H:mm:ss",
15+
"dd/MM/yyyy HH:mm:ss",
16+
"dd/MM/yyyy H:mm",
17+
"dd/MM/yyyy h:mm tt",
18+
"d/M/yyyy",
19+
"dd/MM/yyyy"
20+
)
721

822
$script:CalendarItemTypes = @{
923
'IPM.Schedule.Meeting.Request.AttendeeListReplication' = "AttendeeList"
1024
'IPM.Schedule.Meeting.Canceled' = "Cancellation"
1125
'IPM.OLE.CLASS.{00061055-0000-0000-C000-000000000046}' = "Exception"
1226
'IPM.Schedule.Meeting.Notification.Forward' = "Forward.Notification"
1327
'IPM.Appointment' = "Ipm.Appointment"
14-
'IPM.Appointment.MP' = "Ipm.Appointment"
1528
'IPM.Schedule.Meeting.Request' = "Meeting.Request"
1629
'IPM.CalendarSharing.EventUpdate' = "SharingCFM"
1730
'IPM.CalendarSharing.EventDelete' = "SharingDelete"
@@ -22,6 +35,28 @@ $script:CalendarItemTypes = @{
2235
'(Occurrence Deleted)' = "Exception.Deleted"
2336
}
2437

38+
<#
39+
.SYNOPSIS
40+
Resolves the ItemClass to a friendly type name using the CalendarItemTypes table.
41+
If no exact match is found, progressively strips the last dot-segment and retries.
42+
e.g. IPM.Schedule.Meeting.Resp.Tent.Follow → IPM.Schedule.Meeting.Resp.Tent → "Resp.Tent"
43+
#>
44+
function GetItemType {
45+
param([string]$ItemClass)
46+
$lookup = $ItemClass
47+
while (-not [string]::IsNullOrEmpty($lookup)) {
48+
$type = $script:CalendarItemTypes[$lookup]
49+
if (-not [string]::IsNullOrEmpty($type)) {
50+
return $type
51+
}
52+
# Strip the last segment and try the parent
53+
$lastDot = $lookup.LastIndexOf('.')
54+
if ($lastDot -le 0) { break }
55+
$lookup = $lookup.Substring(0, $lastDot)
56+
}
57+
return $null
58+
}
59+
2560
# ===================================================================================================
2661
# Functions to support the script
2762
# ===================================================================================================
@@ -37,7 +72,7 @@ function MapSharedFolder {
3772
if ($ExternalMasterID -eq "NotFound") {
3873
return "Not Shared"
3974
} else {
40-
$SharedFolders[$ExternalMasterID]
75+
$script:SharedFolders[$ExternalMasterID]
4176
}
4277
}
4378

@@ -63,6 +98,7 @@ Creates a Mapping of ExternalMasterID to FolderName
6398
function CreateExternalMasterIDMap {
6499
# This function will create a Map of the log folder to ExternalMasterID
65100
$script:SharedFolders = [System.Collections.SortedList]::new()
101+
$unknownCount = 0
66102
Write-Verbose "Starting CreateExternalMasterIDMap"
67103

68104
foreach ($ExternalID in $script:GCDO.ExternalSharingMasterId | Select-Object -Unique) {
@@ -72,39 +108,55 @@ function CreateExternalMasterIDMap {
72108

73109
$AllFolderNames = @($script:GCDO | Where-Object { $_.ExternalSharingMasterId -eq $ExternalID } | Select-Object -ExpandProperty OriginalParentDisplayName | Select-Object -Unique)
74110

111+
# Default calendar folder names across the top localized versions of Exchange
112+
# cSpell:ignore Kalender Calendario Calendrier Calendário Календарь
113+
$DefaultCalendarNames = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
114+
[void]$DefaultCalendarNames.Add('Calendar') # English
115+
[void]$DefaultCalendarNames.Add('Kalender') # German, Dutch, Swedish, Norwegian, Danish
116+
[void]$DefaultCalendarNames.Add('Calendario') # Spanish, Italian
117+
[void]$DefaultCalendarNames.Add('Calendrier') # French
118+
[void]$DefaultCalendarNames.Add('Calendário') # Portuguese
119+
[void]$DefaultCalendarNames.Add('Календарь') # Russian
120+
121+
# Remove empty/null entries
122+
$AllFolderNames = @($AllFolderNames | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
123+
75124
if ($AllFolderNames.count -gt 1) {
76-
# We have 2+ FolderNames, Need to find the best one. Remove 'Calendar' from possible names
77-
$AllFolderNames = $AllFolderNames | Where-Object { $_ -notmatch 'Calendar' } # Need a better way to do this for other languages...
125+
# We have 2+ FolderNames, remove default calendar folder names (localized) by exact match
126+
$Filtered = $AllFolderNames | Where-Object { -not $DefaultCalendarNames.Contains($_) }
127+
if ($Filtered.Count -gt 0) {
128+
$AllFolderNames = @($Filtered)
129+
}
130+
# else keep the original list — all entries were default calendar names
78131
}
79132

80133
if ($AllFolderNames.Count -eq 0) {
81-
$SharedFolders[$ExternalID] = "UnknownSharedCalendarCopy"
82-
Write-Host -ForegroundColor red "Found Zero to map to."
83-
}
84-
85-
if ($AllFolderNames.Count -eq 1) {
86-
$SharedFolders[$ExternalID] = $AllFolderNames
87-
Write-Verbose "Found map: [$AllFolderNames] is for $ExternalID"
134+
$unknownCount++
135+
$script:SharedFolders[$ExternalID] = "UnknownSharedCalendarCopy$unknownCount"
136+
Write-Host -ForegroundColor red "Found Zero folder names to map to for ExternalID [$ExternalID]."
137+
} elseif ($AllFolderNames.Count -eq 1) {
138+
$script:SharedFolders[$ExternalID] = $AllFolderNames[0]
139+
Write-Verbose "Found map: [$($AllFolderNames[0])] is for $ExternalID"
88140
} else {
89141
# we still have multiple possible Folder Names, need to chose one or combine
90142
Write-Host -ForegroundColor Red "Unable to Get Exact Folder for $ExternalID"
91-
Write-Host -ForegroundColor Red "Found $($AllFolderNames.count) possible folders"
143+
Write-Host -ForegroundColor Red "Found $($AllFolderNames.count) possible folders: $($AllFolderNames -join ', ')"
92144

93145
if ($AllFolderNames.Count -eq 2) {
94-
$SharedFolders[$ExternalID] = $AllFolderNames[0] + $AllFolderNames[1]
146+
$script:SharedFolders[$ExternalID] = $AllFolderNames[0] + " + " + $AllFolderNames[1]
95147
} else {
96-
$SharedFolders[$ExternalID] = "UnknownSharedCalendarCopy"
148+
$unknownCount++
149+
$script:SharedFolders[$ExternalID] = "UnknownSharedCalendarCopy$unknownCount"
97150
}
98151
}
99152
}
100153

101154
Write-Host -ForegroundColor Green "Created the following Shared Calendar Mapping:"
102-
foreach ($Key in $SharedFolders.Keys) {
103-
Write-Host -ForegroundColor Green "$Key : $($SharedFolders[$Key])"
155+
foreach ($Key in $script:SharedFolders.Keys) {
156+
Write-Host -ForegroundColor Green "$Key : $($script:SharedFolders[$Key])"
104157
}
105-
# ToDo: Need to check for multiple ExternalSharingMasterId pointing to the same FolderName
106158
Write-Verbose "Created the following Mapping :"
107-
Write-Verbose $SharedFolders
159+
Write-Verbose $script:SharedFolders
108160
}
109161

110162
<#
@@ -130,18 +182,20 @@ Builds the CSV output from the Calendar Diagnostic Objects
130182
function BuildCSV {
131183

132184
Write-Host "Starting to Process Calendar Logs..."
133-
$GCDOResults = @()
134185
$script:MailboxList = @{}
186+
# Initialize lookup caches to avoid redundant CN resolution across hundreds of log entries
187+
$script:SMTPAddressCache = @{}
188+
$script:DisplayNameCache = @{}
135189
Write-Host "Creating Map of Mailboxes to CNs..."
136190
CreateExternalMasterIDMap
137191
ConvertCNtoSMTP
138192
FixCalendarItemType($script:GCDO)
139193

140194
Write-Host "Making Calendar Logs more readable..."
141195
$Index = 0
142-
foreach ($CalLog in $script:GCDO) {
196+
$GCDOResults = foreach ($CalLog in $script:GCDO) {
143197
$Index++
144-
$ItemType = $CalendarItemTypes.($CalLog.ItemClass)
198+
$ItemType = GetItemType $CalLog.ItemClass
145199

146200
# CleanNotFounds
147201
$PropsToClean = "FreeBusyStatus", "ClientIntent", "AppointmentSequenceNumber", "AppointmentLastSequenceNumber", "RecurrencePattern", "AppointmentAuxiliaryFlags", "EventEmailReminderTimer", "IsSeriesCancelled", "AppointmentCounterProposal", "MeetingRequestType", "SendMeetingMessagesDiagnostics", "AttendeeCollection"
@@ -152,8 +206,8 @@ function BuildCSV {
152206
}
153207
}
154208

155-
# Record one row
156-
$GCDOResults += [PSCustomObject]@{
209+
# Output one row (collected by the foreach assignment)
210+
[PSCustomObject]@{
157211
#'LogRow' = $Index
158212
'LogTimestamp' = ConvertDateTime($CalLog.LogTimestamp)
159213
'LogRowType' = $CalLog.LogRowType.ToString()
@@ -180,7 +234,7 @@ function BuildCSV {
180234
'CalendarItemType' = $CalLog.CalendarItemType.ToString()
181235
'RecurrencePattern' = $CalLog.RecurrencePattern
182236
'AppointmentAuxiliaryFlags' = $CalLog.AppointmentAuxiliaryFlags.ToString()
183-
'DisplayAttendeesAll' = $CalLog.DisplayAttendeesAll
237+
'DisplayAttendeesAll' = $(if ($CalLog.DisplayAttendeesAll -eq "NotFound") { "-" } else { $CalLog.DisplayAttendeesAll })
184238
'AttendeeCount' = GetAttendeeCount($CalLog.DisplayAttendeesAll)
185239
'AppointmentState' = $CalLog.AppointmentState.ToString()
186240
'ResponseType' = $CalLog.ResponseType.ToString()
@@ -189,10 +243,12 @@ function BuildCSV {
189243
'HasAttachment' = $CalLog.HasAttachment
190244
'IsCancelled' = $CalLog.IsCancelled
191245
'IsAllDayEvent' = $CalLog.IsAllDayEvent
246+
'Sensitivity' = $CalLog.Sensitivity
192247
'IsSeriesCancelled' = $CalLog.IsSeriesCancelled
193248
'SendMeetingMessagesDiagnostics' = $CalLog.SendMeetingMessagesDiagnostics
194249
'AttendeeCollection' = MultiLineFormat($CalLog.AttendeeCollection)
195250
'CalendarLogRequestId' = $CalLog.CalendarLogRequestId.ToString() # Move to front.../ Format in groups???
251+
'CleanGlobalObjectId' = $CalLog.CleanGlobalObjectId
196252
}
197253
}
198254
$script:EnhancedCalLogs = $GCDOResults
@@ -210,7 +266,31 @@ function ConvertDateTime {
210266
$DateTime -eq "NotFound") {
211267
return ""
212268
}
213-
return [DateTime]$DateTime
269+
270+
$InvariantCulture = [System.Globalization.CultureInfo]::InvariantCulture
271+
$DateStyles = [System.Globalization.DateTimeStyles]::None
272+
$Parsed = [DateTime]::MinValue
273+
274+
# Priority 1 & 2: InvariantCulture TryParse
275+
# Handles ISO 8601 and M/d/yyyy (the default EXO output format)
276+
if ([DateTime]::TryParse($DateTime, $InvariantCulture, $DateStyles, [ref]$Parsed)) {
277+
return $Parsed
278+
}
279+
280+
# Priority 3: Explicit non-US formats via TryParseExact
281+
# Reached when day > 12 makes InvariantCulture fail (e.g., "22/07/2024")
282+
if ([DateTime]::TryParseExact($DateTime, $script:DateFormats, $InvariantCulture, $DateStyles, [ref]$Parsed)) {
283+
return $Parsed
284+
}
285+
286+
# Priority 4: Current culture (last resort for unexpected formats)
287+
if ([DateTime]::TryParse($DateTime, [ref]$Parsed)) {
288+
return $Parsed
289+
}
290+
291+
# Priority 5: Return MinValue so sorting is not broken by mixed types
292+
Write-Warning "Unable to parse date: [$DateTime]"
293+
return [DateTime]::MinValue
214294
}
215295

216296
function GetAttendeeCount {

Calendar/CalLogHelpers/CalLogInfoFunctions.ps1

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,15 @@ function SetIsRoom {
3939
}
4040

4141
# Simple logic is if RBA is running on the MB, it is a Room MB, otherwise it is not.
42-
foreach ($CalLog in $CalLogs) {
43-
Write-Verbose "Checking if this is a Room Mailbox. [$($CalLog.ItemClass)] [$($CalLog.ExternalSharingMasterId)] [$($CalLog.LogClientInfoString)]"
44-
if ($CalLog.ItemClass -eq "IPM.Appointment" -and
45-
$CalLog.ExternalSharingMasterId -eq "NotFound" -and
46-
$CalLog.LogClientInfoString -like "*ResourceBookingAssistant*" ) {
47-
return $true
48-
}
42+
$rbaLog = $CalLogs | Where-Object {
43+
$_.ItemClass -eq "IPM.Appointment" -and
44+
$_.ExternalSharingMasterId -eq "NotFound" -and
45+
$_.LogClientInfoString -like "*ResourceBookingAssistant*"
46+
} | Select-Object -First 1
47+
48+
if ($null -ne $rbaLog) {
49+
Write-Host -ForegroundColor Green "Found Room Mailbox indicator: [$($rbaLog.LogClientInfoString)]"
50+
return $true
4951
}
5052
return $false
5153
}
@@ -62,7 +64,8 @@ function SetIsRecurring {
6264
[bool] $IsRecurring = $false
6365
# See if this is a recurring meeting
6466
foreach ($CalLog in $CalLogs) {
65-
if ($CalendarItemTypes.($CalLog.ItemClass) -eq "Ipm.Appointment" -and
67+
if ($null -ne $CalLog.ItemClass -and
68+
(GetItemType $CalLog.ItemClass) -eq "Ipm.Appointment" -and
6669
# Commenting this out will get all the updates for shared calendars, which is important with Delegates.
6770
# $CalLog.ExternalSharingMasterId -eq "NotFound" -and
6871
$CalLog.CalendarItemType.ToString() -eq "RecurringMaster") {
@@ -94,8 +97,8 @@ function CheckForBifurcation {
9497
return $IsBifurcated
9598
}
9699
}
97-
Write-Host -ForegroundColor Red "Did not find any Ipm.Appointments in the CalLogs. If this is the Organizer of the meeting, this could the the Outlook Bifurcation issue."
98-
Write-Host -ForegroundColor Yellow "`t This could be the Outlook Bifurcation issue, where Outlook saves to the Organizers Mailbox on one thread and send to the attendee via transport on another thread. If the save to Organizers mailbox failed, we get into the Bifurcated State, where the Organizer does not have the meeting but the Attendees do."
100+
Write-Host -ForegroundColor Red "Did not find any Ipm.Appointments in the CalLogs. If this is the Organizer of the meeting, this could be the Outlook Bifurcation issue."
101+
Write-Host -ForegroundColor Yellow "`t The Outlook Bifurcation issue is where Outlook saves to the Organizer's Mailbox on one thread and sends to the attendees via transport on another thread. If the save to the Organizer's mailbox failed, we get into the Bifurcated State, where the Organizer does not have the meeting but the Attendees do."
99102
Write-Host -ForegroundColor Yellow "`t See https://support.microsoft.com/en-us/office/meeting-request-is-missing-from-organizers-calendar-c13c47cd-18f9-4ef0-b9d0-d9e174912c4a"
100103
return $IsBifurcated
101104
}

Calendar/CalLogHelpers/CreateTimelineRow.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ function CreateTimelineRow {
7474
}
7575

7676
if ($CalLog.AppointmentCounterProposal -eq "True") {
77-
[array] $Output = "[$($CalLog.Organizer)] send a $($MeetingRespType) response message with a New Time Proposal: $($CalLog.StartTime) to $($CalLog.EndTime)"
77+
[array] $Output = "[$($CalLog.Organizer)] sent a $($MeetingRespType) response message with a New Time Proposal: $($CalLog.StartTime) to $($CalLog.EndTime)"
7878
} else {
7979
switch -Wildcard ($CalLog.TriggerAction) {
8080
"Update" { $Action = "Updated" }

0 commit comments

Comments
 (0)