Archive Emails Older Than X Years in Exchange Online – The Server-Side Way (MRM + PowerShell)
Scenario: A user's mailbox is almost full. They tried Outlook rules / bulk "Move to folder" to archive emails older than 3 years, but it keeps failing after a few thousand items. Fix: Let Exchange Online do it on the server with MRM (Messaging Records Management) + Online Archive – no client, no throttling, any volume.
TL;DR
| Item | Value |
|---|
| Problem | Outlook client-side bulk moves/rules fail on large volumes |
| Solution | Default Policy Tag (DPT) with Move to Archive action + Online Archive |
| Engine | Managed Folder Assistant (MFA) – runs server-side |
| Tooling | Exchange Online PowerShell (ExchangeOnlineManagement module) |
| Time to complete | Minutes to configure; MFA may take one or more cycles on large mailboxes |
| User impact | None – old mail appears under Online Archive in Outlook / OWA with the same folder structure |
Why Outlook Rules Fail
| Approach | Runs where | Limitation |
|---|
| Outlook rule / "Run rules now" | Client | Stops on large item counts, depends on Outlook staying open |
| Drag-and-drop / Move to folder | Client | Throttled, times out, no resume |
| Outlook AutoArchive (.pst) | Client | Creates local PST – not recommended, not available in new Outlook |
| MRM Move-to-Archive tag | Server (Exchange Online) | No client dependency, processes the full mailbox |
What is MRM?
Messaging Records Management = Exchange feature that automatically moves or deletes mailbox items based on age.
| Component | What it is |
|---|
| Retention Tag | Rule: after N days → action (Move to Archive / Delete) |
| Retention Policy | Container of tags; one policy per mailbox |
| Managed Folder Assistant (MFA) | Background process that stamps tags and performs the action |
Retention tag types
| Tag type | Applies to | Allowed actions |
|---|
| Default Policy Tag (DPT) | Whole mailbox (untagged items) | Move to Archive, Delete and Allow Recovery, Permanently Delete |
| Retention Policy Tag (RPT) | A default folder (Inbox, Sent Items, Deleted Items…) | Delete only |
| Personal Tag | Folders/items the user tags in Outlook | Move to Archive, Delete and Allow Recovery, Permanently Delete |
⚠️ You can't create an Inbox-only "Move to Archive" tag. RPTs support delete actions only. For admin-driven archiving, use a DPT (whole mailbox). For folder-specific archiving, users can apply a Personal tag (shown as Archive Policy in Outlook).
Policy rules to remember
| Rule | Detail |
|---|
| DPTs per policy | Max one Move-to-Archive DPT + one Delete DPT (+ one voicemail DPT) |
| Age ordering | Move-to-Archive DPT age must be lower than the Delete DPT age |
| No archive mailbox | Move-to-Archive action does nothing |
| MFA schedule | Processes each mailbox at least once every 7 days; force it with Start-ManagedFolderAssistant |
| Blockers | RetentionHoldEnabled = $true or ElcProcessingDisabled = $true stops processing |
| Disabled user account | Items aren't moved to the archive |
Default MRM Policy (built-in)
| Tag | Type | Age | Action |
|---|
| Default 2 years move to archive | DPT | 730 days | Move to Archive |
| Recoverable Items 14 days move to archive | Recoverable Items | 14 days | Move to Archive |
| Personal 1 / 5 year move to archive, Never move | Personal | 365 / 1825 / – | Move to Archive |
| 1 Week … 5 Years Delete, Never Delete | Personal | 7–1825 / – | Delete and Allow Recovery |
| Junk Email | RPT | 30 days | Delete and Allow Recovery |
💡 If the mailbox already uses Default MRM Policy, simply enabling the archive starts moving items older than 2 years. Use a custom policy (below) when you need a different age, e.g. 3 years.
Prerequisites
| Requirement | Detail |
|---|
| Admin role | Exchange Administrator / Global Administrator (or Recipient + Retention Management roles) |
| PowerShell module | ExchangeOnlineManagement |
| Archive licensing | Exchange Online Plan 2, Microsoft 365 E3/E5, or Exchange Online Archiving add-on (for Plan 1) |
| Auto-expanding archive | Archive must be enabled first; up to 1.5 TB; cannot be disabled once on |
| Mailbox state | Account enabled, no Retention Hold, ELC processing enabled |
Solution Flow
| Step | Action | Cmdlet |
|---|
| 0 | Connect and start transcript | Connect-ExchangeOnline |
| 1 | Baseline: size, oldest Inbox item, current policy, blockers | Get-Mailbox, Get-MailboxStatistics, Get-MailboxFolderStatistics |
| 2 | Enable Online Archive (+ auto-expanding) | Enable-Mailbox -Archive / -AutoExpandingArchive |
| 3 | Review tags in current policy | Get-RetentionPolicy, Get-RetentionPolicyTag |
| 4 | Create 3-year Move-to-Archive DPT | New-RetentionPolicyTag |
| 5 | Create new policy = existing tags + new DPT | New-RetentionPolicy |
| 6 | Assign policy to mailbox | Set-Mailbox -RetentionPolicy |
| 7 | Kick off processing | Start-ManagedFolderAssistant -FullCrawl |
| 8 | Monitor progress | Export-MailboxDiagnosticLogs, Get-MailboxStatistics -Archive |
| 9 | (Optional) Rollback | Set-Mailbox -RetentionPolicy <original> |
🔑 Why create a new policy instead of editing the existing one? Editing a shared policy (like Default MRM Policy) changes every mailbox using it. A cloned policy limits the change to the target mailbox(es) and keeps all their existing tags.
Full PowerShell Script
Run in PowerShell ISE / VS Code. Edit the VARIABLES block, then run each #region one step at a time (select → F8) and review the output before moving on.
<#
=====================================================================================
Archive mailbox items older than N years (Exchange Online - MRM / Online Archive)
- Server-side Managed Folder Assistant moves items with a "Move to Archive" DPT
- Run each STEP individually (select region -> F8)
=====================================================================================
#>
# ====================== VARIABLES (edit these) ======================
$UserMailbox = "user@contoso.com" # Target mailbox (UPN / primary SMTP)
$AgeInDays = 1095 # 3 years
$NewTagName = "Move to Archive - 3 Years" # New DPT (MoveToArchive)
$NewPolicyName = "MRM Policy - Archive After 3 Years" # New retention policy
$EnableAutoExpandingArchive = $true # Needs E3/E5, EXO Plan 2 or EOA add-on
$LogFolder = "C:\Temp\ArchiveMailbox"
# ====================================================================
#region STEP 0 - Prereqs and connect
if (-not (Test-Path $LogFolder)) { New-Item -Path $LogFolder -ItemType Directory | Out-Null }
$Stamp = Get-Date -Format "yyyyMMdd_HHmmss"
Start-Transcript -Path "$LogFolder\ArchiveMailbox_$Stamp.log" -Append
if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force
}
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -ShowBanner:$false
#endregion
#region STEP 1 - BEFORE snapshot (mailbox, archive, policy, holds)
$Mbx = Get-Mailbox -Identity $UserMailbox -ErrorAction Stop
$Mbx | Select-Object DisplayName, PrimarySmtpAddress, RecipientTypeDetails, ProhibitSendQuota,
ProhibitSendReceiveQuota, ArchiveStatus, ArchiveGuid, AutoExpandingArchiveEnabled,
RetentionPolicy, RetentionHoldEnabled, ElcProcessingDisabled, LitigationHoldEnabled, InPlaceHolds |
Format-List
"--- Primary mailbox size ---"
Get-MailboxStatistics -Identity $UserMailbox |
Select-Object DisplayName, ItemCount, TotalItemSize, DeletedItemCount, TotalDeletedItemSize | Format-List
"--- Inbox stats (oldest / newest item) ---"
Get-MailboxFolderStatistics -Identity $UserMailbox -FolderScope Inbox -IncludeOldestAndNewestItems |
Select-Object Name, FolderPath, ItemsInFolder, ItemsInFolderAndSubfolders, FolderAndSubfolderSize,
OldestItemReceivedDate, NewestItemReceivedDate | Format-Table -AutoSize
# Save current retention policy name for rollback
$OriginalPolicy = $Mbx.RetentionPolicy
"Original retention policy: $OriginalPolicy" | Out-File "$LogFolder\OriginalPolicy_$Stamp.txt"
"Original retention policy saved: $OriginalPolicy"
# These block MFA processing - must be False
if ($Mbx.RetentionHoldEnabled -or $Mbx.ElcProcessingDisabled) {
Write-Warning "RetentionHoldEnabled or ElcProcessingDisabled is TRUE - MFA will NOT move items. Fix in STEP 1a."
} else {
Write-Host "Retention hold / ELC processing OK." -ForegroundColor Green
}
#endregion
#region STEP 1a - (ONLY if warning above) Re-enable MFA processing
# Set-Mailbox -Identity $UserMailbox -RetentionHoldEnabled $false
# Set-Mailbox -Identity $UserMailbox -ElcProcessingDisabled $false
#endregion
#region STEP 2 - Enable Online Archive (and auto-expanding archive)
$Mbx = Get-Mailbox -Identity $UserMailbox
if ($Mbx.ArchiveStatus -ne "Active" -and $Mbx.ArchiveGuid -eq [Guid]::Empty) {
Enable-Mailbox -Identity $UserMailbox -Archive
Write-Host "Online Archive enabled." -ForegroundColor Green
} else {
Write-Host "Online Archive already enabled." -ForegroundColor Yellow
}
if ($EnableAutoExpandingArchive) {
$Mbx = Get-Mailbox -Identity $UserMailbox
if (-not $Mbx.AutoExpandingArchiveEnabled) {
try {
Enable-Mailbox -Identity $UserMailbox -AutoExpandingArchive -ErrorAction Stop
Write-Host "Auto-expanding archive enabled (irreversible)." -ForegroundColor Green
} catch {
Write-Warning "Auto-expanding archive not enabled: $($_.Exception.Message)"
}
} else {
Write-Host "Auto-expanding archive already enabled." -ForegroundColor Yellow
}
}
Get-Mailbox -Identity $UserMailbox | Select-Object ArchiveStatus, ArchiveName, ArchiveQuota, AutoExpandingArchiveEnabled | Format-List
#endregion
#region STEP 3 - Review tags in the CURRENT retention policy
$CurrentPolicyName = (Get-Mailbox -Identity $UserMailbox).RetentionPolicy
if ([string]::IsNullOrEmpty($CurrentPolicyName)) { $CurrentPolicyName = "Default MRM Policy" }
$CurrentTagNames = (Get-RetentionPolicy -Identity $CurrentPolicyName).RetentionPolicyTagLinks |
ForEach-Object { $_.ToString() }
$CurrentTags = $CurrentTagNames | ForEach-Object { Get-RetentionPolicyTag -Identity $_ }
"--- Tags in current policy '$CurrentPolicyName' ---"
$CurrentTags | Select-Object Name, Type, RetentionAction, AgeLimitForRetention, RetentionEnabled | Format-Table -AutoSize
# Move-to-Archive DPT age must be LOWER than any Delete DPT age
$DeleteDpt = $CurrentTags | Where-Object {
$_.Type -eq "All" -and $_.RetentionAction -in @("DeleteAndAllowRecovery", "PermanentlyDelete") -and $_.RetentionEnabled
}
if ($DeleteDpt -and $DeleteDpt.AgeLimitForRetention.Days -le $AgeInDays) {
Write-Warning "Delete DPT '$($DeleteDpt.Name)' ($($DeleteDpt.AgeLimitForRetention.Days) days) is not longer than $AgeInDays days. Review before continuing."
} else {
Write-Host "No conflicting Delete DPT." -ForegroundColor Green
}
#endregion
#region STEP 4 - Create the Move-to-Archive Default Policy Tag
if (-not (Get-RetentionPolicyTag -Identity $NewTagName -ErrorAction SilentlyContinue)) {
New-RetentionPolicyTag -Name $NewTagName `
-Type All `
-RetentionAction MoveToArchive `
-AgeLimitForRetention $AgeInDays `
-RetentionEnabled $true `
-Comment "Moves items older than $AgeInDays days to the Online Archive"
Write-Host "Tag '$NewTagName' created." -ForegroundColor Green
} else {
Write-Host "Tag '$NewTagName' already exists." -ForegroundColor Yellow
}
Get-RetentionPolicyTag -Identity $NewTagName | Select-Object Name, Type, RetentionAction, AgeLimitForRetention, RetentionEnabled | Format-List
#endregion
#region STEP 5 - Create new retention policy (existing tags + new DPT)
# Only one Move-to-Archive DPT is allowed per policy - drop the old one
$TagsToKeep = $CurrentTags | Where-Object {
-not ($_.Type -eq "All" -and $_.RetentionAction -eq "MoveToArchive")
} | Select-Object -ExpandProperty Name
$NewPolicyTags = @($TagsToKeep) + $NewTagName | Select-Object -Unique
"Tags for new policy:"; $NewPolicyTags
if (-not (Get-RetentionPolicy -Identity $NewPolicyName -ErrorAction SilentlyContinue)) {
New-RetentionPolicy -Name $NewPolicyName -RetentionPolicyTagLinks $NewPolicyTags
Write-Host "Policy '$NewPolicyName' created." -ForegroundColor Green
} else {
Set-RetentionPolicy -Identity $NewPolicyName -RetentionPolicyTagLinks $NewPolicyTags -Confirm:$false
Write-Host "Policy '$NewPolicyName' already existed - tag links updated." -ForegroundColor Yellow
}
(Get-RetentionPolicy -Identity $NewPolicyName).RetentionPolicyTagLinks
#endregion
#region STEP 6 - Assign the policy to the mailbox
Set-Mailbox -Identity $UserMailbox -RetentionPolicy $NewPolicyName
Get-Mailbox -Identity $UserMailbox | Select-Object DisplayName, RetentionPolicy, ArchiveStatus | Format-List
#endregion
#region STEP 7 - Start the Managed Folder Assistant
# If the archive was just enabled and this errors, wait 15-30 min and re-run.
Start-ManagedFolderAssistant -Identity $UserMailbox -FullCrawl
Write-Host "MFA started. Large mailboxes may need several cycles - re-run daily if required." -ForegroundColor Cyan
#endregion
#region STEP 8 - Monitor progress (re-run after a few hours / next day)
$Diag = [xml](Export-MailboxDiagnosticLogs -Identity $UserMailbox -ExtendedProperties).MailboxLog
$Diag.Properties.MailboxTable.Property |
Where-Object { $_.Name -like "ELC*" } |
Select-Object Name, Value | Format-Table -AutoSize
"--- Primary mailbox ---"
Get-MailboxStatistics -Identity $UserMailbox | Select-Object ItemCount, TotalItemSize | Format-List
"--- Archive mailbox ---"
Get-MailboxStatistics -Identity $UserMailbox -Archive | Select-Object DisplayName, ItemCount, TotalItemSize | Format-List
"--- Inbox oldest item (should move toward the cut-off date) ---"
Get-MailboxFolderStatistics -Identity $UserMailbox -FolderScope Inbox -IncludeOldestAndNewestItems |
Select-Object FolderPath, ItemsInFolder, FolderAndSubfolderSize, OldestItemReceivedDate | Format-Table -AutoSize
#endregion
#region STEP 9 - (OPTIONAL) Rollback to the original policy
# $OriginalPolicyName = Get-Content (Get-ChildItem "$LogFolder\OriginalPolicy_*.txt" | Sort-Object LastWriteTime | Select-Object -Last 1).FullName
# $OriginalPolicyName = $OriginalPolicyName -replace "Original retention policy: ",""
# Set-Mailbox -Identity $UserMailbox -RetentionPolicy $OriginalPolicyName
# Note: items already moved stay in the archive; the user can move them back if needed.
#endregion
#region STEP 10 - Disconnect
Stop-Transcript
Disconnect-ExchangeOnline -Confirm:$false
#endregion
Verifying Results
| Check | What "good" looks like |
|---|
ELCLastSuccessTimestamp (Step 8) | Recent date/time after you started MFA |
Primary TotalItemSize | Decreasing |
Archive ItemCount / TotalItemSize | Increasing |
Inbox OldestItemReceivedDate | Moves closer to today − N years |
| Outlook / OWA | Online Archive folder visible with same folder structure |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|
| Nothing moves | Archive not enabled | Enable-Mailbox -Archive |
| Nothing moves | RetentionHoldEnabled / ElcProcessingDisabled = True | Set both to $false |
| Nothing moves | User account disabled | Items aren't archived for disabled accounts |
Start-ManagedFolderAssistant errors | Archive still provisioning | Wait 15–30 min, re-run |
| Only part of mailbox moved | Large mailbox, MFA works in batches | Re-run Step 7 daily; check Step 8 |
| Some folders not archived | Personal tag "Never move to archive" applied by user | Expected – personal tags override the DPT |
New-RetentionPolicy fails | Two Move-to-Archive DPTs, or archive age ≥ delete age | Keep one archive DPT; archive age must be lower |
| Auto-expanding fails | License doesn't include archiving | Assign EXO Plan 2 / E3 / E5 or EOA add-on |
Key Takeaways
| # | Takeaway |
|---|
| 1 | Don't use Outlook rules for bulk archiving – use server-side MRM |
| 2 | Move to Archive works only with DPTs or Personal tags – not with folder RPTs |
| 3 | Clone the current policy instead of editing a shared one |
| 4 | Always baseline first and save the original policy for rollback |
| 5 | Use Start-ManagedFolderAssistant -FullCrawl to avoid waiting up to 7 days |
| 6 | Auto-expanding archive is irreversible – confirm licensing and intent first |
References (Microsoft Learn)
No comments:
Post a Comment