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)

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 }

Configure a Distribution Group to send a reply message every time (Not an Out-of-Office Message)

Scenario: You need a distribution group to send a ‘specific’ reply message back to the sender, every time a sender sends to it. An Out-of-Office message will not do because it will only send a reply one time.

Note: Distribution Groups do not have this functionality, however there is a work around with using a shared mailbox.

Solution:
1. Create a Shared Mailbox that will host sending the reply messages. We will call it: ReplyMessage@steveman.com

2. Log into an Outlook client as the shared mailbox and create the following inbox rule:

“Apply this rule after the message arrives
have server reply using “blah blah blah


3. Even though this is a Inbox Rule, set your Distribution Group so it allows sending Out-of-Office (OOF) messages which is turned off by default. This is required for return messages:

Set-DistributionGroup DL01@steveman.com -SendOofMessageToOriginatorEnabled:$true

4. Create a Transport Rule OR add that shared mailbox to your distribution list. I chose the Transport Rule because it is cleaner to the end users — they wont start asking questions about this new account. We are using the AnyOfToCCHeader to trigger the rule if it the Distribution list is sent to.

New-TransportRule -name “BCC_Reply” -AnyOfToCCHeader DL01@steveman.com -blindcopyto ReplyMessage@steveman.com

Error: The request was aborted: Could not create SSL/TLS secure channel

Scenario: You are trying to access or download from a URL within PowerShell and you receive this error message:

The request was aborted: Could not create SSL/TLS secure Channel


Solution: Use TLS1.2 in your PowerShell to initiate the download.

#verifies that TLS1.2 is available within your PowerShell
	[Enum]::GetNames([Net.SecurityProtocolType]) -contains 'Tls12'

#If True, Checks to see if TLS1.2 is in use with PS
	[System.Net.ServicePointManager]::SecurityProtocol.HasFlag([Net.SecurityProtocolType]::Tls12)

#If False Run the following to enable the use of TLS1.2
	[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

#Check again for True
[System.Net.ServicePointManager]::SecurityProtocol.HasFlag([Net.SecurityProtocolType]::Tls12)

#Then attempt to initate the install
iex (New-Object Net.WebClient).DownloadString("https://gist.github.com/darkoperator/6152630/raw/c67de4f7cd780ba367cccbc2593f38d18ce6df89/instposhsshdev")

 Error:   Remove-ADUser : The directory service can perform the requested operation only on a leaf object. 

Scenario: When cleaning up Active Directory accounts, you receive the following error:

 Remove-ADUser : The directory service can perform the requested operation only on a leaf object. 

Solution: Our issue is caused by mobile devices being attached within these user accounts. Instead for using the Remove-ADUser commandlet, use the Remove-ADObject with a -recursive commandlet to get ad objects, such as mobile devices, that are attached.

Remove-ADObject “CN=buhbye,OU=Disabled,DC=domain,DC=com” -confirm:$false -recursive

New-ApplicationAccessPolicy

Scenario: A registered App within Azure was created that has various ‘Application’ Exchange Permissions granted to it. You want to apply a scope to that registered app so the app only has permissions to specific mailbox, and not every mailbox by default

Scriptlet: Use the New-ApplicationAccessPolicy to Restrict Access

New-ApplicationAccessPolicy -AccessRight RestrictAccess -AppId “<client app id>” -PolicyScopeGroupId grp-app_Mbx_access -Description “Restrict this app to members of security group grp-app_MBX_access.”

SMTP Error: ‘550 5.7.54 SMTP; Unable to relay recipient in non-accepted domain’ ·

Scenario: After standing up a new Exchange On-Premises Server, users are receiving the bounce back message with wording similar to the following:

For Email Admins: The message couldn’t be sent because it’s an attempt to relay a message to a recipient in a non-accepted domain (open relay) which isn’t allowed.

-or-

‘550 5.7.54 SMTP; Unable to relay recipient in non-accepted domain’

Solution: Make sure the Default Frontend Receive Connector is set to accept AnonymousUsers when connecting AND the ADPermission for AnonymousLogon is applied to the Receive Connector on the new server:

Set-ReceiveConnector “ExSrv1\Default Frontend ExSrv1” -PermissionGroups AnonymousUsers

Get-ReceiveConnector “ExSrv1\Default Frontend ExSrv1” | Add-ADPermission -User ‘NT Authority\Anonymous Logon’ -ExtendedRights MS-Exch-SMTP-Accept-Any-Recipient