Tuesday, July 21, 2015

EWS Outlook Quick Steps Powershell how to

Quickstep's in Outlook is a feature that was added in Outlook in 2010 to help automate repetitive tasks you do with Email in Outlook. In this Post I'm going to go through how you can manipulate the objects associated with Quick steps using EWS and PowerShell.

How Quick Steps are implemented in a Mailbox

The OuickSteps Folder is created by Outlook (eg no Outlook no folder) In the Mailbox's IPM Root as a hidden folder called Quick Steps (with a FolderClass of IPF.Configuration) and within this folder there are FAI (Folder Associated Items) which represent each of the QuickStep Items. On each of those there is a PidTagRoamingXmlStream property which contains a ByteArray that represents an XMLDocument for each of the QuickStep items for example they look something like


<?xml version="1.0"?>
<CombinedAction Ordinal="3200" Tooltip="" Icon="MoveToFolder" Name="Clutter" Version="147153">
 <ActionMoveToFolder>
  <Folder>80010....06F006D0000000000</Folder>
 </ActionMoveToFolder>
 <ActionMarkAsRead>
 </ActionMarkAsRead>
</CombinedAction>

Getting the QuickSteps Folder

To Get the QuickSteps folder (if it exists) you can use the PidTagAdditionalRenEntryIdsEx property on the IPM Root folder which will contain the HexEntryId of the QuickSteps folder in the RSF_PID_COMBINED_ACTIONS PersistBlockType value. Eg you can do this using the following code

function Get-QuickStepsFolder
{
 param(
  [Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
  [Parameter(Position=1, Mandatory=$true)] [PSCredential]$Credentials,
  [Parameter(Position=2, Mandatory=$false)] [Microsoft.Exchange.WebServices.Data.ExchangeService]$service 
 )
 Begin
 {
  if(!$service){
   $service = Connect-Exchange -MailboxName $MailboxName -Credential $Credentials
  }
  $PidTagAdditionalRenEntryIdsEx = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x36D9, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)  
  $psPropset = new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
  $psPropset.Add($PidTagAdditionalRenEntryIdsEx)  
  $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root,$MailboxName)     
  $IPM_ROOT = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid,$psPropset)  
  $binVal = $null;  
  $AdditionalRenEntryIdsExCol = @{}  
  if($IPM_ROOT.TryGetProperty($PidTagAdditionalRenEntryIdsEx,[ref]$binVal)){  
      $hexVal = [System.BitConverter]::ToString($binVal).Replace("-","");  
      ##Parse Binary Value first word is Value type Second word is the Length of the Entry  
      $Sval = 0;  
      while(($Sval+8) -lt $hexVal.Length){  
          $PtypeVal = $hexVal.SubString($Sval,4)  
          $PtypeVal = $PtypeVal.SubString(2,2) + $PtypeVal.SubString(0,2)  
          $Sval +=12;  
          $PropLengthVal = $hexVal.SubString($Sval,4)  
          $PropLengthVal = $PropLengthVal.SubString(2,2) + $PropLengthVal.SubString(0,2)  
          $PropLength = [Convert]::ToInt64($PropLengthVal, 16)  
          $Sval +=4;  
          $ProdIdEntry = $hexVal.SubString($Sval,($PropLength*2))  
          $Sval += ($PropLength*2)  
          #$PtypeVal + " : " + $ProdIdEntry  
          $AdditionalRenEntryIdsExCol.Add($PtypeVal,$ProdIdEntry)   
      }     
  }
  $QuickStepsFolder = $null
  if($AdditionalRenEntryIdsExCol.ContainsKey("8007")){  
      $siId = ConvertFolderid -service $service -MailboxName $MailboxName -hexid $AdditionalRenEntryIdsExCol["8007"]  
      $QuickStepsFolderId = new-object Microsoft.Exchange.WebServices.Data.FolderId($siId.UniqueId.ToString())  
      $QuickStepsFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$QuickStepsFolderId) 
  }
  else{
   Write-Host ("QuickSteps folder not found")
   throw ("QuickSteps folder not found")
  }
  return $QuickStepsFolder
  

 }
 
}


