Search-UnifiedAuditLog

Scenario: The search-MailboxAuditLog command has been deprecated and is no longer producing results. It is now replaced with the search-unifiedAuditLog which pulls in lots of data from various sources, not just Exchange.

I need a simple command to search a mailbox for activity and have it display results similar to before.

Commands:

#Pull Log data into a Variable from Exchange Online PowerShell
#I am running 3 separate searches since the logs cap out at 5000 and adding them all into the $logs variable

$Logs += Search-UnifiedAuditLog -StartDate 3/31/2025 -EndDate 4/3/2025 -UserIds steveman -RecordType ExchangeItem -ResultSize 5000

$Logs += Search-UnifiedAuditLog -StartDate 4/3/2025 -EndDate 4/5/2025 -UserIds steveman -RecordType ExchangeItem -ResultSize 5000

$Logs += Search-UnifiedAuditLog -StartDate 4/5/2025 -EndDate 4/8/2025 -UserIds steveman -RecordType ExchangeItem -ResultSize 5000

#The results that we are after are in JSON.  I am going to get it into a form that is usable within PowerShell or CSV:


 $logResults = foreach ($entry in $logs) {
    if ($entry.AuditData) {
        $entry | Add-Member -MemberType NoteProperty -Name AuditDataJson -Value (ConvertFrom-Json $entry.AuditData) -Force
        $entry
    }
}
#Now I am going to pick out the common stuff that I am after, feel free to add to it.

$Logs_Cleaned = $Logresults.auditdatajson | Select ClientInfoString, CreationTime, Operation, ResultStatus, ClientIP, userID, @{Name="Subject";Expression={$_ | Select -ExpandProperty Item | Select -expandproperty Subject}}, @{Name='ModifiedProperties';Expression={$_.ModifiedProperties -join ','}}

#Now to view it, export it, whatever you want to do, you can just reference the $logs_Cleaned variable.

$Logs_Cleaned

#-or

$Logs_Cleaned  | Out-gridview

#-or

$Logs_Cleaned | export-csv c:\temp\data.csv

Determine SafeAttachment Scan Time for an Email — Because its taking too long…

Scenario: We needed to create a way to identify how long it takes for an attachment to be scanned due to recent complaints. We need to produce a report that shows Message Delivery and Attachment Reinserting post delivery.

We have two SafeAttachment policies; a Block Policy with specific users in the SentTo and a different policy that acts as our default policy configured with Dynamic Delivery for all other users.

Specifically, the two complaints we are receiving are:
– Block Mode – Users are taking a while to receive a message
-Dynamic Delivery Mode – The attachment takes a while to re-insert itself into the message post delivery.

Below is a script that you could build off of


Script: The script below will produce a report that will identify the email messages; When the message was sent, When it was delivered to the mailbox, and when the Attachment was re-inserted into the message. For folks in the Block Policy, they will not have data for post-delivery actions but should experience a delay with the actual message delivery. For folks in Dynamic Delivery, they should have faster delivery times of the email message but have post-delivery events for re-inserting the attachment and the time this takes.

In this script, you will need to edit it to get it to work with your environment. The script is not perfect, nor do I care being the lazy scripter I am. I just need it to work in the environment.

The PreReqs are:

Azure Token – You need a way of obtaining an access token so you make a connection to a Azure Registered App that has permissions to Exchange AND Advanced Hunting Kql. We have a function to pull a token that is referenced in the building of the RestSplat.

Network Message ID – You will need the NetworkMessageID of the message you are targeting.

The Kql commands if you wish to run manually from the Advanced Hunting module instead of the PS script below are as follows:

1. Find the Email Event. Make sure you swap out the Recipient variable. I tend to find the NetworkMessageID from here.

EmailEvents | where RecipientEmailAddress == '$Recipient' and Subject == '$subject' and Timestamp >= $Received_Increment

