Bulk deletion of an email from mailboxes

I have moved the site from pwsh.pro to ramiyer.io. Google may still point to the old site URL in the results page, while it transitions the property.

I have been an Exchange administrator. (And I have fond memories of those days.) Exchange is often an overlooked, underappreciated aspect of Enterprise IT administration. Of course, this is 2020, and we are steadily moving away from emails, replacing it with the likes of Teams, Slack, Wire and what not. I had read an article a long time ago (which I cannot find anymore) that pointed out the inherent flaws in emails; emails were never meant to be secure.

As a natural consequence, spoofers spoof, phishers phish (?) and users fall prey. Despite proudly running a stable Exchange environment, one ticket woke us up in the night or interrupted our weekends—it had nothing to do with the Exchange system itself, but: Email deletion.

The background

Once in a while, a bulk of users in the environment would receive an email looking like one from an executive, or the Purchasing team would receive a rather large quote, or Sales an order running in thousands of dollars. One of the emails we had received was from someone posing as the CEO asking for an iTunes gift card of USD 1000 purchased on his behalf.

Enough story; now, business. Before you do anything about the malicious act itself, you remove the email from all the mailboxes in the Exchange environment so that unsuspecting users do not fall prey. In case of a large environment, it may not be a good idea to query all the mailboxes—tens or hundreds of thousands of them. Of course, it might be simpler with Office 365, but this article focuses on on-premises Exchange. In such a case, if you know the recipients, you would need to perform a clean-up on targeted mailboxes.

Understand that a bulk clean-up operation on mailboxes will use a significant amount of resources. User education is important. As are the necessary records in your DNS (SPF and DKIM records) and other fences like DMARC. No matter what you do, email exchange has serious security limitations. Administrative deletion of emails in Exchange is still a necessity.

The analysis

Our discussion gives us two situations:

  1. Small- or medium-sized environment, with less than ten thousand mailboxes.
  2. Large environment, but targeted set of users.

Let us say that yours is a large environment. The phisher sent an email to about two hundred users. Your CIO was one among the recipients. You have the email, but not the list of recipients.

A good Exchange administrator runs a quick trace of the message. The Exchange Online Protection (EOP) console can help you with this if you are a GUI person. Or you could use PowerShell cmdlets to run a trace within the environment. A quick glance of the email headers and checking the Transport logs would give you the details.

In the former case, or in case you have bulked-up Exchange servers loaded with resources, you could also run a search of the email across the mailboxes in large environments. I would suggest using your judgement. A good Exchange administrator knows his environment like the back of his hand, and based on the data from the trace report, can decide on which approach to take.

The solution

Removing an email from a bulk of mailboxes (or all mailboxes) in the Exchange environment involves the following steps:

  1. Search for mailboxes that have the email (either targeted search or search across all mailboxes—your judgement matters here).
  2. Generate a report of mailboxes that have this email.
  3. Include forwarded/responded emails as well (because some emails get forwarded to others as part of delegation)
  4. Log this information in the Incident report.
  5. Seek necessary approvals with this data.
  6. Perform deletion based on the approval.

The best source for the subjects of automation is your SOP document. Almost every environment has an SOP for email deletions. Different environments have different methods, but this is the gist.

The script

I wrote a PowerShell function that would search for the email by subject, sender, recipient, and the date on which (or the date range within which) the users received the email. The function would also take the email address of the report recipient, and optionally, the name of the folder in which to store the search results, and whether to delete the email in bulk, along with the safeguards, -WhatIf and -Confirm parameters (a.k.a. ShouldProcess).

First, the script (also available in my GitHub repository), and then, the explanation:

