Public/NC.Calendar.ps1

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

# Nebula.Core: Calendar =============================================================================================================================

function Copy-OoOMessage {
    <#
    .SYNOPSIS
        Copies out-of-office settings from one mailbox to another.
    .DESCRIPTION
        Validates Exchange Online connectivity, reads the source mailbox auto-reply configuration,
        and applies the same messages to the destination mailbox. Optionally forces the destination
        to be enabled immediately instead of preserving the source state/schedule.
    .PARAMETER SourceMailbox
        Mailbox from which to read out-of-office configuration. Accepts pipeline input.
    .PARAMETER DestinationMailbox
        Mailbox to update with the cloned configuration.
    .PARAMETER ForceEnable
        Enable auto-replies immediately on the destination, ignoring the source AutoReplyState.
    .PARAMETER PassThru
        Emit the updated auto-reply configuration for the destination mailbox.
    .EXAMPLE
        Copy-OoOMessage -SourceMailbox source@contoso.com -DestinationMailbox destination@contoso.com
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Source', 'Identity')]
        [string]$SourceMailbox,
        [Parameter(Mandatory)]
        [Alias('Destination')]
        [string]$DestinationMailbox,
        [switch]$ForceEnable,
        [switch]$PassThru
    )

    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 {
            $sourceConfig = Get-MailboxAutoReplyConfiguration -Identity $SourceMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Unable to read auto-reply configuration from '$SourceMailbox'. $($_.Exception.Message)" -Level ERROR
            return
        }

        if ([string]::IsNullOrWhiteSpace($sourceConfig.InternalMessage) -and [string]::IsNullOrWhiteSpace($sourceConfig.ExternalMessage)) {
            Write-NCMessage "Source mailbox '$SourceMailbox' has no out-of-office message defined. Destination will still be updated." -Level WARNING
        }

        $targetState = if ($ForceEnable.IsPresent) { 'Enabled' } else { $sourceConfig.AutoReplyState }
        $setParams = @{
            Identity         = $DestinationMailbox
            AutoReplyState   = $targetState
            InternalMessage  = $sourceConfig.InternalMessage
            ExternalMessage  = $sourceConfig.ExternalMessage
            ExternalAudience = $sourceConfig.ExternalAudience
        }

        if (-not $ForceEnable.IsPresent -and $sourceConfig.AutoReplyState -eq 'Scheduled' -and $sourceConfig.StartTime -and $sourceConfig.EndTime) {
            $setParams.StartTime = $sourceConfig.StartTime
            $setParams.EndTime = $sourceConfig.EndTime
        }

        $action = "apply out-of-office settings from $SourceMailbox"
        if (-not $PSCmdlet.ShouldProcess($DestinationMailbox, $action)) {
            return
        }

        try {
            Set-MailboxAutoReplyConfiguration @setParams -ErrorAction Stop
            Write-NCMessage ("Copied out-of-office configuration from {0} to {1}." -f $SourceMailbox, $DestinationMailbox) -Level SUCCESS
        }
        catch {
            Write-NCMessage "Unable to update '$DestinationMailbox'. $($_.Exception.Message)" -Level ERROR
            return
        }

        try {
            $updated = Get-MailboxAutoReplyConfiguration -Identity $DestinationMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Destination updated, but verification failed. $($_.Exception.Message)" -Level WARNING
            return
        }

        if ($PassThru.IsPresent) {
            return $updated
        }

        $updated | Select-Object Identity, AutoReplyState, StartTime, EndTime, ExternalAudience
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Export-CalendarPermission {
    <#
    .SYNOPSIS
        Exports calendar permissions for selected mailboxes.
    .DESCRIPTION
        Collects calendar permissions for specific mailboxes, domains, or all mailboxes, and writes
        the results to a CSV report. Returns the CSV path, or the permission objects when -PassThru
        is specified.
    .PARAMETER SourceMailbox
        Mailbox identities to analyze. Accepts pipeline input.
    .PARAMETER SourceDomain
        Domain to analyze (e.g. contoso.com). All matching mailboxes are included.
    .PARAMETER OutputFolder
        Destination folder for the CSV file. Defaults to the current directory.
    .PARAMETER All
        Scan every mailbox in the tenant (excluding GuestMailUser). Implies CSV export.
    .PARAMETER PassThru
        Emit the collected permission objects in addition to writing the CSV report.
    .EXAMPLE
        Export-CalendarPermission -SourceMailbox info@contoso.com -OutputFolder C:\Temp
    .EXAMPLE
        Export-CalendarPermission -SourceDomain contoso.com -PassThru
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity', 'Mailbox')]
        [string[]]$SourceMailbox,
        [string[]]$SourceDomain,
        [string]$OutputFolder,
        [switch]$All,
        [switch]$PassThru
    )

    begin {
        Set-ProgressAndInfoPreferences
        $mailboxInputs = [System.Collections.Generic.List[string]]::new()
        $domainInputs = [System.Collections.Generic.List[string]]::new()
    }

    process {
        foreach ($entry in $SourceMailbox) {
            if (-not [string]::IsNullOrWhiteSpace($entry)) {
                $mailboxInputs.Add($entry) | Out-Null
            }
        }

        foreach ($domain in $SourceDomain) {
            if (-not [string]::IsNullOrWhiteSpace($domain)) {
                $domainInputs.Add($domain) | Out-Null
            }
        }
    }

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

            if (-not $mailboxInputs.Count -and -not $domainInputs.Count -and -not $All.IsPresent) {
                Write-NCMessage "No mailbox or domain specified; scanning all mailboxes. This may take a while." -Level WARNING
                $All = $true
            }

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

            $targets = [System.Collections.Generic.List[object]]::new()
            $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

            $addMailbox = {
                param($mailbox)
                if (-not $mailbox) { return }
                $key = if ($mailbox.PrimarySmtpAddress) { $mailbox.PrimarySmtpAddress } else { $mailbox.Identity }
                if ([string]::IsNullOrWhiteSpace($key)) { return }
                if ($seen.Add($key)) {
                    $targets.Add($mailbox) | Out-Null
                }
            }

            if ($All.IsPresent) {
                $allMailboxes = Get-Mailbox -ResultSize Unlimited -WarningAction SilentlyContinue
                foreach ($mbx in $allMailboxes) { & $addMailbox $mbx }
            }

            foreach ($domain in $domainInputs) {
                Write-NCMessage ("Analyzing mailboxes in {0} ..." -f $domain) -Level INFO
                $domainMailboxes = Get-Mailbox -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object {
                    $_.RecipientTypeDetails -ne 'GuestMailUser' -and $_.EmailAddresses -like "*@$domain"
                }
                foreach ($mbx in $domainMailboxes) { & $addMailbox $mbx }
            }

            foreach ($identity in $mailboxInputs) {
                try {
                    $resolved = Get-Mailbox -Identity $identity -ErrorAction Stop
                    & $addMailbox $resolved
                }
                catch {
                    Write-NCMessage "Mailbox '$identity' not found. $($_.Exception.Message)" -Level ERROR
                }
            }

            if ($targets.Count -eq 0) {
                Write-NCMessage "No mailboxes found for the specified filters." -Level WARNING
                return
            }

            $results = [System.Collections.Generic.List[object]]::new()
            $counter = 0

            foreach ($mailbox in $targets) {
                $counter++
                $percent = (($counter / $targets.Count) * 100)
                Write-Progress -Activity "Processing $($mailbox.PrimarySmtpAddress)" -Status "$counter of $($targets.Count) ($($percent.ToString('0.00'))%)" -PercentComplete $percent

                try {
                    $exoMailbox = Get-EXOMailbox -Identity $mailbox.Identity -ErrorAction Stop
                }
                catch {
                    Write-NCMessage "Unable to load mailbox '$($mailbox.Identity)'. $($_.Exception.Message)" -Level ERROR
                    continue
                }

                try {
                    $calendarFolder = Get-MailboxFolderStatistics $exoMailbox.PrimarySmtpAddress -FolderScope Calendar -ErrorAction Stop | Where-Object { $_.FolderType -eq 'Calendar' } | Select-Object -First 1
                }
                catch {
                    Write-NCMessage "Unable to read calendar folder for '$($exoMailbox.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level ERROR
                    continue
                }

                if (-not $calendarFolder) {
                    Write-NCMessage "Calendar folder not found for '$($exoMailbox.PrimarySmtpAddress)'." -Level WARNING
                    continue
                }

                try {
                    $folderIdentity = "{0}:{1}" -f $exoMailbox.PrimarySmtpAddress, $calendarFolder.FolderId
                    $folderPerms = Get-MailboxFolderPermission -Identity $folderIdentity -ErrorAction Stop | Where-Object {
                        $_.AccessRights -notlike 'AvailabilityOnly' -and $_.AccessRights -notlike 'None'
                    }
                }
                catch {
                    Write-NCMessage "Unable to retrieve calendar permissions for '$($exoMailbox.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level ERROR
                    continue
                }

                foreach ($perm in $folderPerms) {
                    $results.Add([pscustomobject]@{
                            PrimarySmtpAddress = $exoMailbox.PrimarySmtpAddress
                            User               = $perm.User
                            Permissions        = ($perm.AccessRights -join ', ')
                        }) | Out-Null
                }
            }

            if ($results.Count -eq 0) {
                Write-NCMessage "No calendar permissions found for the selected scope." -Level WARNING
                return
            }

            $csvPath = New-File (Join-Path -Path $reportFolder -ChildPath "$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-CalendarPermissions-Report.csv")
            try {
                $results | Export-Csv -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter
                Write-NCMessage ("Calendar permission report exported to {0}" -f $csvPath) -Level SUCCESS
            }
            catch {
                Write-NCMessage "Unable to write CSV report. $($_.Exception.Message)" -Level ERROR
                return
            }

            if ($PassThru.IsPresent) {
                $results
            }
            else {
                $csvPath
            }
        }
        finally {
            Write-Progress -Activity "Processing calendar permissions" -Completed
            Restore-ProgressAndInfoPreferences
        }
    }
}

