Public/NC.Quarantine.ps1

#Requires -Version 5.0
using namespace System.Management.Automation

# Nebula.Core: Quarantine ===========================================================================================================================

function Export-QuarantineEml {
    <#
    .SYNOPSIS
        Exports a quarantined message as an EML file.
    .DESCRIPTION
        Retrieves the quarantined message by MessageId, writes the decoded EML to the specified folder,
        optionally opens it, and can release the message to all recipients.
    .PARAMETER MessageId
        MessageId of the quarantined e-mail (with or without angle brackets).
    .PARAMETER DestinationFolder
        Folder where the EML file will be written. Defaults to the current directory.
    .PARAMETER OpenFile
        Open the exported file after saving it.
    .PARAMETER ReleaseToAll
        Release the message to all recipients after export.
    .PARAMETER ReportFalsePositive
        Also report the message as a false positive when releasing.
    .EXAMPLE
        Export-QuarantineEml -MessageId 20230617142935.F5B74194B266E458@contoso.com -DestinationFolder C:\Temp -OpenFile -ReleaseToAll
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$MessageId,
        [string]$DestinationFolder,
        [switch]$OpenFile,
        [switch]$ReleaseToAll,
        [switch]$ReportFalsePositive
    )

    begin { Set-ProgressAndInfoPreferences }

    process {
        if (-not (Test-EOLConnection)) {
            Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR
            return
        }

        try {
            $folder = Test-Folder -Path $DestinationFolder
        }
        catch {
            Write-NCMessage "Destination folder is not valid. $($_.Exception.Message)" -Level ERROR
            return
        }

        $normalizedId = ConvertTo-QuarantineMessageId -MessageId $MessageId

        try {
            $message = Get-QuarantineMessage -MessageId $normalizedId -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Unable to find quarantined message '$normalizedId'. $($_.Exception.Message)" -Level ERROR
            return
        }

        try {
            $exported = $message | Export-QuarantineMessage
            $bytes = [Convert]::FromBase64String($exported.eml)
        }
        catch {
            Write-NCMessage "Unable to export quarantined message '$normalizedId'. $($_.Exception.Message)" -Level ERROR
            return
        }

        $invalidChars = [Regex]::Escape(([IO.Path]::GetInvalidFileNameChars() -join ''))
        $safeNameSource = if ($message.Subject) { $message.Subject } else { $message.MessageId }
        $safeBaseName = [Regex]::Replace((Format-OutputString -Value $safeNameSource -MaxLength 60), "[$invalidChars]", '_')
        if ([string]::IsNullOrWhiteSpace($safeBaseName)) {
            $safeBaseName = 'QuarantineMessage'
        }
        $emlPath = New-File (Join-Path -Path $folder -ChildPath "$safeBaseName.eml")

        try {
            [IO.File]::WriteAllBytes($emlPath, $bytes)
            Write-NCMessage ("Saved quarantined message to {0}" -f $emlPath) -Level SUCCESS
        }
        catch {
            Write-NCMessage "Unable to write EML file. $($_.Exception.Message)" -Level ERROR
            return
        }

        if ($OpenFile.IsPresent) {
            try {
                Invoke-Item -LiteralPath $emlPath
            }
            catch {
                Write-NCMessage "File exported but could not be opened automatically. $($_.Exception.Message)" -Level WARNING
            }
        }

        if ($ReleaseToAll.IsPresent) {
            $action = "Release quarantined message '$($message.Subject)'"
            if ($PSCmdlet.ShouldProcess($message.MessageId, $action)) {
                try {
                    $releaseParams = @{
                        Identity            = $message.Identity
                        ReleaseToAll        = $true
                        Confirm             = $false
                        ReportFalsePositive = $ReportFalsePositive.IsPresent
                    }
        Release-QuarantineMessage @releaseParams | Out-Null
                    Write-NCMessage "Released quarantined message $($message.MessageId) to all recipients." -Level SUCCESS
                }
                catch {
                    Write-NCMessage "Unable to release quarantined message. $($_.Exception.Message)" -Level ERROR
                }
            }
        }

        [pscustomobject]@{
            MessageId           = $message.MessageId
            Subject             = $message.Subject
            QuarantineTypes     = $message.QuarantineTypes
            EmlPath             = $emlPath
            ReleasedToAll       = $ReleaseToAll.IsPresent
            ReportFalsePositive = $ReportFalsePositive.IsPresent -and $ReleaseToAll.IsPresent
        }
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Get-QuarantineFrom {
    <#
    .SYNOPSIS
        Lists quarantined messages from specific senders.
    .DESCRIPTION
        Retrieves quarantine entries for the provided sender addresses, expanding message details
        and returning a consistent set of properties.
    .PARAMETER SenderAddress
        One or more sender addresses to query. Accepts pipeline input.
    .PARAMETER IncludeReleased
        Include messages already released (default hides them).
    .EXAMPLE
        Get-QuarantineFrom -SenderAddress mario.rossi@contoso.com
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Sender')]
        [string[]]$SenderAddress,
        [switch]$IncludeReleased
    )

    begin {
        Set-ProgressAndInfoPreferences
        $results = [System.Collections.Generic.List[object]]::new()
    }

    process {
        if (-not (Test-EOLConnection)) {
            Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR
            return
        }

        foreach ($currentSender in $SenderAddress) {
            if ([string]::IsNullOrWhiteSpace($currentSender)) { continue }
            Write-NCMessage ("Searching quarantined messages from {0} ..." -f $currentSender) -Level INFO

            try {
                $messages = Get-QuarantineMessage -SenderAddress $currentSender -ErrorAction Stop
            }
            catch {
                Write-NCMessage "Unable to retrieve messages for '$currentSender'. $($_.Exception.Message)" -Level ERROR
                continue
            }

            foreach ($msg in $messages) {
                try {
                    $details = Get-QuarantineMessage -Identity $msg.Identity -ErrorAction Stop
                }
                catch {
                    Write-NCMessage "Unable to load message details for '$($msg.Identity)'. $($_.Exception.Message)" -Level ERROR
                    continue
                }

                if (-not $IncludeReleased.IsPresent -and $details.Released) {
                    continue
                }

                $results.Add([pscustomobject]@{
                        Subject          = Format-OutputString -Value $details.Subject
                        SenderAddress    = $details.SenderAddress
                        RecipientAddress = $details.RecipientAddress
                        ReceivedTime     = $details.ReceivedTime
                        QuarantineTypes  = $details.QuarantineTypes
                        Released         = $details.Released
                        ReleasedUser     = $details.ReleasedUser
                        MessageId        = $details.MessageId
                        Identity         = $details.Identity
                    }) | Out-Null
            }
        }
    }

    end {
        Restore-ProgressAndInfoPreferences
        $results
    }
}

