Articles in the "Mailbox Delegate Management" series
- Super duper delegate retrieval script [This article]
- PowerShell module for managing Exchange mailbox delegates
- Small update to delegate management module
- Delegate management module updated to v1.3.5
- Delegate management module updated to support Exchange Online
- Delegate management module updated to v1.4.5
- Delegate management module updated to 1.4.6
- Delegate management module updated to v1.5.0
- Delegate management module updated
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).
1 2 3 4 5 6 7 |
$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:
1 2 3 4 5 6 7 8 9 |
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:
1 2 3 4 5 6 7 |
$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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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:
1 2 3 4 |
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:
1 2 3 4 5 |
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:
1 2 3 4 5 6 7 8 9 |
[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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
<# .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 |
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
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.
Where does the file get output to? I have run it, dont get any errors but never get a file with the data.
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.
Outstanding! Used this to change private delegate settings on Shared Mailbox. Worked great on Exchange 2010.
Hi,
Is there a way to run this against every mailbox in our system, rather than one at a time?
Thanks,
JR
The Get-MailboxDelegate cmdlet targets a specific mailbox, but you can pipe the output of Get-Mailbox into it to process all of those mailboxes. For example,
Get-Mailbox -ResultSize unlimited | Get-MailboxDelegate
or, depending on your target environment and how you are connected, something like(Get-Mailbox) | %{Get-MailboxDelegate $_.Identity}
.Thank you so much.