function Remove-Email {
    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High'
    )]
    param (
        # Username
        [Parameter(Mandatory=$true)]
        [string]
        $Sender,

        # Recipients; may be mailbox or distribution group
        [Parameter(Mandatory=$false)]
        [string]
        $Recipient,

        # Subject of the email
        [Parameter(Mandatory=$true)]
        [string]
        $Subject,

        # Date from
        [Parameter(Mandatory=$false)]
        [string]
        $Start = (Get-Date -Format d),

        # Date to
        [Parameter(Mandatory=$false)]
        [string]
        $End = (Get-Date -Format d),

        # Delete the email
        [Parameter(Mandatory=$false)]
        [switch]
        $Delete,

        # Report recipients
        [Parameter(Mandatory=$true)]
        [string]
        $ReportRecipient,

        # Folder to store the search results in
        [Parameter(Mandatory=$false)]
        [string]
        $TargetFolder = 'SearchResults'
    )

    begin {
        try {
            Get-ExchangeServer $env:COMPUTERNAME -ErrorAction Stop | Out-Null
        }
        catch {
            Write-Error "Log into an Exchange server with an account that has the Discovery Management role assigned, and run this on the Exchange Management Shell."
            break
        }

        try {
            Get-Mailbox $ReportRecipient -ErrorAction Stop | Out-Null
        }
        catch {
            Write-Error "Unable to locate the mailbox for $ReportRecipient. Process aborted."
            break
        }
    }

    process {
        if ($Subject -match '^(FW:|RE:)') {
            $Subject = $Subject -replace '^(FW:|RE:)' -replace '^\ '
        }

        $QueryString = "FROM:`"$Sender`" AND SUBJECT:`"$Subject`" AND RECEIVED:$Start..$End"

        if ($Recipient) {
            $RecipientType = (Get-Recipient $Recipient).RecipientType

            switch ($RecipientType) {
                'MailUniversalDistributionGroup' {
                    $Command = "Get-DistributionGroupMember '$Recipient' | Get-Mailbox"
                    break
                }
                'MailNonUniversalGroup' {
                    $Members = Get-ADGroupMember $(Get-Recipient $Recipient).Name | ForEach-Object { Get-Mailbox $PsItem.SamAccountName }
                    $Command = '$Members'
                    break
                }
                'UserMailbox' {
                    $Command = "Get-Mailbox '$Recipient'"
                }
                Default {
                    Write-Error "Invalid recipient type. Please check the Recipient parameter."
                    break
                }
            }
        }
        else {
            $Command = "Get-Mailbox -ResultSize Unlimited"
        }

        $Command += " | Search-Mailbox -SearchQuery '$QueryString' -TargetMailbox '$ReportRecipient' -TargetFolder $TargetFolder -LogLevel Full -LogOnly"

        $EmailStats = Invoke-Expression -Command $Command | Measure-Object -Property ResultItemsCount -Sum

        # Check if the admin wants the email deleted; don't delete it right away.
        if ($Delete) {
            if ($PSCmdlet.ShouldProcess("$($EmailStats.Sum) emails with subject, '$Subject' from $($EmailStats.Count) mailboxes", "Delete")) {
                $Command = $Command.Replace(" -TargetFolder $TargetFolder -LogLevel Full -LogOnly", " -TargetFolder $($TargetFolder + '-Deleted-' + $(Get-Date -Format "yyyy-MM-dd")) -DeleteContent" + ' -Confirm:$false -Force')
            }
            Invoke-Expression -Command $Command | Out-Null
        }
    }
}

Run this function on an Exchange server (and not on your admin workstation) for best results. Depending on the version of Exchange you use and how you have set up your environment, you may not be able to run this on your workstation.

How it works

First, the function checks if you are running it on an Exchange Server. If not, it breaks out of the function. You can change this if you want, but you would need to set up your own connection. I do not support running this on a workstation.

Second, it sees if the report recipient is a valid mailbox. Saving the details of such emails is reasonably critical. Then, it builds the search string, starting with the subject, including handling replies and forwards. It uses Regular Expression matching. You could use the built-in TrimStart() method of System.String as well.

Next, it creates the basic Exchange search string. An example would be:

FROM:"spoof-ceo@dmail.com" AND SUBJECT:"Urgent: Gift card required" AND RECEIVED:01/31/2019..01/31/2019

Specifying the recipient is optional in case of this function, because you might have to run the query across all mailboxes. The function checks if the recipient you have specified is a distribution group, a mail-enabled AD group, or a mailbox, and performs the action accordingly.

The idea is to get the mailbox or the list of mailboxes, and perform the search. In case you do not specify a recipient, the function will search across the entire environment. As an example, if you had to use the Exchange search string across all mailboxes in your environment, it would be:

Get-Mailbox -ResultSize Unlimited | Search-Mailbox -SearchQuery FROM:"spoof-ceo@dmail.com" AND SUBJECT:"Urgent: Gift card required" AND RECEIVED:01/31/2019..01/31/2019 -TargetMailbox 'exchangeadmin@domain.com' -TargetFolder 'MySearch' -LogOnly