function Get-QuarantineFromDomain {
    <#
    .SYNOPSIS
        Lists quarantined messages from specific sender domains.
    .DESCRIPTION
        Retrieves quarantine entries where the sender's domain matches the provided values.
    .PARAMETER SenderDomain
        One or more domains (e.g. contoso.com). Accepts pipeline input.
    .PARAMETER IncludeReleased
        Include messages already released (default hides them).
    .EXAMPLE
        Get-QuarantineFromDomain -SenderDomain contoso.com
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]$SenderDomain,
        [switch]$IncludeReleased
    )

    begin {
        Set-ProgressAndInfoPreferences
        $results = [System.Collections.Generic.List[object]]::new()
    }

    process {
        if (-not (Test-EOLConnection)) {
            Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR
            return
        }

        foreach ($domain in $SenderDomain) {
            if ([string]::IsNullOrWhiteSpace($domain)) { continue }
            Write-NCMessage ("Searching quarantined messages from *@{0} ..." -f $domain) -Level INFO

            try {
                $messages = Get-QuarantineMessage -ErrorAction Stop | Where-Object { $_.SenderAddress -like "*@$domain" }
            }
            catch {
                Write-NCMessage "Unable to retrieve messages for domain '$domain'. $($_.Exception.Message)" -Level ERROR
                continue
            }

            foreach ($msg in $messages) {
                try {
                    $details = Get-QuarantineMessage -Identity $msg.Identity -ErrorAction Stop
                }
                catch {
                    Write-NCMessage "Unable to load message details for '$($msg.Identity)'. $($_.Exception.Message)" -Level ERROR
                    continue
                }

                if (-not $IncludeReleased.IsPresent -and $details.Released) {
                    continue
                }

                $results.Add([pscustomobject]@{
                        Subject          = Format-OutputString -Value $details.Subject
                        SenderAddress    = $details.SenderAddress
                        RecipientAddress = $details.RecipientAddress
                        ReceivedTime     = $details.ReceivedTime
                        QuarantineTypes  = $details.QuarantineTypes
                        Released         = $details.Released
                        ReleasedUser     = $details.ReleasedUser
                        MessageId        = $details.MessageId
                        Identity         = $details.Identity
                    }) | Out-Null
            }
        }
    }

    end {
        Restore-ProgressAndInfoPreferences
        $results
    }
}

