Super duper delegate retrieval script

Update 5/11/12: EWS 1.1 download link no longer valid, so the script has been updated to use 1.2.

Update 3/13/12: The script has been updated to accommodate running in either EMS or PowerShell with remoting. Support for additional delegate retrieval errors has been added, as well as loading the Exchange snap-in directly if no cmdlets are already in the session. Inline code and download have been updated.

Update 11/11/11: Minor update made to resolve a potential issue when translating user name to SID. Inline code and download have been updated.

Getting delegate information has always been tricky. Information is stored in multiple places, you have to use different tools to get that information, and business requirements can add to the data needed to paint a complete picture for a given delegate. Taking advantage of the EWS Managed API to do the heavy lifting for information not easily exposed, I wrote this script to report delegate permissions and settings, mostly to help when troubleshooting why a delegate can’t do something or is getting weird behavior.

Using the GetDelegates() method of the managed API not only retrieves the delegates and their permissions to the well-known folders, but also whether private items are visible, if meeting requests are sent to a specific delegate, and the meeting request handling of the owner. In reality, though, that isn’t enough information to give the total picture. Full mailbox access supersedes folder permissions, so knowing who has that is needed. My company uses the registry modifications that control where deleted items and sent items, which means a delegate needs permission to those folders, so getting that is needed. Lastly, delegates are granted send on behalf of permission (which, incidentally, isn’t even required for a delegate to send a meeting request on behalf of the owner), but if a delegate has send as permission, that takes precedence when sending email messages, so knowing if a delegate has that right is also necessary.

The only required argument is an owner’s identity, such as email address, username, alias, etc. An optional switch parameter is -includeSendAs. I made it an opt-in feature because getting the send as information from AD makes the entire script run over 12 times longer (in my company’s infrastructure): 1.7 seconds versus 20.7 seconds for an owner with five delegates. Although I prefer to not hard-code information that can be determined dynamically, I took the poor man’s route for the EWS URL, so you need to enter that on line 26. If you don’t care about any of the details of the script contents, you can simply stop reading, download it, and run with it.

  Get-Delegates.zip (2.5 KiB)

Thanks to the managed API, retrieving the delegates can be done in seven lines (four, if you specify the DLL path when loading it instead of putting it in a variable, the same for setting the EWS URL, and if you specify the Exchange version directly when creating the service object).

$ewsuri=[system.URI]"https://owa.domain.com/ews/exchange.asmx"
$dllpath = "$env:ProgramFiles\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"
[void][Reflection.Assembly]::LoadFile($dllpath)
$exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1
$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion)
$service.Url = $ewsuri
$service.GetDelegates($owner,$true)

After getting the delegates, full mailbox access and, optionally, send as are retrieved. Because of the added time to get send as, I added a progress bar while information is being retrieved. The delegate properties stored in the owner’s mailbox include email address, display name, and SID. The property returned for full mailbox access and send as is username. Since there isn’t a common property between the two collections, the usernames must be converted to SIDs:

function GetSID($acl)
	{
	$aSID = @()
	$acl | ForEach	{
		$adUser = [System.Security.Principal.NTAccount]($_.User.ToString())
		$aSID += $adUser.Translate([System.Security.Principal.SecurityIdentifier]).Value
		}
	$aSID
	}

Because of the registry changes to store items deleted from the owner’s mailbox in the owner’s Deleted Items folder, and to store items sent by the delegate from the owner in the owner’s Sent Items folder, Author permission or higher is needed for those folders. This is easy to do with Get-MailboxFolderPermission.

Once all the information has been retrieved, you need to loop through the collection of delegates. When an orphaned delegate is on an owner’s mailbox, Exchange returns the delegate in the collection, but only as an error, not with any identifying information. I look for a matching error message and then direct you to the hidden message and property that lists all the delegates to find out which delegate is orphaned:

