Apply Security & Compliance Center Retention Labels to Outlook Folders

5/5 - (6 votes)

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.

 

 

 

 

author avatar
Aaron Guilmette
Helping companies conquer inferior technology since 1997. I spend my time developing and implementing technology solutions so people can spend less time with technology. Specialties: Active Directory and Exchange consulting and deployment, Virtualization, Disaster Recovery, Office 365, datacenter migration/consolidation, cheese.

2 Replies to “Apply Security & Compliance Center Retention Labels to Outlook Folders”

  1. 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?

Comments are closed.