Friday, March 27, 2020

Migrating your Mailbox searches in EWS to the Graph API Part 1 Filters and Search Folders

This is part one of a two part post where I'm going to look at how you can migrate any searches you are doing in EWS to the Graph API. In this first part I'm going to cover SearchFilters (from EWS) and Search-Folders as they have been around the longest and in part 2 I'll look at Searches which has  some new functionality in beta in the Graph.

Lets start by looking at how you might be doing searches in EWS at the moment

  • Search Filters (restrictions) in a FindItem Request that can be run against a Folder or Search Folder
  • QueryString (KQL) in a FindItem Request that can be run against a Folder or Search Folder
  • SearchFolder with a FindItem Request
  • eDiscovery via SearchMailbox which has now been depreciated in Office 365 and no longer supported
Search Filters (Restrictions)

If you have used the EWS Managed API to build your application you use the SearchFilter class which creates a underlying restriction in EWS  The term Restriction came from the Exchange ROP's protocol which is what MAPI uses to talk to the Exchange Store.

In the Microsoft Graph the language you talk in regards to filtering is OData  
OData (Open Data Protocol) is an ISO/IEC approved, OASIS standard that defines a set of best practices for building and consuming RESTful APIs ref
OData filters are therefore a standard that anybody implementing the protocol (like Microsoft have done with the Graph API) should adhere to.

Lets look at some examples (from this blog) of SearchFilters I've used and how they can be converted to Graph oData Filters.

Easy - the easiest query to make is against one of the strongly typed properties like Subject or Sender eg in EWS you might have a search filter like this

SearchFilter SubjectFilter = new SearchFilter.IsEqualTo(ItemSchema.Subject, "Subject");

in the Graph this would just look something like this (applied just against the Inbox)'Inbox')/messages?$filter=subject eq 'test'

This is an Equality search some other things you can do is

Startswith (which you couldn't actually do in EWS)'Inbox')/messages?$filter=startswith(Subject,'test')

Sub-String Searches'Inbox')/messages?$filter=Contains(Subject,'test')

With the later two searches if you have a folder with a large item count then these aren't going to perform like an equality or a Content Index Search would and its a possibility that they will timeout the first time you run the query. (Then on subsequent queries may succeed this is due to the way Exchange applies temporary restrictions for dynamic searches).

Medium - The most used search filter in EWS for me is that date restriction where you want to restrict the emails returned to a certain time frame so an EWS Search Filter like the following

$Sfgt = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThan
([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived, $Startdatetime)
$Sflt = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsLessThan
([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeReceived, $Enddatetime)
$sfCollection = new-object Microsoft.Exchange.WebServices.Data.SearchFilter

In the Graph this would look like'Inbox')/messages?
$filter=(receivedDateTime gt 2020-03-24T13:01:50Z) AND (receivedDateTime lt 2020-03-25T12:59:50Z)

(Just watch the date formatting)

Hard - If your using Extended properties in your SearchFilters then you need to include the Extended property definition in the Filter and use the lambda any or all expression

The first Query looks for Items based on the underlying Message Class (which isn't exposed as a strongly typed property in the Graph)'Inbox')/messages?
$filter=singleValueExtendedProperties/any(ep:ep/id eq 'String 0x001a' and ep/value eq 'IPM.Note')

Another useful filter is to be able to find Messages where a particular property has been set eg this filter looks for messages that have the In-Reply-To header set (So Repsonses only)'Inbox')/messages?
$filter=singleValueExtendedProperties/any(ep: ep/id eq 'String 0x1042' and ep/value ne null)

For non String properties for instance the Message size you need to make sure you cast the value to the Json datatype. Eg to find all messages that are larger the 10 MB you could use'Inbox')/messages?
$filter=singleValueExtendedProperties/any(ep:ep/id eq 'Integer 0x0E08' and cast(ep/value, Edm.Int32) gt 1048576)

In EWS when you did a search that returns a large number of Items that was paged you got back also the total number of items that matched your search query. I've used this in the past to get statistical information about email with a filter without needing to page all the results. The Graph offers the same thing using the $count query parameter eg if I change the above query to find all messages above 1 MB in my mailbox and include the $count parameter this is what I get for my mailbox

Another example of this maybe to look at the count of messages in 2018'Inbox')/messages?
$count=true&$filter=(receivedDateTime gt 2018-01-01) AND (receivedDateTime lt 2019-01-01)

And finally one last example is for pidTagHasAttachments because I know myself and others use this (because it gives a different value than the strongly type hasAttachments for various reasons)'Inbox')/messages?
$filter=singleValueExtendedProperties/any(ep:ep/id eq 'Boolean 0x0E1B' and cast(ep/value, Edm.Boolean) eq true)

Hopefully I've covered off enough examples here for anybody stuck with syntax to be able to get their head around it. If you do have problems try posting a question into stack overflow here


