Monday, January 21, 2013

Exporting mailboxes to PST-files the easy way

When doing a cross domain migration it can be usefull to export the mailboxes to a PST file.

When doing this for a large amount of mailboxes it can be useful to use a script to automate this...

As I am lazy, I searched the internet for such a script, and found one from Steve Goodman which was close to what I wanted.. But as needy as I am, I needed more.

I modified, and added to the script which you find below. And is is as good as I am going to make it. It works like a charm (as I do say so myself)

Also made it so that if you are going to export a lot of mailboxes the target share and the source servers won't die on you you can set the maximum concurrent to a number your environment will survive.
The script will start the first batch, wait until completion, and than start the second batch.

When finished it will write some info to the report folder, and the logs for the completed and the failed requests...

Thanks go to Jeff Wouters which reviewed the script for me. And to his blog for the Add-Module function, which gave me a good idea for an add-SnapIN function which does basically the same but than for SnapIns.. so if you run the script multiple times you won't get an error :-)

# Exchange 2010 SP1 Mailbox Export Script
# Original script from : Steve Goodman (http://www.stevieg.org/2010/07/using-the-exchange-2010-sp1-mailbox-export-features-for-mass-exports-to-pst/)
# input from Jeff Wouters ( www.jeffwouters.nl)
# deviations from original script:
# - option to export all in the organisation
# - option to export all in a list
# - option to include archive mailboxes
# - option to exclude dumpster
# - option to set a maximum concurrent exports
# - no need to run from Exchange PowerShell. Needed library is loaded at runtime
$starttime = get-date -displayhint time
###############
# Settings    #
###############

# Pick ONE of the two below. If you choose both, it will use $Server.
$Server
$Database
$list = "\\server\share\users1.csv"

# Share to export mailboxes to. Needs R/W by Exchange Trusted Subsystem
# Must be a UNC path as this is run by the CAS MRS service.
$ExportShare = "\\server\share\pst\"
if (!(test-path -path $ExportShare -pathtype container))
{
 mkdir $ExportShare
}

# After each run a report of the exports can be dropped into the directory specified below. (The user that runs this script needs access to this share)
# Must be a UNC path or the full path of a local directory.
$ReportShare = "\\server\share\log\"
if (!(test-path -path $ReportShare -pathtype container))
{
 mkdir $ReportShare
}

# Shall we remove the PST file, if it exists beforehand? (The user that runs this script needs access to the $ExportShare share)
# Valid values: $true or $false
$RemovePSTBeforeExport = $true

# Do we want to include Archive Mailboxes
$IncludeArchive = $true

# Do we want to exclude dumpster
# currently not working as intended. workarround in place
$ExcludeDumpster = $false

# How many concurrent exports do we want?
# This must be an even number (2, 4, 6, 50, 100, 200, 1000 etc)
$maxConcurrentExports = 200

###############
# Code        #
###############

# function created by Jeff Wouters (www.jeffwouters.nl)
function Check-LoadedModule
{
  Param( [parameter(Mandatory = $true)][alias("Module")][string]$ModuleName)
  $LoadedModules = Get-Module | Select Name
  if (!$LoadedModules -like "*$ModuleName*") {Import-Module -Name $ModuleName}
}

# Deviation from Jeff Wouters function but for PSAddins
function Check-LoadedSnapIN
{
  Param( [parameter(Mandatory = $true)][alias("SnapIN")][string]$SnapINName)
 if ( (Get-PSSnapin -Name $SnapINName -ErrorAction SilentlyContinue) -eq $null )
 {
     Add-PsSnapin $SnapINName
 }
}

# If Archives are included we get 2 exports at a time. so we need to divide the maximum by 2
if ($includeArchive){
 $maxConcurrentExports = $maxConcurrentExports / 2
}

# load the Exchange powershell snapin if not loaded
Check-LoadedSnapIN Microsoft.Exchange.Management.PowerShell.E2010

# this function will create the MailboxExportRequests.
function exportMailbox ([String]$CurrentUser) {
     if ($RemovePSTBeforeExport -eq $true -and (Get-Item "$($ExportShare)\$($CurrentUser).PST" -ErrorAction SilentlyContinue))
     { 
         Remove-Item "$($ExportShare)\$CurrentUser.PST" -Confirm:$false -ErrorAction SilentlyContinue
         Remove-Item "$($ExportShare)\$CurrentUser-Archive.PST" -Confirm:$false -ErrorAction SilentlyContinue
     }
     if ($ExcludeDumpster){ # workarround if else. Would rather place this inline in the vode.
      New-MailboxExportRequest -BatchName $BatchName -Mailbox $CurrentUser -FilePath "$($ExportShare)\$CurrentUser.pst" -ExcludeDumpster
      if ($IncludeArchive){
       New-MailboxExportRequest -BatchName $BatchName -Mailbox $CurrentUser -FilePath "$($ExportShare)\$CurrentUser-Archive.pst" -IsArchive -ExcludeDumpster
      }
     } else {
      New-MailboxExportRequest -BatchName $BatchName -Mailbox $CurrentUser -FilePath "$($ExportShare)\$CurrentUser.pst"
      if ($IncludeArchive){
       New-MailboxExportRequest -BatchName $BatchName -Mailbox $CurrentUser -FilePath "$($ExportShare)\$CurrentUser-Archive.pst" -IsArchive
      }
  }     
}

