Exchange 2010 Terminated Users Migration Script

At my organization, I ran into an interesting situation. Our company, because of some policies which I do not comprehend, must keep departed personnel’s mailboxes for over one year. Now in most cases, I’d have exported these mailboxes to PSTs, delete the mailbox, then after a year, nix the PSTs.

The burn is that other active employees may need access to the departed person’s mailbox. That makes PST management nightmarish, so we just avoid that as an issue.

What we decided to do was to build a few Mailbox Servers for our terminated users. These servers are not members of any DAG, but just sit alone on less expensive storage because I/O isn’t nearly as critical. We gave the database and logs mount points a whole bunch of storage, so that we could grow and not really worry about this server. But to be on the safe side, I though it best to make sure that the databases grow at about the same rate. Going back to a previous post about balancing mailbox databases and creating moves, I altered that script specifically for terminated users.

The script is a little ugly, does virtually no logging, and only has the simplest error trapping, but this is for terminated user mailboxes, so what’s it matter, right? So now, on to the script:

#################################################
# Script Name:	  Migrate-TerminatedUsers
# Created By:     Kevin Sparenberg
#
# Last Modified:  2014-01-28
#
# This script will search through the "active" user databases on the Exchange 2010
# Servers and search for users with the "TERM" flag in Custom Attrbiute 4.  The
# Attribute's format is such: "TERM YYYY-MM-DD" where "YYYY-MM-DD" is the year,
# month, and date of the users termination.
#
# The script specifically "balances" the mailboxes so that no single mailbox database
# is drastically larger than another.
#
# If mailboxes are found with this attribute and over 30 days have passed since their
# termination date, the server will move them to databases on the terminated user server(s).
#
# Assumptions:
#     The databases that you have configured for terminated mailbox users need to be configured
# with IsExcludedFromProvisioning set to true.
#     The script assumes that there are no current mailbox moves taking place.  If mailbox
# moves ARE taking place, they are not taking into consideration with regards to EDB Sizing.
#
# Indicators of a Terminated Mailbox:
#     CustomAttribute4 is populated with "TERM YYYY-MM-DD" per the description above.
#     The user accounts have been moved to a separate "Disabled Users" Organizational Unit
#         within Active Directory (This is just good practice anyway)
#################################################

# Clean up any variables that are still sitting out there suppressing errors
Get-Variable | Remove-Variable -ErrorAction SilentlyContinue
$Error.Clear()
Clear-Host 

# Check to see if the Exchange 2010 Snapins are Imported.  If not, add them.
if ( -not ( Get-Command -Name Get-MailboxDatabase -ErrorAction SilentlyContinue ) )
{
    Add-PSSnapin -Name Microsoft.Exchange.Management.PowerShell.E2010 -ErrorAction SilentlyContinue
}

#region Variable Declaration
$TerminatedUserServers = "MBXTERM*" # This can be a single server or a wildcard (as shown in this example)
$NumDaysPostTerm       = 30

$CreateMoveRequests    = $true  # Create the move requests or just run through a simulation?
$Suspend               = $true  # Suspend the moves from the beginning
$SuspendOnCompletion   = $false # When you get ready to complete the move, should we go into "AutoSuspended?"

$DisabledOu            = "MyDomain.Root.Local/DisabledUsers/*" # All Users need to also be in this Organizational Unit or children
#endregion

# Get all 2010 Databases Terminated User Databases
$TargetDatabases = Get-MailboxDatabase -Server $TerminatedUserServers -Status | Sort-Object Name

# Build a New Collection to Track the Database Size as mailboxes are added
$TargetDBs = @()

$TargetDatabaseCount = $TargetDatabases.Count; $i = 1 # Total & Counter for Progress Bar
# Cycle through each Target Database
ForEach ( $TargetDatabase in $TargetDatabases )
{
    Write-Progress -Activity "Building Custom Mailbox Database Object Collection" -Status ( " " ) -CurrentOperation ( "Building Custom Object for Database: " + $TargetDatabase.Name ) -PercentComplete ( ( $i / $TargetDatabaseCount ) * 100 )

    $TargetDBItem = New-Object PSObject -Property @{ Name               = $TargetDatabase.Name
                                                     OriginalSize       = $TargetDatabase.DatabaseSize
                                                     OriginalWhiteSpace = $TargetDatabase.AvailableNewMailboxSpace
                                                     ProjectedSize      = $TargetDatabase.DatabaseSize - $TargetDatabase.AvailableNewMailboxSpace
                                                     MailboxCount       = ( Get-Mailbox -ResultSize Unlimited -Database $TargetDatabase | Measure-Object ).Count
                                                     DiskSize           = [Microsoft.Exchange.Data.ByteQuantifiedSize]( Get-WMIObject -ComputerName $TargetDatabase.ServerName -Class Win32_Volume | Where-Object { ( $TargetDatabase.EDBFilePath.PathName -like ( $_.Caption + "*" ) ) -and ( $_.Caption.Length -gt 3 ) } ).Capacity
                                                    }

    $TargetDBItem | Add-Member -MemberType ScriptProperty -Name ProjectedDiskFree -Value { $this.DiskSize - $this.ProjectedSize }

    # Add this item to the collection
    $TargetDBs += $TargetDBItem

    $i++
}
Write-Progress -Activity "Building Custom Mailbox Database Object Collection" -Status "Completed" -Completed

