Exploring Delegates with the EWS Managed API – Part I: The EWS Delegate Functions

As an Exchange administrator, a long-standing source of frustration for me has been the fact that Microsoft does not provide tools for administratively inspecting and repairing delegate settings.  Apart from granting Full Access permissions on the mailbox and logging into it with Outlook, all an Exchange admin can really do is inspect the GrandSendOnBehalfTo attribute (a.k.a. publicDelegates), tinker with relevant folder sharing permissions, and use MfcMAPI to nuke the hidden delegate forwarding rule when it is misbehaving, which is rather drastic as it stops ALL delegate forwarding for that mailbox.

Well, there is another option: turning to (or building) the tools to do the job. A fine example of just such a tool is the MessageOps EWS PowerShell Module, which builds upon Exchange Web Services to add a host of Exchange commands to PowerShell, including tools for dealing with delegates.  While it is a fine tool which I’ve found quite useful, I got it into my head to write my own set of delegate management tools, if for no other reason than to get a better handle on how delegates actually work under the hood. In short, I’m a glutton for punishment, so I set about re-inventing the wheel….

The EWS Managed API

Since I’m not really well-versed in writing code that directly utilizes MAPI, it seemed a no-brainer for me to instead use the other MS-blessed Exchange protocol: Exchange Web Services (EWS). Conveniently, Microsoft provides a handy tool for leveraging that protocol, the EWS Managed API, a .NET-based DLL (downloadable from GitHub) which can be loaded into PowerShell and handles the grunt work of constructing and interpreting the SOAP/XML blobs that make up EWS. While Microsoft doesn’t do such a great job of documenting how to leverage this tool from within PowerShell, developer Glen Scales (who was apparently involved in creating the MessageOps tools) has filled the gap wonderfully with code examples on his blog.

Another convenient thing about the EWS Managed API is that it provides access to EWS-native calls for dealing with delegates, so this looked like it was exactly what I was needing.  As it turned out (as we’ll shortly see), this wasn’t exactly the case, but it is worth taking a look at it anyway.

Load ‘Em Up

To make the tools I wanted to create convenient to use, I decided early on to implement them as standalone global functions as part of a single dot-sourced PowerShell script. Furthermore, since each of those functions would have to go through the process of loading the Managed API dll and creating a web services object, it made since to factor out that functionality into a local utility function, so we’ll start with that.

function Load-EWS {
## Load Managed API dll

First up, we need to search for and load the dll:

<#First, check for the EWS MANAGED API. If it is present, import the highest version. Otherwise, exit. #> 
$EWSDLL = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services'|Sort-Object Name -Descending| Select-Object -First 1 -ExpandProperty Name)).'Install Directory') + "Microsoft.Exchange.WebServices.dll") 
if (Test-Path $EWSDLL) 
 { 
 Write-Verbose " Loading EWS Managed API..."
 Import-Module $EWSDLL 
 } 
else 
 { 
 "This script requires the EWS Managed API." 
 "Please download and install the current version of the EWS Managed API from" 
 "https://github.com/OfficeDev/ews-managed-api" 
 "" 
 "Exiting Script." 
 exit 
 }

Now, create the service object. Note that I prefix the service object variable with “script:” when I instantiate it. This scopes the variable to make it available to whatever function calls this one.

 # Set the Exchange Version variable
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2 

$script:service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)

Now we need to get a little creative in terms of the credentials that EWS will use to access the remote mailbox. (I won’t be discussing impersonation here, since I haven’t really delved into it.) The credentials being used by EWS to access the target mailbox will need to be for an account which has been granted Full Access to the mailbox in question. Now, we could just tell EWS to use whatever credentials the script is running under (setting $service.UseDefaultCredentials = $true), but that presents a problem in our environment.  We currently have a hybrid Exchange deployment, partly on-premise, partly in Office 365 Exchange Online. The UseDefaultCredentials option works fine for on-prem, but breaks in the cloud.  EWS needs to be passed credentials where the username is in appropriate UserPrincipalName format to work in the cloud.  For the purposes of my script, I’ve assumed that the appropriate credentials have been stashed in a variable called $EWScred by using the Get-Credential command.