Getting the Quick Steps Items

The Quick Step items are stored in the Folder Associated Items collection of the Quick Steps folder. To get these using EWS after you get the Quick Steps folder you just need to enumerate the items in the FAI collection by specifying an Associated Items traversal. eg the following code will return the existing Quick Step items

function Get-ExistingSteps{
  param(
  [Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
  [Parameter(Position=1, Mandatory=$true)] [Microsoft.Exchange.WebServices.Data.Folder]$QuickStepsFolder 
 )
 Begin
 {
  $NameList = @{}
  $enc = [system.Text.Encoding]::ASCII
  $PR_ROAMING_XMLSTREAM = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x7C08,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);  
  $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
  $psPropset.Add($PR_ROAMING_XMLSTREAM)
  #Define ItemView to retrive just 1000 Items    
  $ivItemView =  New-Object Microsoft.Exchange.WebServices.Data.ItemView(1000)
  $ivItemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Associated
  $fiItems = $null    
  do{    
      $fiItems = $QuickStepsFolder.FindItems($ivItemView)  
   if($fiItems.Items.Count -gt 0){
       [Void]$service.LoadPropertiesForItems($fiItems,$psPropset)  
       foreach($Item in $fiItems.Items){      
     $propval = $null
     if($Item.TryGetProperty($PR_ROAMING_XMLSTREAM,[ref]$propval)){
      [XML]$xmlVal = $enc.GetString($propval)
      if(!$NameList.ContainsKey($xmlVal.CombinedAction.Name.ToLower())){
       $NameList.Add($xmlVal.CombinedAction.Name.Trim().ToLower(),$Item)
      }
     }         
       }
   }    
      $ivItemView.Offset += $fiItems.Items.Count    
  }while($fiItems.MoreAvailable -eq $true) 
  return $NameList
 }
}

Export QuickStep XML

To Export the XML from a Quick Step Item you can grab the PR_Roaming_XMLStream property and then save this out to a file (it just plain XML) eg

function Export-QuickStepXML{
 param(
     [Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
  [Parameter(Position=1, Mandatory=$true)] [PSCredential]$Credentials,
  [Parameter(Position=2, Mandatory=$true)] [string]$Name,
  [Parameter(Position=3, Mandatory=$true)] [string]$FileName
 )
 Begin{
  #Connect
  $service = Connect-Exchange -MailboxName $MailboxName -Credential $Credentials
  $QuickStepsFolder = Get-QuickStepsFolder -MailboxName $MailboxName -service $service -Credentials $Credentials
  $ExistingSteps = Get-ExistingSteps -MailboxName $MailboxName -QuickStepsFolder $QuickStepsFolder
  if($ExistingSteps.ContainsKey($Name.Trim().ToLower())){
   $PR_ROAMING_XMLSTREAM = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x7C08,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);  
   $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
   $psPropset.Add($PR_ROAMING_XMLSTREAM)
   $propval = $null
   if($ExistingSteps[$Name.Trim().ToLower()].TryGetProperty($PR_ROAMING_XMLSTREAM,[ref]$propval)){
    [System.IO.File]::WriteAllBytes($FileName,$propval)
    Write-Host ('Exported to ' + $FileName)
   }
  } 
 }
}

Creating a QuickStep from XML (Unsupported)

Note creating Quicksteps using anything other then Outlook should be considered unsupported so if you use this make sure you do your own testing. Creating a QuickStep from the XML is just a matter of creating an Item and setting the PR_Roaming_XMLStream property with the appropriate XML from the item. The following cmdlet first reads the existing Quicksteps to ensure your creating a Quickstep with the same name as an existing one. eg

