Get mailbox folder permissions using EWS multithreading

A few weeks ago I was involved in a migration project. At one point in time we needed a script to retrieve permissions on mailboxes on folder-level. Besides this we needed to read the property PR_NT_SECURITY_DESCRIPTOR for folders.

Why we needed to read the property PR_NT_SECURITY_DESCRIPTOR?

Read more about in this article here!

Now back to the script. I know there are many scripts out there, which are doing this job. But we needed something to get the data real quick and a way to retrieve SIDs. I started looking for all options and ended in .NET mutlithreading for the first and MrMAPI for the second need.

I used most of Glen Scales scripts to connect to Exchange. So a huge thanks goes into his direction!

What do you need to run the script?

The script accepts the following parameters:

Parameter

Description

EmailAddress The e-mail address of the mailbox, which will be checked. The script accepts piped objects from Get-Mailbox or Get-Recipient.
Credentials Credentials you want to use. If omitted current user context will be used.
Impersonate Use this switch, when you want to impersonate.
CalendarOnly By default the script will enumerate all folders. If you want to limit to folders with type “IPF.Appointment” use this switch.
RootFolder From where you want to start the search for folders. Default is “MsgFolderRoot”. Other posible value is “Root”.
Server By default the script tries to retrieve the EWS endpoint via Autodiscover. If you want to run the script against a specific server or endpoint, just provide the name in this parameter. Not the URL!
Threads How many threads will be created. Be careful as this is really CPU intensive! By default 15 threads will be created. I limit the number of threads to 20.
MultiThread If you want to run the script multi-threaded use this switch. By default the script don’t use threads.
MaxResultTime The timeout for a job, when using multi-threads. Default is 240 seconds.
MrMapi The path to MrMapi.exe. Only full path is accepted!
UseMrMapi Switch to tell the script to use MrMapi or not.
TrustAnySSL Switch to trust any certificate.

How it looks in action?

By default it runs in the users context and also uses Autodiscover to retrieve the EWS endpoint for the given e-mail address. The root from where the script starts to search for folders is MsgFolderRoot. This is normaly the highest folder a user can reach with Outlook and set any permissions.

MBPermEWS01

In order to run it against a mailbox, where you don’t have access you need to Impersonate

MBPermEWS02

Otherwise you will get an error

MBPermEWS03

Here when you use multithreading

MBPermEWS04

Note: Please be warned that the performance heavily depends on your machine! By default 15 threads will be created, which could overhelm your machine!

When you are using multithreading and you want to stop all of the threads it is not sufficient to stop the script with CTRL+C. This will only stop the script, but the RunspacePool and the queued threads are still there. You would need to close the PS process in order to have all of them stopped.

Therefore I added some logic to the script. If you want to stop all the threads and exit the script just press CTRL+Q. The script is listening to this combination and will start to kill all the queued threads and exit the script.

MBPermEWS05

MBPermEWS06

To see the difference between multithreading vs. not multithreading here some statistics:

MBPermEWS07

48sec. vs 138sec. makes a factor of 2,875 faster. But this was only in my lab, which is not well sized. I did the same test in a productive environment:

MBPermEWS08

Here I got 152sec. vs 1441sec. , which is a factor of 9,48.

You can gain some more speed, when you define the server instead of having the script using AutoD for each mailbox.

As an example what I did during a migration and I needed to retrieve all affected mailboxes:

Get-Mailbox -ResultSize unlimited|
 .\Get-MailboxFolderPermissionEWS.PS1 -Server mail.adatum.com -Impersonate -UseMrMapi -MrMapi C:\Temp\Scripts\mrmapi.exe -MultiThread -Threads 15 |
?{($_.User -notmatch 'ANONYMOUSLOGON')-and ($_.User -ne $null) -and ($_.User -notmatch 'Everyone') -and ($_.SIDinSD -notmatch 'S-1-5-21-398472632-1282482148-99536377')}|
Export-Csv -NoTypeInformation -Path .\SID_Report.csv

With this I had a CSV file from where I could proceed.

Feedback is always welcome!

 