# Tell the service object to use credentials stored in $EWScred variable, defined before invocation by running $EWScred=get-credential

#$service.UseDefaultCredentials = $true
$service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $EWScred.UserName, $EWScred.GetNetworkCredential().Password

For the next step, we have to determine where the mailbox is in order to set the EWS connection endpoint. As mentioned previously, I wrote this script to run in a hybrid environment, so the target mailbox could exist either in our on-premise Exchange 2010 environment or in the cloud. There are two ways to accomplish this. One is to simply use autodiscover to find the mailbox and retrieve the endpoint URL. The other is to perform a Get-Recipient command in the local Exchange Management Shell, look at the recipient type returned, and set a hard-coded endpoint URL as appropriate. Although I was able to successfully use the autodiscover approach (and the appropriate code is shown below, commented-out), that slows down execution, so I opted for hard-coded URLs.

##CAS URL Option 1 Autodiscover
#$Service.EnableSCPLookup = $False #Skip SCP lookups to make autodiscover of cloud mailboxes work.
#$service.AutodiscoverUrl($Identity,{$True})
#The above line will only work if $Identity holds the SMTP address of the target mailbox.

#CAS URL Option 2 Hardcoded 
$localuri=[system.URI] "https://myexchangeCAS.mydomaindomain.com/ews/exchange.asmx"
$clouduri=[system.URI] "https://outlook.office365.com/ews/exchange.asmx"

# Here is the reason for running this in an EMS session.
$script.rec=get-recipient $Identity
if ($rec.RecipientType -eq "MailUser") {
 $script:CloudUser = $True
 $Script:MailboxType = "Remote Mailbox" 
 $service.Url = $clouduri}
else {
 $script:CloudUser = $False
 $Script:MailboxType = "Local Mailbox" 
 $service.Url = $localuri}

} # End of utility function Load-EWS()

And that is all there is too it for our initial setup. We’ll be calling this utility function at the beginning of each of the global functions demonstrated throughout this series of articles. With that housekeeping code out of the way, we can get down to the heavy lifting.

Invoking and Parsing GetDelegates()

Using the EWS GetDelegates() function is actually quite straightforward. First, we’ll define our function and its one input parameter, the UPN identifier for the mailbox we are looking at. After that, we’ll call the utility function that we created to load EWS, then we’ll invoke the GetDelegates() method on our service object.

function Global:Get-EWSDelegates  {
param (
 [parameter(
 Mandatory=$true,
 ValueFromPipeline=$True,
 ValueFromPipelineByPropertyName=$True,
 Position=0,
 HelpMessage=’Identity of target mailbox’)]
 [Alias("Id")]
 [STRING]$Identity ) 

 Load-EWS
Write-Verbose "Using CAS URI $($Service.url) for $($MailboxType) $($rec.PrimarySmtpAddress)"

 #Get Delegates for Mailbox 
Write-Verbose "Getting delegates for $($rec.PrimarySmtpAddress):"
$EWSdelegates = $service.getdelegates($rec.PrimarySmtpAddress,$true)

That really is it. All of our delegate info has been stuffed into the variable $EWSdelegates. Now let’s parse it and display the contents.

# Parse returned object and display results
Write-Host "MeetingRequestsDeliveryScope: $($EWSdelegates.MeetingRequestsDeliveryScope)"
#  Enumerate Delegates and their settings
foreach($Delegate in $EWSdelegates.DelegateUserResponses){ 
    write-host "`n`r`tDelegate: $($Delegate.DelegateUser.UserId.DisplayName) <$($Delegate.DelegateUser.UserId.PrimarySmtpAddress)>"
    write-host "`tReceives copies of meeting messages: $($Delegate.DelegateUser.ReceiveCopiesOfMeetingMessages)"
    write-host "`tDelegate can view private items: $($Delegate.DelegateUser.ViewPrivateItems)"
    $Delegate.DelegateUser.Permissions | select CalendarFolderPermissionLevel,TasksFolderPermissionLevel,InboxFolderPermissionLevel,ContactsFolderPermissionLevel,NotesFolderPermissionLevel | ft
    if ($Delegate.Result -like "Error") {
        Write-Host "Result: $($Delegate.Result)" -ForegroundColor Red
        Write-Host "ErrorCode: $($Delegate.ErrorCode)" -ForegroundColor Red
        Write-Host "ErrorMessage: $($Delegate.ErrorMessage)" -ForegroundColor Red 
    } # End if error 
 } # End foreach Delegate in $EWSDelegates

return $EWSDelegates # Return the object to the EMS command line.
} # End function Get-EWSDelegates()

