Skip to content

Commit df3dc59

Browse files
authored
Merge pull request #2561 from microsoft/QuickCalLogExceptions
Change Quick Exceptions to be the default
2 parents 13873bf + 4fd28a4 commit df3dc59

6 files changed

Lines changed: 333 additions & 109 deletions

Calendar/CalLogHelpers/CalLogCSVFunctions.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ function BuildCSV {
589589
if ($duplicateCount -gt 0) {
590590
Write-Host -ForegroundColor Yellow "Removed [$duplicateCount] duplicate Calendar Log entries before processing."
591591
}
592-
$script:GCDO = @($dedupedCalLogs)
592+
$script:GCDO = $dedupedCalLogs.ToArray()
593593
$script:MailboxList = @{}
594594
# Initialize lookup caches to avoid redundant CN resolution across hundreds of log entries
595595
$script:SMTPAddressCache = @{}

Calendar/CalLogHelpers/CalLogInfoFunctions.ps1

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,29 @@
44
<#
55
.SYNOPSIS
66
Checks if a set of Calendar Logs is from the Organizer.
7+
8+
Modern Sharing can replicate the Organizer's items into a Delegate's mailbox, so a
9+
positive ResponseType/ExternalSharingMasterId match is not sufficient on its own. When
10+
an Identity is supplied this function additionally validates that the From / Organizer
11+
on the qualifying row resolves to the user being analyzed.
712
#>
813
function SetIsOrganizer {
914
param(
10-
$CalLogs
15+
$CalLogs,
16+
[string] $Identity
1117
)
1218
[bool] $IsOrganizer = $false
1319

1420
foreach ($CalLog in $CalLogs) {
1521
if ($CalLog.ItemClass -eq "Ipm.Appointment" -and
1622
$CalLog.ExternalSharingMasterId -eq "NotFound" -and
1723
($CalLog.ResponseType -eq "1" -or $CalLog.ResponseType -eq "Organizer")) {
24+
25+
if (-not [string]::IsNullOrEmpty($Identity) -and -not (Test-CalLogFromMatchesIdentity -CalLog $CalLog -Identity $Identity)) {
26+
Write-Verbose "SetIsOrganizer: ResponseType is Organizer on this row but From does not resolve to [$Identity]. Likely a Modern Sharing copy; skipping."
27+
continue
28+
}
29+
1830
$IsOrganizer = $true
1931
Write-Host -ForegroundColor Green "IsOrganizer: [$IsOrganizer]"
2032
return $IsOrganizer
@@ -24,6 +36,203 @@ function SetIsOrganizer {
2436
return $IsOrganizer
2537
}
2638

39+
<#
40+
.SYNOPSIS
41+
Returns $true when the raw CalLog's From / Sender / SenderEmailAddress resolves to the
42+
supplied Identity (SMTP address). Used to confirm that an Organizer-typed row really
43+
belongs to the user being analyzed, and not a Modern-Sharing copy of someone else's data.
44+
#>
45+
function Test-CalLogFromMatchesIdentity {
46+
param(
47+
$CalLog,
48+
[string] $Identity
49+
)
50+
51+
if ([string]::IsNullOrEmpty($Identity)) {
52+
return $false
53+
}
54+
55+
$candidates = @()
56+
if ($null -ne $CalLog.From) { $candidates += [string]$CalLog.From }
57+
if ($null -ne $CalLog.Sender) { $candidates += [string]$CalLog.Sender }
58+
if ($null -ne $CalLog.SenderEmailAddress) { $candidates += [string]$CalLog.SenderEmailAddress }
59+
60+
$identityLocal = ($Identity -split '@')[0]
61+
62+
foreach ($candidate in $candidates) {
63+
if ([string]::IsNullOrEmpty($candidate)) { continue }
64+
65+
# Try the resolved SMTP first (requires MailboxList to be populated by BuildCSV).
66+
if ($null -ne $script:MailboxList -and $script:MailboxList.Count -gt 0) {
67+
$resolved = GetSMTPAddress $candidate
68+
if (-not [string]::IsNullOrEmpty($resolved) -and
69+
[string]::Equals($resolved.Trim(), $Identity.Trim(), [System.StringComparison]::OrdinalIgnoreCase)) {
70+
return $true
71+
}
72+
}
73+
74+
# Embedded SMTP form: '"Name" <user@contoso.com>'
75+
if ($candidate -match '<([^>]+@[^>]+)>') {
76+
if ([string]::Equals($Matches[1].Trim(), $Identity.Trim(), [System.StringComparison]::OrdinalIgnoreCase)) {
77+
return $true
78+
}
79+
}
80+
81+
# Direct SMTP match
82+
if ([string]::Equals($candidate.Trim(), $Identity.Trim(), [System.StringComparison]::OrdinalIgnoreCase)) {
83+
return $true
84+
}
85+
86+
# Last-resort: the CN often contains the SMTP local-part as a hint.
87+
if (-not [string]::IsNullOrEmpty($identityLocal) -and
88+
$candidate -match [regex]::Escape($identityLocal)) {
89+
return $true
90+
}
91+
}
92+
93+
return $false
94+
}
95+
96+
<#
97+
.SYNOPSIS
98+
Classifies the user being analyzed as a Delegate-of-Organizer, Delegate-of-Attendee,
99+
or Attendee based on the Enhanced CalLogs.
100+
101+
A user is a Delegate when their logs contain a Modern Sharing copy of another mailbox's
102+
calendar entries (SharedFolderName != 'Not Shared') AND they either (a) take changes on
103+
those shared rows themselves, or (b) receive Resp.* messages on behalf of the shared
104+
mailbox owner.
105+
106+
The DelegateForSmtp output is the SMTP of the mailbox the user is delegating for.
107+
When the meeting Organizer's SMTP matches DelegateForSmtp the user is a Delegate of the
108+
Organizer; otherwise the user is a Delegate of some Attendee.
109+
110+
This function must be called after BuildCSV has populated $script:EnhancedCalLogs and
111+
the SMTP lookup caches.
112+
#>
113+
function SetDelegateRole {
114+
param(
115+
[array] $EnhancedCalLogs,
116+
[string] $Identity
117+
)
118+
119+
$script:IsDelegateOfOrganizer = $false
120+
$script:IsDelegateOfAttendee = $false
121+
$script:DelegateForSmtp = $null
122+
123+
if ($null -eq $EnhancedCalLogs -or $EnhancedCalLogs.Count -eq 0) {
124+
return
125+
}
126+
127+
if ($script:IsOrganizer -or $script:IsRoomMB) {
128+
return
129+
}
130+
131+
[array] $sharedRows = $EnhancedCalLogs | Where-Object { $_.SharedFolderName -ne 'Not Shared' }
132+
if ($sharedRows.Count -eq 0) {
133+
Write-Verbose "SetDelegateRole: No Modern Sharing rows for [$Identity]."
134+
return
135+
}
136+
137+
# Determine whether the user takes action on the shared rows themselves.
138+
[array] $userChangesOnShared = $sharedRows | Where-Object {
139+
$_.TriggerAction -in @('Create', 'Update', 'SoftDelete', 'MoveToDeletedItems', 'HardDelete') -and
140+
(Test-ResponsibleUserMatchesIdentity -ResponsibleUser $_.ResponsibleUser -SenderSmtp $_.Sender -Identity $Identity)
141+
}
142+
143+
# Resp.* rows that arrived inside the shared folder are the "received all Resp's" signal.
144+
[array] $sharedRespRows = $sharedRows | Where-Object { $_.ItemClass -like 'Resp.*' }
145+
146+
if ($userChangesOnShared.Count -eq 0 -and $sharedRespRows.Count -eq 0) {
147+
Write-Verbose "SetDelegateRole: User [$Identity] has Modern Sharing rows but no delegate activity."
148+
return
149+
}
150+
151+
# Identify the mailbox the user is delegating for. ReceivedRepresenting is the strongest
152+
# signal because it's set by transport when mail is delivered on behalf of someone else.
153+
[array] $representingValues = $sharedRows |
154+
Where-Object {
155+
-not [string]::IsNullOrEmpty($_.ReceivedRepresenting) -and
156+
$_.ReceivedRepresenting -ne '-' -and
157+
$_.ReceivedRepresenting -ne 'NotFound'
158+
} |
159+
ForEach-Object { ([string]$_.ReceivedRepresenting).Trim() }
160+
161+
if ($representingValues.Count -gt 0) {
162+
$script:DelegateForSmtp = ($representingValues | Group-Object | Sort-Object Count -Descending | Select-Object -First 1).Name
163+
}
164+
165+
# Determine the meeting Organizer's SMTP (most common From across all appointment rows).
166+
$organizerSmtp = Get-MeetingOrganizerSmtp -EnhancedCalLogs $EnhancedCalLogs
167+
168+
# Fall back to the Organizer SMTP if ReceivedRepresenting was not informative.
169+
if ([string]::IsNullOrEmpty($script:DelegateForSmtp)) {
170+
$script:DelegateForSmtp = $organizerSmtp
171+
}
172+
173+
if (-not [string]::IsNullOrEmpty($organizerSmtp) -and
174+
-not [string]::IsNullOrEmpty($script:DelegateForSmtp) -and
175+
[string]::Equals($script:DelegateForSmtp, $organizerSmtp, [System.StringComparison]::OrdinalIgnoreCase)) {
176+
$script:IsDelegateOfOrganizer = $true
177+
Write-Host -ForegroundColor Magenta "IsDelegateOfOrganizer: [$Identity] is acting as a Delegate of the Organizer [$($script:DelegateForSmtp)]."
178+
} else {
179+
$script:IsDelegateOfAttendee = $true
180+
Write-Host -ForegroundColor Magenta "IsDelegateOfAttendee: [$Identity] is acting as a Delegate of the Attendee [$($script:DelegateForSmtp)]."
181+
}
182+
}
183+
184+
<#
185+
.SYNOPSIS
186+
Compares a ResponsibleUser / Sender SMTP value (from EnhancedCalLogs) to the Identity.
187+
#>
188+
function Test-ResponsibleUserMatchesIdentity {
189+
param(
190+
[string] $ResponsibleUser,
191+
[string] $SenderSmtp,
192+
[string] $Identity
193+
)
194+
195+
if ([string]::IsNullOrEmpty($Identity)) { return $false }
196+
197+
foreach ($value in @($ResponsibleUser, $SenderSmtp)) {
198+
if ([string]::IsNullOrEmpty($value) -or $value -eq '-' -or $value -eq 'NotFound') { continue }
199+
if ([string]::Equals($value.Trim(), $Identity.Trim(), [System.StringComparison]::OrdinalIgnoreCase)) {
200+
return $true
201+
}
202+
}
203+
return $false
204+
}
205+
206+
<#
207+
.SYNOPSIS
208+
Returns the most-common From SMTP across appointment rows in the Enhanced CalLogs, which
209+
is treated as the meeting Organizer's SMTP.
210+
#>
211+
function Get-MeetingOrganizerSmtp {
212+
param(
213+
[array] $EnhancedCalLogs
214+
)
215+
216+
if ($null -eq $EnhancedCalLogs -or $EnhancedCalLogs.Count -eq 0) {
217+
return $null
218+
}
219+
220+
[array] $appointmentFrom = $EnhancedCalLogs |
221+
Where-Object {
222+
($_.ItemClass -eq 'Ipm.Appointment' -or $_.ItemClass -like 'Exception*') -and
223+
-not [string]::IsNullOrEmpty($_.From) -and
224+
$_.From -ne '-' -and
225+
$_.From -ne 'NotFound'
226+
} |
227+
ForEach-Object { ([string]$_.From).Trim() }
228+
229+
if ($appointmentFrom.Count -eq 0) {
230+
return $null
231+
}
232+
233+
return ($appointmentFrom | Group-Object | Sort-Object Count -Descending | Select-Object -First 1).Name
234+
}
235+
27236
<#
28237
.SYNOPSIS
29238
Checks if a set of Calendar Logs is from a Resource Mailbox.

Calendar/CalLogHelpers/ExceptionCollectionFunctions.ps1

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ function Convert-AppointmentRecurrenceBlobToBytes {
8787
}
8888

8989
function Get-RecentExceptionCutoff {
90-
if ($FastExceptions.IsPresent -and -not $AllExceptions.IsPresent) {
91-
return (Get-Date).Date.AddMonths(-6)
90+
if (-not $ClassicExceptions.IsPresent -and -not $AllExceptions.IsPresent) {
91+
return (Get-Date).Date.AddMonths(-3)
9292
}
9393

9494
return $null
@@ -308,7 +308,7 @@ function CollectExceptionLogsLegacy {
308308
$ExceptionLogs = $ExceptionLogs | Where-Object { $_.ItemClass -notlike "IPM.Appointment*" }
309309
$cutoffDate = Get-RecentExceptionCutoff
310310
if ($null -ne $cutoffDate) {
311-
Write-Host -ForegroundColor Yellow "Filtering legacy Exception logs to only keep items with OriginalStartDate in the last 6 months."
311+
Write-Host -ForegroundColor Yellow "Filtering legacy Exception logs to only keep items with OriginalStartDate in the last 3 months."
312312
$ExceptionLogs = FilterExceptionLogsByRecency -ExceptionLogs $ExceptionLogs
313313
}
314314

@@ -335,8 +335,8 @@ function CollectExceptionLogsFast {
335335

336336
$cutoffDate = Get-RecentExceptionCutoff
337337
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."
338+
Write-Host -ForegroundColor Cyan "Keeping only Exception dates from the last 3 months: [$($cutoffDate.ToString('yyyy-MM-dd'))] and newer."
339+
Write-Host -ForegroundColor Cyan "[$($exceptionDates.Count)] Exception date(s) match the 3 month time frame."
340340
}
341341

342342
if ($exceptionDates.Count -eq 0) {
@@ -372,7 +372,7 @@ function CollectExceptionLogs {
372372
Write-Verbose "Meeting IsRecurring: $IsRecurring"
373373

374374
if ($IsRecurring) {
375-
if ($FastExceptions.IsPresent -and [string]::IsNullOrEmpty($ExceptionDate)) {
375+
if (-not $ClassicExceptions.IsPresent -and [string]::IsNullOrEmpty($ExceptionDate)) {
376376
try {
377377
CollectExceptionLogsFast -Identity $Identity -MeetingID $MeetingID
378378
} catch {

0 commit comments

Comments
 (0)