The parameter LogOnly will merely log the search and give you a summary of the situation. If you would like complete details about the search, specify the LogLevel as Full:

Get-Mailbox -ResultSize Unlimited | Search-Mailbox -SearchQuery FROM:"spoof-ceo@dmail.com" AND SUBJECT:"Urgent: Gift card required" AND RECEIVED:01/31/2019..01/31/2019 -TargetMailbox 'exchangeadmin@domain.com' -TargetFolder 'MySearch' -LogOnly -LogLevel Full

This will give you a CSV file along with the log summary, which will contain a report of the specified email across all the mailboxes in the environment. Remember that this will also create a folder in the mailbox of exchangeadmin@domain.com with the emails found across user mailboxes. You may want to manage that separately as well, based on how organised you like to keep your mailbox.

Delete the email from the mailboxes

The next administrative step would be to delete the email. Of course, based on what the request is, and whether you receive the approval or not.

To do that, you must remove the LogOnly and LogLevel parameters and replace them with the DeleteContent switch.

Get-Mailbox -ResultSize Unlimited | Search-Mailbox -SearchQuery FROM:"spoof-ceo@dmail.com" AND SUBJECT:"Urgent: Gift card required" AND RECEIVED:01/31/2019..01/31/2019 -TargetMailbox 'exchangeadmin@domain.com' -TargetFolder 'MySearch-Deleted' -DeleteContent

This would ask you for a confirmation. We deal with this separately in our function by making this a little more helpful.

The reason I wrote the function was to take out all the hassle of writing the right Exchange search query, log the presence of the email across all or the specified set of mailboxes, generate the report so you can attach it to the Incident, and in the end, delete the email from all the mailboxes in your Exchange mailboxes: abstraction.

In most cases, someone reports a phishing or spoofing attack on the same day. If this is a case of spoofing, and users have legitimate emails from the specified email address, received on a different day. To preserve the legitimate emails, we specify the date. The function assumes the current date as the start and end date, also fixing the locale issue.

The function begins by building the search string, and then, based on the situation, builds the list of mailboxes where to search for the email. Based on the options you choose, it either fully logs the results or deletes the email with confirmation from you, by giving you a less technical and more administrator-friendly message about what will follow.

After dynamically building the command, it uses Invoke-Expression to actually run the command. Running it without the Delete switch gives you statistics about the number of mailboxes that have this email, included in the ShouldProcess method.

The function calls for the confirmation by setting the ConfirmImpact parameter as High. If you do not want the confirmation prompt, you can delete the line that specifies the ConfirmImpact (although I would suggest against it).

How to use the function

Simple; run the script using the . calling operator on the Exchange Management Shell, like so:

. \\path\to\Remove-Email.ps1

This loads the function onto the session. Now, call the function with the necessary parameters:

Remove-Email -Sender `spoof-ceo@dmail.com` -Subject 'Urgent: Gift card required' -ReportRecipient 'exchangeadmin@domain.com'

If you would like to specify the folder where to store the found emails, use the TargetFolder parameter. By default, the function will create a folder called SearchResults. You will receive an email from Exchange with the search result summary, as well as a report containing the detailed search results.

To delete these emails, add the Delete switch.

Remove-Email -Sender `spoof-ceo@dmail.com` -Subject 'Urgent: Gift card required' -ReportRecipient 'exchangeadmin@domain.com' -Delete

This will create a separate folder to store the searched emails, by appending the name you specified with -Deleted- followed by the current date making organisation easier.

Summary

Spoofing and phishing are a challenge even in 2020. To protect users from divulging personal information and falling prey to malicious activities, the Exchange administrators must act with responsibility and efficiency. Speed and reduction in errors are a critical part of achieving efficiency. Abstraction of querying, reporting and clean-up using a function adds to the efficiency by taking away the hassle of syntaxes and options out of the equation and handling them internally.

If you like this kind of posts, make sure you subscribe to my non-spammy newsletter by entering your email address below. These are not automated bot-sent emails, but handcrafted by me specifically for readers. And I promise to keep the emails short.

Want to learn PowerShell?

The award-winning book, PowerShell Core for Linux Administrators Cookbook, which I co-authored, uses the recipe-based learning approach to give you a deep understanding of PowerShell. And the best part is, the concepts discussed work across platforms!

The best new PowerShell books

powered by TinyLetter