Dismount Recovery Storage Group using Exchange 2007 Shell

Dismounting the RSG from and Exchange Mailbox server is the last step after you have recovered data you needed. I would like to stress here that my experience tells me this is the step when you will encounter issues, not necesarily issues with the process itself but mostly with the GUI interface. I’ve spent a lot of time trying to figure out which a RSG won’t dismount, when the GUI reported success. So I have come up with a script that does this using exchange management shell, which gives you a little more control and additional debugging information.

Here is what you will need to accomplish this:

  • Exchange Server Administrator Rights where the RSGs are located.
  • Exchange Management Shell since this entire procedure is best done using powershell scripting.
  • Run the script as Administrator to avoid errors due to enabled UAC

The Script

#Dismount DB, Remove, DB, Remove Storage Group
$DB_in_RSG = Get-MailboxDatabase -Server <ENTERSERVER> | where {$_.Recovery -like '*true*'}
Write-Host -ForegroundColor Green "The Database in the RSG ($($DB_in_RSG.Name)) will be Dismounted. Press Ctrl+C to cancel or Enter to continue"
$DB_in_RSG | Dismount-Database -Verbose -Debug

Write-Host -ForegroundColor Green "The Database in the RSG will be Removed. Press Ctrl+C to cancel or Enter to continue"
$DB_in_RSG | Remove-MailboxDatabase -Verbose -Debug

#Now deleting the files of the database
$SG_Path = (Get-StorageGroup $DB_in_RSG.StorageGroup).SystemFolderPath
Write-Host -ForegroundColor Green "The actual DB files will be removed (stored in $SG_Path). Please check previous steps completed successfully before continuing. Press Ctrl+C to cancel or Enter to continue."
get-item $SG_Path | del -Force -Recurse -Verbose -Debug

#Now removing storage group
Write-Host -ForegroundColor Green "The RSG will be Removed. Press Ctrl+C to cancel or Enter to continue"
Remove-StorageGroup -Identity $DB_in_RSG.StorageGroup -Verbose –Debug

Learning Points

There are a few steps you need to do:

  1. Grab the RSG (there is only one per server) – stored in the script in $DB_in_RSG
  2. You dismount the database from the server using Dismount-Database cmdlet.
  3. After disomunting the database you remove the Database using Remove-MailboxDatabase cmdlet. This will just remove the DB from Exchange, the files will remain on the file system.
  4. The actual files on the file system get removed using the path from variable $SG_Path. Use del cmdlet with -force -recurse to bypass any confirmation prompts.
  5. The last step is removing the Recovery Storage Group from the Exchange. The commandlet for this is Remove-StorageGroup.

That’s about it, pretty easy actually. Although this script is the last in the series I reccomend you actually run this before anything, before mounting the RSG or at least do a check to see if a RSG is already mounted.

Restore a mailbox in the Recovery Storage Group

Finally I managed to find some more time about posting the next bits of the Exchange mailbox recovery process. Last post we discussed how to mount the database into the RSG. This time we will be discussing how to restore a user’s mailbox in the original mailbox location and how to restore the mailbox data to another temporary mailbox and exporting that out of the temporary mailbox.

Here is what you will need to accomplish this:

  • Exchange Server Administrator Rights where the mailbox(es) are located.
  • Full mailbox access for exporting emails out of the temp mailbox I was talking about
  • Exchange Management Shell since this entire procedure is best done using powershell scripting.
  • Obviously free space both on the Database disk aswell as disk space where the PST file will be saved.

The Script

