PowerShell: Merge mailbox folders


Note: I have removed the script download from this page, as maintaining it in two places seems unnecessary.  The new home for this script and any updates is here: https://code.msdn.microsoft.com/office/PowerShell-Merge-mailbox-e769c529.

 

A couple of years ago I wrote a script to move items from one folder to another in a user's mailbox.  This seems to be one of the most commented posts on this blog, and we also regularly get requests for this kind of functionality.  I haven't had a chance to respond to many of the comments of the previous post, however, I have recently rewritten this script with several enhancements.

To answer a couple of questions that do seem to crop up regularly... This script will not work to merge folders in different mailboxes.  While technically possible, updating the script to support merges between mailboxes would take some time, and we haven't had much call for this as yet.  The script probably won't work with public folder mailboxes, though it could be updated to do so and this shouldn't be too complex (whether I get a chance to test this is another matter, though).

So, moving on to what the script does do...

The only required parameter for the script is MergeFolderList, which is the hash table listing the folders to be merged.  The easiest way to use the script when processing multiple folders is to define the hash table first of all.  For example:

$merge = @{
    "Inbox" = "Inbox2";
    "Calendar" = @("Kalendar", "Agenda");
}

As you can see, the hash table is a standard PowerShell hash table.  The key (the value on the left) is the primary folder (the folder into which all the items will be moved), while the value (on the right) is the secondary folder (all items will be moved out of this).  The value can be an array if you want to combine more than one folder - in the above example, all items from the folders Kalendar and Agenda will be moved into the Calendar folder.  Once you've set the $merge variable, you can call the script:

.\Merge-MailboxFolder.ps1 -MergeFolderList $merge

The above will process the current user's mailbox.

By default, the script won't delete the secondary folder once it has moved the items.  If you want to delete it, then just add the -delete parameter.  You can also create the target folder if it doesn't exist by using the -CreateTargetFolder parameter.  As with previous scripts, you can also call it from an Exchange PowerShell console and use Get-Mailbox to process lots of mailboxes:

$m = Get-Mailbox
$m | ForEach-Object {
     .\Merge-MailboxFolder.ps1 –SourceMailbox $_.PrimarySmtpAddress -MergeFolderList $merge –Delete –Impersonate
}

You'll need application impersonation permissions for the above (and it assumes that you are running the Exchange PowerShell console using the account that has been granted the impersonation permissions).

