interop.ps1
|
#Requires -Version 5.0 function Remove-ComObject { <# .SYNOPSIS Releases one or more COM objects. .DESCRIPTION Wraps Marshal.ReleaseComObject with null checks and best-effort error handling so scripts can safely clean up Outlook and Office interop objects from finally blocks. .EXAMPLE PS> Remove-ComObject $items $folder .LINK https://github.com/adnoctem/winkit/lib/interop.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'COM cleanup must be best-effort during script teardown.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Releases local COM references only; it does not change external system state.')] [CmdletBinding()] param ( [Parameter(ValueFromRemainingArguments = $true)] [AllowNull()] [object[]] $InputObject ) foreach ($_object in $InputObject) { if ($null -eq $_object) { continue } try { if ([System.Runtime.InteropServices.Marshal]::IsComObject($_object)) { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($_object) } } catch { } } } function Invoke-ComGarbageCollection { <# .SYNOPSIS Runs final COM cleanup garbage collection passes. .DESCRIPTION Forces garbage collection and waits for pending finalizers. This is useful after releasing Office COM references so Outlook can close PST files and exit cleanly when requested. .EXAMPLE PS> Invoke-ComGarbageCollection .LINK https://github.com/adnoctem/winkit/lib/interop.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] param () [GC]::Collect() [GC]::WaitForPendingFinalizers() } function Get-OutlookInstallation { <# .SYNOPSIS Finds local Outlook installation directories. .DESCRIPTION Discovers common Microsoft Office and Microsoft 365 installation roots that may contain Outlook.exe or Outlook data-file repair tools such as ScanPST.exe and ScanOST.exe. The function checks App Paths registry entries first, then common Office directory layouts under Program Files. .EXAMPLE PS> Get-OutlookInstallation .LINK https://github.com/adnoctem/winkit/lib/interop.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [OutputType([PSCustomObject[]])] [CmdletBinding()] param () $_candidateDirectories = New-Object System.Collections.Generic.List[string] $_registryPaths = @( 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\OUTLOOK.EXE', 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\App Paths\OUTLOOK.EXE', 'Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\OUTLOOK.EXE' ) foreach ($_registryPath in $_registryPaths) { if (-not (Test-Path -LiteralPath $_registryPath)) { continue } $_property = Get-ItemProperty -LiteralPath $_registryPath -ErrorAction SilentlyContinue if (-not $_property) { continue } $_outlookPath = $_property.'(default)' if ([string]::IsNullOrWhiteSpace($_outlookPath) -and ($_property.PSObject.Properties.Name -contains 'Path')) { $_outlookPath = Join-Path -Path $_property.Path -ChildPath 'OUTLOOK.EXE' } if (-not [string]::IsNullOrWhiteSpace($_outlookPath)) { $_directory = Split-Path -Path ([Environment]::ExpandEnvironmentVariables($_outlookPath)) -Parent if (-not [string]::IsNullOrWhiteSpace($_directory)) { $_candidateDirectories.Add($_directory) } } } $_programRoots = @( $env:ProgramFiles, ${env:ProgramFiles(x86)} ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique $_officeVersions = @('Office16', 'Office15', 'Office14', 'Office12', 'Office11') foreach ($_programRoot in $_programRoots) { $_officeRoot = Join-Path -Path $_programRoot -ChildPath 'Microsoft Office' foreach ($_version in $_officeVersions) { $_candidateDirectories.Add((Join-Path -Path $_officeRoot -ChildPath $_version)) $_candidateDirectories.Add((Join-Path -Path $_officeRoot -ChildPath "root\$_version")) } } $_seen = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase) foreach ($_directory in $_candidateDirectories) { if ([string]::IsNullOrWhiteSpace($_directory)) { continue } if (-not (Test-Path -LiteralPath $_directory -PathType Container)) { continue } $_resolvedDirectory = (Resolve-Path -LiteralPath $_directory).ProviderPath if (-not $_seen.Add($_resolvedDirectory)) { continue } $_outlookPath = Join-Path -Path $_resolvedDirectory -ChildPath 'OUTLOOK.EXE' $_scanPstPath = Join-Path -Path $_resolvedDirectory -ChildPath 'SCANPST.EXE' $_scanOstPath = Join-Path -Path $_resolvedDirectory -ChildPath 'SCANOST.EXE' if ( -not (Test-Path -LiteralPath $_outlookPath -PathType Leaf) -and -not (Test-Path -LiteralPath $_scanPstPath -PathType Leaf) -and -not (Test-Path -LiteralPath $_scanOstPath -PathType Leaf) ) { continue } [PSCustomObject]@{ Path = $_resolvedDirectory OutlookPath = if (Test-Path -LiteralPath $_outlookPath -PathType Leaf) { $_outlookPath } else { $null } ScanPstPath = if (Test-Path -LiteralPath $_scanPstPath -PathType Leaf) { $_scanPstPath } else { $null } ScanOstPath = if (Test-Path -LiteralPath $_scanOstPath -PathType Leaf) { $_scanOstPath } else { $null } } } } function Find-OutlookRepairTool { <# .SYNOPSIS Finds an Outlook data-file repair tool. .DESCRIPTION Resolves ScanPST.exe or ScanOST.exe from discovered Outlook installation directories. ScanPST is present in modern Outlook installs; ScanOST exists only in older Outlook versions. .PARAMETER Name Repair tool executable to find. .EXAMPLE PS> Find-OutlookRepairTool -Name ScanPST .LINK https://github.com/adnoctem/winkit/lib/interop.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [OutputType([PSCustomObject[]])] [CmdletBinding()] param ( [ValidateSet('ScanPST', 'ScanOST')] [string] $Name = 'ScanPST' ) $_propertyName = if ($Name -eq 'ScanOST') { 'ScanOstPath' } else { 'ScanPstPath' } $_fileName = if ($Name -eq 'ScanOST') { 'SCANOST.EXE' } else { 'SCANPST.EXE' } foreach ($_installation in Get-OutlookInstallation) { $_path = $_installation.$_propertyName if ([string]::IsNullOrWhiteSpace($_path)) { continue } if (-not (Test-Path -LiteralPath $_path -PathType Leaf)) { continue } [PSCustomObject]@{ Name = $Name Path = $_path InstallationPath = $_installation.Path } } $_command = Get-Command -Name $_fileName -ErrorAction SilentlyContinue if ($_command -and (Test-Path -LiteralPath $_command.Source -PathType Leaf)) { [PSCustomObject]@{ Name = $Name Path = $_command.Source InstallationPath = Split-Path -Path $_command.Source -Parent } } } function Connect-Outlook { <# .SYNOPSIS Connects to an Outlook COM application and MAPI namespace. .DESCRIPTION Reuses a running Outlook instance when available, otherwise starts one, then logs on to the MAPI namespace without prompting. .EXAMPLE PS> $context = Connect-Outlook .LINK https://github.com/adnoctem/winkit/lib/interop.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Falling back to a new Outlook COM instance is intentional.')] [OutputType([PSCustomObject])] [CmdletBinding()] param () try { $_application = [System.Runtime.InteropServices.Marshal]::GetActiveObject('Outlook.Application') } catch { $_application = New-Object -ComObject Outlook.Application } $_namespace = $_application.GetNamespace('MAPI') $_namespace.Logon($null, $null, $false, $false) [PSCustomObject]@{ App = $_application Namespace = $_namespace } } function Get-OutlookStoreRoot { <# .SYNOPSIS Gets the root folder for an Outlook store. .DESCRIPTION Resolves a named Outlook store by DisplayName, or the default delivery store when no name is supplied. Store COM objects are released as they are inspected; the returned root folder is owned by the caller. .PARAMETER Namespace Outlook MAPI namespace returned by Connect-Outlook. .PARAMETER Name Optional store display name. .EXAMPLE PS> Get-OutlookStoreRoot -Namespace $context.Namespace -Name 'user@example.com' .LINK https://github.com/adnoctem/winkit/lib/interop.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $Namespace, [string] $Name ) $_stores = $Namespace.Stores try { Write-Verbose 'Available Outlook stores:' for ($_index = 1; $_index -le $_stores.Count; $_index++) { $_store = $_stores.Item($_index) try { Write-Verbose (" - {0}" -f $_store.DisplayName) if ([string]::IsNullOrWhiteSpace($Name) -and $_store.IsDefault) { return $_store.GetRootFolder() } if ($_store.DisplayName -eq $Name) { return $_store.GetRootFolder() } } finally { Remove-ComObject $_store } } } finally { Remove-ComObject $_stores } throw "Outlook store '$Name' not found. Run with -Verbose to list available stores." } function Add-OutlookStoreRoot { <# .SYNOPSIS Adds a Unicode PST store and returns its root folder. .DESCRIPTION Calls Outlook Namespace.AddStoreEx with OlStoreType.olStoreUnicode and locates the newly attached store by FilePath. The returned root folder is owned by the caller. .PARAMETER Namespace Outlook MAPI namespace returned by Connect-Outlook. .PARAMETER Path Full path to the PST file to attach or create. .EXAMPLE PS> Add-OutlookStoreRoot -Namespace $context.Namespace -Path 'D:\Archive\mail.pst' .LINK https://github.com/adnoctem/winkit/lib/interop.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $Namespace, [Parameter(Mandatory = $true)] [string] $Path ) $_olStoreUnicode = 2 $_resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) $Namespace.AddStoreEx($_resolvedPath, $_olStoreUnicode) $_stores = $Namespace.Stores try { for ($_index = 1; $_index -le $_stores.Count; $_index++) { $_store = $_stores.Item($_index) try { if ($_store.FilePath -eq $_resolvedPath) { return $_store.GetRootFolder() } } finally { Remove-ComObject $_store } } } finally { Remove-ComObject $_stores } throw "Outlook store was added but could not be located by path: $_resolvedPath" } function Get-OutlookSubFolder { <# .SYNOPSIS Gets or creates an Outlook child folder. .DESCRIPTION Searches a parent folder's Folders collection by display name. When -Create is set, the folder is created if missing. The returned folder is owned by the caller. .PARAMETER ParentFolder Outlook parent folder. .PARAMETER Name Child folder display name. .PARAMETER Create Create the folder when it does not exist. .EXAMPLE PS> Get-OutlookSubFolder -ParentFolder $root -Name '_Review' -Create .LINK https://github.com/adnoctem/winkit/lib/interop.ps1 .NOTES Author: MVProwess <info@mvprowess.com> License: MIT #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object] $ParentFolder, [Parameter(Mandatory = $true)] [string] $Name, [switch] $Create ) $_folders = $ParentFolder.Folders try { for ($_index = 1; $_index -le $_folders.Count; $_index++) { $_folder = $_folders.Item($_index) if ($_folder.Name -eq $Name) { return $_folder } Remove-ComObject $_folder } if ($Create) { return $_folders.Add($Name) } } finally { Remove-ComObject $_folders } return $null } |