2. Find the attachment info if you want to see it:
EmailAttachmentInfo | where RecipientEmailAddress == '$recipient' and NetworkMessageId == '$NetworkMessageId' and Timestamp >= $Received_Increment

3. Find the Postdelivery event where it reinserted the attachment:
EmailPostDeliveryEvents | where RecipientEmailAddress == '$Recipient' and NetworkMessageId == '$NetworkMessageID' and ActionTrigger == 'SpecialAction' and ActionResult == 'Success'

PS Script which wraps the Kql commands:

#PreReq - Fill in the Variables
    $AttachmentReport = @()
    $BlockUsers = Get-SafeAttachmentPolicy SafeAttachments_Block  | Get-SafeAttachmentRule |Select -ExpandProperty SentTo
    $UTC_Offset = -5  #I dont feel like messing around with the conversion code for TimeZones.
    $Received_Increment = '7d'  #Values include: 4h, 3d, etc.
    $NetworkMessageIDs = "0577a857-fc84-4731-b2e9-08dd04acd415","39da4538-640a-4dde-9edb-08dd04ad994d","5202afcb-54a3-476e-57c6-08dd04adda06","836b94fa-0d50-452a-8f76-08dd04ae3336","71b68dcc-1e35-4cb4-7d6d-08dd04bb2ef2"



#Loop through each Network MessageID and build a report.