Note the error handling code. That is pretty important, as we shall see very shortly when we look at how all of this flies off the rails. For now, though, here is some sample output:

PS C:\> Get-EWSDelegates -Id "User, Joe Sr."
MeetingRequestsDeliveryScope: DelegatesAndSendInformationToMe

 Delegate: User, Jane <jane.user@utexas.edu>
 Receives copies of meeting messages: True
 Delegate can view private items: True

 CalendarFolderPermissionLevel TasksFolderPermissionLevel InboxFolderPermissionLevel ContactsFolderPermissionLevel NotesFolderPermissionLevel
 ----------------------------- -------------------------- -------------------------- ----------------------------- --------------------------
 Editor Editor None None None



DelegateUserResponses MeetingRequestsDeliveryScope
--------------------- ----------------------------
{Microsoft.Exchange.WebServices.Data.DelegateUserResponse} DelegatesAndSendInformationToMe

Modifying or Adding Delegates

Adding or modifying delegates is just as straightforward. All we really have to do is perform a GetDelegates() operation, modify the resulting object as needed, and write it back to the server. Here, I’ve documented the steps taken by the function with inline comments. Note that I’ve included a help block to enable Get-Help for the function. This function also provides an example of input validation in the parameter block.

Warning (Feb. 4, 2015): A recently-introduced regression in Office 365 Exchange Online results in the server ignoring the target mailbox spacified in the AddDelegates() EWS call and instead writing the delegates to the mailbox associated with the account that the call is being made under. I’ve opened a PSS ticket with Microsoft to report the bug.
Update (May 27, 2015): The bug is reportedly fixed as of build 15.01.0143 (as seen by the AdminDisplayVersion attribute of the Mailbox object).  That build went right by me, but I can confirm that it works as of Version 15.1 (Build 172.22).
function Global:Add-EWSDelegate {

<#
.SYNOPSIS

Adds delegate to target mailbox or modifies existing delegate.

.DESCRIPTION

Takes the identity of the user and adds a delegate with the specified settings, or modifies the delegate settings if it is already present.

This script must be run in an Exchange Management Shell session. 

Full Access permissions must have been granted on the mailbox prior to running the script. Also, the credentials for the Full Access accunt must be placed in a variable called $EWScred via the Get-Credential command, specifying the username in UserPrincipalName format.

.PARAMETER Identity
 
 Identity of target mailbox

.PARAMETER Delegate

 Identity of the delegate being added

.PARAMETER GetsMeetingRequests

 Boolean value specifying whether or not meeting requests are forwarded to this delegate

.PARAMETER CanSeePrivate

 Boolean value specifying whether or not this delegate can see the delegator's private items

.PARAMETER CalendarRights

 Delegate permissions on the Calendar folder. Allowed values include None, Editor, Reviewer, Author

.PARAMETER TasksRights

 Delegate permissions on the Tasks folder. Allowed values include None, Editor, Reviewer, Author

.PARAMETER InboxRights

 Delegate permissions on the Inbox folder. Allowed values include None, Editor, Reviewer, Author

.PARAMETER ContactsRights

 Delegate permissions on the Contacts folder. Allowed values include None, Editor, Reviewer, Author

.PARAMETER NotesRights

 Delegate permissions on the Notes folder. Allowed values include None, Editor, Reviewer, Author

.EXAMPLE

 Add-EWSDelegate.ps1 -Identity userA@yoyodyne.com -Delegate userB@yoyodyne.com -CalendarRights Editor -GetsMeetingRequests 
#>

param (
 [parameter(
 Mandatory=$true,
 ValueFromPipeline=$True,
 ValueFromPipelineByPropertyName=$True,
 Position=0,
 HelpMessage=’Identity of target mailbox’)]
 [Alias("Id")]
 [STRING]$Identity, 

 [parameter(
 Mandatory=$true,
 Position=1,
 HelpMessage='Identity of delegate')]
 [STRING]$Delegate,

 [BOOLEAN]$GetsMeetingRequests,

 [BOOLEAN]$CanSeePrivate,

 [ValidateSet('None','Editor','Reviewer','Author')]
 [STRING]$CalendarRights,

 [ValidateSet('None','Editor','Reviewer','Author')]
 [STRING]$TasksRights,

 [ValidateSet('None','Editor','Reviewer','Author')]
 [STRING]$InboxRights,

 [ValidateSet('None','Editor','Reviewer','Author')]
 [STRING]$ContactsRights,

 [ValidateSet('None','Editor','Reviewer','Author')]
 [STRING]$NotesRights

) # End parameters

Write-Verbose "Loading EWS"
 Load-EWS

<# Let's get the primarySmtpAddress of the new delegate so that we can properly compare with what is on the list, just in case what has been provided is a secondary address. #>
$NewDelegate=$Null
$NewDelegate = Get-Recipient $Delegate
$Delegate = $NewDelegate.PrimarySmtpAddress
if ($NewDelegate -eq $null) {
 Write-Host "Requested delegate $($Delegate) does not appear to be valid!" -ForegroundColor Red
 return $error[0] }
 
 <# Now, we need to make sure the delegate is in the same place as the target mailbox. Delegation does not work across orgs. For example, an on-premise user cannot be made a delegate for a cloud mailbox. #>
 if ($NewDelegate.RecipientType -eq "MailUser") {$DelegateMailboxType = "Remote Mailbox" }
 elseif ($NewDelegate.RecipientType -eq "UserMailbox") {$DelegateMailboxType = "Local Mailbox" }
 elseif ($NewDelegate.RecipientType -eq "MailUniversalSecurityGroup") {
 Write-Host "Groups are not supported for delegation via EWS." -Foregroundcolor Red # More about this in a bit.
 Return
 }
 else {
 Write-Host "Mailbox type $($NewDelegate.RecipientType), $($NewDelegate.RecipientTypeDetails) unsupported for delegation in EWS." -ForegroundColor Red
 return 
 }
 
 if (($DelegateMailboxType -ne "Group") -and ($DelegateMailboxType -ne $MailboxType)) {
 Write-Host "Delegate and Delegator are not in the same place!" -ForegroundColor Red
 Write-Host "Mailbox $($rec.PrimarySmtpAddress) is a $($MailboxType), but $($Delegate) is a $($DelegateMailboxType)" -ForegroundColor Red
 return
 }

Write-Verbose "Good news, everyone! $($MailboxType) $($rec.PrimarySmtpAddress) and $($DelegateMailboxType) $($Delegate) are in the same place!" 

# Our proposed delegate is legit. Let's get on with it.
# We'll retrieve a copy of the existing delegates.
 $EWSdelegates = $service.GetDelegates($rec.PrimarySmtpAddress,$true)

 # Let's see if the delegate is already present. If so, we'll just modify the settings. Otherwise, add....
 $exists = $false

 Write-Verbose "Checking to see if delegate $($Delegate) is already in the list of $($EWSdelegates.Count) delegates"
 foreach ($ExistingDelegate in $EWSdelegates.DelegateUserResponses) {
 if ($ExistingDelegate.DelegateUser.UserId.PrimarySmtpAddress.ToLower() -eq $Delegate.ToLower()) {
 Write-Host "Delegate $($Delegate) is already present in delegates list. Updating permissions."
 $exists = $True
 
# Here we modify the permissions and settings for this delegate.
 if ($CalendarRights -notlike $null) {
 $ExistingDelegate.DelegateUser.Permissions.CalendarFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$CalendarRights }
 if ($TasksRights -notlike $null) {
 $ExistingDelegate.DelegateUser.Permissions.TasksFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$TasksRights }
 if ($InboxRights -notlike $null) {
 $ExistingDelegate.DelegateUser.Permissions.InboxFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$InboxRights }
 if ($ContactsRights -notlike $null) {
 $ExistingDelegate.DelegateUser.Permissions.ContactsFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$ContactsRights }
 if ($NotesRights -notlike $null) {
 $ExistingDelegate.DelegateUser.Permissions.NotesFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$NotesRights }
 if ($GetsMeetingRequests -notlike $null) {
 $ExistingDelegate.DelegateUser.ReceiveCopiesOfMeetingMessages = $GetsMeetingRequests}
 if ($CanSeePrivate -notlike $null) {
 $ExistingDelegate.DelegateUser.ViewPrivateItems = $CanSeePrivate }

 try { $update= $service.UpdateDelegates($rec.PrimarySmtpAddress,$EWSdelegates.MeetingRequestsDeliveryScope,$ExistingDelegate.DelegateUser)}
 catch {write-host "Update delegates failed!" -ForegroundColor Red
 return $error[0] }

 
 } # End of section for modifying existing delegateee 
 
 } # end foreach existingdelegate

# Here we add the delegate if it isn't already on the list.
 if ($exists -eq $false) {
 Write-Host "Adding delegate $($Delegate)"

 $dgUser = new-object Microsoft.Exchange.WebServices.Data.DelegateUser($Delegate) 
# Use standard defaults on the following if they are not specified in parameters passed to the function.
# For ViewPrivateItems, the default is false. For ReceiveCopiesOfMeetingMessages, the default is true.

if ($CanSeePrivate -notlike $null) {
 $dgUser.ViewPrivateItems = $CanSeePrivate }
else { $dgUser.ViewPrivateItems = $null } 

if ($GetsMeetingRequests -notlike $null) {
 $dgUser.ReceiveCopiesOfMeetingMessages = $GetsMeetingRequests }
else { $dgUser.ReceiveCopiesOfMeetingMessages = $true } 

if ($CalendarRights -notlike $null) 
 {$dgUser.Permissions.CalendarFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$CalendarRights }
else {$dgUser.Permissions.CalendarFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::None }
 
if ($TasksRights -notlike $null) 
 {$dgUser.Permissions.TasksFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$TasksRights }
else {$dgUser.Permissions.TasksFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::None }
 
if ($InboxRights -notlike $null) 
 { $dgUser.Permissions.InboxFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$InboxRights }
else { $dgUser.Permissions.InboxFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::None } 
 
 if ($ContactsRights -notlike $null)
 { $dgUser.Permissions.ContactsFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$ContactsRights } 
else { $dgUser.Permissions.ContactsFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::None }
 
 if ($NotesRights -notlike $null) 
 { $dgUser.Permissions.NotesFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::$NotesRights }
else { $dgUser.Permissions.NotesFolderPermissionLevel = [Microsoft.Exchange.WebServices.Data.DelegateFolderPermissionLevel]::None }
 
 $dgArray = new-object Microsoft.Exchange.WebServices.Data.DelegateUser[] 1 
 $dgArray[0] = $dgUser 
 $service.AddDelegates($rec.PrimarySmtpAddress,$EWSdelegates.MeetingRequestsDeliveryScope,$dgArray) 

 } # End of section adding new delegate
 
} # End global function Add-EWSdelegate