$delegates.DelegateUserResponses | ForEach {
	if ($_.ErrorMessage -eq 'The delegate does not map to a user in the Active Directory.')
		{
		Write-Output "*Orphaned Delegate"
		Write-Output "  Check NON_IPM_SUBTREE\Freebusy Data\LocalFreebusy.eml,"
		Write-Output "  property 0x684A101E to determine orphan entry."
		}

If a delegate has a configuration error, such as not being listed in the publicDelegates attribute or in the Freebusy Data folder’s ACL, the delegate won’t be returned, so this specific error is noted. And if any other error occurs retrieving a delegate, instead of just continuing and throwing errors later, a generic error will be noted:

		elseif ($_.ErrorMessage -eq 'Delegate is not configured properly.')
			{
			Write-Output "*Misconfigured Delegate"
			Write-Output "  Delegate is missing from Freebusy Data folder or publicDelegates attribute."
			Write-Output "  Run delegation wizard in Outlook, edit delegate settings for user not"
			Write-Output "  listed in script output, and save in order to rewrite settings."
			}
		elseif ($_.Result -eq 'Error')
			{
			Write-Output "*Unknown Error with Delegate"
			Write-Output "  An error occurred retrieving the delegate, and there is not a response"
			Write-Output "  configured for it in this script."
			Write-Output "  Error message: $($_.ErrorMessage)"
			}

To report whether a given delegate has full mailbox access or send as permission, the SID of the delegate is matched against the respective array from the GetSID() function above:

if ($fmaSID -match $_.delegateuser.UserId.SID)
	{
	Write-Output "  Mailbox-level permission: Full Mailbox Access"
	}

The collection includes the granted permission to the well-known folders exposed in Outlook’s delegation wizard. Since it is rare for someone to use or grant permission to Journal or Notes, rather than just echo the permission array, I display just the other four folders:

			Write-Output "  Folder permissions"
			Write-Output "    Calendar: $($_.DelegateUser.Permissions.CalendarFolderPermissionLevel.ToString())"
			Write-Output "    Tasks: $($_.DelegateUser.Permissions.TasksFolderPermissionLevel.ToString())"
			Write-Output "    Inbox: $($_.DelegateUser.Permissions.InboxFolderPermissionLevel.ToString())"
			Write-Output "    Contacts: $($_.DelegateUser.Permissions.ContactsFolderPermissionLevel.ToString())"

To include the delegate’s potential permission to Deleted Items and Sent Items, I pass the respective folder permission object to the pipeline looking for a display name match:

[array]$delegateDIPerm = $deletedItemsPerm | where {$_.User -eq $delegateDisplayName}
if ($delegateDIPerm.Count -eq 1)
	{
	Write-Output "    Deleted Items: $($delegateDIPerm[0].AccessRights[0].ToString())"
	}
else
	{
	Write-Output "    Deleted Items: None"
	}

Lastly, whether a delegate receives meeting requests or can view private items is a property of the individual delegate’s entry in the collection. How the owner wants meeting requests handled is not per delegate, so that setting is a property of the root collection. All of this is then output to the success stream, whether that is the screen (the default) or redirection to a file.

You can download the script from the link above, or copy the contents below (double-click in code area to highlight all).

<#
.Synopsis
	Display a mailbox's delegates and permissions.
.Description
	Retrieve the list of delegates for a mailbox and display the mailbox permission,
	folder permissions, meeting invite settings, and (optionally) whether the
	delegate has Send As permission.
.Parameter MailboxOwner
	Valid identity string of the user whose mailbox has the delegates.
.Parameter IncludeSendAs
	Switch to indicate that you want Send As permission to be included.
.Example
	get-delegates.ps1 user@domain.com -includesendas
.Example
	get-delegates.ps1 domain\username
.Notes
	Version: 1.3
	Date: 5/11/12
#>
Param (
	[Parameter(Position = 0,Mandatory = $true,HelpMessage="Identity of mailbox owner")][string]$mailboxOwner,
	[Alias("SA")][switch]$includeSendAs #Perform Send As lookup (takes longer)	
	)

#Region Variables
$ewsuri=[system.URI]"https://owa.company.com/ews/exchange.asmx" #EWS URL
#EndRegion

#Region Functions
function LoadAPI
	{
	$dllpath = "$env:ProgramFiles\Microsoft\Exchange\Web Services\1.2\Microsoft.Exchange.WebServices.dll"
	if (-not(Test-Path $dllpath))
		{
		Write-Output "This system does not have the EWS Managed API 1.2 installed, which is required to run this script."
		Write-Output "The API can be download at http://www.microsoft.com/download/en/details.aspx?id=28952"
		exit
		}
	[void][Reflection.Assembly]::LoadFile($dllpath)
	}

function GetSID($acl)
	{
	$aSID = @()
	$acl | ForEach	{
		$adUser = [System.Security.Principal.NTAccount]($_.User.ToString())
		$aSID += $adUser.Translate([System.Security.Principal.SecurityIdentifier]).Value
		}
	$aSID
	}

function GetDelegates($owner)
	{
	$exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1
	$service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion)
	$service.Url = $ewsuri
	$service.GetDelegates($owner,$true)
	}