function Get-RoomDetails {
    <#
    .SYNOPSIS
        Lists room list members with capacity and location details.
    .DESCRIPTION
        Ensures Exchange Online connectivity, enumerates room lists (optionally filtered by City),
        expands member rooms, and returns/export details. Supports CSV export and grid view.
    .PARAMETER City
        Optional city/name filter applied to room list name or display name.
    .PARAMETER Csv
        Export results to CSV.
    .PARAMETER OutputFolder
        Destination folder for CSV (defaults to current directory).
    .PARAMETER GridView
        Show the results in Out-GridView.
    .PARAMETER PassThru
        Emit the room details objects to the pipeline (also when exporting).
    .EXAMPLE
        Get-RoomDetails -City Milan -Csv -OutputFolder C:\Temp
    .EXAMPLE
        Get-RoomDetails -GridView
    #>

    [CmdletBinding()]
    param(
        [string[]]$City,
        [switch]$Csv,
        [string]$OutputFolder,
        [switch]$GridView,
        [switch]$PassThru
    )

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

    process {}

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

            $roomGroups = Get-DistributionGroup -RecipientTypeDetails RoomList -ResultSize Unlimited -WarningAction SilentlyContinue
            if ($City -and $City.Count -gt 0) {
                $roomGroups = $roomGroups | Where-Object {
                    foreach ($c in $City) {
                        if ($_.Name -like "*$c*" -or $_.DisplayName -like "*$c*") { return $true }
                    }
                    return $false
                }
            }

            if (-not $roomGroups -or $roomGroups.Count -eq 0) {
                Write-NCMessage "No room lists found with the specified filters." -Level WARNING
                return
            }

            $counter = 0
            foreach ($group in $roomGroups) {
                $counter++
                $percentComplete = (($counter / $roomGroups.Count) * 100)
                Write-Progress -Activity "Processing $($group.DisplayName)" -Status "$counter of $($roomGroups.Count) ($($percentComplete.ToString('0.0'))%)" -PercentComplete $percentComplete

                try {
                    $members = Get-DistributionGroupMember -Identity $group.PrimarySmtpAddress -ResultSize Unlimited -ErrorAction Stop
                }
                catch {
                    Write-NCMessage "Unable to retrieve members for room list '$($group.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level ERROR
                    continue
                }

                foreach ($member in $members) {
                    try {
                        $mailbox = Get-Mailbox -Identity $member.PrimarySmtpAddress -ErrorAction Stop
                    }
                    catch {
                        Write-NCMessage "Unable to load mailbox '$($member.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level ERROR
                        continue
                    }

                    $results.Add([pscustomobject]@{
                            Location                    = $group.Name
                            LocationPrimarySmtpAddress  = $group.PrimarySmtpAddress
                            RoomDisplayName             = $member.DisplayName
                            RoomPrimarySmtpAddress      = $member.PrimarySmtpAddress
                            RoomCapacity                = $mailbox.ResourceCapacity
                        }) | Out-Null
                }
            }

            Write-Progress -Activity "Processing room lists" -Completed

            if ($results.Count -eq 0) {
                Write-NCMessage "No room details found. Check filters or RoomList definitions." -Level WARNING
                return
            }

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

            if ($GridView.IsPresent) {
                try {
                    $results | Out-GridView -Title "M365 Rooms Details"
                }
                catch {
                    Write-NCMessage "Unable to show grid view. $($_.Exception.Message)" -Level WARNING
                }
            }

            if ($PassThru.IsPresent -or -not $Csv.IsPresent -or $GridView.IsPresent) {
                $results
            }
            elseif ($csvPath) {
                $csvPath
            }
        }
        finally {
            Restore-ProgressAndInfoPreferences
        }
    }
}

