Cross-Forest Mailbox Move (2)

Note: This is part 2; part 1 can be found here.

After the post on experiences regarding Cross-Forest Mailbox Move, the problems with the “sample” Powershell script and the script created in good ol’ VB, I got lots of requests to publish the script. After thinking this over, I made it ready for publishing. That means stripping excessive code and changing domain names etc. to a more descriptive labels.

The script does require some explanation:

  • The script uses 1 input file (users.txt) and produces 1 output file (output.log) (included below);
  • The reason for sending output to screen (optional) as well as file is to be able to check it properly (e.g. using notepad) when running it for a set of users;
  • users.txt contains a single line with the source and target account names. This is the same file we used for ADMT imput. Reason for having a source as well as a new account name is that in the migration process account are renamed. ADMT can do this for you, but the script will need both the old and the new name name to connect to the objects and copy/set several attributes;
  • It’s VB, had been kept simple and didn’t went through a code beautifier. That means no full function headers, input/output descriptions or extensive error handling;
  • Modify the constants using information from your environment, e.g. source/target domain, servers, LegacyExchangeDN etc.
  • The script uses fixed servers. This is to make sure we’re talking to the same server(s) as ADMT and to prevent replication issues because of lag;
  • Use the script at your own risk. I cannot accept any responsibility for consequences when using this in your production environment;
  • Use it in a lab environment first; test, test, test!

Users.txt

SourceName,TargetSAM
jtest,jtest

CrossForestMovePrep.vbs

'*--------------------------------------------------------------------------------
'* Name     : PrepareForestMove
'* Created By      : Michel de Rooij
'* E-mail    : michel@eightwone.com
'* Date            : 20100217
'* Version    : 0.22
'*--------------------------------------------------------------------------------
'* Changes:
'* 0.21 Initial version
'* 0.22 Made changes to address single-value proxyAddress attributes
'*--------------------------------------------------------------------------------

Option Explicit

Const strUserfile            = "users.txt"
Const strOutputFileName            = "output.log"
Const DEBUGOUTPUT            = 1

Const conSourceServer               = "dc.olddomain.nl"
Const conSourceDomain                = "olddomain.nl"
Const contargetServer                = "dc.newdomain.com"
Const conTargetDomain                = "newdomain.com"

Const conLegacyExchangeDN            = "/o=NEWEXORG/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn="
Const conTargetEmailDomain            = "target.com"

' AD putex cmds
Const ADS_PROPERTY_CLEAR = 1
Const ADS_PROPERTY_UPDATE = 2
Const ADS_PROPERTY_APPEND = 3
Const ADS_PROPERTY_DELETE = 4

' FileSystem
Const ForWriting =2
Const ForReading =1

'*********************************************************
' MAIN
'*********************************************************

Dim oFSO, strFile, objFile, hOutputFileHandle, bProcessLine, strLine
Dim arrUsers, strUser, strCmd, strNewUser

Set oFSO = CreateObject("Scripting.FileSystemObject")
Set hOutputFileHandle= oFSO.OpenTextFile( strOutputFileName, ForWriting, True)

debug("Start")

strFile= strUserFile

if NOT oFSO.fileExists( strFile) then
 die( "Input file "& strFile& " does not exist")
end if

debug("Reading names from "& strFile)
set objFile= oFSO.OpenTextFile( strFile, ForReading, True)

while not objFile.atEndOfStream

 bProcessLine= True
 strLine= objFile.readLine

 if isEmpty(strLine) then
 bProcessLine= False
 Else
 If left(strLine, 1)= ";" Then
 bProcessLine= False
 Else
 If inStr( strLine, ",") > 0 Then
 ' Line OK
 arrUsers= split( strLine, ",")
 strUser= arrUsers(0)
 strNewUser= arrUsers(1)

 If strUser= "SourceName" Then
 ' Input file header, skip        
 bProcessLine= False
 End If
 Else
 bProcessLine= False
 debug("** INFO: Skipping line "& strLine)
 End If    
 End If
 End If

 If bProcessLine Then

 debug(strNewUser& ": Syncing Exchange Attributes from "& struser)
 syncAttributes strUser, conSourceServer, conSourceDomain, strNewUser, conTargetServer, conTargetDomain

 End If
Wend

debug("Finished")

objFile.Close
hOutputFileHandle.Close  

set hOutputFileHandle= Nothing
set objFile= Nothing
set oFSO= Nothing

wscript.quit(0)

'*******************************************************************
' Purpose: output to screen when DEBUGOUTPUT is 1, always to file
'*******************************************************************
Function debug(strMsg)
 hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& strMsg & chr(13)& chr(10))
 if DEBUGOUTPUT=1 then
 wscript.echo strMsg
 end if
End Function

'*********************************************************
' Purpose: terminate with message
'*********************************************************
Function die(strMsg)
 wscript.echo strMsg
 wscript.quit (1)
End Function

'*********************************************************
' displayString
' Returns string from varType item/elements
'*********************************************************
Function displayString( varObj)
 Dim tmp, item
 tmp= ""
 select case VarType( varObj)

 Case vbEmpty
 tmp= "(Empty)"
 Case vbNull
 tmp= "(Null)"
 Case vbInteger, vbLong, vbSingle, vbDouble, vbByte, vbDecimal, vbCurrency, vbDate, vbBoolean
 tmp= cStr( varObj)
 Case vbString
 tmp= varObj
 Case vbObject
 tmp= "(Object)"
 Case vbvariant
 tmp= "(Variant)"
 Case 8209
 tmp= "("& OctetToHexStr( varObj)& ")"
 Case vbArray, 8204
 For each item in varObj
 If tmp="" Then
 tmp= tmp+ item
 Else
 tmp= tmp+ ", "+item
 End If
 Next
 tmp= "["& tmp& "]"
 Case Else

 End Select

 displaystring= tmp& " #"& varType( varObj)

End Function

'*********************************************************
' OctetToHexStr
' Convert OctetString (byte array) to Hex string.
'*********************************************************
Function OctetToHexStr (arrbytOctet)
 Dim k
 OctetToHexStr = ""
 For k = 1 To Lenb (arrbytOctet)
 OctetToHexStr = OctetToHexStr & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2)
 Next
End Function

'*********************************************************
' syncAttributes
' migrates attributes from source to target
'*********************************************************
Function syncAttributes (strUser, SourceServer, SourceDomain, strNewUser, TargetServer, TargetDomain)

 dim strDNSource, strDNTarget, objSource, objTarget, n, strMail
 strDNSource= getDN( struser, SourceServer, SourceDomain, "")
 strDNTarget= getDN( strNewuser, TargetServer, TargetDomain, "")
 If strDNSource<>"" AND strDNTarget <> "" Then
 set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
 set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)

 copyAttribute "mail", objSource, objTarget, False
 copyAttribute "mailNickname", objSource, objTarget, False
 copyAttribute "msExchMailboxGuid", objSource, objTarget, False
 setAttribute "targetaddress", objSource.get( "mail"), objTarget
 copyAttribute "proxyAddresses", objSource, objTarget, True
 addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget

 strMail= objSource.get( "mail")
 n= instr( strMail, "@")
 debug( strMail)
 addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget

 setAttribute "msExchRecipientDisplayType", -2147483642, objTarget
 setAttribute "msExchRecipientTypeDetails", 128, objTarget
 setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget

 objTarget.setInfo

 Else
 debug("*** ERR: Cannot retrieve DNs for Source or Target")
 syncAttributes= False
 End If
End Function

'*********************************************************
' getDN
' Retrieves the DN for a user object
'*********************************************************
Function getDN( struser, strServer, strDomain, strOU)
 dim objConn, objCmd, strQuery, objRS, strAttr, strRDNLDAP, strDNSLDAP
 strRDNLDAP= RDN2LDAPPATH( strOU)
 strDNSLDAP= DNSDomain2LDAPPath( strDomain)
 strAttr= "distinguishedName"
 set objConn= createObject( "ADODB.Connection")
 set objCmd= createObject( "ADODB.Command")
 objConn.Provider= "ADsDSOObject"
 objConn.Open "ADs provider"
 objCmd.ActiveConnection= objConn
 strQuery= "<LDAP://"& strServer
 If strServer <> "" Then
 strQuery= strQuery& "/"
 End If
 strQuery= strQuery& strRDNLDAP
 If strOU <> "" Then
 strQuery= strQuery& ","
 End If
 strQuery= strQuery& strDNSLDAP& ">"
 strQuery= strQuery+ ";(&(objectCategory=person)(objectClass=user)(SAMAccountName="& strUser&"));"& strAttr& ";subtree"
 objCmd.CommandText = strQuery
 on error resume next
 set objRS= objCmd.execute
 if err.number <> 0 Then
 debug( "*** ERR: Error "& err.number& " executing ["& strQuery& "]")
 getDN= ""
 Else
 on error goto 0
 Select Case objRS.recordCount
 Case 0
 debug( "*** ERR: User object "& struser& " not found")
 getDN= ""
 Case 1
 getDN= objRS.Fields( strAttr)
 'debug( getDN)
 Case Else
 debug("*** ERR: Ambigious user object "& struser)
 getDN= ""
 End Select
 End If
 set objRS= Nothing
 set objCmd= Nothing
 set objConn= Nothing
End Function

'*********************************************************
' DNSDomain2LDAPPath( str)
' Makes an LDAP notation for a DNS domain name
' e.g. corp.local => DC=corp,DC=local
'*********************************************************
Function DNSDomain2LDAPPath( str)
 Dim tmp1, tmp2, tmp3
 tmp1= split( str, ".")
 tmp2= ""
 For each tmp3 in tmp1
 If tmp2<>""  then
 tmp2= tmp2& ","
 End If
 tmp2= tmp2& "dc="& tmp3
 Next
 DNSDomain2LDAPPath= tmp2
End Function

'*********************************************************
' RDN2LDAPPath( str)
' Makes an LDAP notation for a Relative Distinguished Name
' e.g. Domain Accounts/3rd party => OU=3rd party,OU=Domain Accounts
'*********************************************************
Function RDN2LDAPPath( str)
 Dim tmp1, tmp2, tmp3
 tmp1= split( str, "/")
 tmp2= ""
 For each tmp3 in tmp1
 If tmp2<>"" then
 tmp2= ","& tmp2
 End If
 tmp2= "ou="& tmp3& tmp2
 Next
 RDN2LDAPPath= tmp2
End Function

'*********************************************************
' copyAttribute
' Copies attribute(s) to target
'*********************************************************
Function copyAttribute( strAttribute, objSource, objTarget, boolMulti)
dim boolUpdate, varItem
If isEmpty( objSource.get( strAttribute)) Then
  debug( strAttribute& " not set, clearing")
  objTarget.PutEx ADS_PROPERTY_CLEAR, strAttribute, 0
