Apps to query exchange message tracking

Product: PowerShell Universal
Version: 5.5.4

Hi,
Has anyone built an app that allows users to poll the on prem exchange server for messagetracking logs?

I am looking at building one and as Enter-PSSESSION doesnt work and Invoke-Command doesnt bring anything back when trying to query the server I am struggling. I have attempted to poll a csv output that is perodically ran but as its over tens of thousands of lines its really slow but is doable.

If anyone is able to share sanatised code it would be really helpful.

Thank you.

I wrote this back in the PSU 3.0 days and I sanitized it so you might wanna test it all out (maybe update some stuff too), but it should get you going.

New-UDPage -Name MessageTracking -Content {
    $Session:MessageTrackingShowTable = $null
    $Session:MessageTrackingData = $null

    New-UDForm -SubmitText "Search" -ButtonVariant 'contained' -Content {
        New-UDRow -Columns {
            New-UDColumn -SmallSize 3 -LargeSize 3 -Content {
                New-UDTextbox -Id "Sender" -Placeholder "Sender"
            }
            New-UDColumn -SmallSize 3 -LargeSize 3 -Content {
                New-UDTextbox -Id "Subject" -Placeholder "Subject"
            }
            New-UDColumn -SmallSize 3 -LargeSize 3 -Content {
                New-UDTextbox -Id "Recipient" -Placeholder "Recipient"
            }
            New-UDColumn -SmallSize 3 -LargeSize 3 -Content {
                New-UDTextbox -Id "MessageID" -Placeholder "Message ID"
            }
        }      
        New-UDRow -Columns {           
            New-UDColumn -SmallSize 3 -LargeSize 3 -Content {
                New-UDDatePicker -Id "StartDate" -Label "Start Date"
            }
            New-UDColumn -SmallSize 3 -LargeSize 3 -Content {
                New-UDTimePicker -Id "StartTime" -Label "Start Time" -Value ((Get-Date).ToString())
            }
            New-UDColumn -SmallSize 3 -LargeSize 3 -Content {
                New-UDDatePicker -Id "EndDate" -Label "End Date"
            }            
            New-UDColumn -SmallSize 3 -LargeSize 3 -Content {
                New-UDTimePicker -Id "EndTime" -Label "End Time" -Value ((Get-Date).ToString())
            }
        }
        New-UDRow -Columns {   
            New-UDColumn -SmallSize 4 -LargeSize 4 -Content {
                New-UDSelect -id "EventID" -Label "EventID" -Option {
                    New-UDSelectOption -Name "ALL" -Value "ALL"
                    New-UDSelectOption -Name "BADMAIL" -Value "BADMAIL"
                    New-UDSelectOption -Name "AGENTINFO" -Value "AGENTINFO"
                    New-UDSelectOption -Name "DEFER" -Value "DEFER"
                    New-UDSelectOption -Name "DELIVER" -Value "DELIVER"
                    New-UDSelectOption -Name "DSN" -Value "DSN"
                    New-UDSelectOption -Name "EXPAND" -Value "EXPAND"
                    New-UDSelectOption -Name "FAIL" -Value "FAIL"
                    New-UDSelectOption -Name "RECEIVE" -Value "RECEIVE"
                    New-UDSelectOption -Name "RESOLVE" -Value "RESOLVE"
                    New-UDSelectOption -Name "SEND" -Value "SEND"
                    New-UDSelectOption -Name "SUBMIT" -Value "SUBMIT"
                    New-UDSelectOption -Name "TRANSFER" -Value "TRANSFER"
                }
            }
            New-UDColumn -SmallSize 4 -LargeSize 4 -Content {
                New-UDButton -Text "Event ID Info" -OnClick {
                    Show-UDModal -Content {
                        New-UDList -Content {
                            New-UDListItem -Label 'BADMAIL' -SubTitle "A message submitted by the Pickup directory or the Replay directory that can't be delivered or returned."
                            New-UDListItem -Label 'AGENTINFO' -SubTitle "This event is used by transport agents to log custom data."
                            New-UDListItem -Label 'DEFER' -SubTitle "Message delivery was delayed."
                            New-UDListItem -Label 'DELIVER' -SubTitle "A message was delivered to a local mailbox."
                            New-UDListItem -Label 'DSN' -SubTitle "A delivery status notification (DSN) was generated."
                            New-UDListItem -Label 'EXPAND' -SubTitle "A distribution group was expanded."
                            New-UDListItem -Label 'FAIL' -SubTitle "Message delivery failed. Sources include SMTP, DNS, QUEUE, and ROUTING."
                            New-UDListItem -Label 'RECEIVE' -SubTitle "A message was received by the SMTP receive component of the transport service or from the Pickup or Replay directories (source: SMTP), or a message was submitted from a mailbox to the Mailbox Transport Submission service (source: STOREDRIVER)."
                            New-UDListItem -Label 'RESOLVE' -SubTitle "A message's recipients were resolved to a different email address after an Active Directory lookup."
                            New-UDListItem -Label 'SEND' -SubTitle "A message was sent by SMTP between transport services."
                            New-UDListItem -Label 'SUBMIT' -SubTitle "The Mailbox Transport Submission service successfully transmitted the message to the Transport service. "
                            New-UDListItem -Label 'TRANSFER' -SubTitle "Recipients were moved to a forked message because of content conversion, message recipient limits, or agents. Sources include ROUTING or QUEUE."
                        }
                    } -Footer {
                        New-UDButton -Text "Close" -OnClick { Hide-UDModal }
                    }
                }
            }
            New-UDColumn -SmallSize 4 -LargeSize 4 -Content {
                New-UDButton -Text "Source Value Info" -OnClick {
                    Show-UDModal -Content {
                        New-UDList -Content {
                            New-UDListItem -Label 'ADMIN' -SubTitle "The event source was human intervention. For example, an administrator used Queue Viewer to delete a message, or submitted message files using the Replay directory."
                            New-UDListItem -Label 'AGENT' -SubTitle "The event source was a transport agent."
                            New-UDListItem -Label 'DNS' -SubTitle "The event source was DNS."
                            New-UDListItem -Label 'DSN' -SubTitle "The event source was a delivery status notification (also known as a DSN, bounce message, non-delivery report, or NDR)."
                            New-UDListItem -Label 'QUEUE' -SubTitle "The event source was a queue."
                            New-UDListItem -Label 'ROUTING' -SubTitle "The event source was the routing resolution component of the categorizer in the Transport service."
                            New-UDListItem -Label 'SMTP' -SubTitle "The message was submitted by the SMTP send or SMTP receive component of the transport service."
                            New-UDListItem -Label 'STOREDRIVER' -SubTitle "The event source was a MAPI submission from a mailbox on the local server."
                        }
                    } -Footer {
                        New-UDButton -Text "Close" -OnClick { Hide-UDModal }
                    }
                }
            }
        }
    } -OnSubmit {
        $PSTargetServer = "ServerName"
        [string]$PSConnectionURI = "http://$PSTargetServer/powershell?serializationLevel=Full;clientApplication=ManagementShell;TargetServer=$PSTargetServer;PSVersion=3.0"
        $EXPSSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $PSConnectionURI -Authentication Kerberos
        Import-PSSession $EXPSSession -AllowClobber -WarningAction:SilentlyContinue -DisableNameChecking -CommandName Get-MessageTrackingLog | Out-Null

        [DateTime]$StartDate = $EventData.StartDate
        [DateTime]$StartTime = $EventData.StartTime
        [DateTime]$EndDate = $EventData.EndDate
        [DateTime]$EndTime = $EventData.EndTime

        [DateTime]$StartDateTime = $StartDate.ToShortDateString() + " " + $StartTime.ToShortTimeString()
        [DateTime]$EndDateTime = $EndDate.ToShortDateString() + " " + $EndTime.ToShortTimeString()

        if ($StartDateTime -gt $EndDateTime)
        {            
            Show-UDToast -Message "Start DateTime must be before End DateTime" -Persistent
        }
        elseif (($EventData.Sender -eq $null -or $EventData.Sender -eq '') -and ($EventData.Recipient -eq $null -or $EventData.Recipient -eq ''))
        {
            Show-UDToast -Message "Must include Sender or Recipient in search" -Persistent
        }
        else
        {
            $MessageParams = @{
                Start               = $StartDateTime
                End                 = $EndDateTime
                ErrorAction        = 'Stop'
            }
            
            if ($EventData.Subject -ne $null -or $EventData.Subject -ne '')
            {
                $MessageParams.MessageSubject = $EventData.Subject
            }
            if ($EventData.Sender -ne $null -or $EventData.Sender -ne '')
            {
                $MessageParams.Sender = $EventData.Sender
            }
            if ($EventData.MessageId -ne $null -or $EventData.MessageId -ne '')
            {
                $MessageParams.MessageId = $EventData.MessageId
            }
            if ($EventData.Recipient -ne $null -or $EventData.Recipient -ne '')
            {
                $MessageParams.Recipients = $EventData.Recipient
            }
            if ($EventData.EventId -ne "ALL")
            {
                $MessageParams.EventId = $EventData.EventId
            }

            Try {            
                $MessageSearchResults = Get-MessageTrackingLog @MessageParams

                $Session:MessageTrackingData = [System.Collections.ArrayList]::new()
                foreach ($Message in $MessageSearchResults)
                {
                    [void]$Session:MessageTrackingData.Add([pscustomobject][ordered]@{
                            Sender         = $Message.Sender
                            Recipients     = $Message.Recipients -join ";"
                            Directionality = $Message.Directionality
                            Timestamp      = $Message.Timestamp.ToString()
                            EventId        = $Message.EventId
                            Subject        = $Message.Subject
                            Source         = $Message.Source
                            ServerHostName = $Message.ServerHostName
                            TotalBytes     = $Message.TotalBytes
                            MessageId      = $Message.MessageId
                            ReturnPath     = $Message.ReturnPath
                            SourceContext  = $Message.SourceContext
                            ConnectorId    = $Message.ConnectorId
                        })
                }
                $Session:MessageTrackingShowTable = $true
                Sync-UDElement -Id 'MessageTrackingResults'
            }
            Catch {
                Show-UDToast -Message "Error during message tracking search: $($_.Exception.Message)" -BackgroundColor Red
                break
            }
        }
    }
    New-UDDynamic -Id 'MessageTrackingResults' -Content {
        if ($Session:MessageTrackingShowTable -and $Session:MessageTrackingData)
        {
            $MessageTrackingColumns = @(
                New-UDTableColumn -Property Directionality -Title Directionality -Filter -FilterType select -IncludeInExport
                New-UDTableColumn -Property Sender -Title Sender -Filter -FilterType text -IncludeInExport
                New-UDTableColumn -Property Recipients -Title Recipients -Filter -FilterType text -IncludeInExport
                New-UDTableColumn -Property Subject -Title Subject -Filter -FilterType text -IncludeInExport
                New-UDTableColumn -Property Timestamp -Title Timestamp -Filter -FilterType date -DefaultSortColumn -IncludeInExport
                New-UDTableColumn -Property EventId -Title EventId -Filter -FilterType select -IncludeInExport
                New-UDTableColumn -Property Source -Title Source -Filter -FilterType select -IncludeInExport
                New-UDTableColumn -Property ServerHostName -Title ServerHostName -Filter -FilterType select -IncludeInExport
                New-UDTableColumn -Property TotalBytes -Title TotalBytes -Filter -FilterType range -IncludeInExport
                New-UDTableColumn -Property MessageId -Title MessageId -Filter -FilterType text -Hidden -IncludeInExport
                New-UDTableColumn -Property ReturnPath -Title ReturnPath -Filter -FilterType text -Hidden -IncludeInExport
                New-UDTableColumn -Property SourceContext -Title SourceContext -Filter -FilterType text -Hidden -IncludeInExport
                New-UDTableColumn -Property ConnectorId -Title ConnectorId -Filter -FilterType text -Hidden -IncludeInExport
            )
            New-UDTable -Data $Session:MessageTrackingData -Columns $MessageTrackingColumns -ShowExport -ShowSort -Paging -PageSize 50 -ShowFilter
        }
    }
}

