Exchange 2016: Recovering a Single Mailbox from an EDB Backup Using a Recovery Database

The Problem

A shared mailbox used by several departments turned up almost empty. The owner opened it in OWA and found two messages where there used to be months of correspondence. Nobody admitted to deleting anything, and the deleted item retention window had already passed.

The plan was standard:

  1. Find which mailbox database hosts the mailbox.
  2. Pull a copy of that database from backup.
  3. Bring the copy to a clean state and mount it as a Recovery Database (RDB).
  4. Restore the single mailbox into a subfolder of the live mailbox.

This guide is the exact sequence I used, including the parts where things broke: a mount failure on a DAG member, a hard repair that killed a database copy, and a cmdlet that refuses to talk to an RDB. It also covers a five minute check that tells you whether the restore is worth running at all.

All commands apply to Exchange 2016 on premises. Server names and identifiers are sanitized.


Step 0: Locate the Database and Work on a Copy

First, find out where the mailbox lives:

Get-Mailbox "shared01" | fl Database, ExchangeGuid

Then restore the database files from backup into a separate working folder, for example D:\Recovery\. You need the .edb file and the transaction logs that came with it.

Do not point anything at your only copy of the backup. Some of the tools below modify the file in place, and one of them can leave it unusable if it fails halfway. More on that later.


Step 1: Read the Database Header

The Exchange binaries folder is not in PATH, so a bare eseutil call fails with CommandNotFoundException. Use the full path or cd into the bin folder:

& "C:\Program Files\Microsoft\Exchange Server\V15\Bin\eseutil.exe" /mh "D:\Recovery\mbx01.edb"

The two lines that matter in the output:

State: Dirty Shutdown
Log Required: 821466-821475 (0xc88da-0xc88e3)

Dirty Shutdown means the database was copied while transactions were still open. This is normal for a VSS snapshot. Before Exchange will mount it, the missing transactions must be replayed from the logs.

Log Required tells you exactly which log files you need. The hex range maps directly to file names. In my case the log prefix was E14 (check the first three characters of your log file names, the default is E00), so the range 0xc88da-0xc88e3 means:

E14000C88DA.log ... E14000C88E3.log

Confirm they exist in the working folder:

ls D:\Recovery\E14000C88D*.log, D:\Recovery\E14000C88E*.log

Step 2: Replay the Logs (Soft Recovery)

eseutil /r E14 /l "D:\Recovery" /d "D:\Recovery" /i
  • /r E14 is the log prefix
  • /l points to the folder with the logs
  • /d points to the folder with the database
  • /i tells eseutil to ignore mismatched attachment info, which you want when the files came from a backup

On a 183 GB database with ten logs to replay this took under five seconds. Verify the result:

eseutil /mh "D:\Recovery\mbx01.edb"
State: Clean Shutdown
Log Required: 0-0 (0x0-0x0)

Clean Shutdown means the database is ready to mount.


Step 3: Create the Recovery Database

