Modules/IdLE.Provider.ExchangeOnline/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 ) # Regex patterns for sanitizing error messages (captured by scriptblock closure) $bearerTokenPattern = 'Bearer\s+[^\s]+' $tokenAssignmentPattern = 'token[^\s]*\s*=\s*[^\s,;]+' $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 = @{} ) try { $result = & $CommandName @Parameters return $result } catch { # Build error message without exposing sensitive data $errorMessage = "Exchange Online command '$CommandName' failed" 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" } $ex = [System.Exception]::new($errorMessage, $_.Exception) 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' } } $this.InvokeSafely('Set-Mailbox', $params) } -Force # GetMailboxAutoReplyConfiguration: Get Out of Office settings $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'] } $this.InvokeSafely('Set-MailboxAutoReplyConfiguration', $params) } -Force return $adapter } |