$NetworkMessageIDs | %{
    $NetworkMessageID = $_
    "
Searching Message with NetworkMessageID = $NetworkMessageID
    
    " 
    
#0. Determine RecipientList
    Write-host "Pulling RecipientList" -ForegroundColor Green
    
    #0a. Build the KQL Query Body Command
        
        $q_Anti = "EmailEvents | where NetworkMessageId == '$NetworkMessageID' and Timestamp >= $Received_Increment"
        $body = ConvertTo-Json -InputObject @{ 'Query' = $q_Anti }
    
    #0b. Build API with KQL Body
        $appuri = "https://api.security.microsoft.com/api/advancedhunting/run"  #For Threat Mgmt
        $RestSplat = @{ 
                    URI         = $appuri
                    Headers     = $(get-accesstoken_AdvancedHunting)
                    Method      = 'POST' 
                    ContentType = "application/json" 
                    Body = $body 
                    }
    
    #0c. Execute API
            $response = Invoke-RestMethod @RestSplat
            $response = $response.Results
            
    #0d. Variable for RecipientList
        $RecipientList = $Response.RecipientEmailAddress 

    
If($RecipientList -ne $null){

#1. Loop Recipients
    #Create Report Variable
    

$Recipientlist | %{
    #1a. Declare Recipient Variable for each loop instance
    $Recipient = $_
    "

    Searching $recipient"

    $DeliveryAction = If($Recipient -in $BlockUsers){"Block"}else{"Dynamic_Deliver"}

    #2. KQL - Find Email Delivery Events
            "...Finding Email Delivery Events"
    
        #2a. Build the KQL Query Body Command
        $q_Anti = "EmailEvents | where RecipientEmailAddress == '$Recipient' and NetworkMessageId == '$NetworkMessageID' and Timestamp >= $Received_Increment"
        
        $body = ConvertTo-Json -InputObject @{ 'Query' = $q_Anti }
    
        #2b. Build API with KQL Body
        $appuri = "https://api.security.microsoft.com/api/advancedhunting/run"  #For Threat Mgmt
        $RestSplat = @{ 
                    URI         = $appuri
                    Headers     = $(get-accesstoken_AdvancedHunting)
                    Method      = 'POST' 
                    ContentType = "application/json" 
                    Body = $body 
                    }
    
        #2c. Execute API
            $response = Invoke-RestMethod @RestSplat
            $response = $response.Results
            

        #2d. Create variables based on Message Delivery
            $DeliveryTimeStamp = [datetime]$response.TimeStamp
            $NetworkMessageID = $response.NetworkMessageID
            $InternetMessageID = $response.InternetMessageId
            $Sender = $Response.SenderFromAddress
            $Subject = $Response.Subject
    

    #3. Pull MessageTraceDetail

            "...Message Trace"
            $MessageTrace_Message = Get-MessageTrace -RecipientAddress $recipient  -messageid $InternetMessageID 
            $MessageTrace_Detail = $messagetrace_Message | get-messagetracedetail
            $MessageTrace_Detail_Start = get-date($MessageTrace_Detail | Select -first 1 -ExpandProperty Date)
            $MessageTrace_Detail_End = get-date($MessageTrace_Detail | Where Event -like "Deliver" | Select  -ExpandProperty Date)
            $MessageTrace_DelayInSeconds = $MessageTrace_Detail_End - $MessageTrace_Detail_Start | Select -ExpandProperty TotalSeconds
            

    #3. KQL - Find Attachment Name
            "...Finding Attachment Data"
            #3a. Build the KQL Query Body Command
                $q_Anti = "EmailAttachmentInfo | where RecipientEmailAddress == '$recipient' and NetworkMessageId == '$NetworkMessageId' and Timestamp >= $Received_Increment"
                $body = ConvertTo-Json -InputObject @{ 'Query' = $q_Anti }
            
            #3b. Build API with KQL Body
                $appuri = "https://api.security.microsoft.com/api/advancedhunting/run"  #For Threat Mgmt
                $RestSplat = @{ 
                    URI         = $appuri
                    Headers     = $(get-accesstoken_AdvancedHunting)
                    Method      = 'POST' 
                    ContentType = "application/json" 
                    Body = $body 
                    }
    
            #3c. Execute API
                $response = Invoke-RestMethod @RestSplat
                $response = $response.Results
            
            #3d. Create variables
                $AttachmentFileName = $Response.FileName
            


    #4. KQL - Find POST Email Delivery Events
            "...Finding Post Email Delivery Data"
        #4a. Build the KQL Query Body Command
            $q_Anti = "EmailPostDeliveryEvents | where RecipientEmailAddress == '$Recipient' and NetworkMessageId == '$NetworkMessageID' and ActionTrigger == 'SpecialAction' and ActionResult == 'Success'"
            $body = ConvertTo-Json -InputObject @{ 'Query' = $q_Anti }
 
        #4b. Build API with KQL Body
            $appuri = "https://api.security.microsoft.com/api/advancedhunting/run"  #For Threat Mgmt
            $RestSplat = @{ 
                    URI         = $appuri
                    Headers     = $(get-accesstoken_AdvancedHunting)
                    Method      = 'POST' 
                    ContentType = "application/json" 
                    Body = $body 
                    }
    
        #4c. Execute API
            $response = Invoke-RestMethod @RestSplat
            $response = $response.Results
            
              
        #4d. Create variables based on Message Delivery
            If($response -ne $null){
                $UpdateTimeStamp = [datetime]$response.Timestamp
                $Delay_InSeconds = $UpdateTimeStamp - $DeliveryTimeStamp | Select -ExpandProperty TotalSeconds
                $Delay_InMinutes = "$($UpdateTimeStamp - $DeliveryTimeStamp | Select -ExpandProperty Minutes) Minutes and $($UpdateTimeStamp - $DeliveryTimeStamp | Select -ExpandProperty Seconds) Seconds"
            }else{$UpdateTimeStamp = "N/A";$Delay_InSeconds = "N/A";$Delay_InMinutes="N/A"}

    #5. Create Report
        "...Adding to report"
            $obj = new-object psObject
                    $obj | Add-Member -membertype noteproperty -Name NetworkMessageID -Value $NetworkMessageID
                    $obj | Add-Member -membertype noteproperty -Name Sender -Value $Sender
                    $obj | Add-Member -membertype noteproperty -Name Recipient -Value $Recipient
                    $obj | Add-Member -membertype noteproperty -Name Subject -Value $Subject
                    $obj | Add-Member -membertype noteproperty -Name SafeAttachment_Mode -Value $DeliveryAction
                    $obj | Add-Member -membertype noteproperty -Name Message_Sent -Value $(get-date(get-date($MessageTrace_Detail_Start)).AddHours($UTC_Offset) -Format "MM/dd/yyyy h:mm:ss tt")
                    $obj | Add-Member -membertype noteproperty -Name Message_Delivered -Value $(get-date(get-date($MessageTrace_Detail_End)).AddHours($UTC_Offset) -Format "MM/dd/yyyy h:mm:ss tt")
                    $obj | Add-Member -membertype noteproperty -Name Message_Delivery_InSeconds -Value $MessageTrace_DelayInSeconds
                    #$obj | Add-Member -membertype noteproperty -Name Kql_Delivery -Value $DeliveryTimeStamp
                    $obj | Add-Member -membertype noteproperty -Name SafeAttachment_Attachment_Inserted -Value $UpdateTimeStamp
                    $obj | Add-Member -membertype noteproperty -Name SafeAttachment_Scanning_InSeconds -Value $Delay_InSeconds
                    #$obj | Add-Member -membertype noteproperty -Name Delay_Time $Delay_InMinutes
                    $obj | Add-Member -membertype noteproperty -Name Attachment -Value $AttachmentFileName
            $AttachmentReport += $obj


            $MessageTrace_Detail_Start = $null
            $MessageTrace_Detail_End =  $null
            $MessageTrace_DelayInSeconds = $null
            $Recipient=$Null
            $Sender = $null
            $DeliveryTimeStamp=$Null
            $UpdateTimeStamp=$Null
            $Delay_InMinutes=$Null
            $AttachmentFileName=$Null

}



}Else{Write-host "Data is not available yet" -ForegroundColor Cyan}
}