And here is some sample output:

PS C:\> Add-EWSDelegate joe.user@austin.utexas.edu -Delegate "User, Todd L" -InboxRights Reviewer -GetsMeetingRequests $false
Adding delegate toddlocaluser@austin.utexas.edu


DelegateUser : Microsoft.Exchange.WebServices.Data.DelegateUser
Result : Success
ErrorCode : NoError
ErrorMessage : 
ErrorDetails : {}
ErrorProperties : {}

Removing a delegate is even more simple.

function Global:Remove-EWSDelegate {

<#
.SYNOPSIS

Removes specified delegate from target mailbox

.DESCRIPTION

Takes the identifier of the target mailbox and removes the specified delegate from that mailbox.

This script must be run in an Exchange Management Shell session. 

Full Access permissions must have been granted on the mailbox prior to running the script. Also, the credentials for the Full Access accunt must be placed in a variable called $EWScred via the Get-Credential command, specifying the username in UserPrincipalName format.

.PARAMETER Identity
 
 Identifier of the target mailbox

.PARAMETER Delegate

 Identifier of delegate to be removed

.EXAMPLE

 Remove-EWSDelegate.ps1 -Id joe.user@mydomain.com -Delegate my.delegate@mydomain.com
#>


param (
 [parameter(
 Mandatory=$true,
 ValueFromPipeline=$True,
 ValueFromPipelineByPropertyName=$True,
 Position=0,
 HelpMessage=’Identifier of target mailbox’)]
 [Alias("Id")]
 [STRING]$Identity,

 [parameter(
 Mandatory=$true,
 Position=1,
 HelpMessage='Identifier of delegate to be removed')]
 [STRING]$Delegate
) 
 

 $DelegateToBeRemoved=$Null
 $DelegateToBeRemoved = Get-Recipient $Delegate

if ($DelegateToBeRemoved.PrimarySmtpAddress -eq $null) {
 Write-Host "Specified delegate $($Delegate) does not appear to be valid!" -ForegroundColor Red
 return $error[0] }

 $Delegate = $DelegateToBeRemoved.PrimarySmtpAddress

 Write-Verbose "Loading EWS"
 Load-EWS

 Write-Verbose "Retrieving current list of delegates."
 $EWSdelegates = $service.GetDelegates($rec.PrimarySmtpAddress,$true)

 $removed=$false

 Write-Verbose "Searching current delegate list for delegate to be removed."
 foreach($CurrentDelegate in $EWSdelegates.DelegateUserResponses){ 
 
 if($CurrentDelegate.DelegateUser.UserId.PrimarySmtpAddress.ToLower() -eq $Delegate.ToLower()){ 
 Write-Host "Removing Delegate $($Delegate)!"
 $service.RemoveDelegates($rec.PrimarySmtpAddress,$Delegate) 
 $removed=$true
 } # End of handler for matching delegate.
} # End iterating through list of delegates.
 if ($removed -eq $false) {Write-Host "Delegate $($Delegate) not found! Verify address." -ForegroundColor Yellow }
} # End of global function Remove-EWSDelegate()