New-MailboxDatabase -Recovery -Name "RecoveryDB" `
  -Server "MAIL1" `
  -EdbFilePath "D:\Recovery\mbx01.edb" `
  -LogFolderPath "D:\Recovery"

Exchange answers with a warning you should not skip:

WARNING: Please restart the Microsoft Exchange Information Store service
on server MAIL1 after adding new mailbox databases.

Step 4: The DAG Gotcha

I skipped the restart and went straight to Mount-Database. On a DAG member that fails like this:

Mount-Database : Failed to mount database "RecoveryDB".
Error: An Active Manager operation failed.
Error: Couldn't find the specified mailbox database with GUID '...'

Active Manager simply does not know the new database object yet. The fix is the restart Exchange asked for:

Restart-Service MSExchangeIS

Plan for this. Restarting the Information Store interrupts mailbox access on that node for a minute or two, and on a DAG it can trigger failover of active copies. Do it in a maintenance window or on the least loaded member.

After the restart:

Mount-Database "RecoveryDB"

No output means success.


Step 5: Look Inside the RDB

Get-MailboxStatistics -Database "RecoveryDB" |
  Select DisplayName, MailboxGuid, ItemCount, TotalItemSize |
  Sort DisplayName

Find your mailbox in the list and note its MailboxGuid. You will use the GUID for everything from here on, because display names with quotes or non Latin characters cause problems in later cmdlets.


Step 6: The Five Minute Check That Saves Hours

Before launching a restore, compare the mailbox in the RDB against the live one.

From the backup:

Get-MailboxStatistics -Database "RecoveryDB" |
  Where {$_.MailboxGuid -eq "<GUID>"} |
  Select DisplayName, ItemCount, TotalItemSize, LastLogonTime
ItemCount      : 191
TotalItemSize  : 521.5 KB (533,966 bytes)

From the live server:

Get-MailboxStatistics -Identity "shared01@domain.com" |
  Select DisplayName, ItemCount, TotalItemSize, LastLogonTime
ItemCount      : 191
TotalItemSize  : 522.7 KB (535,226 bytes)

Stop and read those numbers. The backup and the live mailbox are practically identical. That means the backup was taken after the deletion. Restoring it would return exactly what the user already has.

One command, and you just saved yourself a restore job, a confused user, and an afternoon.

Keep in mind that ItemCount covers every folder in the mailbox: calendar, contacts, sync issues, the lot. A mailbox that looks empty in OWA can still report a couple hundred items. What matters is the difference between backup and live, not the absolute number.


Step 7: Check Recoverable Items on the Live Mailbox

While you are at it, rule out the dumpster:

Get-MailboxFolderStatistics -Identity "shared01@domain.com" -FolderScope RecoverableItems |
  Select FolderPath, ItemsInFolder, FolderSize
FolderPath         ItemsInFolder FolderSize
----------         ------------- ----------
/Recoverable Items             0 0 B (0 bytes)
/Deletions                     0 0 B (0 bytes)
/Purges                        0 0 B (0 bytes)
/Versions                      0 0 B (0 bytes)

All zeros. The items went through the dumpster and out the other side before anyone noticed. The only hope left is an older backup.


When the Older Backup Has Missing Logs

I pulled an older copy of the same database. This time soft recovery failed:

Operation terminated with error -528
(JET_errMissingLogFile, Current log file missing)

The log chain restored from backup had gaps. Eseutil suggests lossy recovery for this:

eseutil /r E14 /l "D:\Recovery2" /d "D:\Recovery2" /i /a

/a accepts the loss of whatever was in the missing logs. In my case even that failed with the same -528, which left hard repair as the last option:

eseutil /p "D:\Recovery2\mbx01.edb"

And here is the war story. The repair ran for a few minutes, got to rebuilding system tables, and died:

Rebuilding MSysObjectsShadow from MSysObjects.
Operation terminated with error -1605
(JET_errKeyDuplicate, Illegal duplicate key)

An integrity check after that returned Database is CORRUPTED, and the header dump showed a fresh DB signature created that morning with Last Objid: 1. Translation: the half finished repair had gutted the file. 183 GB of backup copy, gone. The original backup was fine, but the copy had to be pulled again.

Lessons from this detour:

  • /p modifies the database in place. If pulling the file from backup again is expensive, make a second copy before running it.
  • A failed /p does not roll back. The file you fed it may be dead afterwards.
  • If /p succeeds, run eseutil /d (offline defrag) on the result before mounting, and treat the database as suspect. Hard repair drops whatever pages it cannot reconcile.

Get-MailboxFolderStatistics Does Not Work Against an RDB

With a third, month old copy mounted as the RDB, I wanted to peek at the Sent Items folder before restoring anything. Turns out you cannot do that directly.

Get-MailboxFolderStatistics has no -Database parameter. The documented "RecoveryDB\DisplayName" identity format failed too:

Failure: Couldn't find 'RecoveryDB\...' as a recipient.

The cmdlet resolves identities through Active Directory recipients, not through the RDB content, and a display name full of Cyrillic and quotation marks does not help. The GUID variant failed the same way.

The practical workaround: restore into a test mailbox and look with your own eyes.

New-MailboxRestoreRequest `
  -SourceDatabase "RecoveryDB" `
  -SourceStoreMailbox "<GUID>" `
  -TargetMailbox "admin.test@domain.com" `
  -TargetRootFolder "RDB_Inspect" `
  -AllowLegacyDNMismatch