22 thoughts on “Get mailbox folder permissions using EWS multithreading

  1. Pingback: The good, the bad and sIDHistory | The clueless guy

  2. Pingback: Get-DatabaseEvent: Who deleted my items? | The clueless guy

  3. Pingback: 获取邮箱目录权限 - Exchange中文站

  4. Hi Ingo – I came here on the advice of Glen Scales over in the exchange forums – basically i am trying to do something similar – but I am using EWS to enumerate mailboxes and report on folder retention tags and items per folder across o365 and onprem – this all works – the issue is how to speed it up – so I have looked at your script but when i try to incorporate multithreading into mine – I get nothing back – no results – I suspect its something to do with variable scope or that fact the main body of my original script is now in a function block being read into the script block or somethign. Most greatful if you could have a quick look at mine and see what is wrong? I collect all the folder tag information and store it in a variable $Global:retentioninfo – then output this to CSV – this works fine without trying to incorporate multithreading….. Here is the script details:

    [CmdletBinding()]

    param(

    [Parameter(
    Position = 0,
    Mandatory=$true,
    ValueFromPipelineByPropertyName=$true

    )]
    [String[]]
    $PrimarySmtpAddress,

    [parameter( Mandatory=$false, Position=1)]
    [ValidateRange(0,8)]
    [int]$Threads= ‘6’,

    [parameter( Mandatory=$false, Position=2)]
    [switch]$MultiThread=$false,

    [parameter( Mandatory=$false, Position=3)]
    $MaxResultTime=’240′

    )

    begin{

    $Jobs = @()
    $Sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
    $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $Threads,$Sessionstate, $Host)
    $RunspacePool.ApartmentState = “STA”
    $RunspacePool.Open()

    }

    process {

    function Get-UserFolderRentention{
    param(

    [Parameter(
    Position = 0,
    Mandatory=$true,
    ValueFromPipelineByPropertyName=$true

    )]
    [String[]]
    $PrimarySmtpAddress

    )

    $Global:retentioninfo = @()
    $Global:RetentionTags = @()

    #$startTime = Get-Date
    $error.clear()
    [string]$LogFile = “C:\Temp\Log.txt”
    [int]$j=’1′

    $o365credential = get-credential -credential o365creds

    $ExchangeSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri “https://outlook.office365.com/powershell-liveid/” -Credential $o365credential -Authentication “Basic” -AllowRedirection
    Import-PSSession $ExchangeSession -Prefix o365 -AllowClobber

    $credential = Get-Credential “onpremcreds”

    Add-Type -Path “C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll”
    $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService

    $Global:retentioninfo = @()
    $Global:RetentionTags = @()
    if($LogFile)
    {
    Remove-Item $LogFile -ea SilentlyContinue
    }

    function folderretention()
    {
    Write-Host -ForegroundColor DarkGreen “Hello inside function”

    $FPageSize = 500
    $FOffset = 0
    $MoreItems =$true
    $ItemCount=0
    $folderView = new-object Microsoft.Exchange.WebServices.Data.FolderView($FPageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
    $folderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep
    $oFindFolders = $exchangeService.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$null,$folderView)
    #?{(($_.DisplayName -notlike ‘Calendar’) -and ($_.DisplayName -notlike ‘*contacts’) -and ($_.DisplayName -notlike ‘*recipient*’))}

    $Global:RetentionTags = $exchangeService.GetUserRetentionPolicyTags()

    function GetTagName($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).DisplayName }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.DisplayName }
    }
    }

    function GetRetentionAction($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).RetentionAction }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.RetentionAction }

    }
    }

    function GetRetentionPeriod($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).RetentionPeriod }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.RetentionPeriod }

    }
    }

    # $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($FpageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
    # $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow
    # $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet(
    #[Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly)
    $oFindFolders | %{
    # $oFinditems = $exchangeService.FindItems($_.Id,$itemview)

    $obj = New-Object PSObject
    $obj | add-member noteproperty DisplayName $_.DisplayName
    $obj | add-member noteproperty PolicyTag (GetTagName $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty ArchiveTag (GetTagName $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty RetentionActionPolicyTag (GetRetentionAction $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty RetentionActionArchiveTag (GetRetentionAction $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty RetentionPeriodPolicyTag (GetRetentionPeriod $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty RetentionPeriodArchiveTag (GetRetentionPeriod $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty Usermailbox $user
    $obj | add-member noteproperty FolderItemCount $_.TotalCount
    $Global:retentioninfo += $obj

    }

    $Global:retentioninfo

    }

    try{
    $CurrentUser = get-recipient $user -ErrorAction STOP

    if(($CurrentUser).recipienttype -eq ‘UserMailbox’)
    {

    $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $credential.UserName, $credential.GetNetworkCredential().password
    $id = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList “SmtpAddress”,$user
    $exchangeService.ImpersonatedUserId = $id

    $exchangeService.Url = “https://onpremserver/ews/exchange.asmx”

    folderretention;

    }

    else{

    $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $o365credential.UserName, $o365credential.GetNetworkCredential().password
    $id = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList “SmtpAddress”,$user
    $exchangeService.ImpersonatedUserId = $id
    $exchangeService.Url = “https://outlook.office365.com/EWS/Exchange.asmx”

    folderretention;

    }

    }catch{

    $errcond = $_.Exception.Message
    $timestamp = (get-date).DateTime
    “Time of exception: $timestamp” | Out-File $LogFile -Append
    “User: $user” | out-file $LogFile -Append
    $errcond | out-file -FilePath $LogFile -append
    }

    }

    If($MultiThread) {
    #create scriptblock from function
    $ScriptBlock = [scriptblock]::Create((Get-ChildItem Function:\Get-UserFolderRentention).Definition)
    ForEach($Address in $Primarysmtpaddress) {
    try{
    $j++ | Out-Null
    $MailboxName = $Address
    $PowershellThread = [powershell]::Create().AddScript($ScriptBlock).AddParameter(‘Primarysmtpaddress’,$MailboxName)

    $PowershellThread.RunspacePool = $RunspacePool
    $Handle = $PowershellThread.BeginInvoke()
    $Job = “” | Select-Object Handle, Thread, object
    $Job.Handle = $Handle
    $Job.Thread = $PowershellThread
    $Job.Object = $Address #.ToString()
    $Jobs += $Job
    }
    catch{
    $Error[0].Exception
    }
    }
    }
    }

    end{
    If ($MultiThread) {
    $SleepTimer = 200
    $ResultTimer = Get-Date
    While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0) {
    $Remaining = “$($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).object)”
    If ($Remaining.Length -gt 60){
    $Remaining = $Remaining.Substring(0,60) + “…”
    }
    Write-Progress `
    -id 1 `
    -Activity “Waiting for Jobs – $($Threads – $($RunspacePool.GetAvailableRunspaces())) of $Threads threads running” `
    -PercentComplete (($Jobs.count – $($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).count)) / $Jobs.Count * 100) `
    -Status “$(@($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})).count) remaining – $Remaining”

    ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){
    $Job.Thread.EndInvoke($Job.Handle)
    $Job.Thread.Dispose()
    $Job.Thread = $Null
    $Job.Handle = $Null
    $ResultTimer = Get-Date
    }
    If (($(Get-Date) – $ResultTimer).totalseconds -gt $MaxResultTime){
    Write-Warning “Child script appears to be frozen for $($Job.Object), try increasing MaxResultTime”
    #Exit
    }
    Start-Sleep -Milliseconds $SleepTimer
    # kill all incomplete threads when hit “CTRL+q”
    If ($Host.UI.RawUI.KeyAvailable) {
    $KeyInput = $Host.UI.RawUI.ReadKey(“IncludeKeyUp,NoEcho”)
    If (($KeyInput.ControlKeyState -cmatch ‘(Right|Left)CtrlPressed’) -and ($KeyInput.VirtualKeyCode -eq ’81’)) {
    Write-Host -fore red “Kill all incomplete threads…..”
    ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})){
    Write-Host -fore yellow “Stopping job $($Job.Object) ….”
    $Job.Thread.Stop()
    $Job.Thread.Dispose()
    }
    Write-Host -fore red “Exit script now!”
    Exit
    }
    }
    }
    # clean-up
    $RunspacePool.Close() | Out-Null
    $RunspacePool.Dispose() | Out-Null
    [System.GC]::Collect()
    }
    }

    Like

    • Hi Nicholas,
      first look shows me that you are querying the credentiials in your function Get-UserFolderRentention. This doesn’t work with MultiThreading as the threads cannot prompt you. You need to change this to PSCredentials of the main script and pass it on to the funcion.
      Ciao,
      Ingo

      Like

  5. Hi Ingo and thanks for the response – I’ve spent 2 days at this in frustration – and explanations of the concept are vague on the internet – here is the original EWS script that ‘just works’ – ie processes mailboxes sequentially in a foreach and returns the EWS information for folders – without adding in complexity of multithreading – this is where i am getting bogged down – your script is complex but i am sort of able to follow it: It will be clearer to you with the original EWS script that works without multithreading: Thanks nicholas

    [CmdletBinding()]

    param(

    [Parameter(
    Position = 0,
    Mandatory=$true,
    ValueFromPipelineByPropertyName=$true

    )]
    [String[]]
    $PrimarySmtpAddress

    )

    begin{

    $startTime = Get-Date
    $error.clear()
    [string]$LogFile = “C:\Temp\Log.txt”

    $o365credential = get-credential -credential o365creds

    $ExchangeSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri “https://outlook.office365.com/powershell-liveid/” -Credential $o365credential -Authentication “Basic” -AllowRedirection
    Import-PSSession $ExchangeSession -Prefix o365 -AllowClobber

    $credential = Get-Credential “onpremcreds”

    Add-Type -Path “C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll”
    $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService

    $Global:retentioninfo = @()
    $Global:RetentionTags = @()
    if($LogFile)
    {
    Remove-Item $LogFile -ea SilentlyContinue
    }

    }

    process {

    foreach($user in $PrimarySmtpAddress){

    function folderretention()
    {
    $FPageSize = 500
    $FOffset = 0
    $MoreItems =$true
    $ItemCount=0
    $folderView = new-object Microsoft.Exchange.WebServices.Data.FolderView($FPageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
    $folderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep
    $oFindFolders = $exchangeService.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$null,$folderView)
    #?{(($_.DisplayName -notlike ‘Calendar’) -and ($_.DisplayName -notlike ‘*contacts’) -and ($_.DisplayName -notlike ‘*recipient*’))}

    $Global:RetentionTags = $exchangeService.GetUserRetentionPolicyTags()

    function GetTagName($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).DisplayName }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.DisplayName }
    }
    }

    function GetRetentionAction($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).RetentionAction }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.RetentionAction }

    }
    }

    function GetRetentionPeriod($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).RetentionPeriod }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.RetentionPeriod }

    }
    }

    # $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($FpageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
    # $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow
    # $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet(
    #[Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly)
    $oFindFolders | %{
    # $oFinditems = $exchangeService.FindItems($_.Id,$itemview)

    $obj = New-Object PSObject
    $obj | add-member noteproperty DisplayName $_.DisplayName
    $obj | add-member noteproperty PolicyTag (GetTagName $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty ArchiveTag (GetTagName $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty RetentionActionPolicyTag (GetRetentionAction $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty RetentionActionArchiveTag (GetRetentionAction $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty RetentionPeriodPolicyTag (GetRetentionPeriod $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty RetentionPeriodArchiveTag (GetRetentionPeriod $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty Usermailbox $user
    $obj | add-member noteproperty FolderItemCount $_.TotalCount
    $Global:retentioninfo += $obj

    }

    }

    try{
    $CurrentUser = get-recipient $user -ErrorAction STOP

    if(($CurrentUser).recipienttype -eq ‘UserMailbox’)
    {

    $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $credential.UserName, $credential.GetNetworkCredential().password
    $id = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList “SmtpAddress”,$user
    $exchangeService.ImpersonatedUserId = $id

    $exchangeService.AutodiscoverUrl($user)

    folderretention;

    }

    else{

    $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $o365credential.UserName, $o365credential.GetNetworkCredential().password
    $id = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList “SmtpAddress”,$user
    $exchangeService.ImpersonatedUserId = $id
    $exchangeService.Url = “https://outlook.office365.com/EWS/Exchange.asmx”

    folderretention;

    }

    }catch{

    $errcond = $_.Exception.Message
    $timestamp = (get-date).DateTime
    “Time of exception: $timestamp” | Out-File $LogFile -Append
    “User: $user” | out-file $LogFile -Append
    $errcond | out-file -FilePath $LogFile -append
    }

    }
    $Global:retentioninfo | Export-Csv report1.csv -NoTypeInformation

    }

    end{

    $endTime = Get-Date
    Write-Host “Total time = ” + $($endTime -$StartTime)

    }

    Like

  6. In my original script I just iterate over a collection of mailboxes and store all the data in $Global:retentioninfo and output that to CSV – the thing with these runspaces is trying to pass the parameter required into the function when using ‘$PowershellThread = [powershell]::Create().AddScript($ScriptBlock)’ – in your script I see your declaring the same variables in 2 places – in the function and before the Begin{} block – like in a normal advanced script – also I cant debug the function because that is hidden from me once it is trying to run in the ‘runspace’ – my laptop has 4 logical processors so i presume that means i can get 4 times the performance by spinning up 4 threads and executing mailboxes in parrallel? Again fuzzy on this whole thing – but i know from your script there are big performance benefits to be had. Thanks

    Like

    • Hi Nicholas,
      I’ll spent some time on this. Maybe we get this working, but keep one thing in mind: If you create multiple seesions against EXO, you most likely will be throttled.
      I’ll followup offline and send you an email directly. If we can make it, I’ll publish it.
      Okay?
      Ciao,
      Ingo

      Like

  7. Hi Ingo, I’ve had another go at this and simplified the code a bit while incorporating a bit of your script into mine – I feel I am very close to getting this working but am missing something – and I’m nearly sure its something to do with variable scope or the way the paramaters are being passed into the function being multithreaded – bear in mind that that main folder has a sub function in it – but as far as I’m aware child functions – can look “up” the scope and see variables that it is using in the parent scope – at least that is my understanding – this is how I have changed it but still no joy 😦 will keep trying

    [CmdletBinding()]

    param(

    [Parameter(
    Position = 0,
    Mandatory=$true,
    ValueFromPipelineByPropertyName=$true

    )]
    [String[]]
    $PrimarySmtpAddress,

    [parameter( Mandatory=$false, Position=1)]
    [switch]$MultiThread=$false

    )

    begin{
    $Jobs = @()
    $Sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
    $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, 5,$Sessionstate, $Host)
    $RunspacePool.ApartmentState = “STA”
    $RunspacePool.Open()

    $startTime = Get-Date
    $error.clear()
    [string]$LogFile = “C:\Temp\Log.txt”

    $o365credential = get-credential -credential o365creds

    $ExchangeSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri “https://outlook.office365.com/powershell-liveid/” -Credential $o365credential -Authentication “Basic” -AllowRedirection
    Import-PSSession $ExchangeSession -Prefix o365 -AllowClobber

    # Add-Type -Path “C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll”
    # $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService

    $retentioninfo = @()
    $RetentionTags = @()
    if($LogFile)
    {
    Remove-Item $LogFile -ea SilentlyContinue
    }

    }

    process {

    function Get-MailboxFoldertags()
    {
    param(

    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [string]
    $user,
    $o365credential,
    $retentioninfo = @(),
    $retentiontags = @(),

    $logfile = “C:\Temp\Log.txt”

    )

    Add-Type -Path “C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll”
    $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService

    function folderretention()
    {

    $FPageSize = 500
    $FOffset = 0

    $folderView = new-object Microsoft.Exchange.WebServices.Data.FolderView($FPageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
    $folderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep
    $oFindFolders = $exchangeService.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$null,$folderView)
    #?{(($_.DisplayName -notlike ‘Calendar’) -and ($_.DisplayName -notlike ‘*contacts’) -and ($_.DisplayName -notlike ‘*recipient*’))}

    $RetentionTags = $exchangeService.GetUserRetentionPolicyTags()

    function GetTagName($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).DisplayName }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.DisplayName }
    }
    }

    function GetRetentionAction($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).RetentionAction }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.RetentionAction }

    }
    }

    function GetRetentionPeriod($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).RetentionPeriod }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.RetentionPeriod }

    }
    }

    # $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($FpageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
    # $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow
    # $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet(
    #[Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly)
    $oFindFolders | %{
    # $oFinditems = $exchangeService.FindItems($_.Id,$itemview)

    $obj = New-Object PSObject
    $obj | add-member noteproperty DisplayName $_.DisplayName
    $obj | add-member noteproperty PolicyTag (GetTagName $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty ArchiveTag (GetTagName $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty RetentionActionPolicyTag (GetRetentionAction $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty RetentionActionArchiveTag (GetRetentionAction $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty RetentionPeriodPolicyTag (GetRetentionPeriod $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty RetentionPeriodArchiveTag (GetRetentionPeriod $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty Usermailbox $user
    $obj | add-member noteproperty FolderItemCount $_.TotalCount
    $retentioninfo += $obj

    }

    }

    try{
    $CurrentUser = get-recipient $user -ErrorAction STOP

    if(($CurrentUser).recipienttype -eq ‘UserMailbox’)
    {
    $exchangeService.UseDefaultCredentials =$true
    #$exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $credential.UserName, $credential.GetNetworkCredential().password
    $id = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList “SmtpAddress”,$user
    $exchangeService.ImpersonatedUserId = $id

    $exchangeService.AutodiscoverUrl($user)

    folderretention;

    }

    else{

    $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $o365credential.UserName, $o365credential.GetNetworkCredential().password
    $id = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList “SmtpAddress”,$user
    $exchangeService.ImpersonatedUserId = $id
    $exchangeService.Url = “https://outlook.office365.com/EWS/Exchange.asmx”

    folderretention;

    }

    }catch{

    $errcond = $_.Exception.Message
    $timestamp = (get-date).DateTime
    “Time of exception: $timestamp” | Out-File $LogFile -Append
    “User: $user” | out-file $LogFile -Append
    $errcond | out-file -FilePath $LogFile -append
    }

    $retentioninfo
    }
    if($MultiThread)
    {

    $ScriptBlock = [scriptblock]::Create((Get-ChildItem Function:\Get-MailboxFoldertags).Definition)

    foreach($user in $PrimarySmtpAddress){
    $PowershellThread = [powershell]::Create().AddScript($ScriptBlock)
    $PowershellThread.AddArgument($user) | Out-Null
    $PowershellThread.AddArgument($o365credential) | Out-Null
    $PowershellThread.AddArgument($retentioninfo) | Out-Null
    $PowershellThread.AddArgument($retentiontags) | Out-Null
    $PowershellThread.AddArgument($LogFile) | Out-Null
    $PowershellThread.RunspacePool = $RunspacePool
    $Handle = $PowershellThread.BeginInvoke()
    $Job = “” | Select-Object Handle, Thread, object
    $Job.Handle = $Handle
    $Job.Thread = $PowershellThread
    $Job.Object = $user #.ToString()
    $Jobs += $Job

    }

    }
    }

    end{

    if($MultiThread)
    {
    While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0) {
    ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){
    $Job.Thread.EndInvoke($Job.Handle)
    $Job.Thread.Dispose()
    $Job.Thread = $Null
    $Job.Handle = $Null

    }

    }
    $RunspacePool.Close() | Out-Null
    $RunspacePool.Dispose() | Out-Null
    }

    #$endTime = Get-Date
    #Write-Host “Total time = ” + $($endTime -$StartTime)

    }

    Like

  8. Hi Ingo just as an update – it is definitely executing the function but this is what i am seeing the error log file I have in the try-catch block – Time of exception: 21 July 2017 12:38:58
    User: nicholas.herbert@domain.com
    The term ‘get-recipient’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

    Its doesn’t seem to be recognising the exchange cmdlets for some reason?

    Like

  9. Ingo – I got this working – but need to tidy it up – moved the try-block statement outside the function which is being multithreaded – the concept of the scopes in multithreading and passing parameters was tripping me up – the function doesn’t understand get-recipient or get-mailbox – its in a different scope and doesnt have the exchange cmdlets available to it – it seems – i passed in the EWS object as an argument and the user being processed. Thankyou so much here it is as it stands:

    [CmdletBinding()]

    param(

    [Parameter(
    Position = 0,
    Mandatory=$true,
    ValueFromPipelineByPropertyName=$true

    )]
    [String[]]
    $PrimarySmtpAddress,

    [parameter( Mandatory=$false, Position=1)]
    [switch]$MultiThread=$false

    )

    begin{

    Add-Type -Path “C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll”
    $Jobs = @()
    $Sessionstate = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
    $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, 10,$Sessionstate, $Host)
    $RunspacePool.ApartmentState = “STA”
    $RunspacePool.Open()

    $startTime = Get-Date
    $error.clear()
    [string]$LogFile = “C:\Temp\Log.txt”

    $o365credential = get-credential -credential o365creds

    $ExchangeSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri “https://outlook.office365.com/powershell-liveid/” -Credential $o365credential -Authentication “Basic” -AllowRedirection
    Import-PSSession $ExchangeSession -Prefix o365 -AllowClobber

    Add-Type -Path “C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll”
    $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService

    # $retentioninfo = @()
    # $RetentionTags = @()

    }

    process {

    function Get-MailboxFoldertags()
    {
    param(

    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [string]
    $user,
    [Microsoft.Exchange.WebServices.Data.ExchangeService]$exchangeService

    )

    $retentioninfo = @()
    $retentiontags = @()
    [string]$LogFile = “C:\Temp\Log.txt”
    if($LogFile)
    {
    Remove-Item $LogFile -ea SilentlyContinue
    }

    # Add-Type -Path “C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll”
    # $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService

    write-host -ForegroundColor DarkBlue “DEF in function”
    $FPageSize = 500
    $FOffset = 0

    $folderView = new-object Microsoft.Exchange.WebServices.Data.FolderView($FPageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
    $folderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep
    $oFindFolders = $exchangeService.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$null,$folderView)
    #?{(($_.DisplayName -notlike ‘Calendar’) -and ($_.DisplayName -notlike ‘*contacts’) -and ($_.DisplayName -notlike ‘*recipient*’))}

    $RetentionTags = $exchangeService.GetUserRetentionPolicyTags()

    function GetTagName($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).DisplayName }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.DisplayName }
    }
    }

    function GetRetentionAction($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).RetentionAction }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.RetentionAction }

    }
    }

    function GetRetentionPeriod($tagGUID) {
    if (!$tagGUID) { return ($RetentionTags.RetentionPolicyTags | ? {$_.Type -eq “All”}).RetentionPeriod }
    foreach ($tag in $RetentionTags.RetentionPolicyTags) {
    if ($tag.RetentionId -eq $tagGUID ) { return $tag.RetentionPeriod }

    }
    }

    # $itemView = new-object Microsoft.Exchange.WebServices.Data.ItemView($FpageSize,$FOffset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning)
    # $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow
    # $itemView.PropertySet = new-object Microsoft.Exchange.WebServices.Data.PropertySet(
    #[Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly)
    $oFindFolders | %{
    # $oFinditems = $exchangeService.FindItems($_.Id,$itemview)

    $obj = New-Object PSObject
    $obj | add-member noteproperty DisplayName $_.DisplayName
    $obj | add-member noteproperty PolicyTag (GetTagName $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty ArchiveTag (GetTagName $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty RetentionActionPolicyTag (GetRetentionAction $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty RetentionActionArchiveTag (GetRetentionAction $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty RetentionPeriodPolicyTag (GetRetentionPeriod $_.PolicyTag.RetentionId)
    $obj | add-member noteproperty RetentionPeriodArchiveTag (GetRetentionPeriod $_.ArchiveTag.RetentionId)
    $obj | add-member noteproperty Usermailbox $user
    $obj | add-member noteproperty FolderItemCount $_.TotalCount
    $retentioninfo += $obj

    }

    $retentioninfo
    }
    if($MultiThread)
    {

    $ScriptBlock = [scriptblock]::Create((Get-ChildItem Function:\Get-MailboxFoldertags).Definition)

    foreach($user in $PrimarySmtpAddress){

    try{

    $CurrentUser = get-recipient $user -ErrorAction STOP
    write-host -ForegroundColor DarkGreen “still in function”
    Write-Host $CurrentUser

    if(($CurrentUser).recipienttype -eq ‘UserMailbox’)
    {
    $exchangeService.UseDefaultCredentials =$true
    #$exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $credential.UserName, $credential.GetNetworkCredential().password
    $id = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList “SmtpAddress”,$user
    $exchangeService.ImpersonatedUserId = $id

    $exchangeService.AutodiscoverUrl($user)
    Write-Host -ForegroundColor DarkGreen “test”
    # folderretention;

    }

    else{

    $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials -ArgumentList $o365credential.UserName, $o365credential.GetNetworkCredential().password
    $id = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId -ArgumentList “SmtpAddress”,$user
    $exchangeService.ImpersonatedUserId = $id
    $exchangeService.Url = “https://outlook.office365.com/EWS/Exchange.asmx”

    # folderretention;

    }

    }catch{

    $errcond = $_.Exception.Message
    $timestamp = (get-date).DateTime
    “Time of exception: $timestamp” | Out-File $LogFile -Append
    “User: $user” | out-file $LogFile -Append
    $errcond | out-file -FilePath $LogFile -append
    }

    $PowershellThread = [powershell]::Create().AddScript($ScriptBlock)
    $PowershellThread.AddArgument($user) | Out-Null

    $PowershellThread.AddArgument($ExchangeService) | Out-Null

    $PowershellThread.RunspacePool = $RunspacePool
    $Handle = $PowershellThread.BeginInvoke()
    $Job = “” | Select-Object Handle, Thread, object
    $Job.Handle = $Handle
    $Job.Thread = $PowershellThread
    $Job.Object = $user #.ToString()
    $Jobs += $Job

    }

    }
    }

    end{

    if($MultiThread)
    {
    While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0) {
    ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){
    $Job.Thread.EndInvoke($Job.Handle)
    $Job.Thread.Dispose()
    $Job.Thread = $Null
    $Job.Handle = $Null

    }

    }
    $RunspacePool.Close() | Out-Null
    $RunspacePool.Dispose() | Out-Null
    }

    #$endTime = Get-Date
    #Write-Host “Total time = ” + $($endTime -$StartTime)

    }

    Like

  10. Hi Ingo just as an example now running this multithreaded vs ‘normal of time taken against 100 mailboxes:
    with multithreading – time taken was 46 seconds
    without multithreading – time taken was 3 minutes 5 seconds

    One last question I am running this from the exchange server now – this is a more powerful machine (obviously) looking at the processor information on this server – it is as follows:

    2 – Intel Xeon CPU E5-2680 @ 2.80gHZ, 2793mHz, 10 Cores, 10 logical processors and I am running the script at: CreateRunspacePool(1, 10,$Sessionstate, $Host) or 10 threads – does this mean it will utilize both processors or just 1? Again I am very grateful for your assistance – Thanks Nicholas

    Like

  11. Thanks Ingo – so the threads can run across all 20 cores or 10 cores in each processor in my case? in that case im actually underutilising the runspace pool as I’ve designated a max of 10 – I should probably up it to max of 20 Regards

    Like

    • Hi Nicholas, it depends. When you check yourr Taskmanager and right-click on your PowerShell process, select “Set Affinity”. It should have checked “All Processors” by default. Then it’s using all visible cores.
      Ciao,
      Ingo

      Like

  12. Pingback: Exchange Online migration and TooManyBadItemsPermanetException | The clueless guy

  13. Pingback: Microsoft Graph: Get user info | The clueless guy

Leave a comment