I’ve been doing Exchange cross forest migrations now for about 15 years, since Exchange 5.0! With each major version of Exchange the procedures would changed a bit and the scripts would be updated or recreated as needed. Without 3rd party tools there are many steps that are not covered by the built-in cmdlet or scripts provided by Microsoft.
Microsoft include the Prepare-MoveRequest.ps1 script, starting with Exchange 2010 RTM, which is used to create Mail Enabled Users (MEU) in the target Exchange 2010/2013 environment. The script will create the MEU, copy the SMTP and x500 addresses (include the LegacyExchangeDN), GAL attributes, and a few others attributes. Then New-MoveRequest can be used to migrate the mailbox from Exchange 2003, 2007, 2010, or 2013 in another forest to Exchange 2010/2013. New-MoveRequest will create a mailbox for the user, replace the MEU with a Mailbox enabled user, and replace the Mailbox enabled user in the source environment with a Mail Enabled User. So the Prepare-MoveRequest script and the New-MoveRequest are all that’s normally needed to migrate mailboxes across forest.
But this does address Groups, Contacts, or Public Folders. All of which have properties, mainly SMTP & x500 addresses, that must be migrated also to ensure correct mail-flow and prevent NDRs when users reply to existing messages, meetings, or when using the Nickname cache.
So I’m going to start a series of blog post that will cover the key scripts I used, and created, to handle the other objects. I have several other scripts that I use to find and address many issues that come up when doing a cross forest migration and probably won’t go into all of these.
Export-Group.vbs – Yes this is a VBScript, but I wanted one script that will work when migrating from Exchange 2003, 2007, 2010, and 2013. So I created this script, actually I had a couple of other scripts I used to use before this one but this one has superseded them now.
This script will export SMTP, x500 (including LegacyExchangeDN), and group membership to two CSV files. It assumes the groups have already been created, which can be done via ADMT or via LDIFDE (which I’ve found quicker and easier than using ADMT in smaller environments). It does not export disabled members, since Prepare-MoveRequest doesn’t support disabled mailboxes either, but this can be easily changed.
It will generate two files: GroupAddressess.csv & GroupMembers.csv. GroupAddressess.csv will have one each line per SMTP or x500 address, in the format of: mailNickName, samAccountName, proxyAddress. GroupMembers.csv will have one line per member in the format of mailNickName, samAccountName, member mailnickname, and member primary e-mail address. I include samAccountName in the export file but don’t actually use it since some groups may not be security enabled or members by be other non-security groups or contacts.
Source: http://izzy.org/scripts/Exchange/Migration/Export-Groups.vbs (This is where the latest version will always be posted)
Related Import scripts, to be covered in another blog post very soon:
' ' This script will export the SMTP, x500, & LegancyExchangeDN address and members of all groups with e-mail addressess to GroupSMTP.csv & GroupMembers.csv ' Output file can be used to import members by http://izzy.org/scripts/Exchange/Migration/Import-GroupAddresses.ps1 ' Output file can be used to addresses by http://izzy.org/scripts/Exchange/Migration/Import-GroupMembers.ps1 ' ' Created 3/1/2012 by Jason Sherry (firstname.lastname@example.org) http://jasonsherry.org ' Source: http://izzy.org/scripts/Exchange/Migration/Export-Groups.vbs ' Last Updated: 8/29/2013 ' 05/09/2012 Added legacyExchangeDN export support and fixed SMTP case issue ' 08/28/2013 Added support to export members (disabled users skipped), logging to file for WARNING & ERROR events ' 08/29/2013 Added x500 export support Option Explicit Dim strDomainDN, strBase, strFilter, strAttrs, strScope, rootDSE Dim objCmd, objConn, objRS, objLogFile Dim proxyaddresses, proxyaddress Dim objAddressFile, objfs, GroupAddressFile, GroupMemberFile, objMemberFile Dim name, mailNickName, samAccountName, legacyExchangeDN, adspath Dim member, members, objMember, membermailNickname, membermail, membername, memberSamAccountName, userAccountControl Const ADS_UF_ACCOUNTDISABLE = 2 GroupAddressFile= "GroupAddressess.csv" GroupMemberFile= "GroupMembers.csv" Set rootDSE = GetObject("LDAP://rootDSE") 'strDomainDN = "dc=company,dc=local" #Required if the script needs to get data from a domain other than the one it is being run in strDomainDN = rootDSE.Get("defaultNamingContext") strBase = " strFilter = "(&(mailNickName=*)(proxyAddresses=*)(objectCategory=group));" strAttrs = "name,adspath,mailNickName,proxyaddresses,legacyExchangeDN,samAccountName,member;" strScope = "subtree" Set objfs = CreateObject ("Scripting.FileSystemObject") Set objAddressFile = objfs.CreateTextFile (GroupAddressFile, True) Set objMemberFile = objfs.CreateTextFile (GroupMemberFile, True) Set objLogFile = objfs.CreateTextFile ("Export-Groups.log", True) Set objConn = CreateObject ("ADODB.Connection") objConn.Provider = "ADsDSOObject" objConn.Open "Active Directory Provider" Set objCmd = CreateObject ("ADODB.Command") objCmd.ActiveConnection = objConn objCmd.CommandText = strBase & strFilter & strAttrs & strScope objCmd.Properties ("Page Size") = 5000 Set objRS = objCmd.Execute objRS.MoveFirst objAddressFile.WriteLine "mailNickName,samAccountName,ProxyAddress" objMemberFile.WriteLine "mailNickName,samAccountName,membermailNickname,membermail" while Not objRS.EOF 'Wscript.Echo "name = " & objRS.Fields(0).Value name = objRS.Fields(0).Value adspath = objRS.Fields(1).Value mailNickName = objRS.Fields(2).Value proxyaddresses = objRS.Fields(3) legacyExchangeDN = objRS.Fields(4).Value samAccountName = objRS.Fields(5).Value members = objRS.Fields(6) WScript.Echo "Processing group: [" & samAccountName & "]" WScript.Echo " Path: " & adspath WScript.Echo " SMTP, x500, & legacyExchangeDN addresses:" ' Get SMTP & x500 addresseses For Each proxyaddress in proxyaddresses If UCase(Left(proxyaddress,4)) = "SMTP" Then WScript.Echo " Address: " & Replace(proxyaddress,"smtp:","",1,-1,1) objAddressFile.WriteLine mailNickName & "," & samAccountName & "," & Replace(proxyaddress,"smtp:","",1,-1,1) End If If UCase(Left(proxyaddress,4)) = "x500" Then WScript.Echo " Address: " & Replace(proxyaddress,"x500:","",1,-1,1) objAddressFile.WriteLine mailNickName & "," & samAccountName & "," & Replace(proxyaddress,"x500:","",1,-1,1) End If Next ' Get legacyExchangeDN ' WScript.Echo vbtab & vbtab & "Address: " & legacyExchangeDN objAddressFile.WriteLine mailNickName & "," & samAccountName & "," & legacyExchangeDN ' Get Group Members If TypeName(members) = "Variant()" Then WScript.Echo " Members:" For Each member in members Set objMember = GetObject("LDAP://" & member) membermailNickname = objMember.mailNickname membermail = objMember.mail membername = objMember.name memberSamAccountName = objMember.samAccountName If memberSamAccountName <> "" Then userAccountControl = objMember.userAccountControl If userAccountControl and ADS_UF_ACCOUNTDISABLE Then WScript.Echo " User: [" & memberSamAccountName & "] is disabled, this user will not be exported" objLogFile.WriteLine "WARNING: User: [" & memberSamAccountName & "] is disabled, this user will not be exported" membermail = "" End If End If If membermail <> "" Then If membermailNickname = "" Then WScript.Echo WScript.Echo "**** Unexpected data found ****" WScript.Echo "Empty field for : [" & Replace(membername,"CN=","") & "] mailNickName: [" & membermailNickname & "] mail: [" & membermail & "]" objLogFile.WriteLine "ERROR: Empty field for : [" & Replace(membername,"CN=","") & "] mailNickName: [" & membermailNickname & "] mail: [" & membermail & "]" Else WScript.Echo " Name: [" & Replace(membername,"CN=","") & "] mailNickName: [" & membermailNickname & "] mail: [" & membermail & "]" objMemberFile.WriteLine mailNickName & "," & samAccountName & "," & membermailNickname & "," & membermail End If End If Next End If objRS.MoveNext Wend objAddressFile.Close