I couldn’t really come up with a cool-sounding title for this post, so I just went with the basics of what it does.
Last week, I worked with a customer that wanted to deploy custom retention labels to custom folders inside a user’s mailbox–the idea being that they would create a custom folder structure such as this under a user’s Inbox:
\Inbox
\Inbox\Retention Schedule
\Inbox\Retention Schedule\2 Year (apply a 2-year retention label to everything in this folder)
\Inbox\Retention Schedule\4 Year (apply a 4-year retention label to everything in this folder)
\Inbox\Retention Schedule\7 Year (apply a 7-year retention label to everything in this folder)
\Inbox\Retention Schedule\Forever (apply a ‘Never delete’ retention label to everything in this folder)
Seems easy enough, right? Except that we don’t support anything like that in the Security & Compliance Center.
THE TRAVESTY.
So, I spent a few days cobbling together code snippets from previous projects, and came up with this. It’s not pretty, but it does the job. It can even be bundled into an Azure Run Book.
The Requirements
In order for this to work, you’ll need a few core things:
Retention Labels
The first step is to create and publish some Retention Labels in the Security & Compliance Center. Once that’s done, you can use those labels in Exchange Online. You can learn more about retention labels at https://docs.microsoft.com/en-us/office365/securitycompliance/labels. The names you give the labels will be used in the folder structure.
Retention Folder Parent
Figure out where you want to put this collection of folders. By default, it’s going to go under \Inbox\Retention Schedule\<label name>.
Users
You need some mailboxes to work with.
Admin Rights
The account you run this with will need the ApplicationImpersonation and Mailbox Import/Export roles.
The Script
Below. 🙂 Go get it!
The Code
Finally, here’s the script:
<# .SYNOPSIS Create a managed mailbox folder and apply a personal tag to it. .DESCRIPTION This script can be used to: - Grant impersonation rights - Create a managed folder in one or more user mailboxes - Apply a personal retention policy tag to a managed folder .PARAMETER Confirm Confirm that folder will be created and retention tag applied. .PARAMETER Credential Credential of account used to perform operations. .PARAMETER Folder Specifies the name of the folder to create. .PARAMETER GrantImpersonation Grant impersonation rights. .PARAMETER Identity Specifies one or more SMTP Addresses to run script against. .PARAMETER Logfile Used to specify a log file name to record operations. .PARAMETER Parent Specify Parent Folder. If no folder is specified, the folder is created at MsgRoot (same level as Inbox). .PARAMETER PersonalTag Specifies the name or GUID of the personal tag to apply. .PARAMETER PolicyObjects An array of ComplianceTag objects (add the results of Get-ComplianceTag to an array). .PARAMETER RetentionParent Name of the top-level folder under the Inbox under which to create the subfolder structure. .NOTES Most of this is cobbled together from code snippets I've found on the internet, since EWS is not my bag, baby. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)][System.Management.Automation.PSCredential]$Credential, [string]$FolderName, [ValidateSet('Primary', 'Archive')][string]$FolderType = "Primary", # Archive doesn't work yet [string]$EwsPath, [switch]$GrantImpersonation, [string[]]$Identity, [string]$RetentionParent = "Retention Schedule", [string]$Parent = "Inbox", [string]$Logfile = ".\Create-FolderRetentionpolicyLog.txt", [string]$PersonalTag, [array]$PolicyObjects ) begin { $s = 1 $sTotal = 5 Write-Progress -Activity "Checking for prerequisites." -Id 1 -PercentComplete (($s/$sTotal)*100) If (!$EwsPath) { # Locate EWS Managed API and load Write-Progress -Activity "Locating EWS installation." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 If (Test-Path 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll') { Write-Progress -Activity "Locating EWS installation." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 -CurrentOperation "Found Exchange Web Services DLL in default location." -Completed $WebServicesDLL = "C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll" Import-Module $WebServicesDLL $s++ } ElseIf ($filename = Get-ChildItem 'C:\Program Files' -Recurse -ea silentlycontinue | where { $_.name -eq 'Microsoft.Exchange.WebServices.dll' }) { Write-Progress -Activity "Locating EWS installation." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 -CurrentOperation "Found Exchange Web Services DLL at $($filename.FullName)." -Completed $WebServicesDLL = $filename.FullName Import-Module $WebServicesDLL $s++ } ElseIf (!(Test-Path 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll')) { Write-Progress -Activity "Locating EWS installation." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 -CurrentOperation "Downloading EWS Managed API." wget http://download.microsoft.com/download/8/9/9/899EEF2C-55ED-4C66-9613-EE808FCF861C/EwsManagedApi.msi -OutFile ./EwsManagedApi.msi Write-Progress -Activity "Locating EWS installation." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 -CurrentOperation "Installing EWS Managed API." msiexec /i EwsManagedApi.msi /qb Sleep 60 If (Test-Path 'C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll') { Write-Progress -Activity "Locating EWS installation." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 -CurrentOperation "Found Exchange Web Services DLL in default location." -Completed $WebServicesDLL = "C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll" Import-Module $WebServicesDLL $s++ } Else { Write-Host -ForegroundColor Red "Please download the Exchange Web Services API and try again." Break } } # End EWS location routine } # End If (!$EwsPath) If ($EwsPath) { If (!(Test-Path $EwsPath)) { Write-Error -Message "Microsoft.Exchange.WebServices.dll not found at specified location or $($EwsPath) is not a valid location. Please download the Exchange Web Services API and try again or attempt to use autodetection." -Category ObjectNotFound Break } If (Test-Path $EwsPath -and (Test-Path -PathType Container $EwsPath)) { Write-Progress -Activity "Locating EWS installation." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 -CurrentOperation "Found Exchange Web Services DLL at $($EwsPath)." -Completed $WebServicesDLL = $EwsLocation.TrimEnd("\") + "Microsoft.ExchangeWebServices.dll" Import-Module $WebServicesDLL $s++ Continue } If (Test-Path $EwsPath -and (Test-Path -PathType Leaf $EwsPath)) { Write-Progress -Activity "Locating EWS installation." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 -CurrentOperation "Found Exchange Web Services DLL at $($EwsPath)." -Completed $WebServicesDLL = $EwsLocation Import-Module $WebServicesDLL $s++ } Else { Write-Error -Message "Microsoft.Exchange.WebServices.dll not found at specified location. Please download the Exchange Web Services API and try again or attempt to use autodetection." -Category ObjectNotFound Break } } # End If ($EwsPath) # Check for Exchange Online cmdlets If (!(Get-Command Get-Mailbox -ea silentlycontinue)) { try { Write-Progress -Activity "Connecting to Exchange Online." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credential -Authentication Basic -AllowRedirection Import-PSSession $Session $s++ } catch { "Unable to connect to Exchange Online." Exit } } # Check for Security & Compliance Center cmdlets If (!(Get-Command Get-ComplianceTag -ea SilentlyContinue)) { try { Write-Progress -Activity "Connecting to the Security & Compliance Center." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 $ComplianceSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.compliance.protection.outlook.com/powershell-liveid -Credential $Credential -Authentication Basic -AllowRedirection Import-PSSession $ComplianceSession -AllowClobber -DisableNameChecking $s++ } catch { "Unable to connect to Security & Compliance Center." Exit } } # Check Management Roles $ManagementRoles = Get-ManagementRoleAssignment -AssignmentMethod Direct -RoleAssignee $Credential.UserName If ($ManagementRoles -match "ApplicationImpersonation" -and $ManagementRoles -match "Mailbox Import Export") { Write-Progress -Activity "Checking for Management Roles." -Id 2 -PercentComplete (($s/$sTotal) * 100) -ParentId 1 $s++ } Else { If (!($ManagementRoles -match "Mailbox Import Export")) { Write-Progress -Activity "Attempting to add Mailbox Import Export Role." -Id 3 -PercentComplete (($s/$sTotal) * 100) -ParentId 2 try { New-ManagementRoleAssignment -User $Credential.UserName -Role "Mailbox Import Export" } catch { "Unable to add Mailbox Import Export Role."} } If (!($ManagementRoles -match "ApplicationImpersonation")) { Write-Progress -Activity "Attempting to add ApplicationImpersonation Role." -Id 3 -PercentComplete (($s/$sTotal) * 100) -ParentId 2 try { New-ManagementRoleAssignment -User $Credential.UserName -Role "ApplicationImpersonation" } catch { "Unable to add ApplicationImpersonation Role."} } Write-Error -Message "We have attempted to grant you the required roles. Please log out of your Office 365 session, log back in, and try again." -Category SecurityError Exit } # Create Exchange Service Object Write-Progress -Activity "Connecting to autodiscover endpoint for administrator $($Credential.UserName)" -Id 1 -PercentComplete (($s/$sTotal)*100) $ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1 $Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion) $creds = New-Object System.Net.NetworkCredential($Credential.UserName.ToString(), $Credential.GetNetworkCredential().password.ToString()) $Service.Credentials = $creds $Service.AutodiscoverUrl($Credential.Username, { $true }) function Get-FolderFromPath { param ( [Parameter(Position = 0, Mandatory = $true)] [string]$FolderPath, [Parameter(Position = 1, Mandatory = $true)] [string]$MailboxName, [Parameter(Position = 2, Mandatory = $true)] [Microsoft.Exchange.WebServices.Data.ExchangeService]$service, [Parameter(Position = 3, Mandatory = $false)] [Microsoft.Exchange.WebServices.Data.PropertySet]$PropertySet ) process { $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::$Parent, $MailboxName) $tfTargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, $folderid) $fldArray = $FolderPath.Split("\") for ($lint = 1; $lint -lt $fldArray.Length; $lint++) { $fvFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1) if (![string]::IsNullOrEmpty($PropertySet)) { $fvFolderView.PropertySet = $PropertySet } $SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $fldArray[$lint]) $findFolderResults = $service.FindFolders($tfTargetFolder.Id, $SfSearchFilter, $fvFolderView) if ($findFolderResults.TotalCount -gt 0) { foreach ($folder in $findFolderResults.Folders) { $tfTargetFolder = $folder } } else { Write-host ("Error Folder Not Found check path and try again") $tfTargetFolder = $null break } } if ($tfTargetFolder -ne $null) { return [Microsoft.Exchange.WebServices.Data.Folder]$tfTargetFolder } else { throw ("Folder Not found") } } } function Create-Folder { param ( [Parameter(Position = 0, Mandatory = $true)] [string]$MailboxName, [Parameter(Position = 2, Mandatory = $true)] [String]$NewFolderName, [Parameter(Position = 3, Mandatory = $false)] [String]$ParentFolder, [Parameter(Position = 4, Mandatory = $false)] [String]$FolderClass = "IPF.Note", [Parameter(Position = 5, Mandatory = $false)] [switch]$Impersonation ) Begin { if ($Impersonation.IsPresent) { $service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName) } $NewFolder = new-object Microsoft.Exchange.WebServices.Data.Folder($service) $NewFolder.DisplayName = $NewFolderName if (([string]::IsNullOrEmpty($folderClass))) { $NewFolder.FolderClass = "IPF.Note" } else { $NewFolder.FolderClass = $folderClass } $EWSParentFolder = $null if (([string]::IsNullOrEmpty($ParentFolder))) { # Bind to the MsgFolderRoot folder $folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $MailboxName) $EWSParentFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, $folderid) } else { $EWSParentFolder = Get-FolderFromPath -MailboxName $MailboxName -service $service -FolderPath $ParentFolder } $fvFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1) $SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $NewFolderName) $findFolderResults = $service.FindFolders($EWSParentFolder.Id, $SfSearchFilter, $fvFolderView) if ($findFolderResults.TotalCount -eq 0) { #Write-host ("Folder Doesn't Exist") $NewFolder.Save($EWSParentFolder.Id) #Write-host ("Folder Created") $Global:NewFolderID = $NewFolder.Id } else { #Write-error ("Folder already exists with that Name") } } } function Set-RetentionPolicy($MailboxName,$RetentionFolderId,$RetentionFolderName) { # Write-host "Setting Policy on folder for Mailbox Name:" $MailboxName -foregroundcolor $info Add-Content $LogFile ("Stamping Policy on folder $($RetentionFolderName) for Mailbox Name:" + $MailboxName) #Change the user to Impersonate $service.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $MailboxName); #Search for the folder you want to stamp the property on $oFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1) $ofolderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep $oSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $RetentionFolderName) Switch ($FolderType) { Primary { $global:oFindFolderResults = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::$Parent, $oSearchFilter, $oFolderView) } Archive { $MessageRoot = "ArchiveMsgFolderRoot\$($Parent)" #$global:oFindFolderResults = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::ArchiveMsgFolderRoot, $oSearchFilter, $oFolderView) $global:oFindFolderResults = $service.FindFolders([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::$MessageRoot, $oSearchFilter, $oFolderView) } } if ($oFindFolderResults.TotalCount -eq 0) { Write-Progress -Activity "Folder $($RetentionFolderName) does not exist in Mailbox: $($MailboxName)" -Id 2 -ParentId 1 Add-Content $LogFile ("Folder does not exist in Mailbox:" + $MailboxName) } else { #PR_POLICY_TAG 0x3019 $global:PolicyTag = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary); #PR_RETENTION_FLAGS 0x301D $global:RetentionFlags = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301D, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer); #PR_RETENTION_PERIOD 0x301A $RetentionPeriod = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer); #Bind to the folder found $global:oFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service, $oFindFolderResults.Folders[0].Id) #Same as the value in the PR_RETENTION_FLAGS property $oFolder.SetExtendedProperty($RetentionFlags, 137) #Same as the value in the PR_RETENTION_PERIOD property $oFolder.SetExtendedProperty($RetentionPeriod, 1095) $oFolder.SetExtendedProperty($PolicyTag, $Policy.ExchangeObjectId.ToByteArray()) $oFolder.Update() Write-Progress -Activity "Retention Policy $($FName) stamped." -Id 2 -ParentId 1 Add-Content $LogFile "Retention policy $($FName) stamped! on folder $($RetentionFolderName) - $($RetentionFolderId) in $MailboxName" } $service.ImpersonatedUserId = $null } #End Function SetRetentionPolicyTag } #End Begin process { $i = 1 [int]$iCount = $Identity.Count foreach ($User in $Identity) { Write-Progress -Activity "Processing User $($User)" -Id 1 -PercentComplete (($i/$iCount)*100) # Create the Retention Folder Root Write-Progress -Activity "Creating Retention Parent Folder $($RetentionParent)" -Id 2 -ParentId 1 Create-Folder -MailboxName $User -NewFolderName $RetentionParent -Impersonation -ParentFolder $Parent $p = 1 foreach ($obj in $PolicyObjects) { $global:Policy = $obj $global:FName = "$($Policy.Name)" Write-Progress -Activity "Creating managed folder $($FName)" -PercentComplete (($p/$PolicyObjects.Count)*100) -Id 2 -ParentId 1 Create-Folder -MailboxName $User -NewFolderName $Policy.Name -Impersonation -ParentFolder "Inbox\Retention Schedule" Write-Progress -Activity "Setting retention policy on managed Folder $($FName)" -Id 2 -ParentId 1 # Write-Host "RetentionFolderID is $NewFolderID" Set-RetentionPolicy -MailboxName $User -RetentionFolderID $NewFolderID -RetentionFolderName $FName $p++ } $i++ } } end { }
Next, here’s the how-to:
Creating the Folder Structure
It’s a little more involved for the time being, since this is still a rough script. But, once I hammer out the details, hopefully we’ll make it easier.
Connect to both the Exchange Online PowerShell and Security & Compliance PowerShell
You’ll need to connect to both endpoints, as we’re going to need to ensure that you have some management role assignments.
$Credential = Get-Credential $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credential -Authentication Basic -AllowRedirection Import-PSSession $Session $ComplianceSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.compliance.protection.outlook.com/powershell-liveid -Credential $Credential -Authentication Basic -AllowRedirection Import-PSSession $ComplianceSession -AllowClobber -DisableNameChecking
Create a policy object array
In this step, we’re going to create an array containing the policy objects that you want map to folders. The script will create folder names based on the display name of the label policy underneath the -RetentionParent parameter value.
$Policies = @() $Policies += (Get-ComplianceTag '2 Year' |Select Name,ExchangeObjectId) $Policies += (Get-ComplianceTag '4 Year' |Select Name,ExchangeObjectId) $Policies += (Get-ComplianceTag '7 Year' |Select Name,ExchangeObjectId) $Policies += (Get-ComplianceTag 'Forever' |Select Name,ExchangeObjectId)
Run the script against mailboxes
The important parameters:
- Credential – Use a credential that has global admin rights.
- GrantImpersonation – Use this parameter to ensure your identity has impersonation rights to log into the mailboxes.
- Identity – parameter will take a list of SMTP addresses, which you can populate any way you like.
- PolicyObjects – Use the array object you created in the previous step
- RetentionParent – Optional—Uses “Retention Schedule” by default. This folder will be created under \Inbox.
.\Create-ManagedFolderRetentionPolicy-SubFolderMultiMailbox.ps1 -Credential $cred -GrantImpersonation -Identity (Get-Mailbox -ResultSize Unlimited).PrimarySmtpAddress -PolicyObjects $Policies
Review the output in a mailbox
Navigate to the inbox, expand the folder specified in -RetentionParent (“Retention Schedule” if not specified) and view the folders created. Right-click on the folder, select Assign Policy, and look for the policies referenced in the -PolicyObjects parameter.
Automation Options
Once you’ve reached this point, you’ve got it deployed. So what next? What about care and feeding?
I haven’t embarked upon that yet, but I think this type of process is ripe for automation, such as with Azure Run Books. I would look at potentially passing a parameter for Identity such as:
-Identity (Get-Mailbox -Filter "{WhenCreated -ge (Get-Date).AddHours(-25)}"
to capture everything created in the last day (with a bit of slack time). You can also run it against existing mailboxes–even if you already have the folder structure created. It will just skip folder creation and re-apply policies.
Thank you for this script. Any idea if it’s possible to make this work with EWS & Modern Auth as basic auth will be cut-off in October?
I haven’t gone through this yet, but I believe it will require registering an application with AAD in order to use OAuth. This doc has some steps, but I haven’t done it myself yet.
https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth