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.
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
Great post and script. I do have a question. How about distribution groups with “Linked Mailboxes”? These accounts are technically disabled accounts and your VB script does not export those.
True, the query can be modified in the script. But before Prepare-MoveRequest.ps1 will work the mailbox account must be enabled also; don’t think there is a switch or something for disabled accounts.
Can you remove the check that is concerned with if no value is found/populated in mailNickname attribute, to allow the script to keep running. In fact if any attribute is not populated, can you make it so it continues and does not halt the process. Also, if an account is disabled or not (enabled), can you just report the status in the exported CSV instead (userAccountControl attribute I think), so disabled accounts are not skipped, but rather recorded in the output? Would be great for cross forest migrations I am involved in currently. Much appreciated if this is possible.
Yes I could, but the migration cmdlets don’t support disabled accounts. So that’s why I didn’t include them in the results.
Basic scripting skills are really required for Exchange admins today, so I suggesting doing the research and trying to figure it out for yourself in a lab.
Just wanted to say thanks, I’m busy doing a 2003 to 2010 cross forest migration this weekend and realised my group X500 addressing was missing quite late in the game. Used my own PS1 to import into 2010 but used your .vbs to export from 2003 🙂
I never knew public folders also used X500 addressing, do you have any export scripts for these and contacts from 2003?
Glad I could help and I have those export scripts for contacts & Public Folders addresses:
LikeLiked by 1 person
The izzy.org links to the scripts above and the export-groups.vbs script are no longer working. Do you have an updated location available?
Thanks for letting me know about that broken link. Changed hosting things around a week ago and missed by /Scripts link as being broken.
I tried to download the scripts but unfortunately the provided links don’t work again 😦
Could you please check the link or provide me an alternative one?
Thank you very much in advance!
Hi Jason. Love your script, has helped me in my Cross-Forest migration immensely.
Links still seems to be broken though.
the scripts can be found by modifiying (shortening) the URL
just want to thank you a lot for sharing the hours of work behind those scripts. your group Address Script helps a lot. had to rewrite some minor things for german language Exchange Srv error messages. now it looks nice.
Hi Jason, Great article and script. Kindly share the updated links to download your script.