And here is some sample output:

PS C:\> Remove-EWSDelegate joe.user -Delegate "User, Todd L"
Removing Delegate toddlocaluser@austin.utexas.edu!


DelegateUser : 
Result : Success
ErrorCode : NoError
ErrorMessage : 
ErrorDetails : {}
ErrorProperties : {}

One thing we’ve not yet tinkered with is the MeetingRequestsDeliveryScope, which corresponds the settings in Outlook’s delegate window specifying whether meeting requests and responses are delivered to delegate only but with a copy of requests and responses delivered to delegator, delegates only, or both. That is simple enough to modify.

function Global:Set-EWSDelegates {

<#
.SYNOPSIS

Modifies global delegate settings on target mailbox.

.DESCRIPTION

Sets global delegate settings on target mailbox. This corresponds to the settings in Outlook's delegate window specifying whether meeting requests and responses are delivered to delegate only but with a copy of requests and responses delivered to delegator, delegates only, or both.

This script must be run in an Exchange Management Shell session. 

Full Access permissions must have been granted on the mailbox prior to running the script. Also, the credentials for the Full Access accunt must be placed in a variable called $EWScred via the Get-Credential command, specifying the username in UserPrincipalName format.

.PARAMETER Identifier
 
 Identifier of target mailbox.

.PARAMETER DeliveryScope

 Determines to whom meeting requests and responses are sent.

 This parameter can take the following values:
 * DelegatesOnly
 * DelegatesAndMe
 * DelegatesAndSendInformationToMe
 * NoForward

.EXAMPLE

 Set-EWSDelegates.ps1 -UPN mbx@myDomain.com -DeliveryScope DelegatesAndMe
#>

param (
 [parameter(
 Mandatory=$true,
 Position=0,
 HelpMessage=’Identifier of target mailbox’)]
 [Alias("Id")]
 [STRING]$Identity,

 [parameter(
 Mandatory=$True,
 Position=1,
 HelpMessage=’Who should receive notifications’)]
 [ValidateSet('DelegatesOnly','DelegatesAndMe','DelegatesAndSendInformationToMe','NoForward')]
 [STRING]$DeliveryScope

) 
 Write-Verbose "Loading EWS."
 Load-EWS
 Write-Verbose "Retrieving current delegate settings."
 $EWSdelegates = $service.GetDelegates($rec.PrimarySmtpAddress,$true)
 Write-Verbose "Writing back delegate object with new scope."
 $EWSupdatedDelegates = $service.UpdateDelegates($rec.PrimarySmtpAddress,$DeliveryScope,$EWSdelegates.DelegateUserResponses[0].DelegateUser)

}