# Get all Legacy Mailboxes and Filter off any we don't want
$ActiveDatabases = Get-MailboxDatabase | Where-Object { -not ( $_.IsExcludedFromProvisioning ) }
$TermMailboxes = $ActiveDatabases | Get-Mailbox -ResultSize Unlimited -Filter { CustomAttribute4 -like "TERM*" } | Where-Object { $_.CustomAttribute4 -match "TERM d{4}-d{2}-d{2}" }

# Create an empty collection for the move requests
$MoveRequests = @()

$TermMailboxCount = $TermMailboxes.Count

$i = 1 # Counter

ForEach ( $Mailbox in $TermMailboxes )
{

    Write-Progress -Activity "Building Custom Mailbox Object Collection" -Status " " -CurrentOperation ( "Mailbox: " + $Mailbox.DisplayName ) -PercentComplete ( ( $i / $TermMailboxCount ) * 100 )

    # Initialize from variables for assignment later
    $TermDate = $null
    $DoMove   = $false

    # Check to see if CustomAttribute4 matches the REGEX of 'TERM YYYY-MM-DD'
    if ( $Mailbox.CustomAttribute4 -match "TERM d{4}-d{2}-d{2}" )
    {
        # Extract the date from the CustomAttribute
        $TermDate = Get-Date -Date ( $Matches.Values[0].Split(" ")[-1] )
        # Determine if the date is far enough in the past to process the move
        $DoMove = ( $TermDate.AddDays($NumDaysPostTerm) -lt ( Get-Date ) )
    }

    if ( $Mailbox.OrganizationalUnit -like $DisabledOu )
    {
        $MoveRequest = New-Object PSObject -Property @{ DisplayName = $Mailbox.DisplayName
                                                        TermNote    = $Mailbox.CustomAttrbiute4
                                                        Office      = $Mailbox.Office
                                                        Title       = ( Get-User $Mailbox ).Title
                                                        SourceDB    = $Mailbox.Database.Name
                                                        MBXIdentity = $Mailbox.Identity
                                                        MBXSize     = ( Get-MailboxStatistics $Mailbox ).TotalItemSize
                                                        DoMove      = $DoMove
                                                        TermDate    = $TermDate
                                                        TargetDB    = $null # We'll update this later
                                                        }

        # Add it to the Move Requests Collection
        $MoveRequests += $MoveRequest
    }
    else
    {
        Write-Warning "Mailbox [$( $Mailbox.DisplayName )] does not exist in a Disabled Organizational Unit.`n`tCurrent OU: [$( $Mailbox.OrganizationalUnit )].`n`tThis mailbox will not be moved."
    }
    $i++
}
Write-Progress -Activity "Building Custom Mailbox Object Collection" -Status "Completed" -Completed

# Filter for Only Mailboxes with DoMove set to true and also sort the Mailboxes by Mailbox Size (Largest First)
$MoveRequests = $MoveRequests | Where-Object { $_.DoMove } | Sort-Object MBXSize -Descending

$MoveRequestCount = $MoveRequests.Count; $i = 1 # Total & Counter for Progress Bar
ForEach ( $MoveRequest in $MoveRequests )
{
    Write-Progress -Activity "Determining Mailbox Placement" -Status " " -CurrentOperation ( "Mailbox: " + $MoveRequest.DisplayName ) -PercentComplete ( ( $i / $MoveRequestCount ) * 100 )
    # Assign the target database for this move
    $MoveRequest.TargetDB = ( $TargetDBs | Sort-Object ProjectedSize  )[0].Name
    # "Bump" the projected database size with the mailbox size
    ( $TargetDBs | Sort-Object ProjectedSize  )[0].ProjectedSize += $MoveRequest.MBXSize.Value
    # "Bump" the mailbox count as well
    ( $TargetDBs | Sort-Object ProjectedSize  )[0].MailboxCount++
    $i++
}
Write-Progress -Activity "Determining Mailbox Placement" -Status "Completed" -Completed

if ( $CreateMoveRequests )
{
    $i = 1 # Reset Counter
    ForEach ( $Move in $MoveRequests )
    {
        Write-Progress -Activity "Creating Move Requests" -Status " " -CurrentOperation ( "Creating Move Request for " + $Move.DisplayName ) -PercentComplete ( ( $i / $MoveRequestCount ) * 100 )
        ( $Move.MBXIdentity ) | New-MoveRequest -BatchName ( $Move.DisplayName + "-->" + $Move.TargetDB ) -TargetDatabase ( $Move.TargetDB ) -BadItemLimit 10 -SuspendWhenReadyToComplete:$SuspendOnCompletion -Suspend:$Suspend -WarningAction SilentlyContinue
        $i++
    }
    Write-Progress -Activity "Creating Move Requests" -Status "Completed" -Completed
}
else
{
    $MoveRequests | Out-GridView -Title "Projected Terminated Mailbox Moves"
}

If you have any quemments (questions/comments) please let me know and I’ll try to respond in a reasonable amount of time. Until next time script-kiddies…

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.