Script parameters:

 -SourceMailbox

 The source mailbox to be processed (if missing, current user's mailbox is assumed, though this will only work in a domain environment).  If no target mailbox is specified, then the merge takes place within this mailbox.

 -TargetMailbox  The target mailbox - messages are moved/copied to this mailbox.
 -MergeFolderList  Specifies the folder(s) to be merged.
 -SearchFilter  If specified, only items that match the given AQS filter will be moved (see http://msdn.microsoft.com/EN-US/library/dn579420).
 -ByFolderId  When specified, the folders in MergeFolderList are identified by EwsId (not path).
 -ByEntryId  When specified, the folders in MergeFolderList are identified by EntryId (not path).
 -ProcessSubfolders  When specified, subfolders will also be processed.
 -CombineSubfolders  When specified, items in subfolders will all be moved to specified target folder (hierarchy will NOT be maintained).
 -CreateTargetFolder  When specified, if the target folder doesn't exist, then it will be created (if possible).
 -SourceArchive  When specified, the source mailbox archive is accessed (instead of the main mailbox).
 -TargetArchive  When specified, the target mailbox archive is accessed.
 -Delete  When specified, the source folder will be deleted after the merge (will only work if the source folder is empty).
 -Copy  When specified, items are copied (instead of moved) between folders.
 -Credentials  Credentials used to authenticate with EWS.
 -Username  Username used to authenticate with EWS.
 -Password  Password used to authenticate with EWS.
 -Domain  Domain used to authenticate with EWS.
 -Impersonate  Whether we are using impersonation to access the mailbox.
 -EwsUrl  EWS Url (if omitted, then autodiscover is used).
 -EwsManagedApiPath  Path to managed API (if omitted, a search of standard paths is performed).
 -IgnoreSSLCertificate  Whether to ignore any SSL errors (e.g. invalid certificate) - use with care.
 -AllowInsecureRedirection  Whether to allow insecure redirects when performing autodiscover.
 -LogFile  Log file - activity is logged to this file if specified.
Comments (45)

  1. Peter says:

    Why do you increment $Offset? Keep it at 0 (as you move emails, next iteration then begins from the remaining emails) and no collection into arrays need to be done.

  2. It is much more efficient to send the move requests in batches, you will notice huge performance increases when processing folders with large numbers of items.

  3. Emma says:

    Thanks for posting such a useful script – Do you know if it's possible to script this for Exchange Online? I can't find any resources on it and yours is the most useful by far but from what I can see only for on-premise Exchange.. Many thanks.

  4. This script works for all Exchange versions from 2010 (it will work with 2007 with a minor update, also).  This includes Exchange Online.  For Office 365, you'll need to specify your credentials (either use -Credential parameter, or -Username and -Password).

  5. Sorry, ignore the -Credentials parameter – I haven't added it to this script yet!  Use -Username and -Password (where username is your Office 365 email address).

  6. I've just updated the script so that it does now support -Credentials parameter (use with Get-Credential cmdlet).  Couple of other minor improvements also.

  7. DGoossens says:

    Hi David,

    Thanks a lot for the script. It does indeed run faster than the previous one.

    Will you add the functionality to move content from a primary mailbox into an archive mailbox?

  8. Emma says:

    Thank you for providing an update so quickly.  I'm an absolute PowerShell novice so forgive me as these are extremely basic questions:

    – When I run the script it doesn't prompt for o365 credentials – it fails with 'failed to open message store', so I'm guessing it's looking locally first?

    – If I am going to define a hash table, where do I place it?

    – If you want to run this against another mailbox where should this be specified?

  9. Conrad Schissler says:

    I was going to leave an inquiry about squashing everything into the same single target folder, but then I looked at the script documentation and saw -CombineSubfolders and -ProcessSubfolders, which completely answer my question.

    Absolutely brilliant script – saved my skin (or several dozen hours at least) for sure!

  10. Eyal Solomon says:

    Very Good!

    im also need the option to move content from the online archive into the primary mailbox….

    can you help?

  11. Jon M says:

    Is there anyway to use this script or similar to copy items from a public folder on Office 365 into individual mailboxes?

  12. Blen says:

    Hey guys,

    Thanks for the script, it is really amazing!

    Just wanted to ask if the script provides the ability to merge data from Archive mailbox to a Primary mailbox?

    Thanks

  13. Currently this script can only move messages within a single mailbox.  I will see if I can add support to move between mailboxes (which would include between archive and primary, and public folders and standard mailbox).  If you need this functionality quickly and are able to raise a case through Microsoft support channels, then I'd be able to look at it sooner (otherwise it depends entirely on caseload, which is currently quite high).

  14. Shawn Walsh says:

    will this move items from one public folder mailbox folder to another public folder in the same mailbox ? is anything different ? we need to do that today as a matter of fact as well as moving between two different public folder mailboxes. We can and will open a o365 case today.

  15. This script should move from one public folder to another, though I haven't tried it.  Please note that Office 365 cases are not eligible for developer support, so we would not be able to assist there.  The same goes for cases raised under Software Assurance.  Premier and Professional cases (e.g. partner accounts) can get developer support.  Other developer support is provided via the forums.

  16. ShawnWalsh says:

    well David, I have an o365 case for this that's been open for about a month. hopefully you would be willing to provide some assistance. 🙂

    the SR is: 615022490918533.

    we simply wish to copy a public folder from one PF MBX to another, which happens to be the autosplit mailbox.

    any advice would be greatly appreciated.

  17. OscarRomero says:

    It seems this script will do what I need. icrosoft Exchange Web Services Managed API 2.0.

    However, after running the following;

    $merge = @{

       "Inbox" = "Archive"

    }

    .Merge-MailboxFolder.ps1 -Mailbox historical@comain.com -MergeFolderList $merge -Delete -ProcessSubfolders -CombineSubfolders

    The script goes in to a loop displaying error;

    You cannot call a method on a null-valued expression.

    At F:MigScriptMerge-MailboxFolder.ps1:422 char:44

    +         if ($script:folderCache.ContainsKey <<<< ($parentFolder.ParentFolderId.UniqueId))

       + CategoryInfo          : InvalidOperation: (ContainsKey:String) [], RuntimeException

       + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.

    At F:MigScriptMerge-MailboxFolder.ps1:422 char:44

    +         if ($script:folderCache.ContainsKey <<<< ($parentFolder.ParentFolderId.UniqueId))

       + CategoryInfo          : InvalidOperation: (ContainsKey:String) [], RuntimeException

       + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.

    At F:MigScriptMerge-MailboxFolder.ps1:422 char:44

    +         if ($script:folderCache.ContainsKey <<<< ($parentFolder.ParentFolderId.UniqueId))

       + CategoryInfo          : InvalidOperation: (ContainsKey:String) [], RuntimeException

       + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.

    At F:MigScriptMerge-MailboxFolder.ps1:422 char:44

    +         if ($script:folderCache.ContainsKey <<<< ($parentFolder.ParentFolderId.UniqueId))

       + CategoryInfo          : InvalidOperation: (ContainsKey:String) [], RuntimeException

       + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.

    At F:MigScriptMerge-MailboxFolder.ps1:422 char:44

    +         if ($script:folderCache.ContainsKey <<<< ($parentFolder.ParentFolderId.UniqueId))

       + CategoryInfo          : InvalidOperation: (ContainsKey:String) [], RuntimeException

       + FullyQualifiedErrorId : InvokeMethodOnNull

    Any help would be much appreciated.

  18. Cannot find type [System.Collections.Generic.Dictionary``2[System.String,System.Object]]: says:

    Thank you for the script. It looks like will do what we need it to do.

    I am getting the following error. any help would be much much appreciated.

    [PS] F:MigScript>.1.ps1

    Name                           Value

    —-                           —–

    Inbox                          Archive

    Using managed API 15.00.0516.014 found at: C:Program FilesMicrosoftExchangeWeb Services2.0Microsoft.Exchange.WebServices.dll

    Processing mailbox historical@prosep.com

    New-Object : Cannot find type [System.Collections.Generic.Dictionary“2[System.String,System.Object]]: make sure the assembly containing this type is loaded.

    At F:MigScriptMerge-MailboxFolder.ps1:413 char:41

    +         $script:folderCache = New-Object <<<<  'System.Collections.Generic.Dictionary“2[System.String,System.Object]'

       + CategoryInfo          : InvalidType: (:) [New-Object], PSArgumentException

       + FullyQualifiedErrorId : TypeNotFound,Microsoft.PowerShell.Commands.NewObjectCommand

    You cannot call a method on a null-valued expression.

    At F:MigScriptMerge-MailboxFolder.ps1:422 char:44

    +         if ($script:folderCache.ContainsKey <<<< ($parentFolder.ParentFolderId.UniqueId))

       + CategoryInfo          : InvalidOperation: (ContainsKey:String) [], RuntimeException

       + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.

    At F:MigScriptMerge-MailboxFolder.ps1:422 char:44

    +         if ($script:folderCache.ContainsKey <<<< ($parentFolder.ParentFolderId.UniqueId))

       + CategoryInfo          : InvalidOperation: (ContainsKey:String) [], RuntimeException

       + FullyQualifiedErrorId : InvokeMethodOnNull

    You cannot call a method on a null-valued expression.

    At F:MigScriptMerge-MailboxFolder.ps1:422 char:44

    +         if ($script:folderCache.ContainsKey <<<< ($parentFolder.ParentFolderId.UniqueId))

       + CategoryInfo          : InvalidOperation: (ContainsKey:String) [], RuntimeException

       + FullyQualifiedErrorId : InvokeMethodOnNull

  19. andrew lindzon says:

    I keep getting failed to open message store, I used -Mailbox with my office365 email address. -Username with the same, and -Password with my password.  I have tried them with and without quotes, Am I missing a parameter?

  20. Andrew Lindzon says:

    I figured out my issue with failed to open message store.  I made the following changes.

    1) -Mailbox name with no quotes

    2) -Username and -Password use quotes

    3) added  -AllowInsecureRedirection  (appears to be needed for connections to office365)

    I also had an error with this line

    $script:folderCache = New-Object 'System.Collections.Generic.Dictionary“2[System.String,System.Object]'

    so I changed it to

    $script:folderCache = New-Object 'System.Collections.Generic.Dictionary[string,object]'

    I have a new error

    Exception calling "Bind" with "3" argument(s): "Value cannot be null.

    Parameter name: folderId"

    At C:usersalindzondownloadsMerge-MailboxFolder.ps1:418 char:71

    +     $parentFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind <<<< ($global:service, $Folder.Id, $propset)

       + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException

       + FullyQualifiedErrorId : DotNetMethodException

    Moving from Top of Information StoreOutbox to

    9 items found; attempting to move

    Exception calling "MoveItems" with "3" argument(s): "Value cannot be null.

    Parameter name: DestinationFolderId"

    At C:usersalindzondownloadsMerge-MailboxFolder.ps1:266 char:31

    +             [void]$service.MoveItems <<<< ( $moveIds, $TargetFolderObject.Id, $false )

       + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException

       + FullyQualifiedErrorId : DotNetMethodException

  21. Andrew Lindzon says:

    Found the bug that was causing the error.

    MoveItems($SourceFolderObject, $TargetFolderObject, '', '')

    should read

    MoveItems($SourceFolderObject, $TargetFolderObject)

    then it works.

    But for some reason, no matter how many items there are, it only moves 250 at a time

  22. Andrew Lindzon says:

    Found out why I am only getting 250 items, it is because I am trying to move items in the online archive with a filter.  Read this article.  Perhaps you can add this parameter as an alternative or convert from aqs to it.

    social.technet.microsoft.com/…/exchange-web-services-not-working-as-aspected-finditems

  23. Tracy says:

    Hi.  Thanks for the script.   Any idea what "Failed to open message store" error would be about?  I've tried with and without impersonation, and have tested my impersonation rights in EWSEDITOR and that is successful.  

  24. I've just updated the script so that it works in Exchange 2010 Management Shell (previously it didn't, as I tend to develop in standard PowerShell sessions).  This should resolve most of the errors that have been reported.

  25. Lawrence Dwight says:

    Would this script allow one to merge the archive mailbox back into the main mailbox? Including keeping the folder structure?

  26. 3vian says:

    I can't seem to get past the bind – keep getting (401) Unauthorized when connecting to Office 365. I've ensure EWS is enabled and also used EWSEditor without issue. Could someone please provide a basic example of the required steps? Thanks

  27. 401 is unauthorised, and means exactly that – your authentication information is invalid.  For Office 365, you need to specify username as your Office 365 email address, and also your password.  Domain should be blank.

  28. 3vian says:

    David – Thanks for the quick reply.

    So should the below work?

    $merge = @{"Inbox_To File" = "Inbox_To File2";}

    . .Merge-MailboxFolder.ps1 -Mailbox username@domain.com -MergeFolderList $merge -ProcessSubfolders -CreateTargetFolder -Username "username@domain.com" -Password "password" -EwsUrl outlook.office365.com/…/Exchange.asmx

    The credentials I'm using work without issue in EWSEditor and just accessing the EWS URL with IE, but in the script it has issues on the bind.

    I've posted a question on the main Exchange Server Development forum as I tried getting to the bind step by step.

    social.msdn.microsoft.com/…/ews-via-powershell-401-unauthorized

    I checked that EWS is enabled using the Exchange Online cmdlet Set-CASMailbox username@domain.com EwsEnabled $true, but still nothing.

    Having the same problem across 2 tenants. The users are global admins on both.

    Thanks for the help.

  29. 3vian says:

    I think I've figured it out. Something to do with the setup of the credentials. Probably something to do with secure strings or similar. If I use the below it seems to work. I'll try modifying your script slightly and see how it goes.

    Import-Module -Name "C:Program FilesMicrosoftExchangeWeb Services2.2Microsoft.Exchange.WebServices.dll"

    $cred = Get-Credential

    $exchService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService

    $exchService.Credentials = New-Object System.Net.NetworkCredential -ArgumentList $cred.UserName, $cred.Password, $cred.Domain

    $exchService.URL = New-Object Uri("outlook.office365.com/…/Exchange.asmx")

    $exchService.TraceEnabled = $True

    $mailbox = "username@domain.com"

    $mbx = New-Object Microsoft.Exchange.WebServices.Data.Mailbox($mailbox)

    $folderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $mbx )

    $MailboxRoot = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService, $folderId)

  30. For Office 365, the EWS URL must be:

    -ewsurl "outlook.office365.com/…/Exchange.asmx"

    (i.e. specify the complete URL).  Missing the https could potentially cause issues (though I haven't tried it).

    The script also supports use of credentials supplied using Get-Credential – just use the -Credentials parameter instead of -Username and -Password.

  31. 3vian says:

    I change line 476 from

    $exchangeService.Credentials = $Credentials.GetNetworkCredential()

    to

    $exchangeService.Credentials = New-Object System.Net.NetworkCredential -ArgumentList $Credentials.UserName, $Credentials.Password, $Credentials.Domain

    and it seems to work now when using Get-Credential using the -Credentials parameter.

    FYI – The whole url was supplied – just the automatic formatting.

  32. Morpheus says:

    i try to merge subfolders and get an error?

    Using managed API 15.00.0913.015 / Exchange 2013 (Ver. 15.0 Build 1076.9)

    Method invocation failed because [System.String] does not contain a method named 'FindFolders'.

    At C:test1PS_Exchange_MergerTEstMerge-MailboxFolder.ps1:298 char:9

    +                     $FindFolderResults = $TargetFolderObject.FindFolders($Filter, $FolderVie …

    +                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

       + CategoryInfo          : InvalidOperation: (:) [], RuntimeException

       + FullyQualifiedErrorId : MethodNotFound

    Moving from Top of Information StoreInboxIT-MailsOrdner1Holger to

    2 items found; attempting to move

    Holger could not be deleted as it is not empty.

    Moving from Top of Information StoreInboxIT-MailsOrdner1Peter to

    3 items found; attempting to move

    i think the script can not execute

    $FindFolderResults = $TargetFolderObject.FindFolders($Filter, $FolderView)

    any help?

  33. KursunM says:

    If I try to launch this script from local or server side (Exchange Online).

    I get the following error msg: "failed to open message store"

    is after executing script with Ps: ".Merge-MailboxFolder.ps1 –Mailbox e-mail -MergeFolderList $merge"

  34. RadHaz75 says:

    I am using the script to move all messages from an Exchange Online Archive Mailbox back into the Exchange Online main Mailbox, using impersonation. On an approximately 5GB Archive, after about 1GB I started getting Exception calling "FindItems" with "1" argument(s): "The server cannot service this request right now. Try again later." which looks like I am being throttled. I thought impersonation should help prevent this. Any suggestions?

  35. Impersonation doesn't prevent throttling, it just means that the throttling limits are applied to the mailbox being accessed as opposed to the authenticating account.  The script won't handle throttling, currently, I'll look into adding this.  However, you should be able to leave it a while (for the throttling limits to be reset) and then run it again – it should pick up where if left off (so long as you are moving and not copying).

  36. EricACC says:

    Hello David,

    Trying to run this against Exchange 2007 SP3. I modified the script on line 596:

    $exchangeService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP1)

    So that it allows Exchange2007_SP1 to attempt to connect. However, the method MoveItems is only supported by Exchange 2010_SP1 and higher. You mentioned earlier that it was likely/possible to use this with 2007 with some small changes – can you suggest how I might go about doing that? I'm not a PS master, just trying to clear up a horrible user account, and this is rather my last resort.

  37. For Exchange 2007, the item moves can't be batched.  The script would need to be modified to send a single move request for each item (which would be a lot slower, but it should then work).  Also, I don't think moves between mailboxes will work in Exchange 2007.

  38. Chris says:

    Is the EntryID the same value you get when running Get-MailboxFolderStatistics on a folder? (The FolderID value?)   If so, I can't get the script to work when using those, and including the parameter, -byEntryID.

    I have a unique issue where I have some users that have duplicated contact folders, same exact name.   I figured I could harness the value from Get-MailboxFolderStatistics..  

  39. Chris says:

    I think I figured it out.. well not that there was anything probably wrong with this script, but I just modified it to work for me by changing this line:

    $id.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::EWSId

    to

    $id.Format = [Microsoft.Exchange.WebServices.Data.IdFormat]::OwaId

    I guess a more elegant approach is to add an addition function and parameter to accept this FolderIDs from the Get-MailboxFolderStatistics cmdlet

  40. Lab MS says:

    Hi,

    This is a fantastic script and almost does everything I need.

    Is there a way to specify a folder to use/create when a matching folder is not found when merging from archive to primary mailbox?

    Eg. InboxTest exists in the archive mailbox but does not in the users primary so I want to create InboxUnsortedTest.

    I'm ok with powershell but I just can't see where its doing the check in your script so if you could assist that would be great!

  41. Steve C. says:

    Is there way to move emails that arrived prior to a particular date or within a time frame?  I suspect that SearchFilter could be used for this, but the link to the right that explains this parameter is cut off.

    Thank you.

  42. AQS will allow you to do this.  If you use the link at the top of the page to go to the code, you can find the link to the AQS documentation there.

  43. Steve C. says:

    Thanks David.

    The AQS search worked but I ran into the same 250 limit problem that another poster had.  I couldn't get the SearchFilter to work in place of AQS so I just stuck with AQS and looped it.  It wasn't pretty but I was able to get the results I needed.

  44. Ivars says:

    Hey David,

    I get:

    "Failed to open source message store" what could be the reason?

    Thanks in advance.

    1. Royce Lithgo says:

      I also get failed to open source message store error.

Skip to main content