So, What’s the Catch?

So far so good.  The built-in delegate functions seem to work quite nicely. However, there is one problem. They don’t work with delegation to groups, neither in terms of adding groups as delegates or displaying group delegates. Here is what we see running Get-EWSDelegates if a group has been set as one of the delegates.

PS C:\> Get-EWSDelegates joe.user
MeetingRequestsDeliveryScope: DelegatesAndMe

 Delegate: <>
 Receives copies of meeting messages: 
 Delegate can view private items: 
Result: Error
ErrorCode: ErrorDelegateNoUser
ErrorMessage: The delegate does not map to a user in the Active Directory.

 Delegate: User, Jane <jane.user@utexas.edu>
 Receives copies of meeting messages: True
 Delegate can view private items: True

 CalendarFolderPermissionLevel TasksFolderPermissionLevel InboxFolderPermissionLevel ContactsFolderPermissionLevel NotesFolderPermissionLevel
 ----------------------------- -------------------------- -------------------------- ----------------------------- --------------------------
 Editor Editor None None None



DelegateUserResponses MeetingRequestsDeliveryScope
--------------------- ----------------------------
{Microsoft.Exchange.WebServices.Data.DelegateUserResponse, Microsoft.Exchang... DelegatesAndMe

This is a problem in the Exchange environment which I manage, since we routinely set up mail-enabled universal groups as delegates on shared mailboxes to simplify their management.  The MAPI protocols used by Windows Outlook happily allows this, but the delegate functions within EWS don’t know how to handle it. (This leads me to wonder how Outlook for Mac, which uses EWS, is able to set delegates. At some point I should decrypt and inspect the transactions between Outlook:mac and the server to see how that is pulled off.)

What to do, what to do? We’ll it seemed like the only solution was to bypass the built-in delegate functions and create my own, a topic which we’ll take up in the next installment.

Leave a Reply