# This function makes the script wait for completion of all exports.
function waitForCompletion {
 while ((Get-MailboxExportRequest -BatchName $BatchName | Where {$_.Status -eq "Queued" -or $_.Status -eq "InProgress"}))
 {
     clear
     Write-Output "Waiting for the exports to complete"
     Get-MailboxExportRequest -BatchName $BatchName | Get-MailboxExportRequestStatistics | Where {$_.Status -eq "Queued" -or $_.Status -eq "InProgress"} | Out-Default | Format-Table
     sleep 60
 }
}

# Make batch name
$date=Get-Date
$BatchName = "Export_$($date.Year)-$($date.Month)-$($date.Day)_$($date.Hour)-$($date.Minute)-$($date.Second)"

if ($ExcludeDumpster){
 $additionalParm = "-ExcludeDumpster $true"
} else {
 $additionalParm = ""
}

# the collection of what to be export
if ($Server)
{
 write-output "Using Server"
    if (!(Get-ExchangeServer $Server -ErrorAction SilentlyContinue))
    {
        throw "Exchange Server $Server not found";
    }
    if (!(Get-MailboxDatabase -Server $Server -ErrorAction SilentlyContinue))
    {
        throw "Exchange Server $Server does not have mailbox databases";
    }
    $Mailboxes = Get-Mailbox -Server $Server -ResultSize Unlimited
} elseif ($Database) {
 write-output "Using database" 
    if (!(Get-MailboxDatabase $Database -ErrorAction SilentlyContinue))
    {
        throw "Mailbox database $Database not found"
    }
    $Mailboxes = Get-Mailbox -Database $Database -ResultSize Unlimited
} elseif ($list) {
 write-output "Using list: $list"
 $userlist = Import-CSV $list
} else {
    write-output "None of the above, so exporting all mailboxes.."
    $mailboxes = Get-Mailbox -ResultSize Unlimited
} 

# The Export
if ($list){
 Write-Output "Queuing $($userlist.Count) mailboxes as batch '$($BatchName)'"
 $teller = 0
 # Queue all mailbox export requests
 foreach ($user in $userlist)
 {
     $teller = $teller + 1
     $curuser =  $user.SamAccountName
     exportMailbox $curuser
     if ($teller -gt $maxConcurrentExports){
  Write-Output "Waiting for batch to complete"
  # Wait for mailbox export requests to complete
  waitForCompletion 
  $teller = 0
     }
 }  
 # Wait for mailbox export requests to complete
 waitForCompletion
}elseif ((!$Mailboxes) -and (!$Mailboxes.Count)) {
    throw "No mailboxes found on $Server or single Mailbox."
} else {
 Write-Output "Queuing $($Mailboxes.Count) mailboxes as batch '$($BatchName)'"
 # Queue all mailbox export requests
 $teller = 0
 foreach ($Mailbox in $Mailboxes)
 {
     $teller = $teller + 1
     exportMailBox $Mailbox.alias
     if ($teller -gt $maxConcurrentExports){
  Write-Output "Waiting for batch to complete"
  # Wait for mailbox export requests to complete
  waitForCompletion 
  $teller = 0
     }
 }
 # Wait for mailbox export requests to complete
 waitForCompletion
}

# Write reports if required
if ($ReportShare)
{
    Write-Output "Writing reports to $($ReportShare)"
    $Completed = Get-MailboxExportRequest -BatchName $BatchName | Where {$_.Status -eq "Completed"} | Get-MailboxExportRequestStatistics | Format-List
    if ($Completed)
    {
        $Completed | Out-File -FilePath "$($ReportShare)\$($BatchName)_Completed.txt"
    }
    $Incomplete = Get-MailboxExportRequest -BatchName $BatchName | Where {$_.Status -ne "Completed"} | Get-MailboxExportRequestStatistics
    if ($Incomplete)
    {
        $Incomplete | Format-List | Out-File -FilePath "$($ReportShare)\$($BatchName)_Incomplete_Report.txt"

  if (!$list){
   $list = "$($ReportShare)\$($BatchName)_failedpst.csv"
  }
  set-content -Value "Failed-PST" -Path $list".new"
  foreach ($woops in $Incomplete) {
   add-content -Value $woops.FilePath -Path $list".new"
  }

    }
}

$endtime = get-date -displayhint time

