Private/New-IdleExchangeOnlineAdapter.ps1
|
function New-IdleExchangeOnlineAdapter { <# .SYNOPSIS Creates an internal adapter that wraps Exchange Online Management cmdlets. .DESCRIPTION This adapter provides a testable boundary between the provider and Exchange Online cmdlets. Unit tests can inject a fake adapter without requiring a real Exchange Online environment. The adapter wraps ExchangeOnlineManagement module cmdlets for maximum compatibility. .PARAMETER UseRestApi (Reserved for future use) Switch to indicate use of Graph API REST calls instead of cmdlets. #> [CmdletBinding()] param( [Parameter()] [switch] $UseRestApi ) $adapter = [pscustomobject]@{ PSTypeName = 'IdLE.ExchangeOnlineAdapter' UseRestApi = [bool]$UseRestApi } # Helper to safely invoke cmdlets with error handling $invokeSafely = { param( [Parameter(Mandatory)] [string] $CommandName, [Parameter()] [hashtable] $Parameters = @{} ) # Regex patterns for sanitizing error messages (defined inside scriptblock for reliable scoping) $bearerTokenPattern = 'Bearer\s+[^\s]+' $tokenAssignmentPattern = 'token[^\s]*\s*=\s*[^\s,;]+' # Transient EXO error patterns: server-side 5xx errors and throttling (429) $transientErrorPattern = 'server[\s-]+side[\s-]+error|throttl|too[\s-]+many[\s-]+requests|service[\s-]+unavailable|temporarily[\s-]+unavailable|bad[\s-]+gateway' try { $result = & $CommandName @Parameters return $result } catch { # Build error message without exposing sensitive data $errorMessage = "Exchange Online command '$CommandName' failed" $isTransient = $false if ($_.Exception.Message) { # Sanitize error message to avoid leaking tokens/secrets $sanitized = $_.Exception.Message -replace $bearerTokenPattern, 'Bearer <REDACTED>' $sanitized = $sanitized -replace $tokenAssignmentPattern, 'token=<REDACTED>' $errorMessage += " | $sanitized" # Mark retryable server-side and throttling errors as transient so the # plan executor's Invoke-IdleWithRetry can retry the enclosing step. if ($_.Exception.Message -imatch $transientErrorPattern) { $isTransient = $true } } $ex = [System.Exception]::new($errorMessage, $_.Exception) if ($isTransient) { $ex.Data['Idle.IsTransient'] = $true } throw $ex } } $adapter | Add-Member -MemberType ScriptMethod -Name InvokeSafely -Value $invokeSafely -Force # GetMailbox: Retrieve mailbox details by identity (UPN or SMTP address) $adapter | Add-Member -MemberType ScriptMethod -Name GetMailbox -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken try { $params = @{ Identity = $MailboxIdentity ErrorAction = 'Stop' } $mailbox = $this.InvokeSafely('Get-Mailbox', $params) if ($null -eq $mailbox) { return $null } # Normalize output to hashtable return @{ Identity = $mailbox.Identity PrimarySmtpAddress = $mailbox.PrimarySmtpAddress UserPrincipalName = $mailbox.UserPrincipalName DisplayName = $mailbox.DisplayName RecipientType = $mailbox.RecipientType RecipientTypeDetails = $mailbox.RecipientTypeDetails Guid = $mailbox.Guid } } catch { if ($_.Exception.Message -match 'couldn''t be found|not found|does not exist') { return $null } throw } } -Force # SetMailboxType: Convert mailbox type (User <-> Shared) $adapter | Add-Member -MemberType ScriptMethod -Name SetMailboxType -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter(Mandatory)] [ValidateSet('User', 'Shared', 'Room', 'Equipment')] [string] $Type, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken $params = @{ Identity = $MailboxIdentity ErrorAction = 'Stop' } # Map type to RecipientTypeDetails switch ($Type) { 'User' { $params['Type'] = 'Regular' } 'Shared' { $params['Type'] = 'Shared' } 'Room' { $params['Type'] = 'Room' } 'Equipment' { $params['Type'] = 'Equipment' } } $null = $this.InvokeSafely('Set-Mailbox', $params) } -Force $adapter | Add-Member -MemberType ScriptMethod -Name GetMailboxAutoReplyConfiguration -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken try { $params = @{ Identity = $MailboxIdentity ErrorAction = 'Stop' } $config = $this.InvokeSafely('Get-MailboxAutoReplyConfiguration', $params) if ($null -eq $config) { return $null } # Normalize output to hashtable return @{ Identity = $config.Identity AutoReplyState = $config.AutoReplyState StartTime = $config.StartTime EndTime = $config.EndTime InternalMessage = $config.InternalMessage ExternalMessage = $config.ExternalMessage ExternalAudience = $config.ExternalAudience CreateOOFEvent = $config.CreateOOFEvent OOFEventSubject = $config.OOFEventSubject DeclineAllEventsForScheduledOOF = $config.DeclineAllEventsForScheduledOOF } } catch { if ($_.Exception.Message -match 'couldn''t be found|not found|does not exist') { return $null } throw } } -Force # SetMailboxAutoReplyConfiguration: Update Out of Office settings $adapter | Add-Member -MemberType ScriptMethod -Name SetMailboxAutoReplyConfiguration -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter(Mandatory)] [ValidateNotNull()] [hashtable] $Config, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken $params = @{ Identity = $MailboxIdentity ErrorAction = 'Stop' } # Map config keys to cmdlet parameters if ($Config.ContainsKey('Mode')) { $mode = $Config['Mode'] switch ($mode) { 'Disabled' { $params['AutoReplyState'] = 'Disabled' } 'Enabled' { $params['AutoReplyState'] = 'Enabled' } 'Scheduled' { $params['AutoReplyState'] = 'Scheduled' } default { throw "Invalid Mode value: $mode. Expected Disabled, Enabled, or Scheduled." } } } if ($Config.ContainsKey('Start')) { $params['StartTime'] = $Config['Start'] } if ($Config.ContainsKey('End')) { $params['EndTime'] = $Config['End'] } if ($Config.ContainsKey('InternalMessage')) { $params['InternalMessage'] = $Config['InternalMessage'] } if ($Config.ContainsKey('ExternalMessage')) { $params['ExternalMessage'] = $Config['ExternalMessage'] } if ($Config.ContainsKey('ExternalAudience')) { $params['ExternalAudience'] = $Config['ExternalAudience'] } $null = $this.InvokeSafely('Set-MailboxAutoReplyConfiguration', $params) } -Force # GetMailboxPermissions: Get FullAccess permissions for a mailbox $adapter | Add-Member -MemberType ScriptMethod -Name GetMailboxPermissions -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken try { $params = @{ Identity = $MailboxIdentity ErrorAction = 'Stop' } $permissions = $this.InvokeSafely('Get-MailboxPermission', $params) if ($null -eq $permissions) { return @() } # Normalize output: filter out NT AUTHORITY, SELF, and SID-only entries. # - NT AUTHORITY\*: built-in system accounts (e.g. NT AUTHORITY\SELF from inheritance) # - *\SELF: owner self-permission added automatically by Exchange # - S-1-*: unresolved SIDs that should not be managed as named delegates $result = @() foreach ($perm in $permissions) { $user = [string]$perm.User if ($user -match '^NT AUTHORITY\\|\\SELF$|^S-1-') { continue } foreach ($right in $perm.AccessRights) { $result += @{ MailboxIdentity = $MailboxIdentity User = $user AccessRight = [string]$right IsInherited = [bool]$perm.IsInherited } } } return $result } catch { if ($_.Exception.Message -match 'couldn''t be found|not found|does not exist') { return @() } throw } } -Force # AddMailboxPermission: Grant FullAccess to a mailbox $adapter | Add-Member -MemberType ScriptMethod -Name AddMailboxPermission -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $User, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken $params = @{ Identity = $MailboxIdentity User = $User AccessRights = 'FullAccess' ErrorAction = 'Stop' } $null = $this.InvokeSafely('Add-MailboxPermission', $params) } -Force # RemoveMailboxPermission: Revoke FullAccess from a mailbox $adapter | Add-Member -MemberType ScriptMethod -Name RemoveMailboxPermission -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $User, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken $params = @{ Identity = $MailboxIdentity User = $User AccessRights = 'FullAccess' Confirm = $false ErrorAction = 'Stop' } $null = $this.InvokeSafely('Remove-MailboxPermission', $params) } -Force # GetRecipientPermissions: Get SendAs permissions for a mailbox $adapter | Add-Member -MemberType ScriptMethod -Name GetRecipientPermissions -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken try { $params = @{ Identity = $MailboxIdentity ErrorAction = 'Stop' } $permissions = $this.InvokeSafely('Get-RecipientPermission', $params) if ($null -eq $permissions) { return @() } # Normalize output: filter out NT AUTHORITY entries. # Get-RecipientPermission only returns NT AUTHORITY\SELF for built-in system entries; # unlike Get-MailboxPermission it does not return unresolved SIDs or \SELF owner entries. $result = @() foreach ($perm in $permissions) { $trustee = [string]$perm.Trustee if ($trustee -match '^NT AUTHORITY\\') { continue } $result += @{ MailboxIdentity = $MailboxIdentity Trustee = $trustee AccessControlType = [string]$perm.AccessControlType AccessRight = [string]($perm.AccessRights -join ',') IsInherited = [bool]$perm.IsInherited } } return $result } catch { if ($_.Exception.Message -match 'couldn''t be found|not found|does not exist') { return @() } throw } } -Force # AddRecipientPermission: Grant SendAs to a mailbox $adapter | Add-Member -MemberType ScriptMethod -Name AddRecipientPermission -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Trustee, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken $params = @{ Identity = $MailboxIdentity Trustee = $Trustee AccessRights = 'SendAs' Confirm = $false ErrorAction = 'Stop' } $null = $this.InvokeSafely('Add-RecipientPermission', $params) } -Force # RemoveRecipientPermission: Revoke SendAs from a mailbox $adapter | Add-Member -MemberType ScriptMethod -Name RemoveRecipientPermission -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Trustee, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken $params = @{ Identity = $MailboxIdentity Trustee = $Trustee AccessRights = 'SendAs' Confirm = $false ErrorAction = 'Stop' } $null = $this.InvokeSafely('Remove-RecipientPermission', $params) } -Force # GetMailboxSendOnBehalf: Get the GrantSendOnBehalfTo list for a mailbox $adapter | Add-Member -MemberType ScriptMethod -Name GetMailboxSendOnBehalf -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken try { $params = @{ Identity = $MailboxIdentity ErrorAction = 'Stop' } $mailbox = $this.InvokeSafely('Get-Mailbox', $params) if ($null -eq $mailbox) { return @() } # GrantSendOnBehalfTo returns a MultiValuedProperty - normalize to string array $result = @() foreach ($entry in $mailbox.GrantSendOnBehalfTo) { $result += [string]$entry } return $result } catch { if ($_.Exception.Message -match 'couldn''t be found|not found|does not exist') { return @() } throw } } -Force # SetMailboxSendOnBehalf: Set the GrantSendOnBehalfTo list for a mailbox $adapter | Add-Member -MemberType ScriptMethod -Name SetMailboxSendOnBehalf -Value { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AccessToken', Justification = 'Reserved for future Graph API integration')] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $MailboxIdentity, [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $Delegates, [Parameter()] [AllowNull()] [string] $AccessToken ) # AccessToken is reserved for future Graph API integration $null = $AccessToken $params = @{ Identity = $MailboxIdentity GrantSendOnBehalfTo = $Delegates ErrorAction = 'Stop' } $null = $this.InvokeSafely('Set-Mailbox', $params) } -Force return $adapter } |