SearchFolders give you a way of creating a Virtual Folder that represent an underlying restriction (or Search) that can span one folder or the whole mailbox. While they are more suited to static type searches if you have ever used the mapi fiddler inspector to look at what Outlook is doing under the covers when you do a Search, you can see that Outlook uses Searchfolders to provide a more functional search for dynamic queries.

Another example that is used in the Microsoft graph is the me/Messages endpoint which is a Searchfolder that provides access to all the mail folders in a mailbox.

In EWS when you create a SearchFolder you specify a SearchFilter for that folder to be based on. With the Graph similarly you can create a SearchFolder based on a OData filter which I've detailed above. So looking at something topically if you wanted to create a SearchFolder to show all the Email which had a subject of Coronavirus you could use

  "@odata.type": "microsoft.graph.mailSearchFolder",
  "displayName": "Coronavirus Email",
  "includeNestedFolders": true,
  "sourceFolderIds": ["AQMkADYA…."],
  "filterQuery": "contains(subject, 'Coronavirus')"

One thing that is mentioned in the SearchFolder documentation for the Graph to be aware of is

  1. Search folders expire after 45 days of no usage.
  2. There are limits on the number of search folders that can be created per source folder. When this limit is breached, older search folders are deleted to make way for new ones.
So if you are going to use SearchFolders in your application you will need to make sure you have some appropriate management logic. Searchfolders are pretty powerful like EWS the Graph only implements a subset of what can be done in MAPI so if you are trying to reproduce what you see is possible in Outlook you may not be able to do this with Graph (or EWS).

Friday, March 13, 2020

Automating opening a Search-Mailbox result in Excel using EWS

While the Search-Mailbox cmdlet is now depreciated in Exchange Online, OnPrem its still used a fair bit and also does still have some use in the cloud for specific tasks. I've been using it this week a fair bit for various testing tasks and one pain I found when doing a lot of repeated searches in logging mode is each time to have to go in, open the results message in the discovery search mailbox and download the attachment with the log file, unzip and open it in Excel. So I came up with a way of automating this in powershell which turned out to be pretty simple but effective.

First off the only information you need to get the Results Message gets returned in the Target Folder property of the Search results eg.

 The TargetFolder value tells you what folder in the discovery Search mailbox the results are stored in and the DateTime value that will be in the subject of the Results Message.

So in EWS you can use FindFolder to Find that Folder (using a Split on "\" which will work as long as you don't put that in the displayName) and then FindItem can be used to find the results Item eg.

        $ivItemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView(1)  
        $SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject, $Subject) 
        $findItemResults = $Folder.FindItems($SfSearchFilter, $ivItemView)
        if ($findItemResults.Items.Count -eq 1) {
            return $findItemResults.Items[0]
        else {
            throw "No Item found"

Once you have the Results Message you can download the Attachment using some code like this

        if ($SearchResultItem.HasAttachments) {
            foreach ($Attachment in $SearchResultItem.Attachments) {

I then save it to the default downloads directory using

                $downloadDirectory = (New-Object -ComObject Shell.Application).NameSpace('shell:Downloads').Self.Path
                $fileName = ($downloadDirectory + "\" + $ItemPath.SubString(1).Replace("/", "-").Replace(":", "-") + "-" + $Attachment.Name.ToString())
                $fiFile = new-object System.IO.FileStream($fileName, [System.IO.FileMode]::Create)
                $fiFile.Write($Attachment.Content, 0, $Attachment.Content.Length)

and finally open the ZipFile, Extract the csv datastream from the Archive and save that as a File in the Downloads Directory and then open that file in Excel.

                if ($FileName.contains(".zip")) {                    
                    $Zip = [System.IO.Compression.ZipFile]::OpenRead($FileName)
                    try {
                        foreach ($file in $Zip.Entries) {
                            if ($file.Name.contains(".csv")) {
                                $ms = New-Object System.IO.MemoryStream
                                $ZipStream = $file.Open()
                                $outputfile = $FileName.replace("zip", "")
                                [System.IO.File]::WriteAllBytes($outputfile, $ms.ToArray())
                                Invoke-Item $outputfile
                    catch {
                        Write-Host $_.ScriptStackTrace

An example of using this is

$SearchResult = Search-Mailbox -id meganb -TargetFolder Search1 -SearchQuery from:glen -TargetMailbox "DiscoverySearchMailbox{D919BA05-46A6-415f-80AD-7E09334BB852}" -LogOnly -LogLevel Full


Get-SearchMailboxResultsToExcel -MailboxName "DiscoverySearchMailbox{D919BA05-46A6-415f-80AD-7E09334BB852}" -SearchResultPath $SearchResult.TargetFolder -Verbose

or if you want to use ModerAuth (You will need the Adal dll in the same directory)

Get-SearchMailboxResultsToExcel -MailboxName "DiscoverySearchMailbox{D919BA05-46A6-415f-80AD-7E09334BB852}" -ModernAuth -SearchResultPath $SearchResult.TargetFolder -Verbose

I've put a download of this script on GitHub