$runtimefile = "$($ReportShare)\$($BatchName)_runtime.txt"
$runtimeentry0 = $starttime.tostring() + ' - ' + $endtime.tostring()
$runtimeentry1 = "Completed exports: " + $completed.count
$runtimeentry2 = "Failedexports: " + $Incomplete.count
$runtimeentry3 = "Errored users:"
$runtimeentry4 = ( get-content -Path $list".new" | out-string)
Set-Content -Value $runtimeentry0 -Path $runtimefile
add-content -Value $runtimeentry1 -Path $runtimefile
add-content -Value $runtimeentry2 -Path $runtimefile
add-content -Value $runtimeentry3 -Path $runtimefile
add-content -Value $runtimeentry4 -Path $runtimefile

# Remove Requests
Write-Output "Removing requests created as part of batch '$($BatchName)'"
Get-MailboxExportRequest -BatchName $BatchName | Remove-MailboxExportRequest -Confirm:$false

5 comments:

  1. This is really great and the improvements are exactly what I was looking for.

    ReplyDelete
  2. I need help with the scrip. I get this error. Please help
    PS C:\Users\administrator.CORP> cd C:\pst
    PS C:\pst> .\MassExportnew.ps1
    Using list: \\fs01\Shared\users1.csv
    Queuing 3 mailboxes as batch 'Export_2015-1-23_9-33-7'
    New-MailboxExportRequest : Cannot bind parameter 'Mailbox'. Cannot convert value "" to type "Microsoft.Exchange.Configu
    ration.Tasks.MailboxOrMailUserIdParameter". Error: "Parameter values of type Microsoft.Exchange.Configuration.Tasks.Mai
    lboxOrMailUserIdParameter can't be empty. Specify a value, and try again.
    Parameter name: identity"
    At C:\pst\MassExportnew.ps1:95 char:62
    + New-MailboxExportRequest -BatchName $BatchName -Mailbox <<<< $CurrentUser -FilePath "$($ExportShare)\$CurrentU
    ser.pst"
    + CategoryInfo : InvalidArgument: (:) [New-MailboxExportRequest], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.Exchange.Management.RecipientTasks.NewMailboxEx
    portRequest

    New-MailboxExportRequest : Cannot bind parameter 'Mailbox'. Cannot convert value "" to type "Microsoft.Exchange.Configu
    ration.Tasks.MailboxOrMailUserIdParameter". Error: "Parameter values of type Microsoft.Exchange.Configuration.Tasks.Mai
    lboxOrMailUserIdParameter can't be empty. Specify a value, and try again.
    Parameter name: identity"
    At C:\pst\MassExportnew.ps1:97 char:63
    + New-MailboxExportRequest -BatchName $BatchName -Mailbox <<<< $CurrentUser -FilePath "$($ExportShare)\$Current
    User-Archive.pst" -IsArchive
    + CategoryInfo : InvalidArgument: (:) [New-MailboxExportRequest], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.Exchange.Management.RecipientTasks.NewMailboxEx
    portRequest

    New-MailboxExportRequest : Cannot bind parameter 'Mailbox'. Cannot convert value "" to type "Microsoft.Exchange.Configu
    ration.Tasks.MailboxOrMailUserIdParameter". Error: "Parameter values of type Microsoft.Exchange.Configuration.Tasks.Mai
    lboxOrMailUserIdParameter can't be empty. Specify a value, and try again.
    Parameter name: identity"
    At C:\pst\MassExportnew.ps1:95 char:62
    + New-MailboxExportRequest -BatchName $BatchName -Mailbox <<<< $CurrentUser -FilePath "$($ExportShare)\$CurrentU
    ser.pst"
    + CategoryInfo : InvalidArgument: (:) [New-MailboxExportRequest], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.Exchange.Management.RecipientTasks.NewMailboxEx
    portRequest

    New-MailboxExportRequest : Cannot bind parameter 'Mailbox'. Cannot convert value "" to type "Microsoft.Exchange.Configu
    ration.Tasks.MailboxOrMailUserIdParameter". Error: "Parameter values of type Microsoft.Exchange.Configuration.Tasks.Mai
    lboxOrMailUserIdParameter can't be empty. Specify a value, and try again.
    Parameter name: identity"
    At C:\pst\MassExportnew.ps1:97 char:63
    + New-MailboxExportRequest -BatchName $BatchName -Mailbox <<<< $CurrentUser -FilePath "$($ExportShare)\$Current
    User-Archive.pst" -IsArchive
    + CategoryInfo : InvalidArgument: (:) [New-MailboxExportRequest], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.Exchange.Management.RecipientTasks.NewMailboxEx
    portRequest

    ReplyDelete
  3. Could you post the contents of your CSV file?

    The first line of the csv should have: SamAccountName



    ReplyDelete
  4. Bas, Thanks for quit reply. Here it is


    SamAccountName
    kklepacki
    rklepacki
    mklepacki
    nklepacki

    ReplyDelete
  5. Bas, Script is working correctly. there was an issue in my test lab setup. Thanks again.

    ReplyDelete