function Set-OoO {
    <#
    .SYNOPSIS
        Enables, schedules, or disables out-of-office replies for a mailbox.
    .DESCRIPTION
        Ensures Exchange Online connectivity, applies the provided internal/external messages,
        optionally schedules a start/end interval, or disables auto-replies entirely.
    .PARAMETER SourceMailbox
        Mailbox on which to configure out-of-office. Accepts pipeline input.
    .PARAMETER ChooseDayFromCalendar
        Opens a calendar popup to pick start and end dates for scheduled auto-replies.
    .PARAMETER InternalMessage
        HTML/text used for internal recipients. Defaults to the current configuration or a template.
    .PARAMETER ExternalMessage
        HTML/text used for external recipients. Defaults to InternalMessage when omitted.
    .PARAMETER StartTime
        Optional start time for scheduled auto-replies. Requires -EndTime.
    .PARAMETER EndTime
        Optional end time for scheduled auto-replies. Requires -StartTime.
    .PARAMETER ExternalAudience
        Audience for external replies: None, Known, All. Defaults to All.
    .PARAMETER Disable
        Disable auto-replies on the specified mailbox.
    .PARAMETER PassThru
        Emit the updated auto-reply configuration.
    .EXAMPLE
        Set-OoO -SourceMailbox info@contoso.com -InternalMessage "<p>Back on Monday</p>" -ExternalMessage "<p>Back soon</p>"
    .EXAMPLE
        Set-OoO -SourceMailbox info@contoso.com -Disable
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Enable')]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string]$SourceMailbox,
        [Parameter(ParameterSetName = 'Enable')]
        [switch]$ChooseDayFromCalendar,
        [Parameter(ParameterSetName = 'Enable')]
        [string]$InternalMessage,
        [Parameter(ParameterSetName = 'Enable')]
        [string]$ExternalMessage,
        [Parameter(ParameterSetName = 'Enable')]
        [Nullable[datetime]]$StartTime,
        [Parameter(ParameterSetName = 'Enable')]
        [Nullable[datetime]]$EndTime,
        [Parameter(ParameterSetName = 'Enable')]
        [ValidateSet('None', 'Known', 'All')]
        [string]$ExternalAudience = 'All',
        [Parameter(ParameterSetName = 'Disable', Mandatory)]
        [switch]$Disable,
        [switch]$PassThru
    )

    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 {
            $currentConfig = Get-MailboxAutoReplyConfiguration -Identity $SourceMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Unable to read current auto-reply configuration for '$SourceMailbox'. $($_.Exception.Message)" -Level ERROR
            return
        }

        if ($Disable.IsPresent) {
            if (-not $PSCmdlet.ShouldProcess($SourceMailbox, "Disable auto-replies")) {
                return
            }

            try {
                Set-MailboxAutoReplyConfiguration -Identity $SourceMailbox -AutoReplyState Disabled -ErrorAction Stop
                Write-NCMessage ("Disabled out-of-office for {0}." -f $SourceMailbox) -Level SUCCESS
            }
            catch {
                Write-NCMessage "Unable to disable out-of-office for '$SourceMailbox'. $($_.Exception.Message)" -Level ERROR
                return
            }

            $updatedDisable = Get-MailboxAutoReplyConfiguration -Identity $SourceMailbox -ErrorAction SilentlyContinue
            if ($PassThru.IsPresent -and $updatedDisable) { $updatedDisable }
            return
        }

        if (($StartTime.HasValue -and -not $EndTime.HasValue) -or (-not $StartTime.HasValue -and $EndTime.HasValue)) {
            Write-NCMessage "Both -StartTime and -EndTime must be provided to schedule auto-replies." -Level ERROR
            return
        }

        if ($StartTime.HasValue -and $EndTime.HasValue -and $StartTime.Value -ge $EndTime.Value) {
            Write-NCMessage "StartTime must be earlier than EndTime." -Level ERROR
            return
        }

        if ($ChooseDayFromCalendar.IsPresent -and ($StartTime.HasValue -or $EndTime.HasValue)) {
            Write-NCMessage "Use either -ChooseDayFromCalendar or -StartTime/-EndTime, not both." -Level ERROR
            return
        }

        $defaultTemplate = "I'm out of office and will have limited access to my mailbox.<br />I will reply to your e-mail as soon as possible.<br /><br />Have a nice day."
        $internal = if ($PSBoundParameters.ContainsKey('InternalMessage')) { $InternalMessage } else { $currentConfig.InternalMessage }
        if ([string]::IsNullOrWhiteSpace($internal)) { $internal = $defaultTemplate }

        $external = if ($PSBoundParameters.ContainsKey('ExternalMessage')) { $ExternalMessage } else { $currentConfig.ExternalMessage }
        if ([string]::IsNullOrWhiteSpace($external)) { $external = $internal }

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

            function Get-DateFromCalendar {
                param([string]$Title)

                $form = New-Object Windows.Forms.Form
                $form.Size = New-Object Drawing.Size @(220, 210)
                $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 = $Title

                $selectedDate = $null
                $form.Add_KeyDown({
                        if ($_.KeyCode -eq "Enter") {
                            Set-Variable -Name selectedDate -Value $calendar.SelectionStart -Scope 1
                            $form.Close()
                        }
                        elseif ($_.KeyCode -eq "Escape") {
                            $form.Close()
                        }
                    })

                [void]$form.ShowDialog()
                return $selectedDate
            }

            Write-NCMessage "Select the first day of absence in the popup and press Enter." -Level INFO
            $StartTime = Get-DateFromCalendar -Title "Select OoO start date"
            if (-not $StartTime) {
                Write-NCMessage "You must select at least one day from the calendar." -Level ERROR
                return
            }

            Write-NCMessage "Select the last day of absence in the popup and press Enter." -Level INFO
            $EndTime = Get-DateFromCalendar -Title "Select OoO end date"
            if (-not $EndTime) {
                Write-NCMessage "You must select at least one day from the calendar." -Level ERROR
                return
            }

            $state = 'Scheduled'
        }
        elseif ($StartTime.HasValue -and $EndTime.HasValue) {
            $state = 'Scheduled'
        }

        $setParams = @{
            Identity         = $SourceMailbox
            AutoReplyState   = $state
            InternalMessage  = $internal
            ExternalMessage  = $external
            ExternalAudience = $ExternalAudience
        }

        if ($state -eq 'Scheduled') {
            $setParams.StartTime = $StartTime.Value
            $setParams.EndTime = $EndTime.Value
        }

        $action = if ($state -eq 'Scheduled') {
            "Schedule out-of-office from $($StartTime.Value) to $($EndTime.Value)"
        }
        else {
            "Enable out-of-office replies"
        }

        if (-not $PSCmdlet.ShouldProcess($SourceMailbox, $action)) {
            return
        }

        try {
            Set-MailboxAutoReplyConfiguration @setParams -ErrorAction Stop
            Write-NCMessage ("Out-of-office {0} for {1}." -f $state.ToLowerInvariant(), $SourceMailbox) -Level SUCCESS
        }
        catch {
            Write-NCMessage "Unable to configure out-of-office for '$SourceMailbox'. $($_.Exception.Message)" -Level ERROR
            return
        }

        $updated = Get-MailboxAutoReplyConfiguration -Identity $SourceMailbox -ErrorAction SilentlyContinue
        if ($PassThru.IsPresent -and $updated) { $updated }
    }

    end { Restore-ProgressAndInfoPreferences }
}