#View Report
$AttachmentReport | Sort SafeAttachment_Mode|  Out-gridview
$AttachmentReport | Export-csv C:\temp\AttachmentReport.csv


Using Graph for PowerShell, move email messages from one folder to another

Scenario: Lets say you accidentally deleted the messages from your Inbox, and now they are mixed in with your Deleted Items. Here are a couple Graph commands you run via PowerShell to move data back to your Inbox without doing it via the email client.

Resolution:

#Connect to Graph, if you need to install:  Install-Module Microsoft.Graph -scope AllUsers
Connect-MgGraph

#Variables:
#1. Create Variables
$email = "steve@domain.com"
$folders = get-mgusermailfolder -userid $email -PageSize 999
$Folder_DI = $folders | where displayname -eq "Deleted Items" | Select -ExpandProperty Id
$Folder_In = $folders | where displayname -eq "Inbox" | Select -ExpandProperty Id
$count = 0
$messages = $null
$DaysRange = (Get-Date).AddDays(-2)
$time = Get-Date ($DaysRange).ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z'





#2. Loop through and move messages with modifieddate gt 3/4/2024

do
{
$count++
Write-Host "Pass Count: $Count"

$Messages = Get-MgUserMailFolderMessage -UserId $email -MailFolderId $folder_DI -filter "LastModifiedDateTime ge $time"
"Message Count:$($messages.count)"

#Move messages
$messages | %{
$i = $_.id
Move-MgUserMessage -MessageId $i -UserId $email -DestinationId $folder_in
}

}until ($Messages -eq $null)

Testing the ‘New Outlook’ client

Scenario: Below are my notes for testing the ‘New Outlook’ client within your Microsoft 365 version of Outlook. More to come soon.


As of 2/12/2024:

Client Protocol: ’New Outlook’ uses OWA. MAPI is the client protocol used in the ‘Old Outlook’ versions (Microsoft 365, 2019, 2016).


Search: I can easily search the Primary and Archive mailbox from a single Search


PST/OST Files: The ‘New Outlook’ does not yet support offline files


Sweep: Sweep is sweet. Since it’s OWA-based, you can sweep emails via this client.


