다음을 통해 공유


Accessing The Information Store From Powershell

In the course of troubleshooting Exchange issues, one discovers a lot of instances where scripting against the Information Store is useful. There are a few ways to access the Information Store from scripts, but for this post I will focus exclusively on two methods. First I’ll discuss using Outlook Object Model from Powershell, and then I’ll cover using the new EWS Managed API from Powershell.

Because there’s no using directive in Powershell, things get very verbose in the EWS examples, and many of the lines wrap. To Powershell, a line break means that’s the end of the command, unless you escape it. So, in the EWS examples, you’ll need to pay attention to where the line breaks actually occur. It may help to make your browser window really wide when looking at the examples. If you want to look at a real-world example of a Powershell script using EWS, take a look at the Find-BadCalendarItems script I wrote for this post on the Exchange Team Blog. I'll post a real OOM example in the future.

Outlook Object Model

Outlook Object Model is an API that lets you interact with Outlook. One of the greatest things about it is that it’s old, so there are a lot of examples and resources available on the internet. And because it’s Outlook, you can do practically anything with it. Until the EWS Managed API came out, this was my preferred method for scripting against the IS, and it’s still the method I use if I’m dealing with a server that doesn’t support the EWS Managed API, which is anything older than Exchange 2007 Sp1.

To access the Information Store using OOM, the first step is to instantiate the Outlook application:

$outlook = new-object -com Outlook.Application

One thing to be aware of is that each version of Outlook adds new functionality onto OOM, so you may want to check the version at this point to make sure the customer is running a version that supports what you want to do:

if (!($outlook.Version -like "12.*" –or $outlook.Version -like "14.*"))
{
     ("This script requires Outlook 2007 or 2010.")
     return
}

At this point, you’re ready to get the Session, which you will use to get to folders in the store:

$mapi = $outlook.GetNamespace("MAPI")
$session = $mapi.Session
"Opening the Calendar..."
$olFolderCalendar = 9
$calendar = $session.GetDefaultFolder($olFolderCalendar)

One of the issues with using Powershell is that your constants like olFolderCalendar are not defined for you, so you will have to look these up in MSDN and define the ones you want to use.

If you need to traverse a folder path to find a specific folder by name, this gets more complicated. Even from the command line, this is a challenge, because you can’t use an indexer on the Folders collection for a folder. And even on objects where you can use indexers, such as the session’s Folders collection, beware, because it might behave differently than you would think:

PS D:\> $session = $outlook.GetNamespace("MAPI").Session
PS D:\> $session.Folders | ft Name

Name
----
someone@contoso.com
Public Folders - someone@contoso.com
Internet Calendars
SharePoint Lists

PS D:\> $pfs = $session.Folders[1]
PS D:\> $pfs | ft Name

Name
----
someone@contoso.com

PS D:\> # what?!? if someone@contoso.com was 1, what was 0?
PS D:\> $pfs = $session.Folders[0]
PS D:\> $pfs | ft Name
PS D:\> # 0 is nothing? so pfs must be 2...
PS D:\> $pfs = $session.Folders[2]
PS D:\> $pfs | ft Name

Name
----
Public Folders - someone@contoso.com

PS D:\> # now we got it! Ugh
PS D:\> $pfs.Folders | ft Name

Name
----
Favorites
All Public Folders

PS D:\> $allPfs = $pfs.Folders[1]
Unable to index into an object of type System.__ComObject.
At line:1 char:24
+ $allPfs = $pfs.Folders[ <<<< 1]
+ CategoryInfo : InvalidOperation: (1:Int32) [], RuntimeException
+ FullyQualifiedErrorId : CannotIndex

Because of these issues, it’s very useful to write a function that uses a foreach to retrieve a particular name from a collection. If I need to traverse folders by name in OOM, I include a function like this in my script:

function GetNamedFromCollection($name, $collection)
{
     foreach ($item in $collection)
     {
          if ($item.Name -eq $name -or $item.DisplayName -eq $name)
          {
               return $item
          }
     }
     return $null
}

With that handy function, you can just pass a folder path into your script, and let the script repeatedly call it to traverse the path:

$allPFs = GetNamedFromCollection "All Public Folders" $pfs.Folders
if ($allPFs -eq $null)
{
     "Couldn't find All Public Folders folder."
     return
}

$folderPath = $folderPath.Trim("\")
$folderPathSplit = $folderPath.Split("\")
$folder = $allPFs
if ($folderPath.Length -gt 0)
{
     "Traversing folder path..."
     for ($x = 0; $x -lt $folderPathSplit.Length; $x++)
     {
          $folder = GetNamedFromCollection $folderPathSplit[$x] $folder.Folders
     }

     if ($folder -eq $null)
     {
          ("Could not find folder: " + $folderPath)
          return
     }

     ("Found folder: " + $folder.FolderPath)
}

Once you have the folder you want, you have access to all the MAPI properties of that folder. Some of them are exposed right on the folder object with a nice friendly name. But you can still get the ones that aren’t, like this:

$PR_REPLICA_SERVER = "https://schemas.microsoft.com/mapi/proptag/0x6644001E"
$replicaServer = $folder.PropertyAccessor.GetProperty($PR_REPLICA_SERVER)
("Accessing this folder on server: " + $replicaServer)

You can also grab the Items collection for the folder, and interact with those in much the same way:

$items = $folder.Items
($items.Count.ToString() + " items found in folder " + $folder.FolderPath)
foreach ($item in $items)
{
     ("Checking item: " + $item.Subject)
     # do stuff here
     $item.Save()
     (" Changes saved.")
}

Exchange Web Services (EWS) Managed API

If you’re scripting against Exchange 2007 Sp1 or later, the new EWS Managed API is usually a better choice, for several reasons. First, you don’t need Outlook. All you need is a workstation with Powershell 2.0 and the EWS Managed API package installed. Second, this is an API specifically for managed code, and as a result, there’s no need to redefine constants within your script. Third – and this is an important one – you can do ranged retrieval of messages. What is ranged retrieval and why does it matter? We’ll get to that in a minute.

To use the API, your script will need to start by importing the DLL:

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

With that done, a few simple lines will get you to a user’s Inbox:

$exchService = new-object Microsoft.Exchange.WebServices.Data.ExchangeService
$exchService.UseDefaultCredentials = $true
$exchService.AutodiscoverUrl("someone@contoso.com")
$inbox = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox)

It’s just as easy to get to the public folders:

$pfs = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::PublicFoldersRoot)

