Preventing AutoReply Storms (Part 2)

by [Published on 23 Feb. 2012 / Last Updated on 23 Feb. 2012]

In this 2-part article the author will continue with the development of a script to help detect and prevent e-mail storms caused by Outlook AutoReply rules.

If you would like to read the first part in this article series please go to Preventing AutoReply Storms (Part 1).

Introduction

In the second part of this article we will improve the e-mail sent to administrators, learn how we can schedule the script to run every 15 minutes and see how we can automatically put an end to a storm once it is detected!

Please, make sure you first check part 1 otherwise the code below will not make much sense.

Scheduling the Script

There are several methods of scheduling PowerShell scripts. The one I use is the following:

  1. Place the code in a .ps1 file (e.g. PreventStorm.ps1);
  2. In the same folder, create a batch file (e.g. PreventStorm.bat) with the following code (make sure you update all the folder paths and names accordingly):

    PowerShell.exe -PSConsoleFile "C:\Program Files\Microsoft\Exchange Server\V14\Bin\ExShell.psc1" -Command ". 'E:\Scripts\PreventStorm.ps1'"
  3. Use Windows Task Scheduler to create a new task that runs the batch file every 15 minutes.

Preventing Storms

Once the administrators receive the e-mail, all they have to do is either let the user know of what is happening or login to the user’s mailbox and disable the rule themselves. Both are not ideal as a storm might happen during the night or over the weekend…

So far I found two ways of trying to prevent these storms:

  1. Use the new Exchange 2010 *-InboxRule cmdlet;
  2. Create a Transport Rule to drop/redirect these e-mails.

Let’s have a look at both methods.

*-InboxRule cmdlet

Using the Get-InboxRule –Mailbox <user> cmdlet we can see all the rules that a user has on his/hers mailbox, including the AutoReply rule that caused the storm:


Figure 2.1:
Inbox Rules

We can even explore the rules and see what they exactly do:


Figure 2.2:
Rule Details

The bad news is that in all this detail there is nothing that tells us specifically that a certain rule is an AutoReply rule (notice that the Description is empty)...


Figure 2.3 -
AutoReply Rule Details

We could hope for users to use a name for the rule that would give us some indication of what it does like in this example. If this was the case, we could disable any rule that has a name of *auto*:

Get-InboxRule –Mailbox <user> | ? {$_.Name –match “auto”} | Disable-InboxRule

But there is no guarantee that we will not disable a rule called “Delete AutoTrader Emails” for example... For this reason we cannot use this method to search and disable these rules.

Transport Rule

Another option is to create a transport rule to drop or redirect e-mails that match the e-mails found in the hash table. Once you are confident the script only picks up on actual storms, and then you can implement this method.

I will give the syntax of creating these rules in Exchange 2007 and 2010 since they are very different.

When we go through the hash table we have all the details we need to create the rule. So, let’s add the suitable code for your Exchange version right before the Send-MailMessage cmdlet:

# Get the local part of the e-mail address to use as part of the name for the transport rule

$strName = [Regex]::Split($arrDetails[0], "@")[0]

 

 

# Exchange 2010

New-TransportRule -Name "Prevent Storm - $strName" -Comments "Prevent Outlook AutoReply Storm" -From $arrDetails[0] -SentToScope "NotInOrganization" -RecipientAddressContainsWords $arrDetails[1] -SubjectContainsWords $arrDetails[2] -RedirectMessageTo "quarantine@letsexchange.com" -Enabled $True -Priority 0

 

 

# Exchange 2007

# For every e-mail sent by that user

$condition1 = Get-TransportRulePredicate From

$condition1.Addresses = @(Get-Mailbox $arrDetails[0])

 

# only when the e-mail is going Outside the organization

$condition2 = Get-TransportRulePredicate SentToScope

$condition2.Scope = @("NotInOrganization")

 

# only for e-mails that contain the subject we want

$condition3 = Get-TransportRulePredicate SubjectContains

$condition3.Words = @($arrDetails[2])

 

# Redirect the e-mails e-mail to the Quarantine mailbox

$action = Get-TransportRuleAction RedirectMessage

$action.Addresses = @(Get-Mailbox Quarantine)

 

# Create the Transport Rule itself

New-TransportRule -Name "Prevent Storm - $strName" -Comments "Prevent Outlook AutoReply Storm" -Conditions @($condition1, $condition2, $condition3) -Actions @($action) -Enabled $True -Priority 0

The disadvantage of Exchange 2007 is that we have no easy way of specifying the recipient, which means it will block all automatic replies sent from the user to external recipients... The only way around this would be to create a contact for each recipient and then create the transport rule using that contact. In Exchange 2010 we can simply use the RecipientAddressContainsWords parameter.

Note:
With 2010, we can also tell the Transport Rule to only block e-mails of the AutomaticReply type by specifying -MessageTypeMatches "OOF" but this only matches OOF e-mails...

As we only need to “stop” an AutoReply rule for a few minutes to put an end to the storm, we can delete any existing transport rules we created previously every time the script starts. This will ensure that every rule is only in place for 15 minutes which is normally enough time:

Get-TransportRule | ? {$_.Name –match "Prevent Storm"} | Remove-TransportRule –Confirm:$False

Final Script

Below is the script with everything we discussed so far. Alternatively you can download it here. I left the deletion and creation of the transport rules commented so you can safely test the script first. Don't forget to replace letsexchange.com with your domain name.

# Script:   PreventStorm.ps1

