Articles in the "Retention tag on default folder items" series
- Use EWS to apply retention policy to items in a default folder [This article]
- Script to set retention tag on default folder items updated to v1.1.1
- Default folder retention tag script updated to 1.3
- Updated script that applies retention tag to items in a default folder
When working with retention policies and the types of tags you can apply to folders and items, you can assign a personal tag to any item and to any custom folder. You cannot, however, assign a personal tag to a default folder (such as Deleted Items), even if a retention policy tag has not been assigned to the folder. This means that if no default policy tag has been assigned to the policy, the items in that folder will never expire. The only way for a user to expire items in that folder is to assign a personal tag to each and every item. For the deleted items folder, that can be a lot of items, and its contents are changing daily.
This script uses the EWS Managed API to get all items in the Deleted Items folder that do not have a tag assigned to them and then assign a specific tag to each. To start, you need to connect to EWS:
1 2 3 4 5 6 7 8 9 10 11 12 |
function Connect-WebServices ($smtpAddress) { $exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1 $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion) $exchangeService.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($EXOCreds) #Use hard-coded URL $exchangeService.Url = 'https://outlook.office365.com/EWS/Exchange.asmx' #Impersonate mailbox #$exchangeService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $smtpAddress) $folderID= New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::$DefaultFolder,$smtpAddress) [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService,$folderID) } |
I am using a function because I lifted this code from another one of my scripts, so calling this function returns an object for the Deleted Items folder. Based on the credentials format and EWS URL, you can see that I am connecting to Exchange Online. This can be easily changed to support on-premises. I am not using autodiscover because it is very slow when querying EXO, and since all EXO mailboxes can be accessed with the a single FQDN, it is simpler this way. I am not using impersonation because I am running this against my own mailbox, but you can uncomment the line if you choose to use it.
To search for items that do not have a tag assigned, this is used:
1 2 3 4 5 |
$policyTagProperty = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary) $itemView.PropertySet.Add($policyTagProperty) $itemSearchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+Exists($policyTagProperty) $itemSearchFilterNot = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+Not($itemSearchFilter) $itemSearchResult = $folder.FindItems($itemSearchFilterNot,$itemView) |
The MAPI property that indicates whether a tag has been assigned (whether implicitly or explicitly) is an extended property that you declare and add to a property set. The property is binary and contains the GUID of the tag, but since I am only looking for items without a tag, I only care if the property has a value. To do this, you first define a search filter object that says to include items where the property exists. Then to negate that, so I can find items without that property, you create another search filter object using the Not class that contains the other search filter.
Then you can apply the tag and its corresponding days until expiration value:
1 2 3 4 5 |
$policyTagGUID = New-Object Guid("{33CEDA03-0536-424C-8ECA-E839E0BC5945}") $item.SetExtendedProperty($policyTagProperty, $policyTagGUID.ToByteArray()) $retentionPeriodProperty = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer) $item.SetExtendedProperty($retentionPeriodProperty, 30) $item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite,[Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToNone) |
To assign the tag, you need to know its GUID. You can get this from PowerShell, but if you don’t have access to Exchange to get this, you can manually assign the tag to an item, then use MFCMAPI and look at the item’s properties for the value in PR_POLICY_TAG (0x30190102). The RAW representation of the GUID will need to be converted to the proper byte order, which can be done in a variety of ways, but this site is an easy way. When assigning the tag you also have to set the property that contains the number of days after which the tag is configured to expire. In my case, it is a 30-day tag. (I tested not setting the property and the result in Outlook does show that the tag is assigned but it doesn’t show the expiration date. I don’t know if the property is only used to calculate the displayed date or if MRM actually uses it when expiring items.)
The complete script can be downloaded from the link or copied from the code below. It includes checking for the EWS Managed API and a progress bar (since this is not a fast operation).
Set-DefaultFolderItemsTag.ps1 (9.4 KiB)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
#Apply retention tag to items in deleted items folder that do not have one #1.6.1 Added policy GUID back to EXO mailboxes (issue resolved in service) #1.6 Updated for changes in EXO, change loop to get all items and then update all items, # added parameters for target environment and impersonation #1.5 Added option of updating items that already have a tag #1.4 Added choice of tag to apply #1.3 Fixed processing of calendar items #1.2 Update calendar items that can be, note the count that cannot be #1.1.2 Prefill cred username with email address of target mailbox <# .Synopsis Assign personal tag to all items in a default folder. .Description Get all items in a folder that do not have a retention tag explicitly assigned and assign a personal tag them. .Parameter EmailAddress Email address of mailbox to process .Parameter DefaultFolder The name of the default folder to process. Default value is DeletedItems. .Parameter DaysToRetain Number of days to retain an item for. Must match one of the defined personal tags in the Variables region. .Parameter OverwriteExistingTags Switch to indicate that items with a tag that does not match the tag to be assigned are updated. The default is to update only items that do not have a tag. .Parameter Environment Specify whether the target environment is Exchange Online or Exchange on-premises. The default is Exchange Online. .Parameter UseImpersonation Switch to indicate impersonation should be used instead of full or delegate access. .Example Set-DefaultFolderItemsTag.ps1 user@domain .Example Set-DefaultFolderItemsTag.ps1 user@domain -DefaultFolder SentItems -DaysToRetain 365 .Notes Version: 1.6.1 Date: 12/1/17 #> Param ( [Parameter(Position=0,Mandatory=$true,HelpMessage="Email address of mailbox")][string]$EmailAddress, [Parameter(Position=1,Mandatory=$false,HelpMessage="Name of default folder to process")] [ValidateSet('Calendar','ConversationHistory','DeletedItems','Inbox','Journal','JunkEmail','Notes','SentItems','SyncIssues','Tasks')] [string]$DefaultFolder = 'DeletedItems', [Parameter(Position=2,Mandatory=$true)][ValidateSet(30,365)][int]$DaysToRetain, [ValidateSet('Online','OnPremises')]$Environment = 'Online', [switch]$UseImpersonation, [switch]$OverwriteExistingTags ) #Region Variables #Replace with GUIDs of tag to assign to items and the number of days each tag retains items $30DayTag = '{33CEDA03-0536-424C-8ECA-E839E0BC5945}',30 $365DayTag = '{AE331EBC-934F-43E2-8AB9-8480CE98E457}',365 #EndRegion switch ($DaysToRetain) { 30 { $personalTagGUID = $30DayTag[0] $personalTagRetentionDays = $30DayTag[1] } 365 { $personalTagGUID = $365DayTag[0] $personalTagRetentionDays = $365DayTag[1] } } #Check for EWS API $apiPath = (($(Get-ItemProperty -ErrorAction SilentlyContinue -Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services' | Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name)).'Install Directory') + 'Microsoft.Exchange.WebServices.dll') if (Test-Path $apiPath) { Add-Type -Path $apiPath } else { Write-Error "The Exchange Web Services Managed API is required to use this script." -Category NotInstalled break } function Connect-WebServices ($smtpAddress, $folder) { $exchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1 $exchangeService = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExchangeService($exchangeVersion) $exchangeService.Credentials = New-Object -TypeName Microsoft.Exchange.WebServices.Data.WebCredentials($EXOCreds) #Use hard-coded URL $exchangeService.Url = 'https://outlook.office365.com/EWS/Exchange.asmx' #Enable tracing to debug #$exchangeService.TraceEnabled = $true if ($UseImpersonation) { $exchangeService.ImpersonatedUserId = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $smtpAddress) } $folderID = New-Object -TypeName Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::$folder,$smtpAddress) [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeService,$folderID) } function Apply-RetentionTag { #Bind to folder of mailbox $folder = Connect-WebServices -smtpAddress $EmailAddress -folder $DefaultFolder if (-not($folder)) { Write-Error -Message "Error binding to folder in mailbox for $EmailAddress." -Category ConnectionError } else { $pageSize = 50 $offset = 0 $moreItems = $true $items = $null $firstRun = $true while ($moreItems) { if ($firstRun) { $percentComplete = -1 } else { $percentComplete = $items.Count/$totalItems*100 Write-Verbose -Message "$($items.Count) of $totalItems have been retrieved." } if ($OverwriteExistingTags) { Write-Progress -Activity "Apply retention tag to $DefaultFolder" -CurrentOperation "Retrieving all items in folder" -PercentComplete $percentComplete -Status ' ' } else { Write-Progress -Activity "Apply retention tag to $DefaultFolder" -CurrentOperation "Retrieving items with no tag" -PercentComplete $percentComplete -Status ' ' } #Set up paged search to stay below FindCountLimit $itemView = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ItemView($pageSize,$offset,[Microsoft.Exchange.WebServices.Data.OffsetBasePoint]::Beginning) $itemView.Traversal = [Microsoft.Exchange.WebServices.Data.ItemTraversal]::Shallow $itemView.PropertySet = New-Object -TypeName Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::IdOnly) $itemView.PropertySet.Add([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeCreated) #Property that contains the number of days for the applied tag $retentionPeriodProperty = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301A,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Integer) $itemView.PropertySet.Add($retentionPeriodProperty) if ($OverwriteExistingTags) #Get all items, regardless of whether tag is applied { $itemSearchResult = $folder.FindItems($itemView) } else { #Create the search filter to find items with no retention period applied $itemSearchFilter = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+Exists($retentionPeriodProperty) $itemSearchFilterNot = New-Object -TypeName Microsoft.Exchange.WebServices.Data.SearchFilter+Not($itemSearchFilter) $itemSearchResult = $folder.FindItems($itemSearchFilterNot,$itemView) } if ($firstRun) { if ($itemSearchResult.TotalCount -eq 0) { Write-Host "There are no items to process in $($DefaultFolder)." exit } else { Write-Verbose -Message "Initial search results total item count: $($itemSearchResult.TotalCount)" $totalItems = $itemSearchResult.TotalCount $firstRun = $false } } $items += $itemSearchResult.Items if ($itemSearchResult.MoreAvailable -eq $false) { $moreItems = $false Write-Verbose -Message "More items is set to False." } if ($moreItems) { $offset += $pageSize Write-Verbose -Message "Offset is $offset." } } #Process each item in search result collection $itemCount = 0 foreach ($item in $items) { $itemCount ++ Write-Progress -Activity "Apply retention tag to $DefaultFolder" -CurrentOperation "Updating item $itemCount of $totalItems" -PercentComplete ($itemCount/$totalItems*100) -Status ' ' $policyTagProperty = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x3019,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary) $policyTagGUID = New-Object -TypeName Guid($personalTagGUID) $item.SetExtendedProperty($policyTagProperty, $policyTagGUID.ToByteArray()) $item.SetExtendedProperty($retentionPeriodProperty, $personalTagRetentionDays) $retentionDate = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition(0x301C,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::SystemTime) $item.SetExtendedProperty($retentionDate, $item.DateTimeCreated.AddDays($personalTagRetentionDays)) #Include enum to not send updates; okay to use even when not a meeting $item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite,[Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToNone) } Write-Host $totalItems "items processed in $($DefaultFolder)." } } if (-not($EXOCreds)) { $EXOCreds = Get-Credential -Message 'Enter the credentials to use to access the mailbox.' -UserName $EmailAddress } Apply-RetentionTag $EmailAddress |