Monitor it:

Get-MailboxRestoreRequest | Get-MailboxRestoreRequestStatistics |
  Select TargetAlias, Status, PercentComplete, ItemsTransferred

When it completes, the full folder tree from the backup appears under RDB_Inspect in the test mailbox. Open it in OWA and you see exactly what the backup holds. If the data is there, run the same command again with the real target mailbox and a folder name like Restored_from_backup. The restored items keep their original folder structure and do not mix with current mail.

SourceStoreMailbox accepts a display name, a LegacyDN, or a mailbox GUID. With non Latin display names, use the GUID.


Cleanup

When the restore requests are done:

Get-MailboxRestoreRequest | Remove-MailboxRestoreRequest
Dismount-Database "RecoveryDB"
Remove-MailboxDatabase "RecoveryDB" -Confirm:$false

Remove-MailboxDatabase only deletes the configuration object. The .edb file stays on disk for you to archive or delete manually.


The Verdict in This Case

The month old backup showed 192 items and 574.6 KB against 191 items and 522.7 KB on the live server. Sent Items was empty in every copy I checked, and Recoverable Items was empty on the live side. The mail had been deleted before the oldest backup in rotation was taken. Nothing to restore.

That is a valid outcome. The point of this workflow is not just restoring data, it is reaching a definitive answer fast. The ItemCount comparison in Step 6 is what turns a day of restore jobs into an hour of header checks.


Prevention

Two changes came out of this incident.

First, single item recovery and a longer dumpster window on shared mailboxes, so a purge no longer means instant data loss:

Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited |
  Set-Mailbox -SingleItemRecoveryEnabled $true -RetainDeletedItemsFor 30

Second, mailbox audit on every shared mailbox where more than one person has write access:

Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited |
  Set-Mailbox -AuditEnabled $true `
    -AuditDelegate HardDelete, MoveToDeletedItems, Move, SendAs, SendOnBehalf, Create, Update `
    -AuditAdmin HardDelete, MoveToDeletedItems, Move, SendAs, SendOnBehalf, Create, Update

Check coverage:

Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited |
  Group-Object AuditEnabled |
  Select Name, Count

And when something disappears next time, the log will name the account that did it:

Search-MailboxAuditLog -Identity "shared01@domain.com" `
  -LogonTypes Delegate, Admin `
  -StartDate (Get-Date).AddDays(-30) -EndDate (Get-Date) `
  -ShowDetails |
  Select Operation, LogonUserDisplayName, ClientIPAddress, LastAccessed |
  Sort LastAccessed -Descending

Takeaways

  • eseutil /mh first, always. The header tells you the shutdown state and the exact logs you need.
  • On a DAG, restart the Information Store after New-MailboxDatabase -Recovery, or the mount will fail with an Active Manager error.
  • Compare ItemCount and TotalItemSize between the RDB and the live mailbox before restoring. Matching numbers mean the backup post dates the deletion.
  • eseutil /p is a one way door. Run it on a copy you can afford to lose.
  • You cannot browse folders inside an RDB with Get-MailboxFolderStatistics. Restore into a test mailbox instead.
  • Audit logging and single item recovery cost nothing to enable and turn the next incident from forensics into a lookup.

Leave a Reply

Your email address will not be published. Required fields are marked *