Now we get to the retrieval issue. Retrieving folders and items in the EWS Managed API is more complex than in Outlook Object Model, because there’s no simple Folders member or Items member that gives you all the folders or items. Instead, you have to create a View that specifies a page size – the number of items you want returned at once. This works like the page size in an LDAP search. Then you retrieve the folders or items in batches, using that view.

Why make things so complex? The same reason we use page sizes with LDAP searches – returning a large result set in one huge batch is expensive. Think of what happens when you go into Outlook and navigate to a folder with 100,000 items.

So what’s to stop you from being lazy and creating a View with a page size of 100,000… or better yet, the maximum value of an Int32, which is 2,147,483,647? Nothing, actually. But when the script causes performance issues, we’ll know to blame the scripter and not the API. This is the lazy way:

$itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView(2147483647)
$inboxItems = $inbox.FindItems($itemView)

It’s much better to create a small view and use offsets to retrieve the items in batches. This makes your script more efficient by letting it work with small numbers of items at a time. It goes something like this:

$offset = 0;
$view = new-object Microsoft.Exchange.WebServices.Data.ItemView(100, $offset)
while (($results = $inbox.FindItems($view)).Items.Count -gt 0)
{
     foreach ($item in $results)
     {
          # do something to $item
     }

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

This is quite powerful, because it removes any concern about what’s going to happen when your script tries to iterate through that huge folder.

Well, what if you need to traverse a folder path by name? Do you need to do a ranged retrieval on every level of the hierarchy to avoid the ‘lazy’ approach? Not necessarily.

$tinyView = new-object Microsoft.Exchange.WebServices.Data.FolderView(2)
$displayNameProperty = [Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName
$filter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo($displayNameProperty, "SomeFolder")

We’ve done two things here. We created a very small FolderView that will only retrieve a maximum of two folders, and we created a SearchFilter that will only return folders where the DisplayName is equal to “SomeFolder”. Note the unusual syntax we use to instantiate the filter – “SearchFilter+IsEqualTo”. This is because IsEqualTo is a nested class inside of SearchFilter. There’s no real reason to give the DisplayName property its own variable as I did in this example – it’s just for readability.

With that done, we’re ready to look for the folder:

$results = $pfs.FindFolders($filter, $tinyView)
if ($results.TotalCount -gt 1)
{
     "Ambiguous name."
}
elseif ($results.TotalCount -lt 1)
{
     "Folder not found."
}
$folder = $results.Folders[0]

Of course, in most cases, you’re not going to have so many folders at one level of the hierarchy that you can’t just use a view of, say, 100, to find them all and iterate through them looking for the one you want. But this is an alternate approach, and one you could also use if you needed to find a particular item in a folder with many items.

Retrieving properties on folders and items is fairly straightforward. You use the TryGetProperty method, and pass it the property you want and the variable where you want to hold the value:

$folderClassProperty = [Microsoft.Exchange.WebServices.Data.FolderSchema]::FolderClass
$folderClassValue = $null
$succeeded = $folder.TryGetProperty($folderClassProperty, [ref]$folderClassValue)
if (!($succeeded))
{
     "Couldn't get folder class."
}

Note that TryGetProperty is always going to return $true or $false, so you need to catch the return value in a variable to prevent “True” or “False” from appearing in the script’s output stream.

If the property you want is not in the predefined schema, you can instantiate a property for the ptag you need. Unfortunately, not all MAPI property types are currently supported. Most notably, PT_I8 is not included, which means you can’t retrieve a FID or MID that you might see in certain diagnostics logging output. But for supported property types, you can create an ExtendedPropertyDefinition based on the ptag, and then pass that to TryGetProperty:

$ptagPFAdminDescriptionProperty = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x671C, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String)
$ptagPFAdminDescriptionValue = $null
$succeeded = $folder.TryGetProperty($ptagPFAdminDescriptionValue, [ref]$ptagPFAdminDescriptionValue)

For more information on the EWS Managed API, check out the documentation on MSDN: https://msdn.microsoft.com/en-us/library/dd633696.aspx.

Happy scripting!

Comments

  • Anonymous
    February 10, 2011
    I can noit install the EWS module on my Windows 7 Machine :(

  • Anonymous
    March 04, 2011
    Not that I'm sure this is a good place to ask ... my exchange server is returning folder structures with some folders that don't have a <t:folderclass> entry ? can I assume that those folders are all just "folders" and that it won't be a calendar folder ?

  • Anonymous
    July 20, 2012
    Hi Bill, Thank you for a great post. Especially the part about using PropertyAccessor which turned out to be just what I needed to <a href="enkel-it.dk/blog rooms in BPOS with Powershell</a> Best Regards, Claus