Modules/MDOP2.Actions.psm1
|
Set-StrictMode -Version Latest # Remediation actions (app-only Graph move to inbox, archive, junkemail, deleteditems or recoverableitemsdeletions) and status refresh. # Soft delete only: nothing here hard deletes or purges. $script:WellKnownCache = @{} $script:HoldCache = @{} function Get-MDOP2WellKnownFolderId { param([Parameter(Mandatory)][string]$UserId, [Parameter(Mandatory)][string]$Name) $key = "$UserId|$Name" if (-not $script:WellKnownCache.ContainsKey($key)) { $uid = [uri]::EscapeDataString($UserId) $f = Invoke-MDOP2AppGraph -Method GET -Path "/users/$uid/mailFolders/$Name`?`$select=id" $script:WellKnownCache[$key] = [string]$f.id } $script:WellKnownCache[$key] } function Get-MDOP2WellKnownFolderMap { <# .SYNOPSIS Returns the IDs of well-known folders for several mailboxes as UserId -> folder name -> ID. Folders that aren't cached yet are read with Graph JSON batching; folders that can't be read are left out. #> [CmdletBinding()] param( [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$UserId, [Parameter(Mandatory)][string[]]$Name ) $users = @($UserId | Where-Object { $_ } | Select-Object -Unique) $Name = @($Name | Select-Object -Unique) $requests = [Collections.Generic.List[object]]::new() $keys = @{} foreach ($u in $users) { foreach ($n in $Name) { $key = "$u|$n" if ($script:WellKnownCache.ContainsKey($key)) { continue } $id = "w$($requests.Count)" $keys[$id] = $key $requests.Add(@{ Id = $id; Url = "/users/$([uri]::EscapeDataString($u))/mailFolders/$n`?`$select=id" }) } } if ($requests.Count) { $res = Invoke-MDOP2AppGraphBatch -Request $requests.ToArray() foreach ($id in $keys.get_Keys()) { $r = $res[$id] if ($r -and -not $r.Error -and $r.Body -and $r.Body.PSObject.Properties['id']) { $script:WellKnownCache[$keys[$id]] = [string]$r.Body.id } elseif ($r) { Write-Verbose "Well-known folder $($keys[$id]): $($r.Error)" } } } $map = @{} foreach ($u in $users) { $ids = @{} foreach ($n in $Name) { if ($script:WellKnownCache.ContainsKey("$u|$n")) { $ids[$n] = $script:WellKnownCache["$u|$n"] } } $map[$u] = $ids } $map } function Get-MDOP2MailboxHold { <# .SYNOPSIS Reports whether a mailbox is on hold. Holds don't block moves, but held mail stays recoverable after a soft delete. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Address) $key = $Address.ToLowerInvariant() if ($script:HoldCache.ContainsKey($key)) { return $script:HoldCache[$key] } $result = $null try { $m = Get-EXOMailbox -Identity $Address -PropertySets Hold -ErrorAction Stop $holds = [Collections.Generic.List[string]]::new() if ($m.LitigationHoldEnabled) { $holds.Add('Litigation Hold') } # @($null) counts as one item, so drop empty values before counting $inPlace = @($m.InPlaceHolds | Where-Object { $_ }).Count if ($inPlace) { $holds.Add("$inPlace in-place or retention hold(s)") } if ($m.ComplianceTagHoldApplied) { $holds.Add('Retention label hold') } if ($m.DelayHoldApplied) { $holds.Add('Delay hold') } $result = [pscustomobject]@{ OnHold = $holds.Count -gt 0; Detail = $holds -join ', ' } } catch { Write-Verbose "Hold lookup for $Address failed: $($_.Exception.Message)" } $script:HoldCache[$key] = $result $result } function Invoke-MDOP2MessageAction { <# .SYNOPSIS Runs an action plan from New-MDOP2ActionPlan. Dry run is on unless -DryRun:$false is passed. Every item is written to the audit log, including dry runs and skips. #> [CmdletBinding()] param( [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Plan, [bool]$DryRun = $true ) $i = 0 $activity = if ($DryRun) { 'Dry run' } else { 'Moving messages' } $total = [Math]::Max(@($Plan).Count, 1) foreach ($p in @($Plan)) { $i++ Write-Progress -Activity $activity -Status $p.Address -PercentComplete (100 * $i / $total) $result = [ordered]@{ InternetMessageId = $p.InternetMessageId Address = $p.Address UserId = $p.UserId Action = $p.Action DryRun = $DryRun FromFolder = $p.FolderName Result = $null NewMessageId = $null Hold = $null Error = $null } if ($p.Plan -ne 'Move') { $result.Result = 'Skipped' $result.Error = $p.Reason } else { try { $target = Get-MDOP2WellKnownFolderId -UserId $p.UserId -Name $p.Destination $ri = Get-MDOP2WellKnownFolderId -UserId $p.UserId -Name 'recoverableitemsdeletions' $decision = Get-MDOP2MoveDecision -CurrentFolderId $p.FolderId -TargetFolderId $target -RecoverableItemsFolderId $ri -Action $p.Action -AllowRestore:([bool]$p.AllowRestore) $hold = Get-MDOP2MailboxHold -Address $p.Address if ($hold -and $hold.OnHold) { $result.Hold = $hold.Detail } if (-not $decision.Move) { $result.Result = $decision.Result $result.Error = $decision.Reason } elseif ($DryRun) { $result.Result = 'WouldMove' } else { $uid = [uri]::EscapeDataString($p.UserId) $mid = [uri]::EscapeDataString($p.GraphMessageId) $moved = Invoke-MDOP2AppGraph -Method POST -Path "/users/$uid/messages/$mid/move" -Body @{ destinationId = $p.Destination } $result.Result = 'Moved' $result.NewMessageId = [string]$moved.id } } catch { $sc = [int]($_.Exception.Data['StatusCode'] ?? 0) $ec = [string]$_.Exception.Data['ErrorCode'] $result.Result = if ($sc -eq 404 -and $ec -eq 'ErrorItemNotFound') { 'NotFound' } else { 'Failed' } $result.Error = $_.Exception.Message } } Write-MDOP2Audit -Type Action -Data ([ordered]@{ internetMessageId = $result.InternetMessageId mailbox = $result.Address userId = $result.UserId action = $result.Action destination = $p.Destination dryRun = $DryRun fromFolder = $result.FromFolder result = $result.Result hold = $result.Hold error = $result.Error }) [pscustomobject]$result } Write-Progress -Activity $activity -Completed } function Get-MDOP2MessageStatus { <# .SYNOPSIS Re-checks where a message is now in each mailbox. Message IDs change on every move, so this always searches by Message-ID. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$InternetMessageId, [Parameter(Mandatory)][object[]]$Mailbox ) $found = @(Find-MDOP2MessageCopy -Lookup @($Mailbox | ForEach-Object { @{ UserId = $_.UserId; InternetMessageId = $InternetMessageId } })) for ($i = 0; $i -lt $Mailbox.Count; $i++) { $r = $found[$i] [pscustomobject]@{ InternetMessageId = $InternetMessageId Address = $Mailbox[$i].Address UserId = $Mailbox[$i].UserId State = $r.State Folders = @($r.Locations | ForEach-Object FolderName) Locations = $r.Locations Error = $r.Error } } } Export-ModuleMember -Function Get-MDOP2WellKnownFolderId, Get-MDOP2WellKnownFolderMap, Get-MDOP2MailboxHold, Invoke-MDOP2MessageAction, Get-MDOP2MessageStatus |