PowerShell: Checking the Syntax of an Exchange Mailbox Export Content Filter

A bit of a niche function this one.  When you perform a mailbox export in Exchange using the New-MailboxExportRequest cmdlet you can specify the ContentFilter parameter.  This allows you to pass an OPATH filter to only export SOME of the content from a mailbox.

One of the examples on the page above is;

New-MailboxExportRequest -Mailbox Tony -ContentFilter {(body -like “*company*”) -and (body -like “*profit*”) -and (Received -lt “01/01/2012”)}

So what if you need to check the syntax of a ContentFilter?

Most of the time, you’re just going to try and use the ContentFilter immediately;  if the command fails then you know the filter was incorrect.

There may be times where the filter is specified long before it’s used though;  maybe in a very long script, or where the filter is generated or read from a file.

Here’s a function that does a rudimentary check.

function Get-IsValidContentFilter
{
    param
    (
        [Parameter(Mandatory = $true)]
        [string]$ContentFilter
    )   
    [System.Collections.Hashtable]$ValidContentFilterFirstOperands=@{"All"="[String]";"Attachment"="[string]";"Body"="[string]";"Category"="[string]";
    "Expires"="[DateTime]";"HasAttachment"="[boolean]";"Importance"=@("Low","Normal","High","0","1","2");"IsFlagged"="[boolean]";
    "IsRead"="[boolean]";"MessageKind"=@("Email","Calendar","Task","Note","Doc","Journal","Contact","InstantMessage","Voicemail","Fax","Post","RSSFeed");"MessageLocale"="Culture";"PolicyTag"="[string]";
    "Received"="[DateTime]";"Sent"="[DateTime]";"Size"="Size";"Subject"="[string]"}
    [string[]]$ValidContentFilterComparisonOperators=@("eq","ne","gt","lt","le","like","notlike","match","notmatch","contains","notcontains","replace")
    [string[]]$ValidContentFilterLogicalBinaryOperators=@("and","or","xor","not")
    Write-Verbose "BaseString: $ContentFilter" 
    if($ContentFilter -match "\((?<FirstPart>.+)\) \-(?<Operator>.+) \((?<SecondPart>.+)\)")
    {
        $LogicalOperatorShouldBeUsed=$True
    }elseIf ($ContentFilter -match "\((?<InsideBrackets>.+)\)")
    {
        Get-IsValidContentFilter -ContentFilter $Matches.InsideBrackets.Trim()
    }elseIf ($ContentFilter -Match "(?<FirstPart>.+) \-(?<Operator>.+) (?<SecondPart>.+)") 
    {
        $LogicalOperatorShouldBeUsed=$False       
    }else
    {
        Return $False
    }
    $FirstOperand=$Matches.FirstPart.Trim()
    $Operator=$Matches.Operator.Trim()
    $SecondOperand=$Matches.SecondPart.Trim()
    Write-Verbose "$FirstOperand, $Operator, $SecondOperand" 
    if ($LogicalOperatorShouldBeUsed)
    {
        $FirstPartIsValid=Get-IsValidContentFilter -ContentFilter $FirstOperand
        $SecondPartIsValid=Get-IsValidContentFilter -ContentFilter $SecondOperand
        $OperatorIsValid=$ValidContentFilterLogicalBinaryOperators -contains $Operator
    }else
    {
        $OperatorIsValid=$ValidContentFilterComparisonOperators -contains $Operator
        if (($ValidContentFilterFirstOperands.Keys -contains $FirstOperand))
        {
            Write-Verbose "Matched First Operand Directly" 
            $FirstPartIsValid=$True
            Write-Verbose "Testing 2nd Operand Type" 
            $SecondPartIsValid=$False
                $TestingType=$ValidContentFilterFirstOperands.$FirstOperand
                Write-Verbose "Checking $FirstOperand" 
                Write-Verbose "Corresponding Type: $TestingType" 
                if ($TestingType -is [Array])
                {
                    Write-Verbose "Found Type Array: $TestingType"
                    $SecondPartIsValid=[bool]([string[]]$TestingType -contains $SecondOperand)
                }else
                {
                    switch -regex ($TestingType)
                    {
                        "\[(?<TypeName>.+)\]"
                        {
                            Write-Verbose "Found Type $TestingType" 
                            $SecondPartIsValid=[string]$SecondOperand -as $Matches.TypeName
                            Continue
                        }
                        "Culture"
                        {
                            Write-Verbose "Found Culture Reference" 
                            $SecondPartIsValid=([globalization.cultureinfo]::GetCultures("allCultures") | Where-Object {$_}) -contains $SecondOperand
                            Continue
                        }
                    }
                }   
        }else
        {
            $FirstPartIsValid=$OperatorIsValid=$SecondPartIsValid=$False
        }  
    }
    Write-Verbose "FirstOperand Valid: $FirstPartIsValid" 
    Write-Verbose "Operator Valid: $OperatorIsValid" 
    Write-Verbose "SecondOperand Valid: $SecondPartIsValid" 
    $FirstPartIsValid -and $OperatorIsValid -and $SecondPartIsValid
}
}