Else
  varItem= objSource.get( strAttribute)
  If boolMulti Then
    if isArray( varItem) Then
      debug( "Setting "& strAttribute& " to multi-value "& displayString( varItem))
      objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, varItem
    Else
      debug( "Setting "& strAttribute& " to single-value "& displayString( varItem))
      objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, array( varItem)
    End If
  Else
    debug( "Setting "& strAttribute& " to "& displayString( varItem))
    objTarget.Put strAttribute, varItem
  End If
End If
on error goto 0
objTarget.SetInfo
End Function

'*********************************************************
' setAttribute
' Sets attribute to target
'*********************************************************
Function setAttribute( strAttribute, strValue, objTarget)
 debug("Setting "& strAttribute& " to "& displayString(strValue))
 objTarget.Put strAttribute, strValue
 objTarget.SetInfo
End Function

'*********************************************************
' addAttribute
' Adds attribute to target
'*********************************************************
Function addAttribute( strAttribute, varAttribute, objTarget)
 dim boolUpdate, tmp
 boolUpdate= True
 If isEmpty( varAttribute) Then
 ' not set, skipping
 Else
 'on error resume next
 If isEmpty( objTarget.get( strAttribute)) Then
 boolUpdate= True
 Else
 If isArray( objTarget.get( strAttribute)) Then
 For each tmp in objTarget.get( strAttribute)
 If tmp = varAttribute Then
 boolUpdate= False
 End If
 Next
 Else
 boolUpdate= varAttribute= objTarget.get( strAttribute)
 End If
 End If
 on error goto 0
 If boolUpdate Then
 debug("Adding "& varAttribute& " to "& strAttribute)
 objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, array( varAttribute)
 Else
 debug( varAttribute& " already in "& strAttribute)
 End If
 End If
 objTarget.SetInfo
End Function

Output.log (sample)

[16:24] Start
[16:24] Reading names from users.txt
[16:24] jtest: Syncing Exchange Attributes from jtest
[16:24] Setting mail to jtest@source.nl #8
[16:24] Setting mailNickname to jtest #8
[16:24] Setting msExchMailboxGuid to (70C2360FB0330346A925172CA0473B9F) #8209
[16:24] Setting targetaddress to jtest@target.com #8
[16:24] Setting proxyAddresses to [SMTP:jtest@source.nl, X400:c=US;a= ;p=DemoOrg;o=Exchange;s=Old;g=Mr;] #8204
[16:24] Adding X500:/o=DemoOrg/ou=First Administrative Group/cn=Recipients/cn=jtest to proxyAddresses
[16:24] jtest@source.nl.nl
[16:24] Adding smtp:jtest@target.com to proxyAddresses
[16:24] Setting msExchRecipientDisplayType to -2147483642 #3
[16:24] Setting msExchRecipientTypeDetails to 128 #2
[16:24] Setting legacyExchangeDN to /o=NEWORG/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=Mr Old #8
[16:24] Finished