Access to Shared Mailboxes: Yes, you can access shared mailboxes. However, it has moved to a folder called “Shared with me” in-line with the root folders of your mailbox.  Originally, shared mailboxes were opened a separate mailbox located on the navigational panel where your mailbox/folders exist.


Access to Archive Mailbox: Yes. It’s now located in the “In-Place Archive” folder inline with other root mail folders. It is no longer outside of the primary mailbox.


Look and Feel: It looks and feels like OWA. This client feels lightweight, and does not feel as heavy as the original Outlook client. Because it feels light, I am sure this increases the potential for this client to be missing something important to somebody. Common functionality seems to exist in this client.

It does open multiple ‘New Outlook’ windows if I accidentally click on the full client outlook icon multiple times. Thats annoying.


Rules: The rules in this ‘New Outlook’ are limited to rules you can create in OWA. The full client of Outlook has always been rich in its ruleset and offered conditions and features that were not available for server-side rules. These were commonly known as client-side rules. These rule bells and whistles that are available in the full client, do not appear to be available in the ‘New Outlook’.


CoPilot: The CoPilot technology within this ‘New Outlook’ client (And OWA on the Web) is pretty sweet. The suggestions that CoPilot offers to help draft a new message is helpful, BUT may be a little too formal for my liking. The summary by CoPilot, that automatically reads the email and provides the key points, have been pretty spot on. 

Using Graph to Cleanup Mailbox Folders

Scenario: You (or someone) have many mailbox folders that must be deleted via logic. Yeah, you have been storing mailbox folders over the years….. It’s out of hand, but no shame to you. Hence the reason why I am writing this post. 

Solution: Graph to the rescue! I use the following commandlets to help me out. 

Requirement: You need to be able to connect-mgGraph where you have access to the mailbox.

#Create a variable for your messy messy mailbox
$email = “steve@Messy.com”

#Pull all root folders into a variable (that is if the mess lives in the root of your mailbox)
$Primary_folders = get-mgusermailfolder -UserId $email -All

#Delete all folders where the name of the folder starts with “Search_”
$Primary_folders | where displayname -like “Search_*” | %{ “Removing $($_.DisplayName)”; Remove-mgusermailfolder -MailFolderId $($_.id) -UserId $email }



BUT WAIT, THERE’S MORE…

#What about those child folders that are messy and not sitting on the root of the mailbox. This time I have a ton of folders under my “Search” Folder: 

$folder = $primary_folders | Where displayname -eq “Search”

$Child_Folders = Get-MgUserMailFolderChildFolder -MailFolderId $($folder.id) -UserId $email -All


#Delete all subfolders that match a pattern in my displayname, such as an old year in the naming convention such as “/2018”.
$Child_Folders | Where displayname -like “*/201*” | %{ “removing $($_.DisplayName)”; Remove-mgusermailfolder -MailFolderId $($_.id) -UserId $email }

PowerShell + Logparser to extract successful authentication attempts for Exchange Protocols

Scenario: You want to use LogParser via PowerShell to extract the usernames for all successful authentication attempts to Exchange On-Premises.

Scriptlet:


#Servers
$Servers = get-exchangeserver | Sort Name

#results
$EWS_Results = @()
$MAPI_Results = @()
$OWA_Results = @()
$ECP_Results = @()
$PS_Results = @()
$EAS_Results = @()


#LogParser Loop
$servers.name | %{
    $n = $_
    $logs = get-childitem \\$_\c$\inetpub\logs\logfiles\W3SVC1\*.log

    $logs.fullname | %{
        $l = $_ 
"Searching logs on $l"
"...EWS"
$ews_results += & "C:\Program Files (x86)\Log Parser 2.2\logparser.exe" -i:IISW3C -q:on -rtp:-1 @"
SELECT count(*) as hits, cs-username  from '$l' Where SC-Status=200  AND  cs-uri-stem LIKE '%ews%' AND cs-username NOT LIKE '%healthmailbox%' AND cs-username NOT LIKE 'S-1%' GROUP BY cs-username order by hits desc
"@
"...MAPI"
$MAPI_results += & "C:\Program Files (x86)\Log Parser 2.2\logparser.exe" -i:IISW3C -q:on -rtp:-1 @"
SELECT count(*) as hits, cs-username  from '$l' Where SC-Status=200  AND  cs-uri-stem LIKE '%mapi%' AND cs-username NOT LIKE '%healthmailbox%' AND cs-username NOT LIKE 'S-1%' GROUP BY cs-username order by hits desc
"@

"...OWA"
$OWA_results += & "C:\Program Files (x86)\Log Parser 2.2\logparser.exe" -i:IISW3C -q:on -rtp:-1 @"
SELECT count(*) as hits, cs-username  from '$l' Where SC-Status=200  AND  cs-uri-stem LIKE '%owa%' AND cs-username NOT LIKE '%healthmailbox%' AND cs-username NOT LIKE 'S-1%' GROUP BY cs-username order by hits desc
"@

"...ECP"
$ecp_results += & "C:\Program Files (x86)\Log Parser 2.2\logparser.exe" -i:IISW3C -q:on -rtp:-1 @"
SELECT count(*) as hits, cs-username  from '$l' Where SC-Status=200  AND  cs-uri-stem LIKE '%ecp%' AND cs-username NOT LIKE '%healthmailbox%' AND cs-username NOT LIKE 'S-1%' GROUP BY cs-username order by hits desc
"@

"...PS"
$PS_results += & "C:\Program Files (x86)\Log Parser 2.2\logparser.exe" -i:IISW3C -q:on -rtp:-1 @"
SELECT count(*) as hits, cs-username  from '$l' Where SC-Status=200  AND  cs-uri-stem LIKE '%powershell%' AND cs-username NOT LIKE '%healthmailbox%' AND cs-username NOT LIKE 'S-1%' GROUP BY cs-username order by hits desc
"@

"...EAS"
$EAS_results += & "C:\Program Files (x86)\Log Parser 2.2\logparser.exe" -i:IISW3C -q:on -rtp:-1 @"
SELECT count(*) as hits, cs-username  from '$l' Where SC-Status=200  AND  cs-uri-stem LIKE '%activesync%' AND cs-username NOT LIKE '%healthmailbox%' AND cs-username NOT LIKE 'S-1%' GROUP BY cs-username order by hits desc
"@
    }
}

#View:
$EWS_Results
$MAPI_Results
$OWA_Results 
$ECP_Results 
$PS_Results 
$EAS_Results

Example of pulling a list of all Scheduled Tasks via PowerShell

Scenario: I have a bunch of scripts that run via Scheduled Tasks. I need to identify what account the scripts are executing under AND what script is running.

My 2 goals are:
1. Identify the security account used to run the script. I will then manually replace the older account with my newer account.

2. Identify a list of scripts and perform a search in each script to see if there are any references to an older server listed. I will then manually replace the value if the old server name is listed,


Resolution:

Goal 1: The $Tasks below will show me the script location AND which security account the script runs under.

#Identify a list of Tasks where I manually specify the task path for where I put my tasks:

        $Tasks = @()
        $Tasks = Get-ScheduledTask -TaskPath "\Custom Tasks\*" | Where state -ne "Disabled"
        $Tasks = Get-ScheduledTask -TaskPath "\Exchange Tasks\*" | Where state -ne "Disabled"
        $Tasks += Get-ScheduledTask -TaskPath "\"| Where state -ne "Disabled"


#Pull in the Info
        $Tasks = $tasks | where taskname -notlike user_feed* | Select TaskPath, TaskName,State,Author,@{Name="Run_As";Expression={$_.Principal | Select -expandproperty UserID}},@{Name="Script_Path";Expression={(($_.Actions | Select -expandproperty Arguments) -replace '-executionpolicy unrestricted -file ') -replace '-file '}}