The mailbox content filter has a few valid operands to use for testing (like “body” in the example.  So the script lists them (with the standard operators like “eq”, “lt” etc).  With each I also include what is  valid to use them with as well (so “Received” must be used with a [DateTime] object;

[System.Collections.Hashtable]$ValidContentFilterFirstOperands=@{"All"="[String]";"Attachment"="[string]";"Body"="[string]";"Category"="[string]"; "Expires"="[DateTime]";"HasAttachment"="[boolean]";"Importance"=@("Low","Normal","High","0","1","2");"IsFlagged"="[boolean]"; "IsRead"="[boolean]";"MessageKind"=@("Email","Calendar","Task","Note","Doc","Journal","Contact","InstantMessage","Voicemail","Fax","Post","RSSFeed");"MessageLocale"="Culture";"PolicyTag"="[string]"; "Received"="[DateTime]";"Sent"="[DateTime]";"Size"="Size";"Subject"="[string]"}

There then follow three cases to check for;

1) An expression with bracketed sub-expressions: (body -like “test”) -and (attachment -like “*.csv”)

For this one the script reads each expression and the operator to join them.  That operator should be a logical one (-and, -or etc.

 if($ContentFilter -match "\((?<FirstPart>.+)\) \-(?<Operator>.+) \((?<SecondPart>.+)\)") 
   { $LogicalOperatorShouldBeUsed=$True

2) A bracketed expression (that isn’t caught by the first test): (attachment -like “*.csv”)

For this just strip off the brackets and call the function again on what’s inside them.

elseIf ($ContentFilter -match "\((?<InsideBrackets>.+)\)") 
   { Get-IsValidContentFilter -ContentFilter    $Matches.InsideBrackets.Trim()

3) A standard content filter expression: attachment -like “*.csv”

elseIf ($ContentFilter -Match "(?<FirstPart>.+) \-(?<Operator>.+) (?<SecondPart>.+)")
   { $LogicalOperatorShouldBeUsed=$False

The two operands (“Attachment” and “*.csv” in the example above) and the operator (“like”) are saved.  In this case the operator shouldn’t be a logical one.

After that any spaces are stripped off the three parts to be tested;

$FirstOperand=$Matches.FirstPart.Trim() $Operator=$Matches.Operator.Trim() $SecondOperand=$Matches.SecondPart.Trim()

If a logical comparison is being used the script recursively tests each part and then checks the operator is valid;

    if ($LogicalOperatorShouldBeUsed)
    {
        $FirstPartIsValid=Get-IsValidContentFilter -ContentFilter $FirstOperand
        $SecondPartIsValid=Get-IsValidContentFilter -ContentFilter $SecondOperand
        $OperatorIsValid=$ValidContentFilterLogicalBinaryOperators -contains $Operator
    }else

If it’s a standard expression then the script checks the operator first and then ensures that the first operand is one of the ones listed in $ValidContentFilterFirstOperands.Keys

       if (($ValidContentFilterFirstOperands.Keys -contains $FirstOperand))
        {
            Write-Verbose "Matched First Operand Directly"
            $FirstPartIsValid=$True
            Write-Verbose "Testing 2nd Operand Type"
            $SecondPartIsValid=$False

The script also gets what kind of operand should be second (also from $ValidContentFilterFirstOperands).  Then is performs a different check depending on whether it should be one of an array of values, a specific PowerShell type or a Culture setting.

                $TestingType=$ValidContentFilterFirstOperands.$FirstOperand
                Write-Verbose "Checking $FirstOperand"  
                Write-Verbose "Corresponding Type: $TestingType"  
                if ($TestingType -is [Array])
                {
                    Write-Verbose "Found Type Array: $TestingType"
                    $SecondPartIsValid=[bool]([string[]]$TestingType -contains $SecondOperand)
                }else
                {
                    switch -regex ($TestingType)
                    {
                        "\[(?.+)\]"
                        {
                            Write-Verbose "Found Type $TestingType"  
                            $SecondPartIsValid=[string]$SecondOperand -as $Matches.TypeName
                            Continue
                        }
                        "Culture"
                        {
                            Write-Verbose "Found Culture Reference"  
                            $SecondPartIsValid=([globalization.cultureinfo]::GetCultures("allCultures") | Where-Object {$_}) -contains $SecondOperand
                            Continue
                        }
                    }
                }
            }

The last part defaults all three tests to false if none of the previous criteria were matched and then returns if all three parts of the expression were confirmed correct.

else
        {
            $FirstPartIsValid=$OperatorIsValid=$SecondPartIsValid=$False
        }  
    }
    Write-Verbose "FirstOperand Valid: $FirstPartIsValid"  
    Write-Verbose "Operator Valid: $OperatorIsValid"  
    Write-Verbose "SecondOperand Valid: $SecondPartIsValid"  
    $FirstPartIsValid -and $OperatorIsValid -and $SecondPartIsValid

 

 

 

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s