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?
- You need to have Microsoft Exchange Web Services (EWS) installed
- MrMAPI, when you want to read the property PR_NT_SECURITY_DESCRIPTOR
-
For best performance you should have the role ApplicationImpersonation
-
The script could be downloaded here
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.
In order to run it against a mailbox, where you don’t have access you need to Impersonate
Otherwise you will get an error
Here when you use multithreading
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.
To see the difference between multithreading vs. not multithreading here some statistics:
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:
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!
Pingback: The good, the bad and sIDHistory | The clueless guy
Pingback: Get-DatabaseEvent: Who deleted my items? | The clueless guy
Pingback: 获取邮箱目录权限 - Exchange中文站
This script works great and saves a lot of time. I had to use the -trustanySSL Switch to Workaround my LAB not having proper certs right now. Thanks Ingo!
LikeLike
test
LikeLike
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()
}
}
LikeLike
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
LikeLike
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)
}
LikeLike
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
LikeLike
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
LikeLike
Thanks Ingo really appreciate was so frustrated
LikeLike
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)
}
LikeLike
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?
LikeLike
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)
}
LikeLike
I’m glad it’s working now. Yes, multithreading makes some task more complex and modules are not loaded by default.
LikeLike
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
LikeLike
Hi Nicholas,
that’s a very good question. My understanding is that it doesn’t matter as you are creating threads for a process. The OS will take care to distribute across available cores. Thus all sockets will be used.
I’ll do some testing and come back to you.
LikeLike
Hi Nicholas,
as promised: Have a look here https://msdn.microsoft.com/en-us/library/windows/desktop/ms684841(v=vs.85).aspx. The process (PowerShell in our case) with affinity across all CPU cores. As the threads are forked by the process, they can run in the boundary of the process.
Ciao,
Ingo
LikeLike
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
LikeLike
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
LikeLike
Pingback: Exchange Online migration and TooManyBadItemsPermanetException | The clueless guy
Pingback: Microsoft Graph: Get user info | The clueless guy