function Export-QuickStepXML{
 param(
     [Parameter(Position=0, Mandatory=$true)] [string]$MailboxName,
  [Parameter(Position=1, Mandatory=$true)] [PSCredential]$Credentials,
  [Parameter(Position=2, Mandatory=$true)] [string]$Name,
  [Parameter(Position=3, Mandatory=$true)] [string]$FileName
 )
 Begin{
  #Connect
  $service = Connect-Exchange -MailboxName $MailboxName -Credential $Credentials
  $QuickStepsFolder = Get-QuickStepsFolder -MailboxName $MailboxName -service $service -Credentials $Credentials
  $ExistingSteps = Get-ExistingSteps -MailboxName $MailboxName -QuickStepsFolder $QuickStepsFolder
  if($ExistingSteps.ContainsKey($Name.Trim().ToLower())){
   $PR_ROAMING_XMLSTREAM = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x7C08,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary);  
   $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
   $psPropset.Add($PR_ROAMING_XMLSTREAM)
   $propval = $null
   if($ExistingSteps[$Name.Trim().ToLower()].TryGetProperty($PR_ROAMING_XMLSTREAM,[ref]$propval)){
    [System.IO.File]::WriteAllBytes($FileName,$propval)
    Write-Host ('Exported to ' + $FileName)
   }
  } 
 }
}

I've put all these samples into a QuickSteps EWS module which you can download from here some examples of what you can do with this

to Get the QuickSteps folder

Get-QuickStepsFolder -MailboxName mailbox@domain.com

To Get the existing Quick Steps

 Get-QuickSteps -MailboxName mailbox@domain.com

This returns a HashTable of the QuickSteps to access a Quickstep within the collection use the Index value eg
 $QuickSteps = Get-QuickSteps -MailboxName mailbox@domain.com
 $QuickSteps["clutter"]

To Delete and existing QuickStep

Delete-QuickStep -MailboxName gscales@datarumble.com -Name 'Task Name'

This cmdlet will search for a quickstep based on the Name attribute in the QuickSteps XML document if one is found it will prompt if you want to delete it.

Monday, July 13, 2015

EWS Contacts rollup Powershell module

In this post I'm going to try to rollup a number of different Contact Scripts I've posted over the past couple of years into one module that will hopefully make things a little easier to use when you want to do things with Contacts using EWS and Powershell

I've put the code for this script up in GitHub repo https://github.com/gscales/Powershell-Scripts/blob/master/EWSContacts/EWSContactFunctions.ps1 it's 1K+ lines and I hope to add a few more things to the script as I go (and fix bugs). But if anybody has idea's please use GitHub to submit them. You can download the script as a zip from here

So here's what it can do at the moment

Get-Contact 

This can be used to get a Contact from the Mailbox's default contacts folder, other contacts sub folder or the Global Address List eg to get a contact from the default contact folder by searching using the Email Address (This will return a EWS Managed API Contact object).

Example 1

Get-Contact -MailboxName mailbox@domain.com -EmailAddress contact@email.com

This will search the default contacts folder using the ResolveName operation in EWS, it also caters for contacts that where added from the Global Address List in Outlook. When you add a contact from the GAL the email address that is stored in the Mailbox's contacts Folder uses the EX Address format. So in this case when you go to resolve or search on the SMTP address it won't find the contact that has been added from the GAL with this address. To cater for this the GAL is also searched for the EmailAddress you enter in (using ResolveName), if a GAL entry is returned (that has a matching EmailAddress) then the EX Address is obtained using Autodiscover and the UserDN property and then another Resolve is done against the Contacts Folder using the EX address.

Because ResolveName allows you to resolve against more then just the Email address I've added a -Partial Switch so you can also do partial match searches. Eg to return all the contacts that contain a particular word (note this could be across all the properties that are searched) you can use

Get-Contact -MailboxName mailbox@domain.com -EmailAddress glen -Partial

By default only the Primary Email of a contact is checked when you using ResolveName if you want it to search the multivalued Proxyaddressses property you need to use something like the following

Get-Contact -MailboxName  mailbox@domain.com -EmailAddress smtp:info@domain.com -Partial

Or to search via the SIP address you can use

Get-Contact -MailboxName  mailbox@domain.com -EmailAddress sip:info@domain.com -Partial