82 thoughts on “Cross-Forest Mailbox Move (2)

  1. Pingback: Cross-Forest Mailbox Move « EighTwOne (821)

  2. Now i can copy user and sync attribute with script and move a test mailbox (Smal Size) successfully.

    I do have problem moving Big mailboxes in production, i get bellow message

    Error: An Error occured while updating a user object after the move operation -> The operation coudn’t be performed because object ‘sdcdom.local/User/Alison Durnford’ coudn’t be found on ‘sdc-ad01.sdcdom.local’

    my target domain is sdcdom.local
    my target Active Directory is sdc-ex01.sdcdom.local
    and my user is Alison Durnford

    Any help is much appreciated.

    Regards,

    Like

  3. Running this script just works 🙂 – however there are differences whether the ADMT has taken all attributes or I just excluded the Exchange and takes msExchMailboxGUID, msExchArchiveGUID, msExchArchiveName. It works when just having the three attributes above, but when taking all attributes it says” Database is mandatory on usermailbox” and “You must use the remotetargetdatabase for remote push move request” – any experience on this ?

    Like

    • @PeterD: If you read the first post, you probably noticed we had schema mismatches causing ADMT to only sync a basic set of attributes. This script is to turn the target user object into a mail-enabled user with the proper attributes (linking it to the mailbox in the source forest), after which new-moverequest can do its job. With identical schemas, sync of all attributes will probably cause the target object to become a mailbox user because you’re syncing the attribute identifying the object type (msExchRecipientDisplayType) as well.

      Regarding the “Database is mandatory on usermailbox”, is the mailbox you are trying to move corrupt and it the mailbox specified in AD (with the account) still valid (i.e. no decommissioned mailbox server)? Can you check with get-mailbox | fl

      Regarding the “You must use the remotetargetdatabase for remote push move request”, is the target user already associated with a mailbox (or has one)? The mailbox pull request will only work on target users without a mailbox.
      (See also part 1 and the related technet article http://technet.microsoft.com/en-us/library/ee633491.aspx)

      Like

  4. Could I ask how you got the users in forest A to browse their mailboxes in forest B. At the moment I’m going down the outlook Anywhere path. Any thoughts on this ? Thanks

    Like

    • In our case the resource/account forest is temporary so we’re using a custom .prf which can reconfigure (default) outlook profiles. We’re migrating from Exchange 2003 so autodiscover is not an option. In our case external users connect through VPN so haven’t examined RPC/HTTPS to Anywhere. But if that was the case, properly configure autodiscover and outlookanywhere in Exchange 2010. See http://technet.microsoft.com/en-us/magazine/ff381470.aspx

      Like

  5. Hi

    This script is exactly what i need to complete my migration and as everybody else i too couldnt get the MS ps1 script to work. So thank you for your great work:)
    But as PeterD i also get the “you must use the…” error:
    You must use the RemoteTargetDatabase parameter for remote push move requests.
    + CategoryInfo : InvalidArgument: (admttest12:MailboxOrMailUserIdParameter) [New-MoveRequest
    ], RecipientTaskException
    + FullyQualifiedErrorId : 13BF919D,Microsoft.Exchange.Management.RecipientTasks.NewMoveRequest

    Like

  6. Yes i did, but i properly didnt understand it correctly:)
    You are properly right about syncronising all attributes are causing exchange to believe that the user allready have an mailbox. Even though the user object have just been created.
    Doing what Peter also did, only include the msExchMailboxGUID attribute in ADMT, helped me to complete the new-moverequest.
    Again thank you for your time and for the great work:)

    A bit off topic: Have any of you an idea about what transfering speeds i cant expect with the moverequest? I just comleted moving a very small mailbox (10-20MB) and it took about 3 minutes. And have any of you experienced that a moverequest fails?

    Like

    • Welcome. The script follows TechNet article ee633491 and copies/configures the mandatory attributes (see part 1), msExchMailboxGuid included. After that, the target object is a mail-enabled user, a valid target for move requests.

      Speed depends, lots of variables here (source and target hardware, network speed). Options are things like disabling anti-virus software (@target) and enabling circular logging during bulk transfers but this depends on your situation and requirements (i.e. don’t do this when users are actively working on the target mailbox server).

      Personally I haven’t experienced any move request failures so far.

      Like

  7. i tried following:
    – migrated the user with admt
    – enable users mailbox
    – used your script to set attributes
    – new move request error:

    You must use the RemoteTargetDatabase parameter for remote push move requests.
    + CategoryInfo : InvalidArgument: (MSCXA1:MailboxOrMailUserIdParameter) [New-MoveRequest], RecipientTaskException
    + FullyQualifiedErrorId : 10101896,Microsoft.Exchange.Management.RecipientTasks.NewMoveRequest

    for some reasons it worked with 2 testusers before with the same process. i saw, when using your script the mailbox changed from user mailbox to mail user. that ok?

    Like

    • Mailbox changed from user mailbox to mail user?
      1. The script changes user object in AD, it does nothing with mailboxes;
      2. The script’s purpose is to transform a user object (NO mail attributes) into a mail enabled user, so it can be used as a move request target.

      The procedure we use (but it is specific, your mileage may vary) is as follows:
      – forest A, mailbox-enabled user
      – Create a clone in forest B using ADMT – only basic attributes, we have schema mismatch (see part 1). So I’d suggest you exclude all Exchange attributes
      – use the script to prepare the clone in forest B
      – execute move requests

      Like

  8. its not allowed to enable the mailbox before your script. without its working.

    from another forum: http://social.technet.microsoft.com/Forums/en/exchange2010/thread/fa8c9c24-4c2b-4012-9c1f-9028b474ae00

    ….This problem occurs when the target user is already associated with a mailbox. Often this is because the account is created through the EMC.

    thanks for you great work. lets see if MS upgrades the Prepare-MoveRequest.ps1 so it could run after the user is migrated with ADMT. I think most of the AD User Migrations need to migrate with ADMT because of the SID History.

    Like

  9. Did found out what was wrong with “all attributes” enabled in ADMT. You should take all attributes except “MSExchHomeServerName”. If you migrates all users via ADMT WITH all attributes, then run a script that “clear” the “MSExchHomeServerName” attribute. The “MSExchHomeServerName” property links to the old server and therefor it does not work. Great works with this script.

    Like

  10. iam sorry for the mega double posts, but the system didnt show me my post, so i thought theres something wrong. Maybe someone could delete the double stuff.

    Like

    • No problem, I found several comments in the moderation queue. Akismet (antispam) is a bit strict when users post (too much) links .. should be ok now (for you).

      Like

  11. Hello Michel,

    fantastic job. Thank you very much for publishing this. You earned a crate of beer when you’ll ever visit Braunschweig, Germany 🙂

    Keep Exchanging!

    Like

  12. additional info for the Prepare-MoveRequest.ps1 script. You CAN migrate the users with ADMT, but if the Prepare-MoveRequest.ps1 creates a new user that means, there a some attributes missing. So the script cant match the attributes and instead creates a new user.

    cheers,
    Marco

    Like

  13. Hi Marco,

    I was the one with the thread about “preparemoverequest.ps1” in social technet Exchange 2010. Your script is great, but I am looking for a solution for this scenario:

    Old AD Domain with Exchange 2003/2007. New AD Domain with Exchange 2010. I want to setup mailboxes in the new domain which are from type Linked mailbox, to move then the mailboxes from the old AD domain to the new AD Domain and Exchange 2010 Organization. I’ll not move any user accounts from old to new and I’ll not use ADMT.

    tia

    Bernd

    Like

    • I’d convert the linked mailbox to an mail-enabled user, i.e. remove associated external account etc., and then initiate the script. Or modify the script to do that for you.

      Like

  14. Hi,

    Have you moved any public folders cross-org using interorg to the exchange 2010 setup ? I can get top level folders replicating after creating them but the sub folders do not automatically get created and replicated like how interorg interacts with Exchange 2007. Any ideas on this ?

    Like

  15. Dear mdrooji,

    when there is only one smtp address configured for a user in the source domain, it is not adding the x.500 in the target domain for the user. the log file says the x.500 already in proxy addresses.

    when we added a dummy address in the source domain to a test user account, the script worked perfectly.

    Like

    • That’s odd, I made it work for object with a single smtp address (and tested it).
      (see 1st part of blog, 02/17/2010 at 12:10 am | #18 )

      Like

  16. We added objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, Array(varAttribute) again in the else condition and it worked

    If boolUpdate Then
    debug (“Adding ” & varAttribute & ” to ” & strAttribute)
    objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, Array(varAttribute)
    Else
    debug (varAttribute & ” already in ” & strAttribute)
    objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, Array(varAttribute)
    End If

    Like

    • No time to test that unfortunately. But if I recall correctly, the lookup algorithm for Exchange 2007 is different but that shouldn’t pose a problem as those attributes are cloned as well.

      Like

  17. this script is great to prepare the users on the target domain! thank you

    now, does anyone have a good script to move the actual mailboxes?

    Like

  18. Pingback: Some 2010 Statistics « EighTwOne (821)

  19. Hi,
    We did not use ADMT since our users already exist in the new forest
    didnt setup trust between the forests
    tried the prepare-moverequest… modified so that it dont create the user
    when using the new-moverequest… getting the guid error (cannot find the recipient…)
    wanted to try your script
    not expert on VB… how can we pass the credential to the remote forest ?
    TIA,
    François

    Like

  20. Dear mdrooji,

    I got this error when run New-MoveRequest

    “Error: The destination Active Directory forest isn’t up to date, which prevents the move from proceeding. Verify that Active Directory replication is working”

    But my AD running properly and now error on replication.
    Are you have any clue?
    Thankyou before

    Yanuar

    Like

  21. Pingback: Exchange 2010 Cross Forest Migration: The case of the missing User Account Attributes « More Coffee Anyone?

  22. Odd problem, when I run the script it works and the mailbox is moved. Mail flows and I can use OWA to access the mailbox. Outlook will not open the mailbox however. This happens everywhere for every user I move. It does not for new users/mb.

    Microsoft Outlook
    Cannot display the folder. The attempt to log on to Microsoft Exchange has failed.
    P1: 300032
    P2: 14.0.4763.1000
    P3: mqhw
    P4: 0x8004011D

    Like

  23. looks like i figured it out. i was using domain.hq instead of the external email address domain.com. Once I did this in the script and the new move cmdlet the migration works. i must have changed my notes somewhere.

    Like

  24. In my environment your script crashed on line 185:

    C:\PrepareForestMove.vbs(185, 2) (null): 0x80005000

    This is the function syncAttributes… it line 185 was:

    set objSource= getObject( “LDAP://”& SourceServer& “/”& strDNSource)

    After some investigation using your debug function I discovered that my user’s common name had a / in it. I added the following code to deal with it:

    If instr(strDNSource,”/”) 0 or instr(strDNSource,”\”) 0 or instr(strDNTarget,”/”) 0 or instr(strDNTarget,”\”) 0 Then
    Exit Function
    End If

    Here is the entire function with my modified code:

    ‘*********************************************************
    ‘ syncAttributes
    ‘ migrates attributes from source to target
    ‘*********************************************************
    Function syncAttributes (strUser, SourceServer, SourceDomain, strNewUser, TargetServer, TargetDomain)

    dim strDNSource, strDNTarget, objSource, objTarget, n, strMail
    strDNSource= getDN( struser, SourceServer, SourceDomain, “”)
    strDNTarget= getDN( strNewuser, TargetServer, TargetDomain, “”)
    If strDNSource”” AND strDNTarget “” Then
    If instr(strDNSource,”/”) 0 or instr(strDNSource,”\”) 0 or instr(strDNTarget,”/”) 0 or instr(strDNTarget,”\”) 0 Then
    Exit Function
    End If
    set objSource= getObject( “LDAP://”& SourceServer& “/”& strDNSource)
    set objTarget= getObject( “LDAP://”& TargetServer& “/”& strDNTarget)

    copyAttribute “mail”, objSource, objTarget, False
    copyAttribute “mailNickname”, objSource, objTarget, False
    copyAttribute “msExchMailboxGuid”, objSource, objTarget, False
    setAttribute “targetaddress”, objSource.get( “mail”), objTarget
    copyAttribute “proxyAddresses”, objSource, objTarget, True
    addAttribute “proxyAddresses”, “X500:”& objSource.get( “LegacyExchangeDN”), objTarget

    strMail= objSource.get( “mail”)
    n= instr( strMail, “@”)
    debug( strMail)
    addAttribute “proxyAddresses”, “smtp:”& left( strMail, n-1)& “@”& conTargetEMailDomain, objTarget

    setAttribute “msExchRecipientDisplayType”, -2147483642, objTarget
    setAttribute “msExchRecipientTypeDetails”, 128, objTarget
    setAttribute “legacyExchangeDN”, conLegacyExchangeDN& objSource.get(“cn”), objTarget

    objTarget.setInfo

    Else
    debug(“*** ERR: Cannot retrieve DNs for Source or Target”)
    syncAttributes= False
    End If
    End Function

    Great script mate – Keep up the good work 🙂

    Like

  25. I created a new version that also mail enables destination groups, enters the object in the global address list in the destination forest and has additional bug fixes.

    Mail_Users.csv

    SourceName,TargetSAM
    jtest,jtest

    Mail_Groups.csv

    SourceName,TargetSAM
    testgroup,testgroup

    CrossForestMovePrep.vbs

    '*--------------------------------------------------------------------------------
    '* Name     : PrepareForestMove
    '* Created By      : Michel de Rooij
    '* Modified By	   : Clint Boessen
    '* E-mail    : michel@eightwone.com, clint.boessen@4logic.com.au
    '* Date            : 20111125
    '* Version    : 0.3
    '*--------------------------------------------------------------------------------
    '* Changes:
    '* 0.21 Initial version
    '* 0.22 Made changes to address single-value proxyAddress attributes
    '* 0.3	Modifications by Clint Boessen to include:
    '			- Resolved a problem which caused  if the distinguished names have / characters
    '			- Resolved a bug with the addAttribute function.  If a multivalued attribute in AD 
    '			  only has one value then IsArray will not detect the item as an array.  For example
    '			  if proxyAddresses in Active Directory only has one address the function failed
    '			  and simply did not add any additional proxyAddresses from the source forest.
    '			- Provided support to add the Active Directory objects to to the Global Address List
    '			  in the destination forest.  The original script by Michel did not populate the
    '			  destination global address list.
    '			- Provided support to synchronise mail enabled attributes for distribution groups
    '			  to the destination forest for groups migrated by ADMT.  This means the
    '			  distribution groups will appear in the destination Global Addres List.
    '			- Provided support to either disable or enable the processing of Users or Groups during
    '			  script run.
    '			- Provided additional smarts to script error checking.
    '*--------------------------------------------------------------------------------
    
    
    Option Explicit
    
    'Set to False if you do not wish to process
    Const ProcessUsers = True
    Const ProcessGroups = True
    
    Const strUserfile            = "Mail_Users.csv"
    Const strGroupfile			= "Mail_Groups.csv"
    Const strOutputFileName            = "output.log"
    Const DEBUGOUTPUT            = 1
    
    Const conSourceServer               = "dc01.sourcedomain.local"
    Const conSourceDomain                = "sourcedomain.local"
    Const contargetServer                = "dc01.destinationdomain.local"
    Const conTargetDomain                = "destinationdomain.local"
    
    Const conLegacyExchangeDN            = "/o=Exchange Org/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn="
    Const conTargetEmailDomain            = "contoso.com"
    
    Const conDestinationGAL			= "CN=Default Global Address List,CN=All Global Address Lists,CN=Address Lists Container,CN=COS,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=destinationdomain,DC=local"
    
    ' AD putex cmds
    Const ADS_PROPERTY_CLEAR = 1
    Const ADS_PROPERTY_UPDATE = 2
    Const ADS_PROPERTY_APPEND = 3
    Const ADS_PROPERTY_DELETE = 4
    
    ' FileSystem
    Const ForWriting =2
    Const ForReading =1
    
    '*********************************************************
    ' MAIN
    '*********************************************************
    
    Dim oFSO, objFile, hOutputFileHandle, bProcessLine, strLine
    Dim arrayUsersOrGroups, strUserOrGroup, strCmd, strNewUserOrGroup, strGroups, strNewGroup
    
    Set oFSO = CreateObject("Scripting.FileSystemObject")
    Set hOutputFileHandle= oFSO.OpenTextFile( strOutputFileName, ForWriting, True)
    
    If ProcessUsers Then
    ProcessEntries(strUserfile)
    End If
    
    If ProcessGroups Then
    ProcessEntries(strGroupfile)
    End If
    
    objFile.Close
    hOutputFileHandle.Close  
    
    set hOutputFileHandle= Nothing
    set objFile= Nothing
    set oFSO= Nothing
    
    wscript.quit(0)
    
    '*******************************************************************
    ' Query through the users or groups input file
    '*******************************************************************
    Function ProcessEntries(strFile)
    'put in the user or group file name in as a function variable.
    
    debug("Start")
    
    if NOT oFSO.fileExists( strFile) then
     die( "Input file "& strFile& " does not exist")
    end if
    
    debug("Reading names from "& strFile)
    set objFile= oFSO.OpenTextFile( strFile, ForReading, True)
    
    while not objFile.atEndOfStream
    
    	bProcessLine= True
    	strLine= objFile.readLine
    
    	if isEmpty(strLine) then
    		bProcessLine= False
    	Else
    		If left(strLine, 1)= ";" Then
    			bProcessLine= False
    		Else
    			If inStr( strLine, ",") > 0 Then
    				' Line OK
    				arrayUsersOrGroups= split( strLine, ",")
    				strUserOrGroup= arrayUsersOrGroups(0)
    				strNewUserOrGroup= arrayUsersOrGroups(1)
    
    				If strUserOrGroup= "SourceName" Then
    					' Input file header, skip        
    					bProcessLine= False
    				End If
    			Else
    					bProcessLine= False
    					debug("** INFO: Skipping line "& strLine)
    			End If
    		End If
    	End If
    
    	If bProcessLine Then
    
    		debug(strNewUserOrGroup& ": Syncing Exchange Attributes from "& strUserOrGroup)
    		If ProcessUsers Then
    			syncAttributes strUserOrGroup, conSourceServer, conSourceDomain, strNewUserOrGroup, conTargetServer, conTargetDomain,"user"
    		End If
    		If ProcessGroups Then
    			syncAttributes strUserOrGroup, conSourceServer, conSourceDomain, strNewUserOrGroup, conTargetServer, conTargetDomain,"group"
    		End If
    	End If
    Wend
    
    debug("Finished")
    End Function
    
    '*******************************************************************
    ' Purpose: output to screen when DEBUGOUTPUT is 1, always to file
    '*******************************************************************
    Function debug(strMsg)
     If Err.Number Then
    	hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& "****ERROR HAS OCCURED****")
    	wscript.echo "****ERROR HAS OCCURED****"
    	hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& "Error Number: " & Err.Number)
    	wscript.echo "Error Number: " & Err.Number
    	hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& "Error Description: " & Err.Description)
    	wscript.echo "Error Description: " & Err.Description
    	Exit Function
    End If
     hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& strMsg & chr(13)& chr(10))
     if DEBUGOUTPUT=1 then
    	wscript.echo strMsg
     end if
    
    End Function
    
    '*********************************************************
    ' Purpose: terminate with message
    '*********************************************************
    Function die(strMsg)
     wscript.echo strMsg
     wscript.quit (1)
    End Function
    
    '*********************************************************
    ' displayString
    ' Returns string from varType item/elements
    '*********************************************************
    Function displayString( varObj)
     Dim tmp, item
     tmp= ""
     select case VarType( varObj)
    
    	Case vbEmpty
    	tmp= "(Empty)"
    	Case vbNull
    	tmp= "(Null)"
    	Case vbInteger, vbLong, vbSingle, vbDouble, vbByte, vbDecimal, vbCurrency, vbDate, vbBoolean
    	tmp= cStr( varObj)
    	Case vbString
    	tmp= varObj
    	Case vbObject
    	tmp= "(Object)"
    	Case vbvariant
    	tmp= "(Variant)"
    	Case 8209
    	tmp= "("& OctetToHexStr( varObj)& ")"
    	Case vbArray, 8204
    	For each item in varObj
    		If tmp="" Then
    			tmp= tmp+ item
    		Else
    			tmp= tmp+ ", "+item
    		End If
    	Next
    	tmp= "["& tmp& "]"
    	Case Else
    
     End Select
    
     displaystring= tmp& " #"& varType( varObj)
    
    End Function
    
    '*********************************************************
    ' OctetToHexStr
    ' Convert OctetString (byte array) to Hex string.
    '*********************************************************
    Function OctetToHexStr (arrbytOctet)
     Dim k
     OctetToHexStr = ""
     For k = 1 To Lenb (arrbytOctet)
    	OctetToHexStr = OctetToHexStr & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2)
     Next
    End Function
    
    '*********************************************************
    ' syncAttributes
    ' migrates attributes from source to target
    '*********************************************************
    Function syncAttributes (strUserOrGroup, SourceServer, SourceDomain, strNewUserOrGroup, TargetServer, TargetDomain,CheckUserOrGroup)
    	 dim strDNSource, strDNTarget, objSource, objTarget, n, strMail
    	 If CheckUserOrGroup = "user" Then
    		strDNSource= getDN( strUserOrGroup, SourceServer, SourceDomain, CheckUserOrGroup, "")
    		strDNTarget= getDN( strNewUserOrGroup, TargetServer, TargetDomain, CheckUserOrGroup, "")
    		debug("Processing user: " & strDNSource)
    		If strDNSource"" AND strDNTarget  "" Then
    			If instr(strDNSource,"/")  0 or instr(strDNSource,"\")  0 or instr(strDNTarget,"/")  0 or instr(strDNTarget,"\")  0 Then
    				debug("**ERROR User DN is in wrong format.  Skipping user: " & strDNSource)
    				Exit Function
    			End If
    			set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
    			set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)
    			debug("Source Server Connection: LDAP://"& SourceServer& "/"& strDNSource)
    			debug("Target Server Connection: LDAP://"& TargetServer& "/"& strDNTarget)
    
    			copyAttribute "mail", objSource, objTarget, False
    			copyAttribute "mailNickname", objSource, objTarget, False
    			copyAttribute "msExchMailboxGuid", objSource, objTarget, False
    			setAttribute "targetaddress", objSource.get( "mail"), objTarget
    			copyAttribute "proxyAddresses", objSource, objTarget, True
    			addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget
    			addAttribute "showInAddressBook", conDestinationGAL, objTarget
    
    			strMail= objSource.get( "mail")
    			n= instr( strMail, "@")
    			debug( strMail)
    			addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget
    
    			setAttribute "msExchRecipientDisplayType", -2147483642, objTarget
    			setAttribute "msExchRecipientTypeDetails", 128, objTarget
    			setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget
    
    			objTarget.setInfo
    		Else
    			debug("*** ERR: Cannot retrieve DNs for Source or Target")
    			syncAttributes= False
    		End If
    	 End If
    	 
    	  If CheckUserOrGroup = "group" Then
    		strDNSource= getDN( strUserOrGroup, SourceServer, SourceDomain, CheckUserOrGroup, "")
    		strDNTarget= getDN( strNewUserOrGroup, TargetServer, TargetDomain, CheckUserOrGroup, "")
    		debug("Processing group: " & strDNSource)
    		If strDNSource"" AND strDNTarget  "" Then
    			If instr(strDNSource,"/")  0 or instr(strDNSource,"\")  0 or instr(strDNTarget,"/")  0 or instr(strDNTarget,"\")  0 Then
    				debug("**ERROR Group DN is in wrong format.  Skipping group: " & strDNSource)
    				Exit Function
    			End If
    			set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
    			set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)
    			debug("Source Server Connection: LDAP://"& SourceServer& "/"& strDNSource)
    			debug("Target Server Connection: LDAP://"& TargetServer& "/"& strDNTarget)
    
    			copyAttribute "mail", objSource, objTarget, False
    			copyAttribute "mailNickname", objSource, objTarget, False
    			copyAttribute "proxyAddresses", objSource, objTarget, True
    			addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget
    			addAttribute "showInAddressBook", conDestinationGAL, objTarget
    
    			strMail= objSource.get( "mail")
    			n= instr( strMail, "@")
    			debug( strMail)
    			addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget
    
    			setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget
    
    			objTarget.setInfo
    		Else
    			debug("*** ERR: Cannot retrieve DNs for Source or Target")
    			syncAttributes= False
    		End If
    	 End If
    End Function
    
    '*********************************************************
    ' getDN
    ' Retrieves the DN for a user object
    '*********************************************************
    Function getDN( strUserOrGroup, strServer, strDomain, CheckUserOrGroup, strOU)
    	dim objConn, objCmd, strQuery, objRS, strAttr, strRDNLDAP, strDNSLDAP
    	strRDNLDAP= RDN2LDAPPATH( strOU)
    	strDNSLDAP= DNSDomain2LDAPPath( strDomain)
    	strAttr= "distinguishedName"
    	set objConn= createObject( "ADODB.Connection")
    	set objCmd= createObject( "ADODB.Command")
    	objConn.Provider= "ADsDSOObject"
    	objConn.Open "ADs provider"
    	objCmd.ActiveConnection= objConn
    	strQuery= "<LDAP://"& strServer
    	If strServer  "" Then
    		strQuery= strQuery& "/"
    	End If
    	strQuery= strQuery& strRDNLDAP
    	If strOU  "" Then
    		strQuery= strQuery& ","
    	End If
    	strQuery= strQuery& strDNSLDAP& ">"
    	strQuery= strQuery+ ";(&(objectClass=" & CheckUserOrGroup & ")(SAMAccountName="& strUserOrGroup&"));"& strAttr& ";subtree"
    	objCmd.CommandText = strQuery
    	on error resume next
    	set objRS= objCmd.execute
    	if err.number  0 Then
    		debug( "*** ERR: Error "& err.number& " executing ["& strQuery& "]")
    		getDN= ""
    	Else
    		on error goto 0
    		Select Case objRS.recordCount
    			Case 0
    			debug( "*** ERR: " & CheckUserOrGroup & " object "& strUserOrGroup& " not found")
    			getDN= ""
    			Case 1
    			getDN= objRS.Fields( strAttr)
    			'debug( getDN)
    			Case Else
    			debug("*** ERR: Ambigious user object "& strUserOrGroup)
    			getDN= ""
    		End Select
    	End If
    	set objRS= Nothing
    	set objCmd= Nothing
    	set objConn= Nothing
    End Function
    
    '*********************************************************
    ' DNSDomain2LDAPPath( str)
    ' Makes an LDAP notation for a DNS domain name
    ' e.g. corp.local => DC=corp,DC=local
    '*********************************************************
    Function DNSDomain2LDAPPath( str)
    	Dim tmp1, tmp2, tmp3
    	tmp1= split( str, ".")
    	tmp2= ""
    	For each tmp3 in tmp1
    		If tmp2""  then
    			tmp2= tmp2& ","
    		End If
    		tmp2= tmp2& "dc="& tmp3
    	Next
    	DNSDomain2LDAPPath= tmp2
    End Function
    
    '*********************************************************
    ' RDN2LDAPPath( str)
    ' Makes an LDAP notation for a Relative Distinguished Name
    ' e.g. Domain Accounts/3rd party => OU=3rd party,OU=Domain Accounts
    '*********************************************************
    Function RDN2LDAPPath( str)
    	Dim tmp1, tmp2, tmp3
    	tmp1= split( str, "/")
    	tmp2= ""
    	For each tmp3 in tmp1
    		If tmp2"" then
    			tmp2= ","& tmp2
    		End If
    		tmp2= "ou="& tmp3& tmp2
    	Next
    	RDN2LDAPPath= tmp2
    End Function
    
    '*********************************************************
    ' copyAttribute
    ' Copies attribute(s) to target
    '*********************************************************
    Function copyAttribute( strAttribute, objSource, objTarget, boolMulti)
    	dim boolUpdate, varItem
    	If isEmpty( objSource.get( strAttribute)) Then
    	  debug( strAttribute& " not set, clearing")
    	  objTarget.PutEx ADS_PROPERTY_CLEAR, strAttribute, 0
    	Else
    	  varItem= objSource.get( strAttribute)
    	  If boolMulti Then
    		if isArray( varItem) Then
    		  debug( "Setting "& strAttribute& " to multi-value "& displayString( varItem))
    		  objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, varItem
    		Else
    		  debug( "Setting "& strAttribute& " to single-value "& displayString( varItem))
    		  objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, array( varItem)
    		End If
    	  Else
    		debug( "Setting "& strAttribute& " to "& displayString( varItem))
    		objTarget.Put strAttribute, varItem
    	  End If
    	End If
    	on error goto 0
    	objTarget.SetInfo
    End Function
    
    '*********************************************************
    ' setAttribute
    ' Sets attribute to target
    '*********************************************************
    Function setAttribute( strAttribute, strValue, objTarget)
     debug("Setting "& strAttribute& " to "& displayString(strValue))
     objTarget.Put strAttribute, strValue
     objTarget.SetInfo
    End Function
    
    '*********************************************************
    ' addAttribute
    ' Adds attribute to target
    '*********************************************************
    Function addAttribute( strAttribute, varAttribute, objTarget)
     dim boolUpdate, tmp
     boolUpdate= True
     On Error Resume Next
     If isEmpty( varAttribute) Then
    	' not set, skipping
     Else
    	If isEmpty( objTarget.get( strAttribute)) Then
    		boolUpdate= True
    	Else
    		If strAttribute = "showInAddressBook" or strAttribute = "proxyAddresses" Then
    			If isArray( objTarget.get( strAttribute)) Then
    				debug(strAttribute & " was detected as an array")
    				For each tmp in objTarget.get( strAttribute)
    					If tmp = varAttribute Then
    						boolUpdate= False
    					End If
    				Next
    			Else
    				'If a multivalued attribute in AD only has one value then it IsArray will not detect the item as an array.
    				debug(strAttribute & " was not detected as an array - the object only has one entry in AD")
    				If varAttribute = objTarget.get( strAttribute) Then
    					boolUpdate= False
    				End If
    				'boolUpdate= varAttribute= objTarget.get( strAttribute)
    			End If
    		End If
    	End If
    	If boolUpdate Then
    		debug("Adding "& varAttribute& " to "& strAttribute)
    		objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, array( varAttribute)
    	Else
    		debug( varAttribute& " already in "& strAttribute)
    	End If
     End If
     objTarget.SetInfo
     If Err.Number Then
    	debug("error check")
     End If
     On Error GoTo 0
    End Function

    Like

  26. If the destination user account is a mailbox user, the script currently changes the msExchRecipientDisplayType and msExchRecipientTypeDetails attributes back to those of a Mail Enabled User istead of a Mailbox User. As a result this breaks any mailbox users declared in the CSV file. Version 0.31 of the script checks to see if the destination user account is a mailbox user, if so then do not change these two attributes for the mailbox.

    Enjoy!

    '*--------------------------------------------------------------------------------
    '* Name     : PrepareForestMove
    '* Created By      : Michel de Rooij
    '* Modified By	   : Clint Boessen
    '* E-mail    : michel@eightwone.com, clint.boessen@4logic.com.au
    '* Date            : 20111125
    '* Version    : 0.3
    '*--------------------------------------------------------------------------------
    '* Changes:
    '* 0.21 Initial version
    '* 0.22 Made changes to address single-value proxyAddress attributes
    '* 0.3	Modifications by Clint Boessen to include:
    '			- Resolved a problem which caused  if the distinguished names have / characters
    '			- Resolved a bug with the addAttribute function.  If a multivalued attribute in AD 
    '			  only has one value then IsArray will not detect the item as an array.  For example
    '			  if proxyAddresses in Active Directory only has one address the function failed
    '			  and simply did not add any additional proxyAddresses from the source forest.
    '			- Provided support to add the Active Directory objects to to the Global Address List
    '			  in the destination forest.  The original script by Michel did not populate the
    '			  destination global address list.
    '			- Provided support to synchronise mail enabled attributes for distribution groups
    '			  to the destination forest for groups migrated by ADMT.  This means the
    '			  distribution groups will appear in the destination Global Addres List.
    '			- Provided support to either disable or enable the processing of Users or Groups during
    '			  script run.
    '			- Provided additional smarts to script error checking.
    '* 0.31	Added a checkIfMailbox function to check if the destination object is already a "mailbox user".  Without
    '  this function in place, if the script runs against a "mailbox user", not a "mail enabled" user it breaks the user
    '  account.
    '*--------------------------------------------------------------------------------
    
    
    Option Explicit
    
    'Set to False if you do not wish to process
    Const ProcessUsers = True
    Const ProcessGroups = True
    
    Const strUserfile            = "Mail_Users.csv"
    Const strGroupfile			= "Mail_Groups.csv"
    Const strOutputFileName            = "output.log"
    Const DEBUGOUTPUT            = 1
    
    Const conSourceServer               = "dc01.sourcedomain.local"
    Const conSourceDomain                = "sourcedomain.local"
    Const contargetServer                = "dc01.destinationdomain.local"
    Const conTargetDomain                = "destinationdomain.local"
    
    Const conLegacyExchangeDN            = "/o=COS/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn="
    Const conTargetEmailDomain            = "contoso.com"
    
    Const conDestinationGAL			= "CN=Default Global Address List,CN=All Global Address Lists,CN=Address Lists Container,CN=COS,CN=Microsoft 
    
    Exchange,CN=Services,CN=Configuration,DC=destinationdomain,DC=local"
    
    ' AD putex cmds
    Const ADS_PROPERTY_CLEAR = 1
    Const ADS_PROPERTY_UPDATE = 2
    Const ADS_PROPERTY_APPEND = 3
    Const ADS_PROPERTY_DELETE = 4
    
    ' FileSystem
    Const ForWriting =2
    Const ForReading =1
    
    '*********************************************************
    ' MAIN
    '*********************************************************
    
    Dim oFSO, objFile, hOutputFileHandle, bProcessLine, strLine
    Dim arrayUsersOrGroups, strUserOrGroup, strCmd, strNewUserOrGroup, strGroups, strNewGroup
    
    Set oFSO = CreateObject("Scripting.FileSystemObject")
    Set hOutputFileHandle= oFSO.OpenTextFile( strOutputFileName, ForWriting, True)
    
    If ProcessUsers Then
    ProcessEntries(strUserfile)
    End If
    
    If ProcessGroups Then
    ProcessEntries(strGroupfile)
    End If
    
    objFile.Close
    hOutputFileHandle.Close  
    
    set hOutputFileHandle= Nothing
    set objFile= Nothing
    set oFSO= Nothing
    
    wscript.quit(0)
    
    '*******************************************************************
    ' Query through the users or groups input file
    '*******************************************************************
    Function ProcessEntries(strFile)
    'put in the user or group file name in as a function variable.
    
    debug("Start")
    
    if NOT oFSO.fileExists( strFile) then
     die( "Input file "& strFile& " does not exist")
    end if
    
    debug("Reading names from "& strFile)
    set objFile= oFSO.OpenTextFile( strFile, ForReading, True)
    
    while not objFile.atEndOfStream
    
    	bProcessLine= True
    	strLine= objFile.readLine
    
    	if isEmpty(strLine) then
    		bProcessLine= False
    	Else
    		If left(strLine, 1)= ";" Then
    			bProcessLine= False
    		Else
    			If inStr( strLine, ",") > 0 Then
    				' Line OK
    				arrayUsersOrGroups= split( strLine, ",")
    				strUserOrGroup= arrayUsersOrGroups(0)
    				strNewUserOrGroup= arrayUsersOrGroups(1)
    
    				If strUserOrGroup= "SourceName" Then
    					' Input file header, skip        
    					bProcessLine= False
    				End If
    			Else
    					bProcessLine= False
    					debug("** INFO: Skipping line "& strLine)
    			End If
    		End If
    	End If
    
    	If bProcessLine Then
    
    		debug(strNewUserOrGroup& ": Syncing Exchange Attributes from "& strUserOrGroup)
    		If ProcessUsers Then
    			syncAttributes strUserOrGroup, conSourceServer, conSourceDomain, strNewUserOrGroup, conTargetServer, conTargetDomain,"user"
    		End If
    		If ProcessGroups Then
    			syncAttributes strUserOrGroup, conSourceServer, conSourceDomain, strNewUserOrGroup, conTargetServer, conTargetDomain,"group"
    		End If
    	End If
    Wend
    
    debug("Finished")
    End Function
    
    '*******************************************************************
    ' Purpose: output to screen when DEBUGOUTPUT is 1, always to file
    '*******************************************************************
    Function debug(strMsg)
     If Err.Number Then
    	hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& "****ERROR HAS OCCURED****")
    	wscript.echo "****ERROR HAS OCCURED****"
    	hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& "Error Number: " & Err.Number)
    	wscript.echo "Error Number: " & Err.Number
    	hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& "Error Description: " & Err.Description)
    	wscript.echo "Error Description: " & Err.Description
    	On Error GoTo 0
    	Exit Function
    End If
     hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& strMsg & chr(13)& chr(10))
     if DEBUGOUTPUT=1 then
    	wscript.echo strMsg
     end if
    
    End Function
    
    '*********************************************************
    ' Purpose: terminate with message
    '*********************************************************
    Function die(strMsg)
     wscript.echo strMsg
     wscript.quit (1)
    End Function
    
    '*********************************************************
    ' displayString
    ' Returns string from varType item/elements
    '*********************************************************
    Function displayString( varObj)
     Dim tmp, item
     tmp= ""
     select case VarType( varObj)
    
    	Case vbEmpty
    	tmp= "(Empty)"
    	Case vbNull
    	tmp= "(Null)"
    	Case vbInteger, vbLong, vbSingle, vbDouble, vbByte, vbDecimal, vbCurrency, vbDate, vbBoolean
    	tmp= cStr( varObj)
    	Case vbString
    	tmp= varObj
    	Case vbObject
    	tmp= "(Object)"
    	Case vbvariant
    	tmp= "(Variant)"
    	Case 8209
    	tmp= "("& OctetToHexStr( varObj)& ")"
    	Case vbArray, 8204
    	For each item in varObj
    		If tmp="" Then
    			tmp= tmp+ item
    		Else
    			tmp= tmp+ ", "+item
    		End If
    	Next
    	tmp= "["& tmp& "]"
    	Case Else
    
     End Select
    
     displaystring= tmp& " #"& varType( varObj)
    
    End Function
    
    '*********************************************************
    ' OctetToHexStr
    ' Convert OctetString (byte array) to Hex string.
    '*********************************************************
    Function OctetToHexStr (arrbytOctet)
     Dim k
     OctetToHexStr = ""
     For k = 1 To Lenb (arrbytOctet)
    	OctetToHexStr = OctetToHexStr & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2)
     Next
    End Function
    
    '*********************************************************
    ' syncAttributes
    ' migrates attributes from source to target
    '*********************************************************
    Function syncAttributes (strUserOrGroup, SourceServer, SourceDomain, strNewUserOrGroup, TargetServer, TargetDomain,CheckUserOrGroup)
    	 dim strDNSource, strDNTarget, objSource, objTarget, n, strMail, btnIsMailbox
    	 If CheckUserOrGroup = "user" Then
    		strDNSource= getDN( strUserOrGroup, SourceServer, SourceDomain, CheckUserOrGroup, "")
    		strDNTarget= getDN( strNewUserOrGroup, TargetServer, TargetDomain, CheckUserOrGroup, "")
    		debug("Processing user: " & strDNSource)
    		If strDNSource"" AND strDNTarget  "" Then
    			If instr(strDNSource,"/")  0 or instr(strDNSource,"\")  0 or instr(strDNTarget,"/")  0 or instr(strDNTarget,"\")  0 Then
    				debug("**ERROR User DN is in wrong format.  Skipping user: " & strDNSource)
    				Exit Function
    			End If
    			set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
    			set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)
    			debug("Source Server Connection: LDAP://"& SourceServer& "/"& strDNSource)
    			debug("Target Server Connection: LDAP://"& TargetServer& "/"& strDNTarget)
    
    			'Check to see if destination account is a mailbox user.
    			btnIsMailbox = checkIfMailbox(objTarget)
    						
    			copyAttribute "mail", objSource, objTarget, False
    			copyAttribute "mailNickname", objSource, objTarget, False
    			copyAttribute "msExchMailboxGuid", objSource, objTarget, False
    			setAttribute "targetaddress", objSource.get( "mail"), objTarget
    			copyAttribute "proxyAddresses", objSource, objTarget, True
    			addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget
    			addAttribute "showInAddressBook", conDestinationGAL, objTarget
    
    			strMail= objSource.get( "mail")
    			n= instr( strMail, "@")
    			debug( strMail)
    			addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget
    			
    			If btnIsMailbox Then
    				debug(objTarget.get("displayName") & " is already a mailbox user in the destination forest." & _
    				" As a result msExchRecipientDisplayType and msExchRecipientTypeDetails attributes will not be changed.")
    			Else
    				setAttribute "msExchRecipientDisplayType", -2147483642, objTarget
    				setAttribute "msExchRecipientTypeDetails", 128, objTarget
    			End If
    			
    			setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget
    
    			objTarget.setInfo
    		Else
    			debug("*** ERR: Cannot retrieve DNs for Source or Target")
    			syncAttributes= False
    		End If
    	 End If
    	 
    	  If CheckUserOrGroup = "group" Then
    		strDNSource= getDN( strUserOrGroup, SourceServer, SourceDomain, CheckUserOrGroup, "")
    		strDNTarget= getDN( strNewUserOrGroup, TargetServer, TargetDomain, CheckUserOrGroup, "")
    		debug("Processing group: " & strDNSource)
    		If strDNSource"" AND strDNTarget  "" Then
    			If instr(strDNSource,"/")  0 or instr(strDNSource,"\")  0 or instr(strDNTarget,"/")  0 or instr(strDNTarget,"\")  0 Then
    				debug("**ERROR Group DN is in wrong format.  Skipping group: " & strDNSource)
    				Exit Function
    			End If
    			set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
    			set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)
    			debug("Source Server Connection: LDAP://"& SourceServer& "/"& strDNSource)
    			debug("Target Server Connection: LDAP://"& TargetServer& "/"& strDNTarget)
    
    			copyAttribute "mail", objSource, objTarget, False
    			copyAttribute "mailNickname", objSource, objTarget, False
    			copyAttribute "proxyAddresses", objSource, objTarget, True
    			addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget
    			addAttribute "showInAddressBook", conDestinationGAL, objTarget
    
    			strMail= objSource.get( "mail")
    			n= instr( strMail, "@")
    			debug( strMail)
    			addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget
    
    			setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget
    
    			objTarget.setInfo
    		Else
    			debug("*** ERR: Cannot retrieve DNs for Source or Target")
    			syncAttributes= False
    		End If
    	 End If
    End Function
    
    '*********************************************************
    ' getDN
    ' Retrieves the DN for a user object
    '*********************************************************
    Function getDN( strUserOrGroup, strServer, strDomain, CheckUserOrGroup, strOU)
    	dim objConn, objCmd, strQuery, objRS, strAttr, strRDNLDAP, strDNSLDAP
    	strRDNLDAP= RDN2LDAPPATH( strOU)
    	strDNSLDAP= DNSDomain2LDAPPath( strDomain)
    	strAttr= "distinguishedName"
    	set objConn= createObject( "ADODB.Connection")
    	set objCmd= createObject( "ADODB.Command")
    	objConn.Provider= "ADsDSOObject"
    	objConn.Open "ADs provider"
    	objCmd.ActiveConnection= objConn
    	strQuery= "<LDAP://"& strServer
    	If strServer  "" Then
    		strQuery= strQuery& "/"
    	End If
    	strQuery= strQuery& strRDNLDAP
    	If strOU  "" Then
    		strQuery= strQuery& ","
    	End If
    	strQuery= strQuery& strDNSLDAP& ">"
    	strQuery= strQuery+ ";(&(objectClass=" & CheckUserOrGroup & ")(SAMAccountName="& strUserOrGroup&"));"& strAttr& ";subtree"
    	objCmd.CommandText = strQuery
    	on error resume next
    	set objRS= objCmd.execute
    	if err.number  0 Then
    		debug( "*** ERR: Error "& err.number& " executing ["& strQuery& "]")
    		getDN= ""
    	Else
    		on error goto 0
    		Select Case objRS.recordCount
    			Case 0
    			debug( "*** ERR: " & CheckUserOrGroup & " object "& strUserOrGroup& " not found")
    			getDN= ""
    			Case 1
    			getDN= objRS.Fields( strAttr)
    			'debug( getDN)
    			Case Else
    			debug("*** ERR: Ambigious user object "& strUserOrGroup)
    			getDN= ""
    		End Select
    	End If
    	set objRS= Nothing
    	set objCmd= Nothing
    	set objConn= Nothing
    End Function
    
    '*********************************************************
    ' DNSDomain2LDAPPath( str)
    ' Makes an LDAP notation for a DNS domain name
    ' e.g. corp.local => DC=corp,DC=local
    '*********************************************************
    Function DNSDomain2LDAPPath( str)
    	Dim tmp1, tmp2, tmp3
    	tmp1= split( str, ".")
    	tmp2= ""
    	For each tmp3 in tmp1
    		If tmp2""  then
    			tmp2= tmp2& ","
    		End If
    		tmp2= tmp2& "dc="& tmp3
    	Next
    	DNSDomain2LDAPPath= tmp2
    End Function
    
    '*********************************************************
    ' RDN2LDAPPath( str)
    ' Makes an LDAP notation for a Relative Distinguished Name
    ' e.g. Domain Accounts/3rd party => OU=3rd party,OU=Domain Accounts
    '*********************************************************
    Function RDN2LDAPPath( str)
    	Dim tmp1, tmp2, tmp3
    	tmp1= split( str, "/")
    	tmp2= ""
    	For each tmp3 in tmp1
    		If tmp2"" then
    			tmp2= ","& tmp2
    		End If
    		tmp2= "ou="& tmp3& tmp2
    	Next
    	RDN2LDAPPath= tmp2
    End Function
    
    '*********************************************************
    ' copyAttribute
    ' Copies attribute(s) to target
    '*********************************************************
    Function copyAttribute( strAttribute, objSource, objTarget, boolMulti)
    	dim boolUpdate, varItem
    	If isEmpty( objSource.get( strAttribute)) Then
    	  debug( strAttribute& " not set, clearing")
    	  objTarget.PutEx ADS_PROPERTY_CLEAR, strAttribute, 0
    	Else
    	  varItem= objSource.get( strAttribute)
    	  If boolMulti Then
    		if isArray( varItem) Then
    		  debug( "Setting "& strAttribute& " to multi-value "& displayString( varItem))
    		  objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, varItem
    		Else
    		  debug( "Setting "& strAttribute& " to single-value "& displayString( varItem))
    		  objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, array( varItem)
    		End If
    	  Else
    		debug( "Setting "& strAttribute& " to "& displayString( varItem))
    		objTarget.Put strAttribute, varItem
    	  End If
    	End If
    	on error goto 0
    	objTarget.SetInfo
    End Function
    
    '*********************************************************
    ' setAttribute
    ' Sets attribute to target
    '*********************************************************
    Function setAttribute( strAttribute, strValue, objTarget)
     debug("Setting "& strAttribute& " to "& displayString(strValue))
     objTarget.Put strAttribute, strValue
     objTarget.SetInfo
    End Function
    
    '*********************************************************
    ' addAttribute
    ' Adds attribute to target
    '*********************************************************
    Function addAttribute( strAttribute, varAttribute, objTarget)
    	 dim boolUpdate, tmp
    	 boolUpdate= True
    	 On Error Resume Next
    	 If isEmpty( varAttribute) Then
    		' not set, skipping
    	 Else
    		If isEmpty( objTarget.get( strAttribute)) Then
    			boolUpdate= True
    		Else
    			If strAttribute = "showInAddressBook" or strAttribute = "proxyAddresses" Then
    				If isArray( objTarget.get( strAttribute)) Then
    					debug(strAttribute & " was detected as an array")
    					For each tmp in objTarget.get( strAttribute)
    						If tmp = varAttribute Then
    							boolUpdate= False
    						End If
    					Next
    				Else
    					'If a multivalued attribute in AD only has one value then it IsArray will not detect the item as an array.
    					If varAttribute = objTarget.get( strAttribute) Then
    						boolUpdate= False
    					End If
    				End If
    			End If
    		End If
    		If boolUpdate Then
    			debug("Adding "& varAttribute& " to "& strAttribute)
    			objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, array( varAttribute)
    		Else
    			debug( varAttribute& " already in "& strAttribute)
    		End If
    	 End If
    	 objTarget.SetInfo
    	 If Err.Number Then
    		debug("error check")
    	 End If
    End Function
    
    '************************************************************
    ' checkIfMailbox
    ' This function checks if the destination object is a mailbox
    '************************************************************
    Function checkIfMailbox(objTarget)
    	checkIfMailbox = False
    	If objTarget.Get("msExchRecipientDisplayType")  "-2147483642" Then
    		checkIfMailbox = True
    	End If
    End Function

    Like

  27. Great script.,

    The old version works perfect.

    In the new version i get an error
    Line 235
    Char 17
    Eroor: expected ‘Then’
    Code 800a03F9
    source: microsoft vbscript compilation error

    Pleas can you help me!

    Like

  28. Version 0.32 of the script below. Additional error handling and the ability to check if destination account is mailbox enabled.

    '*--------------------------------------------------------------------------------
    '* Name     : PrepareForestMove
    '* Created By      : Michel de Rooij
    '* Modified By	   : Clint Boessen
    '* E-mail    : michel@eightwone.com, clint.boessen@4logic.com.au
    '* Date            : 20111202
    '* Version    : 0.32
    '*--------------------------------------------------------------------------------
    '* Changes:
    '* 0.21 Initial version
    '* 0.22 Made changes to address single-value proxyAddress attributes
    '* 0.3	Modifications by Clint Boessen to include:
    '			- Resolved a problem which caused  if the distinguished names have / characters
    '			- Resolved a bug with the addAttribute function.  If a multivalued attribute in AD 
    '			  only has one value then IsArray will not detect the item as an array.  For example
    '			  if proxyAddresses in Active Directory only has one address the function failed
    '			  and simply did not add any additional proxyAddresses from the source forest.
    '			- Provided support to add the Active Directory objects to to the Global Address List
    '			  in the destination forest.  The original script by Michel did not populate the
    '			  destination global address list.
    '			- Provided support to synchronise mail enabled attributes for distribution groups
    '			  to the destination forest for groups migrated by ADMT.  This means the
    '			  distribution groups will appear in the destination Global Addres List.
    '			- Provided support to either disable or enable the processing of Users or Groups during
    '			  script run.
    '			- Provided additional smarts to script error checking.
    '* 0.31	Added a checkIfMailbox function to check if the destination object is already a "mailbox user".  Without
    '  this function in place, if the script runs against a "mailbox user", not a "mail enabled" user it breaks the user
    '  account.
    '* 0.32	"isEmpty( objSource.get( strAttribute))" in the copyAttributes function could not deal with the proxyAddresses
    '  attribute being blank.  Added additional error handling to deal with this.
    '*--------------------------------------------------------------------------------
    
    
    Option Explicit
    
    'Set to False if you do not wish to process
    Const ProcessUsers = True
    Const ProcessGroups = True
    
    Const strUserfile            = "Mail_Users.csv"
    Const strGroupfile			= "Mail_Groups.csv"
    Const strOutputFileName            = "output.log"
    Const DEBUGOUTPUT            = 1
    
    Const conSourceServer               = "devdc6.stirling"
    Const conSourceDomain                = "stirling"
    Const contargetServer                = "devdc1.cos.local"
    Const conTargetDomain                = "cos.local"
    
    Const conLegacyExchangeDN            = "/o=COS/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn="
    Const conTargetEmailDomain            = "stirling.wa.gov.au"
    
    Const conDestinationGAL			= "CN=Default Global Address List,CN=All Global Address Lists,CN=Address Lists Container,CN=COS,CN=Microsoft 
    
    Exchange,CN=Services,CN=Configuration,DC=cos,DC=local"
    
    ' AD putex cmds
    Const ADS_PROPERTY_CLEAR = 1
    Const ADS_PROPERTY_UPDATE = 2
    Const ADS_PROPERTY_APPEND = 3
    Const ADS_PROPERTY_DELETE = 4
    
    ' FileSystem
    Const ForWriting =2
    Const ForReading =1
    
    '*********************************************************
    ' MAIN
    '*********************************************************
    
    Dim oFSO, objFile, hOutputFileHandle, bProcessLine, strLine
    Dim arrayUsersOrGroups, strUserOrGroup, strCmd, strNewUserOrGroup, strGroups, strNewGroup
    
    Set oFSO = CreateObject("Scripting.FileSystemObject")
    Set hOutputFileHandle= oFSO.OpenTextFile( strOutputFileName, ForWriting, True)
    
    If ProcessUsers Then
    ProcessEntries(strUserfile)
    End If
    
    If ProcessGroups Then
    ProcessEntries(strGroupfile)
    End If
    
    objFile.Close
    hOutputFileHandle.Close  
    
    set hOutputFileHandle= Nothing
    set objFile= Nothing
    set oFSO= Nothing
    
    wscript.quit(0)
    
    '*******************************************************************
    ' Query through the users or groups input file
    '*******************************************************************
    Function ProcessEntries(strFile)
    'put in the user or group file name in as a function variable.
    
    debug("Start")
    
    if NOT oFSO.fileExists( strFile) then
     die( "Input file "& strFile& " does not exist")
    end if
    
    debug("Reading names from "& strFile)
    set objFile= oFSO.OpenTextFile( strFile, ForReading, True)
    
    while not objFile.atEndOfStream
    
    	bProcessLine= True
    	strLine= objFile.readLine
    
    	if isEmpty(strLine) then
    		bProcessLine= False
    	Else
    		If left(strLine, 1)= ";" Then
    			bProcessLine= False
    		Else
    			If inStr( strLine, ",") > 0 Then
    				' Line OK
    				arrayUsersOrGroups= split( strLine, ",")
    				strUserOrGroup= arrayUsersOrGroups(0)
    				strNewUserOrGroup= arrayUsersOrGroups(1)
    
    				If strUserOrGroup= "SourceName" Then
    					' Input file header, skip        
    					bProcessLine= False
    				End If
    			Else
    					bProcessLine= False
    					debug("** INFO: Skipping line "& strLine)
    			End If
    		End If
    	End If
    
    	If bProcessLine Then
    
    		debug(strNewUserOrGroup& ": Syncing Exchange Attributes from "& strUserOrGroup)
    		If ProcessUsers Then
    			syncAttributes strUserOrGroup, conSourceServer, conSourceDomain, strNewUserOrGroup, conTargetServer, conTargetDomain,"user"
    		End If
    		If ProcessGroups Then
    			syncAttributes strUserOrGroup, conSourceServer, conSourceDomain, strNewUserOrGroup, conTargetServer, conTargetDomain,"group"
    		End If
    	End If
    Wend
    
    debug("Finished")
    End Function
    
    '*******************************************************************
    ' Purpose: output to screen when DEBUGOUTPUT is 1, always to file
    '*******************************************************************
    Function debug(strMsg)
     If Err.Number Then
    	hOutputFileHandle.writeline ("["& FormatDateTime(now(),4) & "] "& "****ERROR HAS OCCURED****")
    	wscript.echo "****ERROR HAS OCCURED****"
    	hOutputFileHandle.writeline ("["& FormatDateTime(now(),4) & "] "& "Error Number: " & Err.Number)
    	wscript.echo "Error Number: " & Err.Number
    	hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& "Error Description: " & Err.Description)
    	wscript.echo "Error Description: " & Err.Description
    	On Error GoTo 0
    	Exit Function
    End If
     hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& strMsg & chr(13)& chr(10))
     if DEBUGOUTPUT=1 then
    	wscript.echo strMsg
     end if
    
    End Function
    
    '*********************************************************
    ' Purpose: terminate with message
    '*********************************************************
    Function die(strMsg)
     wscript.echo strMsg
     wscript.quit (1)
    End Function
    
    '*********************************************************
    ' displayString
    ' Returns string from varType item/elements
    '*********************************************************
    Function displayString( varObj)
     Dim tmp, item
     tmp= ""
     select case VarType( varObj)
    
    	Case vbEmpty
    	tmp= "(Empty)"
    	Case vbNull
    	tmp= "(Null)"
    	Case vbInteger, vbLong, vbSingle, vbDouble, vbByte, vbDecimal, vbCurrency, vbDate, vbBoolean
    	tmp= cStr( varObj)
    	Case vbString
    	tmp= varObj
    	Case vbObject
    	tmp= "(Object)"
    	Case vbvariant
    	tmp= "(Variant)"
    	Case 8209
    	tmp= "("& OctetToHexStr( varObj)& ")"
    	Case vbArray, 8204
    	For each item in varObj
    		If tmp="" Then
    			tmp= tmp+ item
    		Else
    			tmp= tmp+ ", "+item
    		End If
    	Next
    	tmp= "["& tmp& "]"
    	Case Else
    
     End Select
    
     displaystring= tmp& " #"& varType( varObj)
    
    End Function
    
    '*********************************************************
    ' OctetToHexStr
    ' Convert OctetString (byte array) to Hex string.
    '*********************************************************
    Function OctetToHexStr (arrbytOctet)
     Dim k
     OctetToHexStr = ""
     For k = 1 To Lenb (arrbytOctet)
    	OctetToHexStr = OctetToHexStr & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2)
     Next
    End Function
    
    '*********************************************************
    ' syncAttributes
    ' migrates attributes from source to target
    '*********************************************************
    Function syncAttributes (strUserOrGroup, SourceServer, SourceDomain, strNewUserOrGroup, TargetServer, TargetDomain,CheckUserOrGroup)
    	 dim strDNSource, strDNTarget, objSource, objTarget, n, strMail, btnIsMailbox
    	 If CheckUserOrGroup = "user" Then
    		strDNSource= getDN( strUserOrGroup, SourceServer, SourceDomain, CheckUserOrGroup, "")
    		strDNTarget= getDN( strNewUserOrGroup, TargetServer, TargetDomain, CheckUserOrGroup, "")
    		debug("Processing user: " & strDNSource)
    		If strDNSource"" AND strDNTarget  "" Then
    			If instr(strDNSource,"/")  0 or instr(strDNSource,"\")  0 or instr(strDNTarget,"/")  0 or instr(strDNTarget,"\")  0 Then
    				debug("**ERROR User DN is in wrong format.  Skipping user: " & strDNSource)
    				Exit Function
    			End If
    			set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
    			set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)
    			debug("Source Server Connection: LDAP://"& SourceServer& "/"& strDNSource)
    			debug("Target Server Connection: LDAP://"& TargetServer& "/"& strDNTarget)
    
    			'Check to see if destination account is a mailbox user.
    			btnIsMailbox = checkIfMailbox(objTarget)
    						
    			copyAttribute "mail", objSource, objTarget, False
    			copyAttribute "mailNickname", objSource, objTarget, False
    			copyAttribute "msExchMailboxGuid", objSource, objTarget, False
    			setAttribute "targetaddress", objSource.get( "mail"), objTarget
    			copyAttribute "proxyAddresses", objSource, objTarget, True
    			addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget
    			addAttribute "showInAddressBook", conDestinationGAL, objTarget
    
    			strMail= objSource.get( "mail")
    			n= instr( strMail, "@")
    			debug( strMail)
    			addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget
    			
    			If btnIsMailbox Then
    				debug(objTarget.get("displayName") & " is already a mailbox user in the destination forest." & _
    				" As a result msExchRecipientDisplayType and msExchRecipientTypeDetails attributes will not be changed.")
    			Else
    				setAttribute "msExchRecipientDisplayType", -2147483642, objTarget
    				setAttribute "msExchRecipientTypeDetails", 128, objTarget
    			End If
    			
    			setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget
    
    			objTarget.setInfo
    		Else
    			debug("*** ERR: Cannot retrieve DNs for Source or Target")
    			syncAttributes= False
    		End If
    	 End If
    	 
    	  If CheckUserOrGroup = "group" Then
    		strDNSource= getDN( strUserOrGroup, SourceServer, SourceDomain, CheckUserOrGroup, "")
    		strDNTarget= getDN( strNewUserOrGroup, TargetServer, TargetDomain, CheckUserOrGroup, "")
    		debug("Processing group: " & strDNSource)
    		If strDNSource"" AND strDNTarget  "" Then
    			If instr(strDNSource,"/")  0 or instr(strDNSource,"\")  0 or instr(strDNTarget,"/")  0 or instr(strDNTarget,"\")  0 Then
    				debug("**ERROR Group DN is in wrong format.  Skipping group: " & strDNSource)
    				Exit Function
    			End If
    			set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
    			set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)
    			debug("Source Server Connection: LDAP://"& SourceServer& "/"& strDNSource)
    			debug("Target Server Connection: LDAP://"& TargetServer& "/"& strDNTarget)
    
    			copyAttribute "mail", objSource, objTarget, False
    			copyAttribute "mailNickname", objSource, objTarget, False
    			copyAttribute "proxyAddresses", objSource, objTarget, True
    			addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget
    			addAttribute "showInAddressBook", conDestinationGAL, objTarget
    
    			strMail= objSource.get( "mail")
    			n= instr( strMail, "@")
    			debug( strMail)
    			addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget
    
    			setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget
    
    			objTarget.setInfo
    		Else
    			debug("*** ERR: Cannot retrieve DNs for Source or Target")
    			syncAttributes= False
    		End If
    	 End If
    End Function
    
    '*********************************************************
    ' getDN
    ' Retrieves the DN for a user object
    '*********************************************************
    Function getDN( strUserOrGroup, strServer, strDomain, CheckUserOrGroup, strOU)
    	dim objConn, objCmd, strQuery, objRS, strAttr, strRDNLDAP, strDNSLDAP
    	strRDNLDAP= RDN2LDAPPATH( strOU)
    	strDNSLDAP= DNSDomain2LDAPPath( strDomain)
    	strAttr= "distinguishedName"
    	set objConn= createObject( "ADODB.Connection")
    	set objCmd= createObject( "ADODB.Command")
    	objConn.Provider= "ADsDSOObject"
    	objConn.Open "ADs provider"
    	objCmd.ActiveConnection= objConn
    	strQuery= "<LDAP://"& strServer
    	If strServer  "" Then
    		strQuery= strQuery& "/"
    	End If
    	strQuery= strQuery& strRDNLDAP
    	If strOU  "" Then
    		strQuery= strQuery& ","
    	End If
    	strQuery= strQuery& strDNSLDAP& ">"
    	strQuery= strQuery+ ";(&(objectClass=" & CheckUserOrGroup & ")(SAMAccountName="& strUserOrGroup&"));"& strAttr& ";subtree"
    	objCmd.CommandText = strQuery
    	on error resume next
    	set objRS= objCmd.execute
    	if err.number  0 Then
    		debug( "*** ERR: Error "& err.number& " executing ["& strQuery& "]")
    		getDN= ""
    	Else
    		on error goto 0
    		Select Case objRS.recordCount
    			Case 0
    			debug( "*** ERR: " & CheckUserOrGroup & " object "& strUserOrGroup& " not found")
    			getDN= ""
    			Case 1
    			getDN= objRS.Fields( strAttr)
    			'debug( getDN)
    			Case Else
    			debug("*** ERR: Ambigious user object "& strUserOrGroup)
    			getDN= ""
    		End Select
    	End If
    	set objRS= Nothing
    	set objCmd= Nothing
    	set objConn= Nothing
    End Function
    
    '*********************************************************
    ' DNSDomain2LDAPPath( str)
    ' Makes an LDAP notation for a DNS domain name
    ' e.g. corp.local => DC=corp,DC=local
    '*********************************************************
    Function DNSDomain2LDAPPath( str)
    	Dim tmp1, tmp2, tmp3
    	tmp1= split( str, ".")
    	tmp2= ""
    	For each tmp3 in tmp1
    		If tmp2""  then
    			tmp2= tmp2& ","
    		End If
    		tmp2= tmp2& "dc="& tmp3
    	Next
    	DNSDomain2LDAPPath= tmp2
    End Function
    
    '*********************************************************
    ' RDN2LDAPPath( str)
    ' Makes an LDAP notation for a Relative Distinguished Name
    ' e.g. Domain Accounts/3rd party => OU=3rd party,OU=Domain Accounts
    '*********************************************************
    Function RDN2LDAPPath( str)
    	Dim tmp1, tmp2, tmp3
    	tmp1= split( str, "/")
    	tmp2= ""
    	For each tmp3 in tmp1
    		If tmp2"" then
    			tmp2= ","& tmp2
    		End If
    		tmp2= "ou="& tmp3& tmp2
    	Next
    	RDN2LDAPPath= tmp2
    End Function
    
    '*********************************************************
    ' copyAttribute
    ' Copies attribute(s) to target
    '*********************************************************
    Function copyAttribute( strAttribute, objSource, objTarget, boolMulti)
    	dim boolUpdate, varItem
    	On Error Resume Next
    	If isEmpty( objSource.get( strAttribute)) Then
    		debug( strAttribute& " not set, clearing")
    		objTarget.PutEx ADS_PROPERTY_CLEAR, strAttribute, 0
    	Else
    		varItem= objSource.get( strAttribute)
    		If boolMulti Then
    			If isArray( varItem) Then
    				debug( "Setting "& strAttribute& " to multi-value "& displayString( varItem))
    				objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, varItem
    			Else
    				debug( "Setting "& strAttribute& " to single-value "& displayString( varItem))
    				objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, array( varItem)
    			End If
    		Else
    			debug( "Setting "& strAttribute& " to "& displayString( varItem))
    			objTarget.Put strAttribute, varItem
    		End If
    	End If
    	objTarget.SetInfo
    	If Err.Number Then
    		debug("error check")
    	 End If
    End Function
    
    '*********************************************************
    ' setAttribute
    ' Sets attribute to target
    '*********************************************************
    Function setAttribute( strAttribute, strValue, objTarget)
     debug("Setting "& strAttribute& " to "& displayString(strValue))
     objTarget.Put strAttribute, strValue
     objTarget.SetInfo
    End Function
    
    '*********************************************************
    ' addAttribute
    ' Adds attribute to target
    '*********************************************************
    Function addAttribute( strAttribute, varAttribute, objTarget)
    	 dim boolUpdate, tmp
    	 boolUpdate= True
    	 On Error Resume Next
    	 If isEmpty( varAttribute) Then
    		' not set, skipping
    	 Else
    		If isEmpty( objTarget.get( strAttribute)) Then
    			boolUpdate= True
    		Else
    			If strAttribute = "showInAddressBook" or strAttribute = "proxyAddresses" Then
    				If isArray( objTarget.get( strAttribute)) Then
    					debug(strAttribute & " was detected as an array")
    					For each tmp in objTarget.get( strAttribute)
    						If tmp = varAttribute Then
    							boolUpdate= False
    						End If
    					Next
    				Else
    					'If a multivalued attribute in AD only has one value then it IsArray will not detect the item as an array.
    					If varAttribute = objTarget.get( strAttribute) Then
    						boolUpdate= False
    					End If
    				End If
    			End If
    		End If
    		If boolUpdate Then
    			debug("Adding "& varAttribute& " to "& strAttribute)
    			objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, array( varAttribute)
    		Else
    			debug( varAttribute& " already in "& strAttribute)
    		End If
    	 End If
    	 objTarget.SetInfo
    	 If Err.Number Then
    		debug("error check")
    	 End If
    End Function
    
    '************************************************************
    ' checkIfMailbox
    ' This function checks if the destination object is a mailbox
    '************************************************************
    Function checkIfMailbox(objTarget)
    	checkIfMailbox = False
    	If objTarget.Get("msExchRecipientDisplayType")  "-2147483642" Then
    		checkIfMailbox = True
    	End If
    End Function

    Like

  29. I foolishly ran an older version of the script 0.22, with users in the new mail server and it has changed them from mailbox to Mail User.

    Is it possible to reverse this?

    Like

  30. No they dont, i can still archive them to pst files though, which is bizarre. So the mailbox still exists, its just the user comes up as “Mail User” rather than “User Mailbox”. Luckily it was only old users, however if i can get them back to how they were, it would be preferred.

    Cheers.

    Like

  31. If i run that on one of the users, I get: “This task does not support recipients of this type. The specified recipient domain.local/Old Users/User Name is of type UserMailbox. Please make sure that this recipient matches the required recipient type for this task.”

    Then right clicking on that user in the EMC, it gives “The operation couldn’t be performed because object ‘domain.local/Old Users/User Name’ couldn’t be found on ‘dc.domain.local’. It was running the command ‘Get-Mailbox -Identity ‘domain.local/Old Users/User Name’ -ReadFromDomainController”

    However i can see the user on the dc.

    Cheers for your help.

    Like

  32. *Fixed* Had to open ADUC and in the attribute editor for the problem user, change msExchRecipientType from 128 to 1. It then turned the user back to UserMailbox and i can now open then as normal.

    Like

    • Glad it’s fixed. Note that other attributes might still be missing for a “true” mailbox enabled user (eg msExchangeHomeServerName, msExchangeMailboxGuid) but those might still be present with values from before the issue started.

      Like

  33. Just came across this script after doing some digging on migrating Ex2010 to Ex2010 across forest (obviously) – looking at testing this out in my lab before looking at production but VBS was never my strong point and I’m getting an error in the script and I’m not sure what’s causing it?

    When I run it I get the following
    Script: F:\BuildFiles\Extras\User Migration Scripts\PrepareForestMove.vbs
    Line: 242
    Char: 17
    Error: Expected ‘Then’
    Code: 800A03F9
    Source: Microsoft VBScript compilation error

    Looking at line 242 in the script it reads as follows
    If strDNSource”” AND strDNTarget “” Then

    Any thoughts folks (nothing like reviving a blog post that hasn’t been commented on for over 2 years 🙂

    Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.