#Display the Task Info

       $Tasks
        


Goal 2: For each script, perform a search to find a specific value. If the value is found, record it in $Results

#Identify Script Locations
        $scripts = @()
        $scripts = $Tasks.Script_Path       
        

#Find the value in each script and record it in $Results

        $str = "ExServer1"
        $results = @()
        $Scripts | %{
            $fn = $_
            $str | %{
                $s = $_
                $D = get-content $fn | Select-String $s
                If($D -ne $Null){
                    Write-Host "
                    Found String in File:  $fn" -ForegroundColor Cyan
                    $results += $fn
                    } #ENdIF
            }##end $Str loop
 }#End get-childitem loop



#Display the results
     $Results

Error: “Install-Package : Unable to find repository. Use Get-PSRepository to see all available repositories”

Scenario: We just discovered that our Exchange Online Mangement tools were no longer connecting. When we would run connect-exchangeonline we would receive an error message stating something similar to: ResourceUnavailable: (:) [New-ExoPSSession]

After investigation, we saw that the ExchangeOnlineManagment module was an older module, so we decided to try to update the module. When we ran this command Update-Module ExchangeOnlineManagment we ran into this Error:

PackageManagement\Install-Package : Unable to find repository ‘https://www.powershellgallery.com/api/v2’. Use Get-PSRepository to see all available repositories.

Solution: In order to fix this, I had to register and trust the PSGallery repository, and then I was successful with funning this module:

[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

Register-PSRepository -Default -Verbose

Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted

Update-Module ExchangeOnlineManagement



Mail Flow Error: 450 4.4.317 MSG=UntrustedRoot

Scenario: Messages are getting deferred in Exchange Online when trying to send to our newly added Hybrid Exchange On-Premises servers. When investigating, we receive UntrustedRoot in the details of the connection error.

Cause/Solution: Although the new Hybrid Exchange On-Premises servers have the correct Exchange Certificate installed, we still needed to add the TLSCertificateName AND the TLSDomainCapabilities values into the Front-End Connector for each of the new servers.

For Troubleshooting purposes, here is what we did:

1. Ran the Get-exchangecertificate on each server and verified that SMTP was associated with the correct third-party certificate. It is represented with an S (for smtp) in the services.

2. Ran the Get-transportservice to verify the InternalTransportCertificateThumbprint is the correct thumbprint of the cert that we verified in step 1.

3. Ran the Get-ReceiveConnector on the received connector that should be receiving SMTP connections from Exchange Online (For us it was the default front end). THIS WAS OUR SOLUTION: We had to set the TLSCertificateName AND TLSDomainCapabilities properties on this receive connector:

TLSCertificateName = “CN=<cert identifiable value”
TLSDomainCapabilities = “mail.protection.outlook.com:AcceptCloudServicesMail”

PowerShell: Remove a single value from an Array property where it exists for multiple datasets

Scenario: You are running a get-mailbox command and you want to remove a specific array value for reporting purposes, in our case it was a InPlaceHolds value, from any mailbox where this value exists. Example: We want to remove this Hold GUID from the report for visibility to not confuse folks when they see it for each mailbox: mbxf246564cda4f41faae611cb6e6198a2f:2

Scriptlet: Here is how we did that.

#0. Collect all Mailboxes via Get-Mailbox
$AllMBX += get-exomailbox -Properties alias,LitigationHoldEnabled,InPlaceHolds,recipienttypedetails -ResultSize unlimited 



#1. Loop through AllMbx and remove the value that we do not want
$Allmbx | Where InPlaceHolds -ne $null | %{
            "$_.Name"
            $IPH = $_.InPlaceHolds
            $IPH.Remove("mbxf246564cda4f41faae611cb6e6198a2f:2") 
            }



#2. Now, when you display $allMbx, you will see that this value no longer exists for the dataset
$AllMbx  | Select Alias, InPlaceHolds