function GetFMA($identity)
	{
	Get-MailboxPermission $identity | Where-Object {$_.IsInherited -eq $false}
	}
	
function GetSendAs($identity)
	{
	Get-ADPermission $identity | Where-Object {$_.IsInherited -eq $false -and $_.ExtendedRights -eq 'Send-As'}
	}

function GetFolderPermission($mailbox,$folder)
	{
	Get-MailboxFolderPermission "$mailbox`:\$folder"
	}
#EndRegion

#Region Body
#Connect/Load Exchange 2010 snap-in
$testcmd = Get-Command Get-Mailbox -ErrorAction SilentlyContinue
if (-not($testcmd)){Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010}

LoadAPI
try
	{
	$user = Get-User $mailboxOwner -Filter {RecipientType -eq 'UserMailbox'} -ErrorAction Stop
	}
catch
	{
	Write-Output "`"$mailboxOwner`" cannot be found or does not have a mailbox.  Verify the entered information."
	exit
	}

#Get list of delegates and permissions from EWS
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving Delegates" -PercentComplete 0
$delegates = GetDelegates $user.WindowsEmailAddress.ToString()

#Get list of users with full mailbox access
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving FMA List" -PercentComplete 40
$fullMailboxAccess = GetFMA $user.Identity
$fmaSID = GetSID $fullMailboxAccess #Convert username to SID
if ($includeSendAs)
	{
	#Get list of users with send as permission from AD
	Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving Send As List" -CurrentOperation "(This part takes the longest.)" -PercentComplete 70
	$sendAs = GetSendAs $user.Identity
	$saSID = GetSID $sendAs #Convert username to SID
	}
#Get permissions for additional folders
Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Status "Retrieving additional folder permissions" -PercentComplete 90
$deletedItemsPerm = GetFolderPermission $user.Identity 'Deleted Items'
$sentItemsPerm = GetFolderPermission $user.Identity 'Sent Items'

Write-Progress -Activity "Getting Permissions for $($user.DisplayName)" -Completed $true

Write-Output "`n$($user.FirstName) $($user.LastName) has $($delegates.DelegateUserResponses.Count) delegate(s)"
#Loop through list of delegates
if ($delegates.DelegateUserResponses.Count -gt 0)
	{
	Write-Output "`nGlobal Delegate Meeting Request Handling: $($delegates.MeetingRequestsDeliveryScope)"
	$delegates.DelegateUserResponses | ForEach {
		Write-Output `n
		#Delegate account deleted in AD but still listed in list
		if ($_.ErrorMessage -eq 'The delegate does not map to a user in the Active Directory.')
			{
			Write-Output "*Orphaned Delegate"
			Write-Output "  Check NON_IPM_SUBTREE\Freebusy Data\LocalFreebusy.eml,"
			Write-Output "  property 0x684A101E to determine orphan entry."
			}
		elseif ($_.ErrorMessage -eq 'Delegate is not configured properly.')
			{
			Write-Output "*Misconfigured Delegate"
			Write-Output "  Delegate is missing from Freebusy Data folder or publicDelegates attribute."
			Write-Output "  Run delegation wizard in Outlook, edit delegate settings for user not"
			Write-Output "  listed in script output, and save in order to rewrite settings."
			}
		elseif ($_.Result -eq 'Error')
			{
			Write-Output "*Unknown Error with Delegate"
			Write-Output "  An error occurred retrieving the delegate, and there is not a response"
			Write-Output "  configured for it in this script."
			Write-Output "  Error message: $($_.ErrorMessage)"
			}
		else
			{
			$delegateDisplayName = $_.delegateuser.userid.displayname
			Write-output "*$delegateDisplayName `($($_.delegateuser.userid.primarysmtpaddress)`)"
			if ($fmaSID -match $_.delegateuser.UserId.SID)
				{
				Write-Output "  Mailbox-level permission: Full Mailbox Access"
				}
			else
				{
				Write-Output "  Mailbox-level permission: None"
				}
			if ($includeSendAs)
				{
				if ($saSID -match $_.delegateuser.UserId.SID)
					{
					Write-Output "  Send As permission: True"
					}
				else
					{
					Write-Output "  Send As permission: False"
					}
				}
			Write-Output "  Folder permissions"
			Write-Output "    Calendar: $($_.DelegateUser.Permissions.CalendarFolderPermissionLevel.ToString())"
			Write-Output "    Tasks: $($_.DelegateUser.Permissions.TasksFolderPermissionLevel.ToString())"
			Write-Output "    Inbox: $($_.DelegateUser.Permissions.InboxFolderPermissionLevel.ToString())"
			Write-Output "    Contacts: $($_.DelegateUser.Permissions.ContactsFolderPermissionLevel.ToString())"
			[array]$delegateDIPerm = $deletedItemsPerm | Where-Object {$_.User -eq $delegateDisplayName}
			if ($delegateDIPerm.Count -eq 1)
				{
				Write-Output "    Deleted Items: $($delegateDIPerm[0].AccessRights[0].ToString())"
				}
			else
				{
				Write-Output "    Deleted Items: None"
				}
			[array]$delegateSIPerm = $sentItemsPerm | Where-Object {$_.User -eq $delegateDisplayName}
			if ($delegateSIPerm.Count -eq 1)
				{
				Write-Output "    Sent Items: $($delegateSIPerm[0].AccessRights[0].ToString())"
				}
			else
				{
				Write-Output "    Sent Items: None"
				}
			
			Write-Output "  Receives Meeting Requests: $($_.delegateuser.receivecopiesofmeetingmessages)"
			Write-Output "  Can View Private Items: $($_.delegateuser.viewprivateitems)"
			}
		}
	}
Write-Output `n
#EndRegion

5 thoughts on “Super duper delegate retrieval script

  1. Hi Scott
    I came across your site after looking for a script to get delegate info from an Exchange 2010 mailbox.
    I’ve downloaded your Get-Delegates.ps1 script and have followed the instructions you’ve kindly published…..but I’m getting errors when running the script and cannot see what needs to be amended. Can you look at the output errors and advise where the problem may be please?

    When I run the script against my mailbox, I get the following output:
    —————————————————————
    [PS] C:\scripts>.\Get-Delegates.ps1 matthew.pollock -includeSendAs
    Exception calling “GetDelegates” with “3” argument(s): “The specified object was not found in the store.”
    At C:\scripts\Get-Delegates.ps1:56 char:23
    + $service.GetDelegates <<<< ($owner,$true)
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    New-Object : Constructor not found. Cannot find an appropriate constructor for type System.Security.Principal.NTAccount
    .
    At C:\scripts\Get-Delegates.ps1:46 char:23
    + $adUser = New-Object <<<< System.Security.Principal.NTAccount($_.User)
    + CategoryInfo : ObjectNotFound: (:) [New-Object], PSArgumentException
    + FullyQualifiedErrorId : CannotFindAppropriateCtor,Microsoft.PowerShell.Commands.NewObjectCommand

    You cannot call a method on a null-valued expression.
    At C:\scripts\Get-Delegates.ps1:47 char:29
    + $aSID += $adUser.Translate <<<< ([System.Security.Principal.SecurityIdentifier]).Value
    + CategoryInfo : InvalidOperation: (Translate:String) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

    Matt Pollock has delegate(s)
    —————————————————————

    Many Thanks

    Matt Pollock

  2. Exception calling “GetDelegates” with “2” argument(s): “The specified object was not found in the store.”
    At \\cfpbgfs01\users\justinr\Documents\DelegateManagement\DelegateManagement.psm1:680 char:4
    + $currentDelegates = $exchangeService.GetDelegates($EWSMailbox,$true)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ServiceResponseException

    I see a few people getting this. I donot have delegates on my account and I get no error. However I cant run this script against people that I know have delegates.

  3. Where does the file get output to? I have run it, dont get any errors but never get a file with the data.

  4. The output is a PowerShell object which, by default, will display in the shell/console. If you want to output to a file, you can pipe the object to Out-File or another Out- cmdlet.

  5. Outstanding! Used this to change private delegate settings on Shared Mailbox. Worked great on Exchange 2010.

Leave a Reply

Your email address will not be published. Required fields are marked *

*