#Parameters Section
param (
 [parameter(Mandatory = $true)]

#Restore mailbox section
$Filter = "SamAccountName -like '$paramRestoredUser'"
$SourceAlias = Get-Mailbox -Filter $Filter -IgnoreDefaultScope

If ($SourceAlias -eq $null) {
 Write-Host -foregroundcolor Red "No Mailbox for SamAccountName $paramRestoredUser found.Script will quit"

$RSG_DB = Get-MailboxDatabase -Server $SourceAlias.ServerName | where {$_.Recovery -like '*true*'}
if ($RSG_DB -eq $null) {
 Write-Host -ForegroundColor Red "No RSG was found on $($SourceAlias.ServerName). Script will quit now!"
 exit }

$TargetAlias = Get-Mailbox e2k7_Restore_MBX
Write-Host -ForegroundColor Green "Input the date of the restored mailbox:"
$RestoreDate = Read-Host

$TargetFolder = "$($SourceAlias)_$RestoreDate"
Restore-Mailbox -Identity $TargetAlias.Alias -RSGMailbox $SourceAlias.ExchangeGuid -RSGDatabase $RSG_DB -TargetFolder $TargetFolder -BadItemLimit 1000 -Verbose -Debug -Confirm -ValidateOnly

echo "-ValidateOnly switch from the command above is removed. Please check no errors occured above and everything is configured properly. Press Enter to continue CTRL+C to cancel the script!"
Restore-Mailbox -Identity $TargetAlias.Alias -RSGMailbox $SourceAlias.ExchangeGuid -RSGDatabase $RSG_DB -TargetFolder $TargetFolder -BadItemLimit 1000 -Verbose -Debug -Confirm

#building an excluded folders list, just in case we did another restore and the previous content was not deleted
$IncludedFolders = $TargetFolder
$ExclFoldersList = @()
$ExcludedFolders = Get-MailboxFolderStatistics $TargetAlias | where-object { $_.FolderPath -notlike "*$($IncludedFolders)*"} | Select-Object FolderPath | foreach-object {
 $ExclFoldersList += $_.FolderPath }
$ExclFoldersList = ([string]::join(",",$ExclFoldersList)).Replace("/","\")

$UserName = $TargetAlias
$PSTPath = "Q:\UserPersonalFolders"
"Excluded folders list: $ExclFoldersList"
export-Mailbox -Identity $UserName.SamAccountName -BadItemLimit 1000 -DeleteContent $True -PSTFolderPath $PSTPath -ExcludeFolders $ExclFoldersList

The details for the Script

First the script gathers data about the restored mailbox, and from that info the Recovery Storage Group ($SourceAlias). It also needs a target mailbox, where the restored data will be stored ($TargetAlias) (needless to say I did in a “forest friendly way, using -Filter cmdlet looking up the given SamAccountName). The data will be moved from the mailbox in the RSG into the $TargetAlias mailbox. It will be placed in a folder, named with the date of the restored data ($RestoreDate). Restoring the data is done with this command:

Restore-Mailbox -Identity $TargetAlias.Alias -RSGMailbox $SourceAlias.ExchangeGuid -RSGDatabase $RSG_DB -TargetFolder $TargetFolder -BadItemLimit 1000 -Verbose -Debug -Confirm -ValidateOnly

Now we want to export the data from the $TargetAlias into a PST file. This is easy to do via the Export-Mailbox cmdlet. The problem appears when you do for instance multiple exports in the same target mailbox and then a mass export, or just have left old data in the target mailbox. You do get data separated into folders, if your $TargetFolder name is unique, but the Export-Mailbox cmdlet cannot export data based on this information (it can only do some filtering (date,content,subject) and I believe it is resource intensive). What Export-Mailbox does have is the “-Excludefolders” parameter which lets you not export certain data.

The trick I came up with was to scan the $TargetAlias mailbox for foldernames (I used Get-MailboxFolderStatistics for that) and build a list of the folders that did not contain the value I entered in the $TargetFolder variable upon restore. Besides that you also have to do some parsing of the output from get mailboxfolderstatistics into something Export-Mailbox understands. This is what I did:

$IncludedFolders = $TargetFolder
$ExclFoldersList = @()
$ExcludedFolders = Get-MailboxFolderStatistics $TargetAlias | where-object { $_.FolderPath -notlike "*$($IncludedFolders)*"} | Select-Object FolderPath | foreach-object {
 $ExclFoldersList += $_.FolderPath }
$ExclFoldersList = ([string]::join(",",$ExclFoldersList)).Replace("/","\")
downtime romtelecom

In the end I just ran the Export-Mailbox on the target mailbox, specifying the excluded files and that was that.

The whole process is pretty easy, the actual restore is done in a one liner (Restore-Mailbox), but as you can see error checking and failproofing the script make up the rest of the work. Hope you enjoy this, next up is how to dismount a RSG, the scripted “clean-way”.

Removing specific message(s) from multiple Exchange 2007 mailboxes

I seem to be doing quite a bit Powershell scripting these days, and some of related to MS Exchange 2007. One issue we had recently was that loose permissions on Distribution Lists with hundreds of users + too much spare time for some users generated a lot of unwanted message traffic. I don’t want to discuss prevention measures like restricting who can send emails to big DLs or using Microsoft AD RMS to restrict what can be done with emails. Our goal here is to clean up the mess ;).

Essentially you can get some info about the message and mailboxes, and use it with Export-mailbox to remove the data. That is how I initially found this link, but what is not written there is that you need to have all the prerequisites for running export-mailbox, and also running it on hundreds of mailboxes may take a while. I decided to do it my way,by building on what I found on that blog.

This is “Mass Remove message(s) from mailboxes – My Way”.Depending on your situation you can apply these steps multiple times:

  1. Identify the message that started it all
  2. Track the message on the Exchange Servers and compile a list of unique recipients of the message.
  3. Remove the message from the offended mailboxes (there may be special requirements to perform the task, see here)

Identify Message

Getting the information should be pretty easy, someone probably forwarded you the copy of the message(s) to be dealt with. You want to get this info as a minimum: subject,sender,date and time message(s) was/were sent. When you have enough info, open the Exchange Management Console > Tools > Message Tracking and from there identify which of the events represent the time the message originally arrived on the Transport Servers. For that event grab the “MessageID” Value. We will use this in the following steps to find all events relating to that specific messageID.

Track Message

Assuming the worst case scenario you have to do tracking across all Exchange Transport Servers, the speed of the process depends on how close to your Exchange Transport Role Servers you are running the tracking. I suggest you make sure this process runs in the same LAN as the Exchange, especially the export-mailbox part. Anyhow, to get all messages sent by “baduser@foo.com” across all transport servers in your Exchange run this:

$TrackingLogResults = get-transportserver | where {$_.Name -like "<optional filter>"} | foreach-object  {Get-MessageTrackingLog -EventId DELIVER -MessageID <MessageID from Step1> -ResultSize Unlimited -server $_}
  • Get-TransportServer gives you all the transport servers in the organization
  • Where clause filters the servers list, you can leave it out, it is helpful if your HUB transport servers are named in a specific way, and you know the message did not leave the organization, so you can exclude a search on the Edge Servers.
  • Foreach-Object cycles through all servers and performs the search
  • Get-MessageTrackingLog searches each transport server tracking log for DELIVER Events that correspond to messages with that specific MessageID. It returns unlimited results. The server that is being searched is piped from the Foreach cmdlet.
  • If you run the last cmdlet without the EventID filter, you will get lots of other EventID’s like fail,send,receive,routing,expand. You just need deliver, DELIVER is important because it basically says “OK, this message passed all of my checks I am now sending it to the Mailbox Server so it can submit it to the mailbox store”, so you get a list of just the actually affected mailboxes.

This may take a while to run. Once it is finished we have to get the list of people that the message was sent to. The easy answer would be “why not just do $TrackingLogsResults | select-object Recipients and pipe it along to something else?”

Well you can do that, but in some cases Recipients means actually a bunch of other addresses, and each recipient may appear multiple times in the entire list.

e.g. – this could be a list returned by the “easy” command




Having duplicates is inefficient, everything will take longer in next steps. What I wanted was to have a list without duplicates, plus I get to show you some more “nice” scripting stuff 😉

Compile Recipients List

I spent quite some time figuring this out, so someone out there better find it useful :). The next step involved a “google shovel” to “dig up” how to break up those objects into one big list. Then the plan was to have a list that just had the unique email addresses – ideally. So here’s the “magic”:

$RecipientsExpanded = @()
$RecipientsExpanded = $TrackingLogResults | foreach-object {$RecipientsExpanded  = $RecipientsExpanded  + ($_.Recipients)}
$RecipientsGrouped = $RecipientsExpanded | group-object
$UniqueRecipients = $RecipientsGrouped | select-object Name | sort-object -property name
  • We created a blank array object that will host all recipients addresses in “expanded form”.
  • For each result from the TrackingLog we added the array ($_.Recipients) to the $RecipientsExpanded array. At the end of this we have a single array with all the addresses, each an individual element in the array.
  • The Group-Object cmdlet is used to group all addresses by their name and in the end you have the list of unique email addresses.

Actually remove offending messages

Please see this link if you are planning to export the messages to PST. What is left to do is to take a page from the MSExchangeTeam blog and run get-mailbox| export-mailbox combo, only we are doing it on a reduced scale, only on the mailboxes that need it, that why I went through all the trouble of making that list!

$MailboxesList = $UniqueRecipients | foreach-object {
      $Filter = "PrimarySmtpAddress -eq '"+$_.Name+"'"
      get-mailbox -ignoredefaultscope -resultsize unlimited -Filter $Filter}

The code above handles this task for forests with child domains. I covered reasoning and use of –Ignoredefaultscope and –Filter in a previous post.

#get current admin UserPrincipalName
$Admin = [Security.Principal.WindowsIdentity]::GetCurrent().Name
#elevating the administrator's account to fulll access over all affected mailboxes
Add-MailboxPermission $MailboxesList -AccessRights FullAccess -User $Admin
export-Mailbox -Identity $MailboxesList –ContentKeywords <enter part of message body> -Recipients <add recipients list> –TargetMailbox admin_ –TargetFolder "RecoveredEmails" –DeleteContent
  • The final step grants the admin user full access over the mailbox. The account being granted that right is $Admin, the account under which the script is running, it contains the UserPrincipalName of that account.
  • You also need to have admin rights on the “TargetMailbox” and the “TargetFolder” should also exist beforehand.
  • We export the offending message(s) using Export-Mailbox. Here it is important to be very careful and make the filtering as strict as possible, since here you cannot remove a message based on the MessageID, so you could end up removing many more messages. Refer to the documentation for export-mailbox, for all available switches for this purpose.

After you run the last command get ready for some really long waiting, as it goes through all the mailboxes. Once it is finished, remove your permissions from those mailboxes.

Remove-MailboxPermission $MailboxesList -AccessRights FullAccess -User $Admin

Phew this was a long post, but validating everything I explained here, took a while. The post is also packed with bits and pieces that can be your building block for other Exchange Shell scripts. I tried to show you how to take Exchange TrackingLog data and build a list of unique recipient addresses that you can use to filter out an unwanted message you tracked in the logs, and do that using export-mailbox commandlet. If you have any feedback/corrections/omissions please feel free to leave a comment.

Happy Scripting!

Get Forwarding Email Address of Forwarded Mailboxes

This second article, let’s start by talking a little about the “-Filter” parameter, used in most Exch Shell cmd-lets that I’ve found myself abusing of these past days. I have supporting role in an Exchange 2007 Migration Project, and we were tasked with “cleaning up” what is left of the Legacy Exchange Servers in terms of mailboxes. The Domain structure contains multiple forests, one of which has quite a few child domains. In “everything is a possibility” domain structures say you may be required to

Task #1 Get a list of all legacy mailboxes that have forwarding addresses enabled or that DO NOT have a forwarding address at all.

and then

Task #2Based on the list from 1, create a report with forwarded email address and the forwarding address.

Task #1Is done via this one liner

$LegacyFWList = get-mailbox -ResultSize unlimited -RecipientTypeDetails LegacyMailbox -IgnoreDefaultScope -Filter {(ForwardingAddress -like '*')} | select-object ForwardingAddress,WindowsEmailAddress,RealFWAddress

Code Breakdown:

get-mailbox -ResultSize unlimited -RecipientTypeDetails LegacyMailbox -IgnoreDefaultScope -Filter {(ForwardingAddress -like '*')} -ResultSize unlimited
  • By default the cmdlet returns only the first 1000 results. I used unlimited but it also accepts a number as input
  • get-mailbox cmdlet extracts only “LegacyMailbox” objects – this means mailboxes on servers pre Exchange 2007.
  • IgnoreDefaultScope performs a forest-wide search. Without this only legacy MBX’s on exchange servers in the forest root domain would have been returned.
  • Using this does have some limitations to it, check the help for what those are.
  • Biggest limitation is that –Identity must now be the a valid DN of the object.
  • I did not want to extract all DN’s in the forest and pipe them to the cmdlet, I looked for an alternative to this.

I discovered the –Filter parameter. It allows you to apply filters to the search on more Object Properties, much better than –Identity which we cannot use because of the –IgnoreDefaultScope.

A great thing about it is that it can be used without any limitations with IgnoreDefaultScope. Full details about how to use –Filter Parameter and what properties of AD objects are filterable can be found here and here. Back to our task, you can see what we did.

-Filter {(ForwardingAddress -like '*')}

We looked for any objects that contained any value in the “ForwardingAddress” Property of the returned object

    But say you want the opposite of this “get all legacy mailboxes that have no forwarding address”…if would stand to reason that this should work:

    -Filter {(ForwardingAddress -notlike '*')}

    You are out of luck, it returns the entire list of legacy recipients. I went through a few filters until I found this:

      -Filter {(ForwardingAddress -ne $null)}


      -Filter {(ForwardingAddress -ne*')}

      I have to say that it was a surprise when I stumbled upon this, as it doesn’t really make sense. TechNet articles say –like, and I assume also –notlike accept wildcards, while –eq does not, you can also assume -ne does not. Well -ne does take wildcards, which I particularly do not mind in this case. I’m not sure if this is a bug, a feature, or just a flaw in my logic somehow and it’s working because it is supposed to.

      Moving on…

        select-object ForwardingAddress,WindowsEmailAddress,RealFWAddress
        • I can save you running a “| get-member” on what the get-mailbox returned previously and tell you that only “ForwardingAddress,WindowsEmailAddress” are properties returned by get-mailbox.
        • RealFWAddress” is something I made up. Why? Well unless you know already ForwardingAddress is not an address actually, it is a collection of properties for the recipient where emails are forwarded, the list contains DN, ObjectGUID, CN and others, not an actual email address.
        • RealFWAddress is a blank property, attached to the $LegacyFWList object, as a placeholder for when we do script a way to get that email address…which is now! read on

        Task #2 – Create a list of forwarded addresses and forwarding addresses.

        Now we come to the second part of my post, the “get-recipientcmdlet. Since there is no way for us to know to what object are the emails forwarded to [it could be a DL, a contact, a mailbox, another legacy user] we have to make our search as broad as possible. If you try to run get-mailbox on a contact object you will get nothing obviously. We are using foreach-object to go through the entire list. ForwardingAddress is actually the CN of the object. We pass the DN of the Property to a cmdlet that will return a primary email address, if any.

        This following code will accomplish just that:

        $LegacyFWList | foreach-object {
         $ForwardingDN = $_.ForwardingAddress.DistinguishedName
         $Filter = "DistinguishedName -like '$ForwardingDN'"
         $_.RealFWAddress = get-recipient -Filter $Filter -IgnoreDefaultScope | select-object PrimarySmtpAddress}
        $LegacyFWList | export-csv "c:\Change\This\Path.csv"

        Code Breakdown:

        $ForwardingDN = $_.ForwardingAddress.DistinguishedName
        $Filter = "DistinguishedName -like '$ForwardingDN'"
        • I used these 2 variables in the code, $DistinguishedName and $Filter, trying to write a one liner resulted in somehow “deformed” filter command that get-recipient would not understand.
        $_.RealFWAddress = get-recipient -Filter $Filter -IgnoreDefaultScope | select-object PrimarySmtpAddress
        • Lastly, RealFWAddress property I introduced above gets populated with PrimarySmtpAddress value.
        $LegacyFWList | export-csv "c:\Change\This\Path.csv"
        • In the end all that’s left is export the $LegacyFWList variable to CSV and process however you wish, or just store it in xml / pass it on to another part of a script.

        I have to admit the last part of the code is less ideal. Why? Well, the export does not output a clean forwarding email address. I have tried various ways to fix it, but in the end I concluded that it took me 30 minutes of trying to get a nicer export, and 15 seconds to fix it after importing the csv in excel.

        UPDATE 1: 26.01.2010 – added proper way to filter people with no forwarding address
        UPDATE 1: 21.02.2010 – added syntax highlighting and small rewrites