UI looks like this and will load the messages into a table at the bottom if it finds results

Thank you, ill give it a try.

So I figured it out but what I cannot figure out is why I get access denied doing a pSSESSIon.
When I manually do it outside of PS it connects. When I do it in PS it says access denied.
I am pulling a secret from the variables portions that is a PSCredential.

The details are correct but it still says access denied.

We ended up moving all Exchange functions into automation scripts then have the UI call the scripts. Since you can set environments and credentials to use for each script. It resolved all the hassle of remote powershell sessions

Hi,

I hope my English is sufficient.

In my setup, PSU runs as a Windows service on a server where the Exchange Management Tools are installed. The PSU service runs under a service account that has the required Exchange permissions.

When Exchange cmdlets need to be executed directly from a dashboard, I load a dedicated PSU environment that runs a standard Windows PowerShell 5.1 session. As the startup script for this environment, I execute the following code:

. "E:\Program Files\Microsoft\Exchange Server\V15\bin\RemoteExchange.ps1" # adjust path as needed
Connect-ExchangeServer -ServerFqdn EXCHANGESERVERNAME # adjust server name as needed

This approach has worked best for me. When using PSSession directly, I ran into similar issues as you described.

I solved message tracking differently. Since a large number of servers (>50) need to be queried, the workload is parallelized using jobs. The frontend is similar to what was described earlier; when the form is submitted, the script is triggered:

-OnSubmit {
    # build htParameter hashtable
    ...
    ...

    $job = Invoke-PSUScript -Script 'Get-CCMSMessageTracking.ps1' -htParameter $htParameter
    $page:JobID = $job.Id

    Sync-UDElement -Id 'dynJobsRunning'
    Sync-UDElement -Id 'dynResultTable'
}

Get-CCMSMessageTracking.ps1:

param([hashtable]$htParameter)

Import-Module CCMS-ScriptModule

foreach ($server in $alleExchangeServer.Name) {
    $null = Invoke-Command -ComputerName $server -AsJob -ScriptBlock {
        $s = New-PSSession `
            -Name "Session_$($using:server)" `
            -ConfigurationName Microsoft.Exchange `
            -ConnectionUri http://$($using:server)/PowerShell/ `
            -Authentication Kerberos `
            -Credential $using:credCCMS

        $null = Import-PSSession $s -CommandName Get-MessageTrackingLog
        Get-MessageTrackingLog -Server $using:server @using:htParameter
        Remove-PSSession -Name "Session_$($using:server)"
    } -JobName "Job_MessagingTracking_$server" -Authentication Kerberos
}

$results = Get-Job -Name "Job_MessagingTracking_*" |
           Wait-Job |
           Receive-Job |
           Select-Object TimeStamp, Sender, Recipients, MessageSubject, EventID,
                         ServerHostname, MessageID, Source, SourceContext

Get-Job -Name "Job_MessagingTracking_*" | Remove-Job

$results

Within the script module, the Exchange server names are stored as a global variable ($alleExchangeServer.Name), and the service account credentials are stored as a credential object ($credCCMS). Each mailbox server returns its own message tracking results.

Once the PSU job has completed, the results can be retrieved and displayed as a table in the dashboard.

Since other Exchange scripts are also executed as PSU jobs, I implemented a dedicated job overview page for our admins. This page allows previous jobs to be reviewed, displayed, or re-run. For long-running scripts, users can leave the page and later return via the job page once execution has completed. A job badge in the menu bar indicates whether jobs are still running in the context of the current admin user.

I hope this helps.