(using the Partial switch is required in this case because the EmailAddress your search on won't match the PrimaryAddress of the contact so in this case also you can get partial matches back).

There is also a -SearchGal switch for this cmdlet which means only the GAL is searched eg

Get-Contact -MailboxName mailbox@domain.com -EmailAddress gscales@domain.com -SearchGal

In this case the contact object returned will be read only (although you can save it into a contacts folder which I've used in another cmdlet).

Finally if your contacts aren't located in the default contacts folder you can use the folder parameter to enter in the path to folder that you want to search eg

Get-Contact -MailboxName mailbox@domain.com -EmailAddress gscales@domain.com -Folder "\Contacts\SubFolder"

Create-Contact

This can be used to create a contact, I've added parameters for all the most common properties you would set in a contact, some contact property need to be set via Extended properties (if you need to set this you can either add it in yourself or after you create the contact use Get-Contact and update the contact object).

Example 1 to create a contact in the default contacts folder

Create-Contact -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -FirstName John -LastName Doe -DisplayName "John Doe"

to create a contact and add a contact picture

Create-Contact -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -FirstName John -LastName Doe -DisplayName "John Doe" -photo 'c:\photo\Jdoe.jpg'

to create a contact in a user created subfolder

 Create-Contact -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -FirstName John -LastName Doe -DisplayName "John Doe" -Folder "\MyCustomContacts"

This cmdlet uses the EmailAddress as unique key so it wont let you create a contact with that email address if one already exists.

Update-Contact

This Cmdlet can be used to update an existing contact the Primary email address is used as a unique key so this is the one property you can't update. It will take the Partial switch like the other cmdlet but will always prompt before updating in this case.

Example1 update the phone number of an existing contact

Update-Contact  -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -MobilePhone 023213421

Example 2 update the phone number of a contact in a users subfolder

Update-Contact  -Mailboxname mailbox@domain.com -EmailAddress contactEmai@domain.com -MobilePhone 023213421 -Folder "\MyCustomContacts"

Delete-Contact

This Cmdlet can be used to delete a contact from a contact folders

eg to delete a contact from the default contacts folder

Delete-Contact -MailboxName mailbox@domain.com -EmailAddress email@domain.com

to delete a contact from a non user subfolder

Delete-Contact -MailboxName mailbox@domain.com -EmailAddress email@domain.com -Folder \Contacts\Subfolder

Export-Contact

This cmdlet can be used to export a contact to a VCF file, this takes advantage of EWS ability to provide the contact as a VCF stream via the MimeContent property.

To export a Contact to a vcf use

Export-Contact -MailboxName mailbox@domain.com -EmailAddress address@domain.com -FileName c:\export\filename.vcf

If the file already exists it will handle creating a unique filename

To export from a contacts subfolder use

Export-Contact -MailboxName mailbox@domain.com -EmailAddress address@domain.com -FileName c:\export\filename.vcf -folder \contacts\subfolder

Export-GALContact

This cmdlet exports a Global Address List entry to a VCF file, unlike the Export-Contact cmdlet which can take advantage of the MimeStream provided by the Exchange Store with GAL Contact you don't have this available. The script creates aVCF file manually using the properties returned from ResolveName. By default the GAL photo is included with the file but I have included a -IncludePhoto switch which will use the GetUserPhoto operation which is only available on 2013 and greater.

Example 1 to save a GAL Entry to a vcf

Export-GalContact -MailboxName user@domain.com -EmailAddress email@domain.com -FileName c:\export\export.vcf

Example 2 to save a GAL Entry to vcf including the users photo

Export-GalContact -MailboxName user@domain.com -EmailAddress email@domain.com -FileName c:\export\export.vcf -IncludePhoto

Copy-Contacts.GalToMailbox

This Cmdlet copies a contact from the Global Address list to a local contacts folder. The EmailAddress in used as a unique key so the same contact won't be copied into a local contacts folder if it already exists. By default the GAL photo isn't included but I have included a -IncludePhoto switch which will use the GetUserPhoto operation which is only available on 2013 and greater.

Example 1 to Copy a Gal contacts to local Contacts folder

Copy-Contacts.GalToMailbox -MailboxName mailbox@domain.com -EmailAddress email@domain.com

Example 2 Copy a GAL contact to a Contacts subfolder

Copy-Contacts.GalToMailbox -MailboxName mailbox@domain.com -EmailAddress email@domain.com  -Folder \Contacts\UnderContacts