As a result of last week’s virus outbreak, I’ve been getting some questions about how to clean viruses out of public folders. Unfortunately, there’s no equivalent of Export-Mailbox to pull infected messages out of public folders. However, it’s pretty easy to write a script to accomplish this through Exchange Web Services or Outlook Object Model based on my previous post about accessing the Information Store from Powershell.

This script is an example that uses EWS through the EWS Managed API, so it can be used for public folders on Exchange 2007 or 2010. You can run it from a Powershell 2.0 command prompt on a machine where you’ve installed the EWS Managed API. The Exchange tools aren’t required.

Note that because this script accesses public folders via EWS, which is a client API, you must have the proper client permissions to the public folder in order to see and delete the items in question. Being an Exchange Administrator does not mean you will be able to delete items out of the public folders through a client. For more on admin rights versus client rights, see my post on that topic.

For an example that uses Outlook Object Model, which allows it to run against Exchange 2003 (or 2007 or 2010), see my next post.

# Delete-PublicFolderItems.ps1
#
This script requires Powershell 2.0 and .NET Framework 3.5.
#
# Syntax:
#
# .\Delete-PublicFolderItems <email address for autodiscover> <path to folder> <recurse>
#
# Examples:
#
# .\Delete-PublicFolderItems administrator@contoso.com \TopLevelFolder\Subfolder $false
# This example runs against one specific folder and does not recurse.
#
# .\Delete-PublicFolderItems administrator@contoso.com "" $true
# This example recursively runs against every folder in the PF hierarchy.
#
# The email address you specify should typically be the email address of the admin
# you're logged in as. Otherwise you may see some strange autodiscover errors.

param([string]$autoDiscoverEmail, [string]$folderPath, [bool]$recurse)

# Remember to install the EWS managed API from here:
#
http://www.microsoft.com/downloads/en/details.aspx?displaylang=en&FamilyID=c3342fb3-fbcc-4127-becf-872c746840e1
#
# Then point the following command to the location of the DLL:

Import-Module -Name "C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"

# If the following is set to $true, we run in READ ONLY mode. We report what we would have deleted,
# but we don't delete anything.
#
# Changing this to $false will make the script DELETE ITEMS OUT OF YOUR PUBLIC FOLDERS.
# Make sure you have tested this first and you have a backup.

$readOnly = $true

# Use AutoDiscover to get the ExchangeService object

$exchService = new-object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
if ($exchService -eq $null)
{
    "Could not instantiate ExchangeService object."
    return
}
$exchService.UseDefaultCredentials = $true
$exchService.AutodiscoverUrl($autoDiscoverEmail)
if ($exchService.Url -eq $null)
{
    return
}

# Bind to the public folders

$pfs = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::PublicFoldersRoot)
if ($pfs -eq $null)
{
    return
}

# Now let's define the function we'll call to process the folders.

function DoFolder([Microsoft.Exchange.WebServices.Data.Folder]$folder, [string]$path)
{
    ("Scanning folder: " + $path)
   
     try
     {
        # Scan the items in this folder
        $offset = 0;
        $view = new-object Microsoft.Exchange.WebServices.Data.ItemView(100, $offset)
        while (($results = $folder.FindItems($view)).Items.Count -gt 0)
        {
            foreach ($item in $results)
            {
                ############################################################
                # Here's the most important part. This is where we determine
                # whether to delete the item or not.
                #
                # By default this script will delete any item that contains
                # the phrase "Here you have" in the subject. If this is not
                # what you want, you need to edit the criteria here.
                if ($item.Subject.Contains("Here you have"))
                {
                    if ($readOnly)
                    {
                        ("    Would have deleted item: " + $item.Subject)
                    }
                    else
                    {
                        ("    Deleting item: " + $item.Subject)
                        $item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete)
                    }
                }
                #
                ############################################################
            }

            $offset += $results.Items.Count
            $view = new-object Microsoft.Exchange.WebServices.Data.ItemView(100, $offset)
        }

        if ($recurse)
        {
            # Recursively do subfolders
            $folderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(2147483647)
            $subfolders = $folder.FindFolders($folderView)   
            foreach ($subfolder in $subfolders)
            {
                try
                {
                    DoFolder $subfolder ($path + "\" + $subfolder.DisplayName)
                }
                catch { "Error processing folder: " + $subfolder.DisplayName }
            }
        }
     }
     catch
     {
         throw
     }
}

# Call DoFolder on the top folder.
# If $recurse is true, it will recursively call itself.

if ($folderPath.Length -lt 1)
{
    DoFolder $pfs ""
}
else
{
    $folderPathTrim = $folderPath.Trim("\")
    $folderPathSplit = $folderPathTrim.Split("\")
    $tinyView = new-object Microsoft.Exchange.WebServices.Data.FolderView(2)
    $displayNameProperty = [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName
    $thisFolder = $pfs
    for ($x = 0; $x -lt $folderPathSplit.Length; $x++)
    {
        $filter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($displayNameProperty, $folderPathSplit[$x])
        $results = $thisFolder.FindFolders($filter, $tinyView)
        if ($results.TotalCount -gt 1)
        {
            "Ambiguous folder name."
            return
        }
        elseif ($results.TotalCount -lt 1)
        {
            "Folder not found."
            return
        }
        $thisFolder = $results.Folders[0]
    }
   
    DoFolder $thisFolder $folderPath
}

# When we return here, we're done!

"Done!"