function Get-QuarantineToRelease {
    <#
    .SYNOPSIS
        Retrieves quarantine messages pending release.
    .DESCRIPTION
        Pulls quarantined messages within a date range, optionally shows a grid for selection,
        exports reports, and can release or delete the selected entries.
    .PARAMETER ChooseDayFromCalendar
        Pick a single day using a calendar popup.
    .PARAMETER Interval
        Number of days back from today to search (1-30). Ignored when using -ChooseDayFromCalendar.
    .PARAMETER GridView
        Display results in Out-GridView and return only the selected rows.
    .PARAMETER Csv
        Export all retrieved entries to CSV in the chosen folder (or current directory).
    .PARAMETER Html
        Export all retrieved entries to HTML using PSWriteHTML if available.
    .PARAMETER OutputFolder
        Target folder for CSV/HTML exports.
    .PARAMETER ReleaseSelected
        Release selected (or all) entries. Requires confirmation (supports -WhatIf).
    .PARAMETER DeleteSelected
        Delete selected (or all) entries. Requires confirmation (supports -WhatIf).
    .PARAMETER ReportFalsePositive
        When releasing, also report messages as false positives.
    .EXAMPLE
        Get-QuarantineToRelease -Interval 7 -GridView -ReleaseSelected -ReportFalsePositive
    #>

    [CmdletBinding(DefaultParameterSetName = 'Interval', SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(ParameterSetName = 'Calendar')]
        [switch]$ChooseDayFromCalendar,
        [Parameter(ParameterSetName = 'Interval', Mandatory)]
        [ValidateRange(1, 30)]
        [int]$Interval,
        [switch]$GridView,
        [switch]$Html,
        [switch]$Csv,
        [string]$OutputFolder,
        [switch]$ReleaseSelected,
        [switch]$DeleteSelected,
        [switch]$ReportFalsePositive
    )

    begin {
        if ($ReleaseSelected.IsPresent -and $DeleteSelected.IsPresent) {
            throw "Specify either -ReleaseSelected or -DeleteSelected, not both."
        }
        Set-ProgressAndInfoPreferences
    }

    process {
        if (-not (Test-EOLConnection)) {
            Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR
            return
        }

        $startDate = $null
        $endDate = $null

        if ($ChooseDayFromCalendar.IsPresent) {
            [void][Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
            [void][Reflection.Assembly]::LoadWithPartialName("System.Drawing")

            $form = New-Object Windows.Forms.Form
            $form.Size = New-Object Drawing.Size @(200, 190)
            $form.StartPosition = "CenterScreen"
            $form.KeyPreview = $true

            $calendar = New-Object System.Windows.Forms.MonthCalendar
            $calendar.ShowTodayCircle = $true
            $calendar.MaxSelectionCount = 1
            $form.Controls.Add($calendar)
            $form.Topmost = $true
            $form.Text = "Select the day to be analyzed"

            $selectedDate = $null
            $form.Add_KeyDown({
                    if ($_.KeyCode -eq "Enter") {
                        $script:selectedDate = $calendar.SelectionStart
                        $form.Close()
                    }
                    elseif ($_.KeyCode -eq "Escape") {
                        $form.Close()
                    }
                })

            [void]$form.ShowDialog()

            if ($selectedDate) {
                $startDate = $selectedDate.Date.AddDays(-1)
                $endDate = $selectedDate.Date
            }
            else {
                Write-NCMessage "You must select at least one day from the calendar." -Level ERROR
                return
            }
        }
        else {
            $startDate = (Get-Date).AddDays(-$Interval)
            $endDate = Get-Date
        }

        $page = 1
        $quarantined = @()
        do {
            try {
                $pageData = Get-QuarantineMessage -StartReceivedDate $startDate.Date -EndReceivedDate $endDate -PageSize 1000 -ReleaseStatus NotReleased -Page $page
            }
            catch {
                Write-NCMessage "Unable to retrieve quarantine page $page. $($_.Exception.Message)" -Level ERROR
                break
            }

            $page++
            if ($pageData) {
                $quarantined += $pageData
            }
        } until (-not $pageData)

        if (-not $quarantined -or $quarantined.Count -eq 0) {
            Write-NCMessage "No quarantined messages found in the selected interval." -Level WARNING
            return
        }

        $items = $quarantined | ForEach-Object {
            [pscustomobject]@{
                SenderAddress    = $_.SenderAddress
                RecipientAddress = $_.RecipientAddress
                Subject          = $_.Subject
                ReceivedTime     = $_.ReceivedTime
                QuarantineTypes  = $_.QuarantineTypes
                Released         = $_.Released
                MessageId        = $_.MessageId
                Identity         = $_.Identity
            }
        }

        Write-NCMessage ("Retrieved {0} quarantined items from {1:d} to {2:d}." -f $items.Count, $startDate, $endDate) -Level INFO

        if ($Csv.IsPresent) {
            try {
                $folder = Test-Folder -Path $OutputFolder
                $csvPath = New-File (Join-Path -Path $folder -ChildPath "$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-QuarantineToRelease-Report.csv")
                $items | Export-Csv -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter
                Write-NCMessage ("CSV exported to {0}" -f $csvPath) -Level SUCCESS
            }
            catch {
                Write-NCMessage "Unable to export CSV. $($_.Exception.Message)" -Level ERROR
            }
        }

        if ($Html.IsPresent) {
            if (-not (Get-Module -Name PSWriteHTML -ListAvailable)) {
                Write-NCMessage "PSWriteHTML module is not available. Install it to use -Html output." -Level WARNING
            }
            else {
                try {
                    Import-Module PSWriteHTML -ErrorAction Stop
                    $folder = Test-Folder -Path $OutputFolder
                    $htmlPath = New-File (Join-Path -Path $folder -ChildPath "$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-QuarantineToRelease-Report.html")
                    $items | Out-GridHtml | Set-Content -LiteralPath $htmlPath -Encoding UTF8
                    Write-NCMessage ("HTML exported to {0}" -f $htmlPath) -Level SUCCESS
                }
                catch {
                    Write-NCMessage "Unable to export HTML report. $($_.Exception.Message)" -Level ERROR
                }
            }
        }

        $selection = $items
        if ($GridView.IsPresent) {
            $title = "{0} to {1} - {2} items" -f $startDate.Date, $endDate.Date, $items.Count
            $selection = $items | Sort-Object -Descending ReceivedTime | Out-GridView -Title $title -PassThru
            if (-not $selection) {
                Write-NCMessage "No items selected." -Level WARNING
                return
            }
        }

        if (-not $ReleaseSelected.IsPresent -and -not $DeleteSelected.IsPresent) {
            return $selection | Sort-Object -Property Subject
        }

        $processed = [System.Collections.Generic.List[object]]::new()
        $counter = 0
        foreach ($item in $selection) {
            $counter++
            $percentComplete = (($counter / $selection.Count) * 100)
            Write-Progress -Activity "Processing $($item.Subject)" -Status "$counter of $($selection.Count) ($($percentComplete.ToString('0.00'))%)" -PercentComplete $percentComplete

            if ($ReleaseSelected.IsPresent) {
                if ($PSCmdlet.ShouldProcess($item.Subject, "Release quarantined message")) {
                    try {
                        $releaseParams = @{
                            Identity            = $item.Identity
                            ReleaseToAll        = $true
                            Confirm             = $false
                            ReportFalsePositive = $ReportFalsePositive.IsPresent
                        }
                        Release-QuarantineMessage @releaseParams | Out-Null
                        $details = Get-QuarantineMessage -Identity $item.Identity
                        $processed.Add([pscustomobject]@{
                                Subject       = Format-OutputString -Value $details.Subject
                                SenderAddress = Format-OutputString -Value $details.SenderAddress
                                Released      = $details.Released
                                ReleasedUser  = $details.ReleasedUser
                            }) | Out-Null
                    }
                    catch {
                        Write-NCMessage "Unable to release message '$($item.Subject)'. $($_.Exception.Message)" -Level ERROR
                    }
                }
            }
            elseif ($DeleteSelected.IsPresent) {
                if ($PSCmdlet.ShouldProcess($item.Subject, "Delete quarantined message permanently")) {
                    try {
                        Delete-QuarantineMessage -Identity $item.Identity -Confirm:$false
                        $processed.Add([pscustomobject]@{
                                Subject       = Format-OutputString -Value $item.Subject
                                SenderAddress = Format-OutputString -Value $item.SenderAddress
                                Deleted       = $true
                            }) | Out-Null
                    }
                    catch {
                        Write-NCMessage "Unable to delete message '$($item.Subject)'. $($_.Exception.Message)" -Level ERROR
                    }
                }
            }
        }

        Write-Progress -Activity "Processing quarantined messages" -Completed

        if ($processed.Count -gt 0) {
            Write-NCMessage ("{0} item(s) processed." -f $processed.Count) -Level SUCCESS
            return $processed
        }
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Unlock-QuarantineFrom {
    <#
    .SYNOPSIS
        Releases quarantined messages from specific senders.
    .DESCRIPTION
        Retrieves messages for the given senders and releases them to all recipients,
        optionally reporting them as false positives.
    .PARAMETER SenderAddress
        One or more sender addresses. Accepts pipeline input.
    .PARAMETER ReportFalsePositive
        Also report the released messages as false positives.
    .EXAMPLE
        Unlock-QuarantineFrom -SenderAddress mario.rossi@contoso.com -ReportFalsePositive
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Sender')]
        [string[]]$SenderAddress,
        [switch]$ReportFalsePositive
    )

    begin {
        Set-ProgressAndInfoPreferences
        $results = [System.Collections.Generic.List[object]]::new()
    }

    process {
        if (-not (Test-EOLConnection)) {
            Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR
            return
        }

        foreach ($currentSender in $SenderAddress) {
            if ([string]::IsNullOrWhiteSpace($currentSender)) { continue }
            Write-NCMessage ("Search for quarantined messages from {0} ..." -f $currentSender) -Level INFO

            try {
                $messages = Get-QuarantineMessage -SenderAddress $currentSender -ErrorAction Stop | Where-Object { $_.ReleaseStatus -ne "Released" -and $null -ne $_.QuarantinedUser }
                Write-NCMessage "Found $($messages.Count) message(s) from $currentSender not yet released." -Level VERBOSE
            }
            catch {
                Write-NCMessage "Unable to retrieve messages for '$currentSender'. $($_.Exception.Message)" -Level ERROR
                continue
            }

            foreach ($msg in $messages) {
                if ($PSCmdlet.ShouldProcess($msg.Identity, "Release quarantined message")) {
                    try {
                        Write-NCMessage "Trying to release $($msg.Identity) to $($msg.RecipientAddress) ..." -Level VERBOSE
                        $releaseParams = @{
                            Identity            = $msg.Identity
                            ReleaseToAll        = $true
                            Confirm             = $false
                            ReportFalsePositive = $ReportFalsePositive.IsPresent
                        }
                        Release-QuarantineMessage @releaseParams | Out-Null
                        $details = Get-QuarantineMessage -Identity $msg.Identity
                        $results.Add([pscustomobject]@{
                            Subject       = Format-OutputString -Value $details.Subject 40
                            SenderAddress = $details.SenderAddress
                            ReceivedTime  = $details.ReceivedTime
                            Released      = $details.Released
                            ReleasedUser  = $details.ReleasedUser
                        }) | Out-Null
                    }
                    catch {
                        Write-NCMessage "Unable to release message '$($msg.Identity)'. $($_.Exception.Message)" -Level ERROR
                    }
                }
            }
        }
    }

    end {
        Restore-ProgressAndInfoPreferences
        $results | Select-Object Subject, SenderAddress, ReceivedTime, ReleasedUser | Format-Table -AutoSize
    }
}

Set-Alias -Name rqf -Value Unlock-QuarantineFrom -Description "Release Quarantine from (function)"

function Unlock-QuarantineMessageId {
    <#
    .SYNOPSIS
        Releases quarantined messages by MessageId.
    .DESCRIPTION
        Accepts MessageId values (with or without angle brackets), releases the messages to all recipients,
        and returns the release status.
    .PARAMETER MessageId
        One or more MessageId values. Accepts pipeline input.
    .PARAMETER ReportFalsePositive
        Also report the released messages as false positives.
    .EXAMPLE
        Unlock-QuarantineMessageId -MessageId CAH_w85uSio_cz4HsFxJAGQDd-kzxGijLaMagZU95m3A1G8hWBA@mail.contoso.com
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [string[]]$MessageId,
        [switch]$ReportFalsePositive
    )

    begin {
        Set-ProgressAndInfoPreferences
        $results = [System.Collections.Generic.List[object]]::new()
    }

    process {
        if (-not (Test-EOLConnection)) {
            Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR
            return
        }

        foreach ($id in $MessageId) {
            $normalizedId = ConvertTo-QuarantineMessageId -MessageId $id

            try {
                $messages = Get-QuarantineMessage -MessageId $normalizedId -ErrorAction Stop | Where-Object { $_.ReleaseStatus -ne "Released" -and $_.QuarantinedUser }
            }
            catch {
                Write-NCMessage "Unable to retrieve quarantined message '$normalizedId'. $($_.Exception.Message)" -Level ERROR
                continue
            }

            if (-not $messages -or $messages.Count -eq 0) {
                Write-NCMessage "No quarantined messages to release with id $normalizedId (already released or not found yet)." -Level WARNING
                continue
            }

            foreach ($msg in $messages) {
                if ($PSCmdlet.ShouldProcess($msg.Identity, "Release quarantined message")) {
                    try {
                        $releaseParams = @{
                            Identity            = $msg.Identity
                            ReleaseToAll        = $true
                            Confirm             = $false
                            ReportFalsePositive = $ReportFalsePositive.IsPresent
                        }
                        Release-QuarantineMessage @releaseParams | Out-Null
                        $details = Get-QuarantineMessage -Identity $msg.Identity
                        $results.Add([pscustomobject]@{
                                Subject       = Format-OutputString -Value $details.Subject 40
                                SenderAddress = $details.SenderAddress
                                ReceivedTime  = $details.ReceivedTime
                                Released      = $details.Released
                                ReleasedUser  = $details.ReleasedUser
                            }) | Out-Null
                    }
                    catch {
                        Write-NCMessage "Unable to release message '$($msg.Identity)'. $($_.Exception.Message)" -Level ERROR
                    }
                }
            }
        }
    }

    end {
        Restore-ProgressAndInfoPreferences
        $results
    }
}

Set-Alias -Name qrel -Value Unlock-QuarantineMessageId -Description "Releases quarantined messages by MessageId (function)"