# Purpose:  Analyze the Transport Logs for possible AutoReply Storms

# Author:   Nuno Mota

# Version:  3.0 (Jan 2012)

 

 

# Initialize variables

[Int] $intTime = 15

[Int] $intNumEmails = 25

[Bool] $boolSendEmail = $False

 

# Get the date and time for 15 minutes ago. This will be the starting point to search the transport logs

[DateTime] $dateStartFrom = (Get-Date).AddMinutes(-$intTime)

 

$hashEmails = @{}

 

[String] $strEmailBody = ""

$strEmailBody += "`n**********************************"

$strEmailBody += "`n*                                *"

$strEmailBody += "`n*    WARNING: Storm Detected!    *"

$strEmailBody += "`n*                                *"

$strEmailBody += "`n**********************************"

 

 

# Check if we previously created any Transport Rule to prevent a storm. If yes, delete it

#Get-TransportRule | ? {$_.Name -match "Prevent Storm"} | Remove-TransportRule -Confirm:$False

 

 

# Get all the users who sent 25 or more e-mails in the last 15 minutes

# If you have multiple AD sites or are running in a co-existance scenario with multiple Exchange versions, you might want to include -and $_.Recipients -notmatch "letsexchange.com

$logEntries = Get-TransportServer | Get-MessageTrackingLog -ResultSize Unlimited -Start $dateStartFrom -EventId SEND | ? {$_.Source -eq "SMTP" -and $_.Recipients -notmatch "letsexchange.com"} | Group Sender | ? {$_.Count -ge $intNumEmails}

If ($logEntries -eq $null) { Exit }

 

 

# For each sender, analyze all the e-mails they sent and put them in the HashTable

ForEach ($logEntry in $logEntries)

{

      ForEach ($email in (Get-TransportServer | Get-MessageTrackingLog -ResultSize Unlimited -Start $dateStartFrom -Sender $logEntry.Name -EventId SEND | ? {$_.Source -eq "SMTP" -and $_.Recipients -notmatch "letsexchange.com"} | Select MessageSubject, Sender, Recipients))

      {

            $hashEmails["$($email.Sender) ô $($email.Recipients) ô $($email.MessageSubject)"] += 1

      }

}

 

 

# To sort/filter a Hash Table, we need to transform its content into individual objects by using the GetEnumerator method

# If we find any key with a value of at least 25, then we have detected a storm

ForEach ($storm in ($hashEmails.GetEnumerator() | ? {$_.Value -ge $intNumEmails} | Sort Name))

{

      $boolSendEmail = $True

      $arrDetails = [Regex]::Split($storm.Name, " ô ")

 

      $strEmailBody += "`n`nSender:     ", $arrDetails[0]

      $strEmailBody += "`nRecipient:  ", $arrDetails[1]

      $strEmailBody += "`nSubject:    ", $arrDetails[2]

      $strEmailBody += "`n# e-mails:  ", $storm.Value

 

      # Get the local part of the e-mail address to use as part of the name for the transport rule

#     $strName = [regex]::split($arrDetails[0], "@")[0]

 

#     $ruleResult = New-TransportRule -Name "Prevent Storm - $strName" -Comments "Prevent Outlook AutoReply Storm" -From $arrDetails[0] -SentToScope "NotInOrganization" -RecipientAddressContainsWords $arrDetails[1] -SubjectContainsWords $arrDetails[2] -RedirectMessageTo "quarantine@letsexchange.com" -Enabled $True -Priority 0

#     If (!$ruleResult)

#     {

#           $strEmailBody += "`nRule Created? NO"

#     } Else {

#           $strEmailBody += "`nRule Created? Yes"

#     }

     

#     # For every e-mail sent by that user

#     $condition1 = Get-TransportRulePredicate From

#     $condition1.Addresses = @(Get-Mailbox $arrDetails[0])

#

#     # only when the e-mail is going Outside the organization

#     $condition2 = Get-TransportRulePredicate SentToScope

#     $condition2.Scope = @("NotInOrganization")

#

#     # only for e-mails that contain the subject we want

#     $condition3 = Get-TransportRulePredicate SubjectContains

#     $condition3.Words = @($arrDetails[2])

#

#     # Redirect the e-mails e-mail to the Quarantine mailbox

#     $action = Get-TransportRuleAction RedirectMessage

#     $action.Addresses = @(Get-Mailbox Quarantine)

#

#     # Get the local part of the e-mail address to use as part of the name for the transport rule

#     $strName = [regex]::split($email.Sender, "@")[0]

#

#     # Create the Transport Rule itself

#     New-TransportRule -Name "Prevent Storm - $strName" -Comments "Prevent Outlook AutoReply Storm" -Conditions @($condition1, $condition2, $condition3) -Actions @($action) -Enabled $True -Priority 0

}

 

 

# Send an e-mail to the administrator(s)

If ($boolSendEmail) { Send-MailMessage -From "Administrator@letsexchange.com" -To "AdminNuno@letsexchange.com" -Subject "Storm Detected!" -Body $strEmailBody -SMTPserver "smtp.letsexchange.com" -DeliveryNotificationOption onFailure }

Conclusion

Storms caused by Outlook AutoReply rules are a common issue many Exchange Administrators face. With no easy way of preventing them without disabling the whole functionality altogether, in this article I provided a script to try to help administrators detect and prevent these storms. Hopefully Exchange will make it easier to prevent these in the future.

If you would like to read the first part in this article series please go to Preventing AutoReply Storms (Part 1).

Featured Links